import { createHash, randomUUID } from "node:crypto"; import { normalizeBeforeAgentStartInput, type BeforeAgentStartInput } from "../before-agent-start.ts"; import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { ContextEditEntryDraft, ContextUsage, ExtensionAPI, ExtensionContext, ProjectedSessionEntry, SessionBeforeCompactEvent, } from "@earendil-works/pi-coding-agent"; import { DEFAULT_SOFT_COMPACTION, readEffectiveCompactionSettings, resolveModelThresholdOverride, type EffectiveCompactionSettings, type SoftCompactionSettings, } from "./compaction-settings.ts"; import { COMPACTION_LEASE_TIMEOUT_MS, NATIVE_FALLBACK_COMPACTION_MARKER, type CompactionArbiter, type CompactionLease, type CompactionOwner, type CompactionTrigger, } from "./compaction-arbiter.ts"; import { deriveCompactionThreshold, deriveLinkedCompactionThreshold, effectiveReserveTokens, OUTPUT_CLAMP_BAND_MARGIN_RATIO, OUTPUT_CLAMP_SAFETY_TOKENS, type CompactionThresholdDerivation, type LinkedCompactionThresholdModel, } from "./compaction-threshold.ts"; import { autoCompactionIdleStatus, COMPACTION_MODE_STATUS_KEY, COMPACTION_STATUS_KEY, disableInvalidBudgetThinking, formatCompactionStatus, resolveConfiguredCompactionModel, } from "./maestro-compaction.ts"; import { loadPiCompactionInternals, type PiCompactionInternals } from "./pi-internals.ts"; export { disableInvalidBudgetThinking } from "./maestro-compaction.ts"; import { SPILL_THRESHOLD_CHARS, SPILL_PREVIEW_CHARS, spillToolResult, spillPath as resolveSpillPath, generatePreview, buildSpillReplacementText, cleanupSpillDir, validateSpillPath, } from "./tool-result-spill.ts"; import { compactLossless, type LosslessKind, } from "./lossless.ts"; import { detectContentType } from "./content-detector.ts"; import { scoreRelevanceBatch, tokenizeRelevance, RELEVANCE_MAX_QUERY_TOKENS, type RelevanceMode } from "./relevance.ts"; import { dedupBlocks, type DedupBlock } from "./dedup.ts"; import { isTeammateForkStartup, publishTeammateCompactionCapability, publishTeammateCompactionState, publishTeammateCompactionWakeReceipt, type TeammateCompactionPhase, type TeammateCompactionProducer, type TeammateCompactionWakeState, } from "./teammate-compaction-relay.ts"; const PROTECTED_THRESHOLD_CHARS = 500; // `resource` is URI-addressable (pr://, issue://, skill://, rule://, agent://): // the paired toolCall keeps the uri argument in context and the placeholder // below names it explicitly, so its large outputs are the cheapest results to // replay (agent:// is even backed by an immutable on-disk store). const REPLAYABLE_TOOL_NAMES = new Set(["read", "grep", "glob", "search", "find", "resource"]); // Bulk data tools whose large non-error output is transient and safe to evict // under sustained pressure. Control tools (e.g. todo) are deliberately absent so // their state-bearing output is never pruned. const EVICTABLE_BULK_TOOL_NAMES = new Set(["bash", "shell", "edit", "write"]); const PROTECTED_TOOL_NAMES = new Set(["todo", "goal", "run-control", "ask-user-question", "plan-update", "plan-confirm"]); // Content-aware chars-per-token ratios: a flat /4 miscounts the two content // types that dominate coding sessions — fenced code is token-denser (~3.5) and // whitespace-heavy logs/tables are token-sparser (~6). Ordinary content keeps the // proven /4 default so low-pressure estimates stay stable. const TOKEN_RATIO_CODE = 3.5; const TOKEN_RATIO_WHITESPACE_HEAVY = 6; const TOKEN_RATIO_DEFAULT = 4; const RELEVANCE_DOCUMENT_SAMPLE_CHARS = 32_000; const RELEVANCE_QUERY_SAMPLE_CHARS = 8_000; const RELEVANCE_MAX_CANDIDATES = 64; const RELEVANCE_TOTAL_SAMPLE_CHARS = 512_000; /** Fixed token estimate per image content block, matching Pi's ESTIMATED_IMAGE_CHARS / 4. */ /** * Proportional-image pressure floor: a single small image is never estimated * below this many tokens, matching the historical fixed estimate so * token-pressure behavior for typical screenshots stays compatible. */ const ESTIMATED_IMAGE_TOKENS = 1200; /** Decoded bytes per token for the size-aware image heuristic. */ const IMAGE_TOKEN_BYTES_PER_TOKEN = 512; /** Fixed token estimate per document block. A base64 PDF in source.data must * not reach the JSON text estimator — a 1MB PDF is ~1.33M base64 chars → * ~325k estimated tokens, vs the ~2000 the API actually charges. Same order as * Claude Code's IMAGE_MAX_TOKEN_SIZE semantics. */ const ESTIMATED_DOCUMENT_TOKENS = 2000; const CONTINUE_PROMPT = "Continue the interrupted task from the compacted session checkpoint. Do not wait for another user request."; const COMPACTION_RETRY_PROMPT = "Automatic compaction failed after the request was stopped because the context was exhausted. Retry compaction, then continue the interrupted task. Do not restart or wait for another user request."; const OUTPUT_LIMIT_RETRY_PROMPT = "Automatic compaction failed after the previous response was cut off at the model output token limit. Retry compaction, then continue exactly where the interrupted response stopped. Do not restart or wait for another user request."; const OUTPUT_LIMIT_CONTINUE_PROMPT = "Your previous response was cut off at the model output token limit, and the context was just compacted to free room. Continue exactly from where the interrupted response stopped and complete it. Do not restart or wait for another user request."; const OUTPUT_LIMIT_DIRECT_CONTINUE_PROMPT = "Your previous response was cut off at the model output token limit. Continue exactly from where the interrupted response stopped and complete it. Do not restart or wait for another user request."; const DEFAULT_OUTPUT_LIMIT_RATIO = 0.8; export const MAX_OUTPUT_LIMIT_COMPACTIONS = 2; export const MAX_OUTPUT_LIMIT_CONTINUATIONS = 8; /** * Consecutive compact-action evaluations at/above the loop-critical band an * active tool loop must hold before it is interrupted for compaction. A run * about to settle naturally relieves pressure within one or two requests; only * a genuinely sustained loop pays the interruption. */ export const LOOP_CRITICAL_PERSIST_EVALUATIONS = 3; const PRUNE_STATE_ENTRY_TYPE = "maestro-auto-prune-state"; const PENDING_INTENT_ENTRY_TYPE = "maestro-auto-compaction-intent"; const PENDING_INTENT_VERSION = 4; const RECOVERY_WAKE_VERSION = 1; /** Matches the existing teammate recovery ceiling; retries and restarts never reset it. */ const RECOVERY_WAKE_DEADLINE_MS = 300_000; const PRUNE_STATE_VERSION = 6; function autoRecoveryId(sessionId: string | undefined, intent: PendingCompactionIntent): string { return `${sessionId ?? "unknown"}:auto:${intent.triggerKey}`; } function recoveryDeliveryMarker(recoveryId: string): string { return `[maestro-recovery-delivery:${createHash("sha256").update(recoveryId).digest("hex").slice(0, 24)}]`; } function recoveryDeliveryPrompt(prompt: string, recoveryId: string): string { return `${prompt}\n\n${recoveryDeliveryMarker(recoveryId)}`; } export const MAX_PRUNE_DELTAS_BETWEEN_CHECKPOINTS = 32; /** * Drafts re-emitted for one prune while its commit stays unobserved. If * another boundary writer keeps superseding the target, we defer to it and * leave journal coverage in place rather than appending edits forever. */ const MAX_CONTEXT_EDIT_DRAFTS = 4; export const MAX_OFF_BRANCH_PRUNE_ENTRIES = 128; export const MAX_OFF_BRANCH_PRUNE_BYTES = 128 * 1024; export type CompactionSettings = Pick< EffectiveCompactionSettings, "enabled" | "reserveTokens" | "keepRecentTokens" | "model" | "modelThresholds" | "payloadLimitBytes" > & { soft?: EffectiveCompactionSettings["soft"] }; export interface ContextEstimate { tokens: number; usageTokens: number; trailingTokens: number; } interface AppliedPrunes { messages: AgentMessage[]; prunedToolResults: number; savedTokens: number; pendingSavedTokens: number; } export interface PruneManifestEntry { replacement: AgentMessage; savedTokens: number; introducedAtUsageEpoch?: string; spillPath?: string; /** SHA-256 of the persisted spill file; distinct from the source message digest. */ spillContentDigest?: string; level?: PruneLevel; contentDigest?: string; writerId?: string; /** Session entry that made this replacement authoritative on its branch. */ checkpointId?: string; /** For level "dedup": the referenced tool result that stays in context. */ refCallId?: string; /** * This prune's context_edit is confirmed on the session branch: the * projection was observed serving the replacement bytes, so the prune * journal no longer carries it. Set only on observation — emitting a draft * proves nothing because the host may still drop the whole boundary batch. * In-memory only — never serialized into the journal. */ editCommitted?: boolean; /** Boundary drafts emitted for this prune without a commit being observed yet. */ editDrafts?: number; } export type PruneLevel = "pruned" | "spill" | "minimal" | "lossless" | "dedup" | "payload"; interface PersistedPruneEntry { callId: string; level: PruneLevel; spillPath?: string; /** SHA-256 of the persisted spill file; required before a restored path is advertised. */ spillContentDigest?: string; introducedAtUsageEpoch?: string; contentDigest?: string; writerId?: string; refCallId?: string; /** Session entry that made this replacement authoritative on its branch. */ checkpointId?: string; /** Exact replacement text for context-dependent levels (dedup). */ replacementText?: string; } /** * The prune manifest owns the derived replacement, its token saving, and the * usage epoch it was introduced at — that trio is what keeps the request prefix * byte-identical within a compaction epoch. * * This was once `Set | Map<...>` for a legacy signature. The Set arm * could express none of the entry fields, so it silently degraded to * recomputing replacements and could never mark savings pending; no production * caller ever passed one. Narrowing it removes a path that looked equivalent * but quietly broke the cache-stability guarantee. */ export type PruneManifest = Map; export type ContextPressureBand = "normal" | "nudge" | "auto-prune" | "critical"; /** What the pressure policy actually does, separated from the band label. */ export type ContextAction = "none" | "prune" | "compact"; export interface ContextDecision { band: ContextPressureBand; action: ContextAction; reasons: string[]; } /** * Telemetry signals derived from the current context. velocity is added once a * tracker lands; cacheHitRatio is undefined when no usable provider usage exists. */ export interface ContextSignals { fullnessRatio: number; criticalGap: number; prunableFraction: number; /** Fraction of estimated tokens occupied by stale duplicate tool outputs. */ redundantFraction?: number; cacheHitRatio: number | undefined; /** Estimated request-body bytes of the current context (text + base64). */ payloadBytes?: number; /** Effective payload ceiling; present when a limit is configured. */ payloadLimitBytes?: number; /** True when payloadBytes exceeds the configured ceiling. */ payloadLimitExceeded?: boolean; } export interface VelocitySample { epoch: string; tokens: number; } /** Short-lived ring buffer of per-epoch context-size samples; owned by evaluate(). */ export interface VelocityTracker { samples: VelocitySample[]; } export interface VelocityInfo { /** Median tokens-per-epoch slope; undefined until >=3 samples exist. */ slope: number | undefined; /** True when the recent window shows >=2 consecutive positive diffs. */ robustGrowth: boolean; /** Estimated epochs until the critical threshold at the current slope. */ epochsToCritical: number | undefined; } export interface ContextPressureResult { messages: AgentMessage[]; band: ContextPressureBand; estimatedTokens: number; thresholdTokens: number; prunedToolResults: number; savedTokens: number; action: ContextAction; reasons: string[]; velocityTracker: VelocityTracker; velocity: VelocityInfo; } /** * Immutable, generation-scoped pressure facts shared by diagnostics and * Agent-visible Todo boundary guidance. The values are copied from the * linked threshold and the final pressure result; callers must not derive a * second threshold model from them. */ export interface ContextPressureSnapshot { generation: number; band: ContextPressureBand; estimatedTokens: number; contextWindow: number; hardThresholdTokens: number; remainingToHard: number; nudgeTokens?: number; pruneTokens?: number; } /** * Pre-derived soft band boundaries in absolute tokens. When provided, the * policy uses these instead of recomputing ratios against the window, so the * output-budget-aware bands from the threshold model drive the runtime. */ export interface SoftPressureBands { nudgeTokens: number; pruneTokens: number; pruneTargetTokens: number; } interface PendingCompactionIntent { generation: number; triggerKey: string; /** Stable logical recovery identity; absent only on legacy journal records. */ recoveryId?: string; estimate: ContextEstimate; linkedThreshold: LinkedCompactionThresholdModel & { usable: true }; settings: CompactionSettings; effectiveSettings: CompactionSettings; contextExhausted: boolean; /** The provider request was blocked before HTTP because budget thinking was invalid. */ requestBlocked?: boolean; /** Sustained critical-band pressure inside an active tool loop aborted the run for immediate settle. */ loopCritical?: boolean; } export interface RecoveryWake { version: 1; recoveryId: string; wakeId: string; producer: TeammateCompactionProducer; sessionId: string; branchCheckpointId: string; branchCheckpointDepth: number; producerGeneration: number; runtimeGeneration: number; state: TeammateCompactionWakeState; promptKind: string; prompt: string; sequence: number; deadlineAt: number; wakeAttempts: number; messageId?: string; turnId?: string; reason?: string; } interface PendingOutputLimitIntent { generation: number; /** Producer-scoped identity; output-limit generations are not comparable to auto intents. */ recoveryId: string; /** A continuation prompt is durably staged and must be replayed after refresh. */ phase?: "pending" | "submitted" | "continuation"; settings: CompactionSettings; usage: ContextUsage; threshold: number; } export interface AutoCompactionState { running: boolean; generation: number; nextOwner: number; activeOwner?: number; /** * Owner of a submission whose watchdog expired but whose host compact() has * neither completed nor errored yet. New submissions are held until the * zombie settles or the grace deadline passes, so a late completion cannot * race a fresh compaction. */ zombieOwner?: number; /** Wall-clock deadline after which an unsettled zombie is presumed dead. */ zombieDeadlineMs?: number; /** Logical request owner, independent of whether an arbiter lease exists. */ activeRequestOwner?: CompactionOwner; activeLease?: CompactionLease; pendingIntent?: PendingCompactionIntent; /** Compaction succeeded, but the interrupted task still needs a durable continuation. */ pendingContinuationIntent?: PendingCompactionIntent; pendingOutputLimitIntent?: PendingOutputLimitIntent; /** One durable logical wake shared by auto and output-limit producers. */ recoveryWake?: RecoveryWake; /** Native fallback callback fence; invalidated by lifecycle/reset changes. */ nativeFallbackNonce: number; lastTriggerKey?: string; /** Turn at which the last internals-resolution warning was shown; undefined re-arms it. */ internalsWarningTurn?: number; lastNoCompactableKey?: string; pruneManifest: Map; restoredPrunes: Map; sessionId?: string; /** Per-guard writer namespace prevents one live owner deleting another's spill. */ writerId: string; persistedPruneKey?: string; /** Materialized prune journal at the current branch checkpoint. */ persistedPrunes: Map; forcePruneCheckpoint: boolean; pruneDeltasSinceCheckpoint: number; activeCheckpointId?: string; reachableCheckpoints: Map; restoredPruneAges: Map; restoredPruneSequence: number; persistedIntentKey?: string; settingsSnapshot?: CompactionSettings; settingsCwd?: string; /** Current lifecycle's hard threshold, cached by the context hook for tool-call checks. */ latestThreshold?: { generation: number; linkedThreshold: LinkedCompactionThresholdModel & { usable: true }; settings: CompactionSettings; effectiveSettings: CompactionSettings; }; /** Last complete pressure cycle in this lifecycle, if pressure is known. */ latestPressureSnapshot?: ContextPressureSnapshot; velocityTracker: VelocityTracker; /** Held whole so the two fields can never be written back out of step. */ breaker: CompactionBreakerState; breakerNotified: boolean; turnCount: number; /** Consecutive completed turns whose threshold intent was deferred instead of run. */ highPressureDroppedTurns: number; /** Consecutive evaluations sitting below the escalate band while an intent is live. */ belowEscalateStreak: number; /** Consecutive compact-action evaluations at/above the loop-critical band within the current run. */ criticalLoopStreak: number; /** Pressure notifications already emitted in the current compaction cycle. */ notifiedPressureKeys: Set; outputLimitCompactions: number; outputLimitContinuations: number; outputLimitBreakerNotified: boolean; outputLimitContinuationNotified: boolean; /** Last context frame passed toward the provider, including stable prune replacements. */ lastProviderMessages?: AgentMessage[]; /** One reactive provider-pressure compaction is allowed until a valid replay succeeds. */ providerPressureAttempted: boolean; /** Suppresses ordinary Stop hooks while a synthetic abort is being recovered. */ providerPressureBlocked: boolean; /** The one-shot replay also failed; no compaction completion may resume it. */ providerPressureTerminal: boolean; /** A loop-critical abort interrupted the run; Goal/Stop lifecycle treats the settlement as synthetic recovery. */ loopCriticalBlocked: boolean; /** Provider usage epoch the cache ratio below was sampled at. */ cacheEpoch?: string; cacheRatio?: number; /** Epoch during which new prunes were introduced, pending its cache bill. */ prunedDuringEpoch?: string; /** Cache-ratio movement attributed to the prunes of the previous epoch. */ cacheDelta?: number; } export interface AutoCompactionDependencies { loadInternals?: () => Promise; readSettings?: (projectRoot: string) => CompactionSettings; arbiter?: CompactionArbiter; /** Test hook: override the watchdog/lease timeout (defaults to COMPACTION_LEASE_TIMEOUT_MS). */ leaseTimeoutMs?: number; } export interface MessageRecord { role?: unknown; content?: unknown; toolCallId?: unknown; toolName?: unknown; isError?: unknown; usage?: unknown; stopReason?: unknown; timestamp?: unknown; details?: unknown; } export interface ProjectedCompactionInput { event: SessionBeforeCompactEvent; estimatedInputTokens?: number; prunedToolResults: number; } export function commitProjectedCompactionInput( event: SessionBeforeCompactEvent, projected: ProjectedCompactionInput, ): void { event.preparation.messagesToSummarize = projected.event.preparation.messagesToSummarize; event.preparation.turnPrefixMessages = projected.event.preparation.turnPrefixMessages; } /** * Result returned to the host tool_call hook when hard-threshold pressure must * stop further tool execution. `block` prevents the current tool; `terminate` * ends a top-level batch. Nested executors consume that flag locally, so their * outer operation is also aborted through the real host context. */ export interface ToolBoundaryGateResult { block: true; terminate: true; reason: string; } const TOOL_BOUNDARY_BLOCK_REASON = "Tool execution deferred: context crossed the hard compaction threshold. Compaction will run next; the task resumes automatically."; /** Snapshot of the mid-turn compaction module state for diagnostics (/compaction-status). */ export interface MidTurnCompactionStatus { turnCount: number; running: boolean; /** Latest complete pressure-cycle facts for Agent/tool-boundary guidance. */ pressureSnapshot?: ContextPressureSnapshot; activeRequestOwner?: CompactionOwner; /** Owner of a timed-out submission that has not settled yet, if any. */ zombieOwner?: number; pendingIntent?: { tokens: number; thresholdTokens: number; contextExhausted: boolean; requestBlocked: boolean; loopCritical: boolean; }; outputLimitIntentPending: boolean; breaker: { consecutiveFailures: number; trippedAtTurn?: number; cooldownRemainingTurns?: number; }; } export function createMidTurnAutoCompaction(pi: ExtensionAPI, dependencies: AutoCompactionDependencies = {}): { onSessionStart(ctx: ExtensionContext, event?: { reason?: string }): void; /** Trusted input boundary: only a matching raw recovery prompt may become consumed. */ onBeforeAgentStart(event: BeforeAgentStartInput | string, ctx: ExtensionContext): void; /** Associated lifecycle boundary: only the wake consumed immediately before it may start. */ onAgentStart(ctx: ExtensionContext): void; /** Idempotently dispatch the current logical wake when settled/idle. */ ensureRecoveryWake(ctx: ExtensionContext): boolean; evaluate(messages: AgentMessage[], ctx: ExtensionContext): Promise; /** * Gate tool execution at the first hard-threshold boundary. Top-level calls * settle via block+terminate; nested calls additionally abort the outer host * operation because their executor does not propagate terminate. */ onToolCall(ctx: ExtensionContext, parentToolCallId?: string): ToolBoundaryGateResult | undefined; projectCompactionInput(event: SessionBeforeCompactEvent, ctx: ExtensionContext): Promise; beforeProviderRequest(payload: unknown, ctx: ExtensionContext): Promise; /** * Draft one persistent context_edit per recorded prune whose commit is not * yet observed on the branch (call from the turn_end boundary). Once the * host commits the draft its projection replays the same replacement bytes * on every later request — including resume, fork, and compaction * preparation — so persistPruneManifest stops journaling confirmed entries. */ contextEditDrafts(contextEntries: readonly ProjectedSessionEntry[]): ContextEditEntryDraft[]; shouldSkipStopHook(): boolean; isProviderPressureRecoveryActive(): boolean; /** * True while a synthetic interruption (provider-pressure replay or * loop-critical tool-boundary gate) owns the current settlement. Root * Goal/Stop lifecycle must treat such settlements as recovery, never as * ordinary turn endings. */ isSyntheticCompactionInterruptionActive(): boolean; /** * True when a queued mid-turn or output-limit intent will take over * compaction at agent settle. Native threshold compaction may only be * cancelled for completed-turn preservation when this returns true. */ hasPendingTakeover(): boolean; /** * True when the completed turn has consumed Pi's output safety headroom and * the already-started native threshold compaction must retain ownership. */ shouldRetainNativeThreshold(ctx: ExtensionContext): boolean; /** Breaker pause explanation (with remaining cooldown), or undefined when attempts are allowed. */ describeBreakerPause(): string | undefined; describeState(): MidTurnCompactionStatus; onAgentEnd(ctx: ExtensionContext): Promise; onOutputLimit(messages: AgentMessage[], ctx: ExtensionContext): Promise; /** Returns false when the event belongs to a replaced session lifecycle. */ onCompact(completedOwner?: CompactionOwner, ctx?: ExtensionContext): boolean; onSessionShutdown(ctx?: ExtensionContext): void; reset(ctx?: ExtensionContext): void; refreshSettings(): void; } { const state: AutoCompactionState = { running: false, generation: 0, nextOwner: 0, internalsWarningTurn: undefined, pruneManifest: new Map(), restoredPrunes: new Map(), persistedPrunes: new Map(), forcePruneCheckpoint: false, pruneDeltasSinceCheckpoint: 0, reachableCheckpoints: new Map(), restoredPruneAges: new Map(), restoredPruneSequence: 0, writerId: randomUUID(), velocityTracker: EMPTY_VELOCITY_TRACKER, breaker: resetCompactionBreaker(), breakerNotified: false, turnCount: 0, highPressureDroppedTurns: 0, belowEscalateStreak: 0, criticalLoopStreak: 0, notifiedPressureKeys: new Set(), outputLimitCompactions: 0, outputLimitContinuations: 0, outputLimitBreakerNotified: false, outputLimitContinuationNotified: false, nativeFallbackNonce: 0, providerPressureAttempted: false, providerPressureBlocked: false, providerPressureTerminal: false, loopCriticalBlocked: false, }; let transformTail = Promise.resolve(); async function withTransformLock(run: () => Promise): Promise { const previous = transformTail; let release!: () => void; transformTail = new Promise((resolve) => { release = resolve; }); await previous; try { return await run(); } finally { release(); } } const loadInternals = dependencies.loadInternals ?? loadPiCompactionInternals; const readSettings = dependencies.readSettings ?? readEffectiveCompactionSettings; const leaseTimeoutMs = dependencies.leaseTimeoutMs ?? COMPACTION_LEASE_TIMEOUT_MS; // Settings are reparsed at most once per turn boundary (F11): the snapshot is // reused across the many context-hook evaluations within a turn and re-read only // when the cwd changes or refreshSettings() invalidates it. function settingsFor(ctx: ExtensionContext): CompactionSettings { if (!state.settingsSnapshot || state.settingsCwd !== ctx.cwd) { state.settingsSnapshot = readSettings(ctx.cwd); state.settingsCwd = ctx.cwd; } return state.settingsSnapshot; } function sameSettings(left: CompactionSettings, right: CompactionSettings): boolean { // Summary tuning does not change pruning/admission. Older durable intents // lack this group and must still recover under the new defaults. const withoutSummary = (key: string, value: unknown) => key === "summary" ? undefined : value; return JSON.stringify(left, withoutSummary) === JSON.stringify(right, withoutSummary); } function notifyPressureOnce(ctx: ExtensionContext, key: string, message: string): void { if (state.notifiedPressureKeys.has(key)) return; state.notifiedPressureKeys.add(key); ctx.ui.notify(message, "warning"); } function loopCriticalForReplacement(intent: PendingCompactionIntent | undefined): true | undefined { return state.loopCriticalBlocked || intent?.loopCritical === true ? true : undefined; } function recoveryIdFor(intent: PendingCompactionIntent): string { // The producer marker is part of the wire identity. Auto and new-context // generations are independent counters and must never be compared globally. return intent.recoveryId ?? autoRecoveryId(state.sessionId, intent); } function newAutoRecoveryId(triggerKey: string): string { return `${state.sessionId ?? "unknown"}:auto:${triggerKey}:${randomUUID()}`; } function publishRecoveryState( intent: PendingCompactionIntent, phase: TeammateCompactionPhase, reason?: string, ): void { if (!intent.requestBlocked && !intent.contextExhausted && !intent.loopCritical) return; const recoveryId = recoveryIdFor(intent); const wakeGeneration = state.recoveryWake?.recoveryId === recoveryId ? state.recoveryWake.producerGeneration : intent.generation; publishTeammateCompactionState({ recoveryId, producer: "auto", phase, generation: wakeGeneration, ...(state.recoveryWake?.recoveryId === recoveryId ? { wakeProtocolVersion: 1 as const, wakeId: state.recoveryWake.wakeId, wakeDeadlineAt: state.recoveryWake.deadlineAt, } : {}), ...(reason ? { reason } : {}), }); } function outputRecoveryIdFor(intent: PendingOutputLimitIntent): string { return intent.recoveryId; } function publishOutputRecoveryState( intent: PendingOutputLimitIntent, phase: TeammateCompactionPhase, reason?: string, ): void { const recoveryId = outputRecoveryIdFor(intent); const wakeGeneration = state.recoveryWake?.recoveryId === recoveryId ? state.recoveryWake.producerGeneration : intent.generation; publishTeammateCompactionState({ recoveryId, producer: "output-limit", phase, generation: wakeGeneration, ...(state.recoveryWake?.recoveryId === recoveryId ? { wakeProtocolVersion: 1 as const, wakeId: state.recoveryWake.wakeId, wakeDeadlineAt: state.recoveryWake.deadlineAt, } : {}), ...(reason ? { reason } : {}), }); } function notifyBestEffort( ctx: ExtensionContext, message: string, level: "info" | "warning" | "error", ): void { try { ctx.ui.notify(message, level); } catch (error) { console.warn(`[pi-maestro-flow] UI notification failed: ${error instanceof Error ? error.message : String(error)}`); } } let consumedWakeAwaitingStart: string | undefined; let dispatchedWakeThisLifecycle: string | undefined; function runtimeGeneration(): number { const value = Number(process.env.PI_TEAMMATE_RUNTIME_GENERATION); return Number.isSafeInteger(value) && value >= 0 ? value : 0; } function branchCheckpointId(ctx: ExtensionContext): string { const branch = (ctx.sessionManager as { getBranch?: () => Array<{ id?: string }> }).getBranch?.() ?? []; const sessionId = state.sessionId ?? sessionIdOf(ctx) ?? "unknown-session"; return branch.at(-1)?.id ?? `session:${sessionId}:root`; } function wakeBranch(ctx: ExtensionContext): Array<{ id?: string; type?: string; message?: unknown }> { return (ctx.sessionManager as { getBranch?: () => Array<{ id?: string; type?: string; message?: unknown }>; }).getBranch?.() ?? []; } function userMessageText(message: unknown): string | undefined { if (!message || typeof message !== "object") return undefined; const value = message as { role?: unknown; content?: unknown }; if (value.role !== "user") return undefined; if (typeof value.content === "string") return value.content; if (!Array.isArray(value.content)) return undefined; const text = value.content.flatMap((part) => { if (!part || typeof part !== "object") return []; const shape = part as { type?: unknown; text?: unknown }; return shape.type === "text" && typeof shape.text === "string" ? [shape.text] : []; }).join(""); return text || undefined; } function entriesAfterWakeCheckpoint(ctx: ExtensionContext, wake: RecoveryWake): Array<{ id?: string; type?: string; message?: unknown }> | undefined { const branch = wakeBranch(ctx); if (wake.branchCheckpointId === `session:${wake.sessionId}:root`) { return branch.slice(wake.branchCheckpointDepth); } const checkpointIndex = branch.findIndex((entry) => entry.id === wake.branchCheckpointId); return checkpointIndex < 0 ? undefined : branch.slice(checkpointIndex + 1); } function branchMessageForWake(ctx: ExtensionContext, wake: RecoveryWake): { id?: string } | undefined { const expected = recoveryDeliveryPrompt(wake.prompt, wake.wakeId); return entriesAfterWakeCheckpoint(ctx, wake)?.findLast((entry) => entry.type === "message" && userMessageText(entry.message) === expected); } function branchHasSupersedingInput(ctx: ExtensionContext, wake: RecoveryWake): boolean { const expected = recoveryDeliveryPrompt(wake.prompt, wake.wakeId); return entriesAfterWakeCheckpoint(ctx, wake)?.some((entry) => { if (entry.type !== "message") return false; const text = userMessageText(entry.message); return text !== undefined && text !== expected; }) === true; } function branchMatchesWake(ctx: ExtensionContext, wake: RecoveryWake): boolean { return entriesAfterWakeCheckpoint(ctx, wake) !== undefined; } function wakeIsTerminal(wake: RecoveryWake): boolean { return wake.state === "turn-started" || wake.state === "cancelled" || wake.state === "failed"; } function persistTerminalWake(wake: RecoveryWake, ctx: ExtensionContext): void { if (persistPendingIntent(pi, state)) { publishWake(wake); return; } notifyBestEffort(ctx, `Recovery wake ${wake.wakeId} terminal receipt could not be persisted; retrying fail-closed.`, "error"); queueMicrotask(() => { if (state.recoveryWake !== wake || !wakeIsTerminal(wake)) return; if (persistPendingIntent(pi, state)) publishWake(wake); else notifyBestEffort(ctx, `Recovery wake ${wake.wakeId} terminal receipt is still not durable.`, "error"); }); } function transitionWakeTerminal( wake: RecoveryWake, terminalState: "cancelled" | "failed", reason: string, ctx: ExtensionContext, ): void { if (wakeIsTerminal(wake)) return; wake.state = terminalState; wake.reason = reason; wake.sequence += 1; if (state.pendingIntent && recoveryIdFor(state.pendingIntent) === wake.recoveryId) state.pendingIntent = undefined; if (state.pendingContinuationIntent && recoveryIdFor(state.pendingContinuationIntent) === wake.recoveryId) { state.pendingContinuationIntent = undefined; } if (state.pendingOutputLimitIntent?.recoveryId === wake.recoveryId) state.pendingOutputLimitIntent = undefined; consumedWakeAwaitingStart = undefined; dispatchedWakeThisLifecycle = undefined; persistTerminalWake(wake, ctx); } function expireWakeIfNeeded(wake: RecoveryWake, ctx: ExtensionContext): boolean { if (Date.now() < wake.deadlineAt) return false; transitionWakeTerminal( wake, "failed", "recovery wake deadline expired before an authoritative turn-started receipt", ctx, ); notifyBestEffort(ctx, wake.reason ?? "Recovery wake expired.", "error"); return true; } function cancelMatchingWake(recoveryId: string, reason: string, ctx: ExtensionContext): void { const wake = state.recoveryWake; if (!wake || wake.recoveryId !== recoveryId) return; transitionWakeTerminal(wake, "cancelled", reason, ctx); } function failMatchingWake(recoveryId: string, reason: string, ctx: ExtensionContext): void { const wake = state.recoveryWake; if (!wake || wake.recoveryId !== recoveryId) return; transitionWakeTerminal(wake, "failed", reason, ctx); } function publishWake(wake: RecoveryWake): void { publishTeammateCompactionWakeReceipt({ recoveryId: wake.recoveryId, producer: wake.producer, generation: wake.producerGeneration, wakeId: wake.wakeId, state: wake.state, sequence: wake.sequence, deadlineAt: wake.deadlineAt, runtimeGeneration: wake.runtimeGeneration, sessionId: wake.sessionId, branchCheckpointId: wake.branchCheckpointId, ...(wake.messageId ? { messageId: wake.messageId } : {}), ...(wake.turnId ? { turnId: wake.turnId } : {}), ...(wake.reason ? { reason: wake.reason } : {}), }); } function prepareWake( recoveryId: string, producer: TeammateCompactionProducer, producerGeneration: number, promptKind: string, prompt: string, ctx: ExtensionContext, ): RecoveryWake | undefined { const existing = state.recoveryWake; if (existing) { if (existing.recoveryId === recoveryId && existing.producerGeneration === producerGeneration) return existing; if (!wakeIsTerminal(existing)) { notifyBestEffort( ctx, `Recovery wake ${recoveryId} was not staged because ${existing.recoveryId} still owns the durable handoff.`, "error", ); return undefined; } } if (!pi.appendEntry) { notifyBestEffort(ctx, "Recovery wake capability unavailable: this Pi ExtensionAPI cannot durably append the prepared input.", "error"); return undefined; } const wake: RecoveryWake = { version: RECOVERY_WAKE_VERSION, recoveryId, wakeId: randomUUID(), producer, sessionId: state.sessionId ?? sessionIdOf(ctx) ?? "unknown-session", branchCheckpointId: branchCheckpointId(ctx), branchCheckpointDepth: wakeBranch(ctx).length, producerGeneration, runtimeGeneration: runtimeGeneration(), state: "prepared", promptKind, prompt, sequence: 1, deadlineAt: Date.now() + RECOVERY_WAKE_DEADLINE_MS, wakeAttempts: 0, }; state.recoveryWake = wake; if (!persistPendingIntent(pi, state)) { state.recoveryWake = existing; return undefined; } publishWake(wake); return wake; } function ensureRecoveryWake(ctx: ExtensionContext): boolean { const wake = state.recoveryWake; if (!wake || wakeIsTerminal(wake)) return false; // followUp delivery still enters host prompt preflight, which rejects while // compact() is active. Only its callback proves the host owner has settled. if (state.running || state.zombieOwner !== undefined) return false; if (expireWakeIfNeeded(wake, ctx)) return false; if (!branchMatchesWake(ctx, wake)) { transitionWakeTerminal(wake, "failed", "recovery wake branch checkpoint is no longer active", ctx); return false; } if (branchHasSupersedingInput(ctx, wake)) { transitionWakeTerminal(wake, "cancelled", "a newer user message superseded recovery wake", ctx); return false; } if (wake.state === "consumed") { notifyBestEffort(ctx, `Recovery wake ${wake.wakeId} was already consumed; this Pi runtime cannot transactionally restart its associated turn without duplicate input.`, "error"); return false; } if (branchMessageForWake(ctx, wake)) { // The durable transcript proves insertion, but this API cannot prove the // input was transactionally consumed or restart its associated turn. notifyBestEffort(ctx, `Recovery wake ${wake.wakeId} is already present in the session; awaiting its authoritative lifecycle boundary.`, "warning"); return false; } if (dispatchedWakeThisLifecycle === wake.wakeId) return true; wake.wakeAttempts += 1; wake.sequence += 1; if (!persistPendingIntent(pi, state)) { notifyBestEffort(ctx, "Recovery wake dispatch paused: its durable attempt could not be saved.", "error"); return false; } publishWake(wake); dispatchedWakeThisLifecycle = wake.wakeId; try { // ExtensionAPI is fire-and-forget. Synchronous return is deliberately not // promoted to queued/consumed and never clears the durable obligation. // A compaction callback can run after the agent has already become idle; // followUp has no future stop boundary to drain in that state. const delivery = ctx.isIdle?.() === true ? undefined : { deliverAs: "followUp" as const }; pi.sendUserMessage(recoveryDeliveryPrompt(wake.prompt, wake.wakeId), delivery); return true; } catch (error) { dispatchedWakeThisLifecycle = undefined; notifyBestEffort(ctx, `Recovery wake dispatch failed synchronously: ${error instanceof Error ? error.message : String(error)}`, "error"); return false; } } // Pi drains a queued follow-up with a low-level agent.continue(), which emits // agent_start but never before_agent_start. On that path the wake stayed // "prepared" even though the transcript now proves the input landed and its // turn completed, so the durable handoff expired at its deadline instead of // reaching a terminal receipt. Promote it from ground-truth branch evidence; // the entry is appended mid-run, so agent_start is too early — only the // turn boundary observes it. function promoteDrainedWake(ctx: ExtensionContext): void { const wake = state.recoveryWake; if (!wake || wakeIsTerminal(wake)) return; if (wake.state !== "prepared" && wake.state !== "queued") return; if (expireWakeIfNeeded(wake, ctx)) return; if (!branchMatchesWake(ctx, wake)) return; const recorded = branchMessageForWake(ctx, wake); if (!recorded) return; if (branchHasSupersedingInput(ctx, wake)) { transitionWakeTerminal(wake, "cancelled", "a newer user message superseded recovery wake", ctx); return; } const previous = { ...wake }; if (recorded.id) wake.messageId = recorded.id; wake.state = "turn-started"; wake.turnId = randomUUID(); wake.sequence += 1; const pendingContinuation = state.pendingContinuationIntent; const pendingOutput = state.pendingOutputLimitIntent; if (wake.producer === "auto") { state.pendingContinuationIntent = undefined; state.pendingIntent = undefined; } else if (wake.producer === "output-limit") { state.pendingOutputLimitIntent = undefined; } consumedWakeAwaitingStart = undefined; if (!persistPendingIntent(pi, state)) { Object.assign(wake, previous); state.pendingContinuationIntent = pendingContinuation; state.pendingOutputLimitIntent = pendingOutput; state.recoveryWake = wake; notifyBestEffort(ctx, "Recovery input was drained but its correlated receipt could not be persisted; cleanup is fail-closed.", "error"); return; } publishWake(wake); } function stageInterruptedWake( intent: PendingCompactionIntent, ctx: ExtensionContext, prompt: string, publishCompleted: boolean, ): void { if (state.pendingIntent === intent) state.pendingIntent = undefined; const durableContinuation = !intent.requestBlocked && (intent.contextExhausted || intent.loopCritical); const shouldContinue = intent.requestBlocked || durableContinuation; const superseded = Boolean(ctx.hasPendingMessages?.()); if (!shouldContinue || superseded) { if (state.pendingContinuationIntent === intent) state.pendingContinuationIntent = undefined; if (superseded) { cancelMatchingWake(recoveryIdFor(intent), "a newer user message superseded compaction recovery", ctx); } else { persistPendingIntent(pi, state); } return; } if (durableContinuation) state.pendingContinuationIntent = intent; else persistPendingIntent(pi, state); // provider-pressure remains in-process only const recoveryId = recoveryIdFor(intent); const promptKind = prompt === COMPACTION_RETRY_PROMPT ? "retry-compaction" : "continue"; const producerGeneration = state.recoveryWake?.recoveryId === recoveryId ? state.recoveryWake.producerGeneration : intent.generation; const wake = prepareWake(recoveryId, "auto", producerGeneration, promptKind, prompt, ctx); if (!wake) { notifyBestEffort(ctx, "Mid-turn continuation paused: durable recovery wake could not be saved.", "error"); return; } if (publishCompleted) publishRecoveryState(intent, "completed"); publishRecoveryState(intent, "continuation"); ensureRecoveryWake(ctx); } function queueInterruptedContinuation(intent: PendingCompactionIntent, ctx: ExtensionContext, prompt: string, publishCompleted: boolean): void { stageInterruptedWake(intent, ctx, prompt, publishCompleted); } function settleInterruptedContinuation(intent: PendingCompactionIntent, ctx: ExtensionContext): void { stageInterruptedWake(intent, ctx, CONTINUE_PROMPT, true); } async function linkedThresholdFor( ctx: ExtensionContext, settings: CompactionSettings, ): Promise { const sessionModel = ctx.model; if (!sessionModel) { return deriveLinkedCompactionThreshold({ reserveTokens: settings.reserveTokens, sessionContextWindow: undefined, soft: settings.soft, }); } const compactionModel = await resolveConfiguredCompactionModel(settings.model, sessionModel, ctx); return deriveLinkedCompactionThreshold({ reserveTokens: settings.reserveTokens, sessionContextWindow: sessionModel.contextWindow, sessionMaxTokens: sessionModel.maxTokens, compactionContextWindow: compactionModel.contextWindow, compactionMaxTokens: compactionModel.maxTokens, enforceCompactionHeadroom: settings.model !== undefined, soft: settings.soft, sessionThresholdTokensOverride: resolveModelThresholdOverride(settings, sessionModel), }); } function isContextExhausted(ctx: ExtensionContext, estimatedTokens: number): boolean { const contextWindow = ctx.model?.contextWindow; return contextWindow === undefined || estimatedTokens >= contextWindow; } /** * A tool loop can hold the context between the derived threshold and the * window limit indefinitely: the host only checks its native threshold at * run boundaries, and agent settle never fires mid-run. Once critical-band * pressure (window minus effective reserve — where Pi itself would compact) * persists across several requests, interrupt the run so the settled-phase * compaction fires well before exhaustion. Reuses the exhausted pathway: * settle sees a continuation-needing intent, submits it immediately, and the * task resumes automatically. Single-owner semantics stay untouched — the * context hook still never submits compaction itself. The interruption * re-asserts the abort on later evaluations while the intent survives (the * host may swallow an abort), mirroring the exhausted path; the intent * lifecycle bounds the retries. */ function maybeInterruptForCriticalLoop( ctx: ExtensionContext, estimatedTokens: number, linkedThreshold: LinkedCompactionThresholdModel & { usable: true }, ): boolean { const loopCriticalTokens = linkedThreshold.contextWindow - linkedThreshold.effectiveReserveTokens; if (estimatedTokens < loopCriticalTokens) { state.criticalLoopStreak = 0; return false; } state.criticalLoopStreak += 1; const intent = state.pendingIntent; if (!intent || intent.contextExhausted || intent.requestBlocked) return false; if (state.criticalLoopStreak < LOOP_CRITICAL_PERSIST_EVALUATIONS) return false; if (state.running) return false; // Never interrupt a run that cannot hand off immediately: a zombie // submission may still settle, a foreign owner is already relieving // pressure, and a tombstone means a timed-out compaction is unresolved. if (state.zombieOwner !== undefined) return false; if (dependencies.arbiter?.currentOwner()) return false; if (dependencies.arbiter?.timeoutTombstone()) return false; if (ctx.hasPendingMessages?.()) return false; if (!compactionBreakerAllows(state.breaker, state.turnCount).allowed) return false; intent.loopCritical = true; state.loopCriticalBlocked = true; persistPendingIntent(pi, state); publishRecoveryState(intent, "pending"); ctx.abort(); notifyPressureOnce( ctx, `loop-critical:${linkedThreshold.contextWindow}`, `Context held above ${loopCriticalTokens.toLocaleString("en-US")} tokens inside an active tool loop (${estimatedTokens.toLocaleString("en-US")}/${linkedThreshold.contextWindow}). Interrupting to compact; the task resumes automatically.`, ); return true; } /** * Every field a fresh lifecycle must start from. onSessionStart used to clear * only 13 of these and never touched the concurrency trio, so a session * switched in-process without a shutdown inherited `running: true` and * silently disabled compaction for the whole new session while stranding the * arbiter lease. Both entry points now clear the same set. */ function releaseInFlight(): void { // Invalidate callbacks from the replaced lifecycle before releasing the // lease; the native fallback fence remains valid only for its own session. state.nativeFallbackNonce += 1; state.running = false; state.activeOwner = undefined; state.zombieOwner = undefined; state.zombieDeadlineMs = undefined; state.activeRequestOwner = undefined; state.activeLease?.release(); state.activeLease = undefined; state.pendingIntent = undefined; state.pendingContinuationIntent = undefined; state.pendingOutputLimitIntent = undefined; state.recoveryWake = undefined; consumedWakeAwaitingStart = undefined; dispatchedWakeThisLifecycle = undefined; state.lastTriggerKey = undefined; state.lastNoCompactableKey = undefined; state.highPressureDroppedTurns = 0; state.belowEscalateStreak = 0; state.criticalLoopStreak = 0; state.notifiedPressureKeys.clear(); state.internalsWarningTurn = undefined; state.lastProviderMessages = undefined; state.latestThreshold = undefined; state.latestPressureSnapshot = undefined; state.providerPressureAttempted = false; state.providerPressureBlocked = false; state.providerPressureTerminal = false; state.loopCriticalBlocked = false; } function resetCycleState(): void { state.velocityTracker = EMPTY_VELOCITY_TRACKER; state.breaker = resetCompactionBreaker(); state.breakerNotified = false; state.highPressureDroppedTurns = 0; state.belowEscalateStreak = 0; state.criticalLoopStreak = 0; state.notifiedPressureKeys.clear(); state.outputLimitCompactions = 0; state.outputLimitContinuations = 0; state.outputLimitBreakerNotified = false; state.outputLimitContinuationNotified = false; state.cacheEpoch = undefined; state.cacheRatio = undefined; state.prunedDuringEpoch = undefined; state.cacheDelta = undefined; state.latestPressureSnapshot = undefined; state.loopCriticalBlocked = false; } /** * Phase 1 of a context hook: apply the stable prune transform and decide * whether pressure warrants a full compaction. Returns the messages to send. */ async function evaluateInner( messages: AgentMessage[], ctx: ExtensionContext, generation: number, ): Promise { syncActivePruneBranch(state, ctx); const settings = settingsFor(ctx); return await runPressureCycle(messages, ctx, generation, settings); } /** * The whole pressure cycle for one context hook: stable transform, new * prunes, spill escalation, status, then the compaction trigger. */ async function runPressureCycle( messages: AgentMessage[], ctx: ExtensionContext, generation: number, settings: CompactionSettings, ): Promise { publishIdleStatus(ctx, settings.enabled); if (!ctx.model) { state.latestThreshold = undefined; state.latestPressureSnapshot = undefined; clearPressureStatus(ctx); return undefined; } const linkedThreshold = await linkedThresholdFor(ctx, settings); if (generation !== state.generation) return undefined; if (!settings.enabled || !linkedThreshold.usable || linkedThreshold.contextWindow <= settings.reserveTokens) { state.latestThreshold = undefined; state.latestPressureSnapshot = undefined; if (state.pendingIntent) { state.pendingIntent = undefined; state.lastTriggerKey = undefined; persistPendingIntent(pi, state); } state.criticalLoopStreak = 0; if (state.sessionId) void cleanupSpillDir(state.sessionId, state.writerId); state.pruneManifest.clear(); state.restoredPrunes.clear(); persistPruneManifest(pi, state); clearPressureStatus(ctx); return undefined; } const capacityWindow = linkedThreshold.contextWindow; const effectiveSettings: CompactionSettings = { ...settings, reserveTokens: linkedThreshold.effectiveReserveTokens, }; state.latestThreshold = { generation, linkedThreshold, settings, effectiveSettings, }; const downgraded = await hydrateRestoredPrunes(state, messages, generation); if (state.generation !== generation) return undefined; // A dead spill path was downgraded to the plain placeholder; persist the // downgrade now so a crash before the end-of-cycle persist cannot resume // into the old entry that advertised a nonexistent file. if (downgraded) persistPruneManifest(pi, state); retainVisiblePrunes(state, messages); // The first post-prune cache bill may arrive on a final assistant-text // response, which is not a complete tool-result batch. Advance attribution // before that gate so a later tool turn cannot be misattributed to the // original prune epoch. observeCacheAttribution(state, messages, 0); if (!endsWithCompleteToolResultBatch(messages)) { // A partial frame has no pressure result. Do not let a prior complete // batch mislead the next Todo boundary in this generation. state.latestPressureSnapshot = undefined; clearPressureStatus(ctx); const stable = applyRecordedPrunes(messages, state.pruneManifest); persistPruneManifest(pi, state); return stable.prunedToolResults > 0 ? stable.messages : undefined; } const softBands: SoftPressureBands | undefined = linkedThreshold.soft ? { nudgeTokens: linkedThreshold.soft.nudgeTokens, pruneTokens: linkedThreshold.soft.pruneTokens, pruneTargetTokens: linkedThreshold.soft.pruneTargetTokens, } : undefined; const recordedBeforePolicy = new Set(state.pruneManifest.keys()); let pressure = applyContextPressurePolicy( messages, capacityWindow, effectiveSettings, state.pruneManifest, state.velocityTracker, false, softBands, linkedThreshold.thresholdTokens, ); state.velocityTracker = pressure.velocityTracker; // One-shot early warning at the nudge band, which is display-only: tell the // user output headroom or automatic pruning is approaching. if (pressure.band === "nudge" && softBands && linkedThreshold.soft) { const percent = Math.round((pressure.estimatedTokens / capacityWindow) * 100); const headroomWarning = linkedThreshold.soft.outputConstrained ? " Full-size response headroom is nearly exhausted." : ""; notifyPressureOnce( ctx, `nudge:${capacityWindow}:${softBands.nudgeTokens}:${softBands.pruneTokens}:${linkedThreshold.thresholdTokens}`, `Context is at ${percent}% (estimated ${pressure.estimatedTokens.toLocaleString("en-US")}/${capacityWindow.toLocaleString("en-US")} tokens).${headroomWarning} Automatic pruning starts at ${softBands.pruneTokens.toLocaleString("en-US")} tokens; hard compaction starts above ${linkedThreshold.thresholdTokens.toLocaleString("en-US")} tokens.`, ); } // Only entries recorded by THIS policy pass may still be escalated to // spill. Entries recorded earlier (including a prior pass whose spill // write failed) already published their replacement to the provider // prefix; upgrading them later would mutate bytes the cache treats as // stable for the rest of the epoch. const newPruneCallIds = new Set(); for (const callId of state.pruneManifest.keys()) { if (!recordedBeforePolicy.has(callId)) newPruneCallIds.add(callId); } if (pressure.prunedToolResults > 0 && state.sessionId) { const upgraded = await upgradeNewPrunesWithSpill(messages, pressure.messages, state.pruneManifest, state.sessionId, newPruneCallIds, state.writerId); // reset()/onSessionStart may have run during that await, bumping the // generation and clearing the manifest. Writing our stale view back // would resurrect prunes for a session that no longer exists. if (state.generation !== generation) return undefined; pressure = adjustPressureAfterReplacementChange( pressure, upgraded.tokenDelta, capacityWindow, effectiveSettings, softBands, ); if (pressure.band === "critical" && upgraded.callIds.size > 0) { const minimized = compactNewSpilledPrunes(pressure.messages, state.pruneManifest, upgraded.callIds); pressure = adjustPressureAfterReplacementChange( pressure, minimized.tokenDelta, capacityWindow, effectiveSettings, softBands, ); } } observeCacheAttribution(state, messages, newPruneCallIds.size); const attributedCacheDelta = state.cacheDelta; state.latestPressureSnapshot = { generation, band: pressure.band, estimatedTokens: pressure.estimatedTokens, contextWindow: capacityWindow, hardThresholdTokens: linkedThreshold.thresholdTokens, remainingToHard: Math.max(0, linkedThreshold.thresholdTokens - pressure.estimatedTokens), ...(softBands ? { nudgeTokens: softBands.nudgeTokens, pruneTokens: softBands.pruneTokens, } : {}), }; updatePressureStatus(ctx, pressure, attributedCacheDelta); state.cacheDelta = undefined; persistPruneManifest(pi, state); // The late host callback still owns this interrupted task. Pressure can // prune its input, but must not replace the obligation it will settle. if (state.zombieOwner !== undefined && state.pendingIntent && (state.pendingIntent.contextExhausted || state.pendingIntent.loopCritical || state.pendingIntent.requestBlocked)) { return pressure.prunedToolResults > 0 ? pressure.messages : undefined; } if (pressure.action !== "compact") { state.lastNoCompactableKey = undefined; state.criticalLoopStreak = 0; const interruptedIntent = state.pendingIntent?.generation === generation && (state.pendingIntent.contextExhausted || state.pendingIntent.requestBlocked || state.pendingIntent.loopCritical) ? state.pendingIntent : undefined; if (interruptedIntent) { // The request was already aborted. A smaller post-abort frame (for // example the aborted tool result after pruning) cannot turn that // recovery into an ordinary pressure intent: agent settlement still // owns compaction and continuation of the interrupted task. state.belowEscalateStreak = 0; return pressure.prunedToolResults > 0 ? pressure.messages : undefined; } state.lastTriggerKey = undefined; // Escalation: pruning ran (or found nothing) yet the estimate still sits // within 3% of the hard trigger. Queue the intent now instead of letting // the next request run against a clamped output budget — the prune band // is already pulled down to the truncation point when the output ceiling // binds, so reaching this window means pruning cannot relieve pressure. // The deferral counter must survive this path: an escalated intent is // still a live intent, so zeroing the counter on every evaluation here // would defer compaction forever — the 2-turn threshold on agent settle // could never be reached. const escalate = pressure.action === "prune" && pressure.estimatedTokens >= pressure.thresholdTokens - Math.floor(capacityWindow * OUTPUT_CLAMP_BAND_MARGIN_RATIO); if (escalate) { state.belowEscalateStreak = 0; const estimate = { ...estimateContextTokens(pressure.messages), tokens: pressure.estimatedTokens }; const triggerKey = `${estimate.tokens}:${pressure.thresholdTokens}:${messages.length}`; // A matching trigger normally avoids re-queuing identical intent. // Re-check exhaustion nevertheless: model/window changes can happen // between pressure evaluations, and the current provider request must // be aborted if it no longer fits even in the escalate branch. const contextExhausted = isContextExhausted(ctx, estimate.tokens); if (state.pendingIntent?.triggerKey !== triggerKey) { state.pendingIntent = { generation, triggerKey, recoveryId: newAutoRecoveryId(triggerKey), estimate, linkedThreshold, settings, effectiveSettings, contextExhausted, loopCritical: loopCriticalForReplacement(state.pendingIntent), }; state.lastTriggerKey = triggerKey; persistPendingIntent(pi, state); if (contextExhausted) ctx.abort(); // Dedup on the stable threshold, not the volatile triggerKey // (tokens + message count change every evaluation within a turn). notifyPressureOnce( ctx, `escalate:${pressure.thresholdTokens}`, `Context remains near the compaction threshold after pruning (${estimate.tokens.toLocaleString("en-US")}/${pressure.thresholdTokens.toLocaleString("en-US")} tokens). Compaction will run after this turn; responses may be truncated until then.`, ); } else if (contextExhausted) { ctx.abort(); } } else if (pressure.band === "critical" || pressure.band === "auto-prune") { // Still inside the prune/critical band but below the escalate window: // pruning relieved some pressure, yet the intent must survive a // single-band bounce. Zeroing the counter on every evaluation here // let an estimate oscillating around the escalate boundary defer // compaction forever — the 2-turn threshold on agent settle could // never be reached. Only drop the intent after two consecutive // below-escalate evaluations genuinely clear the band. state.belowEscalateStreak += 1; if (state.belowEscalateStreak >= 2) { state.pendingIntent = undefined; state.lastTriggerKey = undefined; persistPendingIntent(pi, state); state.highPressureDroppedTurns = 0; state.belowEscalateStreak = 0; } } else { state.pendingIntent = undefined; state.lastTriggerKey = undefined; persistPendingIntent(pi, state); // Pressure is genuinely relieved (intent dropped): reset the // consecutive-defer counter so a later fresh spike starts clean. state.highPressureDroppedTurns = 0; state.belowEscalateStreak = 0; } return pressure.prunedToolResults > 0 ? pressure.messages : undefined; } const estimate = { ...estimateContextTokens(pressure.messages), tokens: pressure.estimatedTokens }; const thresholdTokens = pressure.thresholdTokens; const triggerKey = `${estimate.tokens}:${thresholdTokens}:${messages.length}`; // Early-exit on an identical trigger key keeps the same epoch from // re-queuing the intent on every evaluation. Re-derive contextExhausted // anyway: a mid-session model/context-window switch (which does not bump // generation) can make the stored flag stale, and an actually overflowing // request must not fall through to the provider while waiting for the // settled-phase compaction owner. if (state.pendingIntent?.triggerKey === triggerKey) { const stillExhausted = isContextExhausted(ctx, estimate.tokens); if (stillExhausted) ctx.abort(); else maybeInterruptForCriticalLoop(ctx, estimate.tokens, linkedThreshold); return pressure.messages; } // The context transform owns pruning only. Full compaction is submitted // after the agent settles, where it can re-read the branch and arbitrate // with Pi's native compaction without a context-hook TOCTOU race. // ctx.model may have been replaced across the awaits above; treat a // missing window as fully exhausted so an unbounded request never slips // through to the provider. const contextExhausted = isContextExhausted(ctx, estimate.tokens); state.pendingIntent = { generation, triggerKey, recoveryId: newAutoRecoveryId(triggerKey), estimate, linkedThreshold, settings, effectiveSettings, contextExhausted, loopCritical: loopCriticalForReplacement(state.pendingIntent), }; state.lastTriggerKey = triggerKey; persistPendingIntent(pi, state); if (contextExhausted) publishRecoveryState(state.pendingIntent!, "pending"); if (contextExhausted) { // An actually overflowing request must never fall through to the // provider while waiting for the settled-phase compaction owner. ctx.abort(); } else { maybeInterruptForCriticalLoop(ctx, estimate.tokens, linkedThreshold); } return pressure.messages; } function fallbackToNativeCompaction( ctx: ExtensionContext, intent: PendingCompactionIntent, ): void { // Explicit native recovery stays exhausted-only (governance): a // loop-critical interruption still has headroom below the window and // resumes through continuation instead of unowned native recovery. if (!intent.contextExhausted || ctx.hasPendingMessages?.()) return; ctx.ui.notify( "Mid-turn compaction repeatedly failed; falling back to Pi native compaction to recover the interrupted request.", "warning", ); const fallbackNonce = ++state.nativeFallbackNonce; const fallbackGeneration = state.generation; const fallbackSessionId = state.sessionId ?? sessionIdOf(ctx); let callbackClaimed = false; const ownsFallbackCallback = (): boolean => { if (callbackClaimed || state.nativeFallbackNonce !== fallbackNonce) return false; if (state.generation < fallbackGeneration || state.generation > fallbackGeneration + 1) return false; const currentSessionId = state.sessionId ?? sessionIdOf(ctx); return currentSessionId === fallbackSessionId; }; const claimFallbackCallback = (): boolean => { if (!ownsFallbackCallback()) return false; callbackClaimed = true; // A late callback must not be able to mutate this lifecycle or wake it a // second time. onCompact intentionally leaves this nonce untouched for // the one expected generation transition. state.nativeFallbackNonce += 1; return true; }; try { // Deliberately omit an owner tag. The session_before_compact arbiter // still observes this as native, while the marker exempts the recovery // request from completed-turn threshold preservation after its original // user turn was already aborted. ctx.compact({ customInstructions: NATIVE_FALLBACK_COMPACTION_MARKER, onComplete: () => { if (!claimFallbackCallback()) return; state.breaker = resetCompactionBreaker(); state.breakerNotified = false; queueInterruptedContinuation(intent, ctx, CONTINUE_PROMPT, true); }, onError: (error) => { if (!claimFallbackCallback()) return; const message = error instanceof Error ? error.message : String(error); publishRecoveryState(intent, "failed", message); ctx.ui.notify( `Native fallback compaction failed: ${message}`, "error", ); }, }); } catch (error) { // A synchronous rejection belongs to this submission. Invalidate all // late callbacks before recording the failure. if (state.nativeFallbackNonce === fallbackNonce) state.nativeFallbackNonce += 1; const message = error instanceof Error ? error.message : String(error); publishRecoveryState(intent, "failed", message); ctx.ui.notify( `Native fallback compaction could not start: ${message}`, "error", ); } } async function settlePendingCompaction(ctx: ExtensionContext): Promise { const intent = state.pendingIntent; if (!intent || state.running || intent.generation !== state.generation) return; if (state.zombieOwner !== undefined) { // A watchdog timeout does not prove that the host operation stopped. Keep // the tombstone until its callback settles; otherwise a late callback can // race a second unknown-state compaction and wake the task twice. notifyPressureOnce( ctx, `zombie:${state.zombieOwner}`, "Mid-turn compaction held: a previously timed-out compaction may still be settling. New submissions resume once it completes or fails.", ); return; } if (!sameSettings(settingsFor(ctx), intent.settings)) { state.pendingIntent = undefined; state.lastTriggerKey = undefined; persistPendingIntent(pi, state); if (intent.requestBlocked) { ctx.ui.notify( "Request stopped because compaction settings changed while provider-pressure recovery was pending. Retry the request with the new settings.", "error", ); publishRecoveryState(intent, "failed", "compaction settings changed before recovery ran"); } else if (intent.contextExhausted || intent.loopCritical) { // An interrupted task must never lose its settlement silently. ctx.ui.notify( "Interrupted task paused: compaction settings changed before the queued compaction ran. Send any message to continue the task.", "warning", ); publishRecoveryState(intent, "failed", "compaction settings changed before recovery ran"); } return; } const clearPending = () => { if (state.pendingIntent === intent) state.pendingIntent = undefined; if (state.lastTriggerKey === intent.triggerKey) state.lastTriggerKey = undefined; persistPendingIntent(pi, state); }; const notifyBreakerPaused = () => { if (state.breakerNotified) return; state.breakerNotified = true; ctx.ui.notify( `Mid-turn compaction paused after ${state.breaker.consecutiveFailures} consecutive failures; it can retry after ${COMPACTION_BREAKER_COOLDOWN_TURNS} subsequent turns.`, "warning", ); }; // Shared post-failure continuation policy for an interrupted task // (contextExhausted / loopCritical). Breaker-tripped cycles fall back to // native compaction (exhausted) or a bare continue (loop-critical); // otherwise a retry prompt asks the model to re-run compaction first. // This policy runs only before submission or after a host callback: a // watchdog timeout cannot prove that either compaction or prompt is safe. const recoverInterruptedTask = (intent: PendingCompactionIntent): void => { if (intent.requestBlocked) return; if ((!intent.contextExhausted && !intent.loopCritical) || ctx.hasPendingMessages?.()) return; if (state.breaker.trippedAtTurn !== undefined) { notifyBreakerPaused(); if (intent.loopCritical) { state.loopCriticalBlocked = false; queueInterruptedContinuation(intent, ctx, CONTINUE_PROMPT, false); } else { fallbackToNativeCompaction(ctx, intent); } return; } state.loopCriticalBlocked = false; queueInterruptedContinuation(intent, ctx, COMPACTION_RETRY_PROMPT, false); }; const breakerCheck = compactionBreakerAllows(state.breaker, state.turnCount); if (state.breaker.trippedAtTurn !== undefined && breakerCheck.breaker.trippedAtTurn === undefined) { state.breakerNotified = false; } state.breaker = breakerCheck.breaker; if (!breakerCheck.allowed) { if (intent.requestBlocked) { ctx.ui.notify( "Request stopped because reactive compaction is paused by the compaction circuit breaker. Start a new session or retry after the cooldown.", "error", ); } else { notifyBreakerPaused(); } publishRecoveryState(intent, "failed", "compaction circuit breaker is open"); clearPending(); return; } const recoverBeforeSubmission = (error: unknown, message: string, options: { notify: boolean } = { notify: true }) => { if (intent.generation !== state.generation) return; state.breaker = recordCompactionFailure(state.breaker, state.turnCount); clearPressureStatus(ctx); if (options.notify) ctx.ui.notify( `${message}: ${error instanceof Error ? error.message : String(error)}`, intent.requestBlocked ? "error" : "warning", ); const interrupted = !intent.requestBlocked && (intent.contextExhausted || intent.loopCritical); if (!interrupted) clearPending(); recoverInterruptedTask(intent); }; let internals: PiCompactionInternals | undefined; try { internals = await loadInternals(); state.internalsWarningTurn = undefined; } catch (error) { // The host compact() path performs the same preparation check. A packaged // extension may not be able to resolve Pi's optional private module, so // keep the preflight best-effort instead of disabling compaction entirely. const shouldWarn = state.internalsWarningTurn === undefined || state.turnCount - state.internalsWarningTurn >= COMPACTION_BREAKER_COOLDOWN_TURNS; if (shouldWarn) { state.internalsWarningTurn = state.turnCount; notifyBestEffort( ctx, `Mid-turn compaction preflight unavailable; using host validation: ${error instanceof Error ? error.message : String(error)}`, "warning", ); } } if (intent.generation !== state.generation || state.running) return; const failForeignProviderPressure = (): void => { clearPending(); state.providerPressureAttempted = false; state.providerPressureBlocked = false; state.providerPressureTerminal = false; ctx.ui.notify( "Request stopped because another compaction owner was already active. Retry after that compaction completes.", "error", ); }; if (dependencies.arbiter?.currentOwner()) { if (intent.requestBlocked) failForeignProviderPressure(); else { clearPending(); // The in-flight foreign compaction will relieve pressure, but the // dropped intent is not requeued — say so instead of failing silently, // and tell the user how to resume an interrupted task. ctx.ui.notify( `Mid-turn compaction intent dropped: a ${dependencies.arbiter.currentOwner()} compaction is already active. If a task was interrupted, send any message after it completes to resume.`, "info", ); } return; } if (internals) { const branch = ctx.sessionManager.getBranch(); let preparation: unknown; try { preparation = internals.prepareCompaction(branch, intent.settings); } catch (error) { recoverBeforeSubmission(error, "Mid-turn compaction preparation failed"); return; } if (!preparation) { ctx.ui.setStatus( COMPACTION_STATUS_KEY, `CTX CRITICAL ${intent.estimate.tokens}/${intent.linkedThreshold.thresholdTokens}`, ); const noCompactableKey = `${intent.requestBlocked ? "provider-pressure" : intent.contextExhausted ? "exhausted" : intent.loopCritical ? "loop-critical" : "critical"}:${intent.linkedThreshold.thresholdTokens}:${intent.settings.keepRecentTokens}:${branch.length}`; if (state.lastNoCompactableKey !== noCompactableKey) { state.lastNoCompactableKey = noCompactableKey; if (intent.requestBlocked) { ctx.ui.notify( "Request stopped because extended thinking had no legal output budget and Pi has no compactable history. Start a new session or reduce static prompt/tool scope.", "error", ); } else if (intent.contextExhausted) { ctx.ui.notify( `Mid-turn request stopped: context is already ${intent.estimate.tokens}/${ctx.model?.contextWindow ?? intent.linkedThreshold.contextWindow} tokens and Pi has no compactable history. Start a new session or reduce static prompt/tool scope.`, "error", ); } else if (intent.loopCritical) { ctx.ui.notify( `Mid-turn loop interrupted: context is already ${intent.estimate.tokens}/${ctx.model?.contextWindow ?? intent.linkedThreshold.contextWindow} tokens and Pi has no compactable history. Start a new session or reduce static prompt/tool scope.`, "error", ); } else { ctx.ui.notify( "Mid-turn compaction skipped: Pi has no compactable history; context pressure is inside the recent keep window or static prompt overhead.", "warning", ); } } publishRecoveryState(intent, "failed", "Pi reported no compactable history"); clearPending(); return; } } state.lastNoCompactableKey = undefined; const midTurnTrigger = buildMidTurnTrigger({ estimatedTokens: intent.estimate.tokens, threshold: intent.linkedThreshold, configuredReserveTokens: intent.settings.reserveTokens, ...(intent.requestBlocked ? { recovery: "provider-pressure" as const } : {}), }); const lease = dependencies.arbiter?.request("mid-turn", midTurnTrigger); if (dependencies.arbiter && !lease) { if (intent.requestBlocked) failForeignProviderPressure(); else { clearPending(); const tombstone = dependencies.arbiter.timeoutTombstone(); ctx.ui.notify( tombstone ? `Mid-turn compaction skipped: a timed-out compaction may still be settling (~${Math.ceil(tombstone.remainingMs / 1000)}s hold left). It will retry on the next trigger.` : "Mid-turn compaction skipped: the compaction arbiter is busy. If a task was interrupted, send any message once pressure clears to resume.", "info", ); } return; } // A different producer may have announced a deferred request while another // recovery owned the parent. Re-announce only when this request actually // acquires the lease so the parent can bind the correct settlement owner. publishRecoveryState(intent, "pending"); state.running = true; state.activeRequestOwner = "mid-turn"; state.activeLease = lease; const owner = ++state.nextOwner; state.activeOwner = owner; // Persist admission before invoking the host. The intent remains attached // to the submitted owner until session_compact/onComplete/onError settles // it, so a process exit during a long summary can recover the interrupted // task instead of leaving a durable pending:null gap. if (!persistPendingIntent(pi, state)) { state.running = false; state.activeOwner = undefined; state.activeRequestOwner = undefined; state.activeLease?.release(); state.activeLease = undefined; recoverBeforeSubmission( new Error("submitted compaction state could not be persisted"), "Mid-turn compaction could not start safely", ); return; } let watchdog: NodeJS.Timeout | undefined; const disarmWatchdog = () => { if (watchdog) { clearTimeout(watchdog); watchdog = undefined; } }; const armWatchdog = () => { // The arbiter lease expires on its own, but `running` is independent of // it: if the host's compact() never settles (swallowed call, hung // summarization) neither onComplete nor onError fires and the lock sticks, // silently disabling compaction AND pruning for the rest of the session. // Mirror the lease timeout so a legitimately long summarization cannot trip. watchdog = setTimeout(() => { if (state.activeOwner !== owner) return; // The host submission is unconfirmed, not dead: remember it so a late // completion cannot race the next compaction attempt. state.zombieOwner = owner; state.zombieDeadlineMs = Date.now() + leaseTimeoutMs; state.breaker = recordCompactionFailure(state.breaker, state.turnCount); state.running = false; state.activeOwner = undefined; state.activeRequestOwner = undefined; state.lastTriggerKey = undefined; // Do not release the lease here: watchdog timeout does not prove the // host operation is dead. The late callback owns the release. clearPressureStatus(ctx); if (intent.requestBlocked) { state.providerPressureAttempted = false; state.providerPressureBlocked = false; state.providerPressureTerminal = false; ctx.ui.notify( "Reactive compaction timed out; the blocked request was not sent. Retry after the compaction settles, or reduce static prompt/tool scope.", "error", ); return; } ctx.ui.notify( "Mid-turn compaction timed out locally, but the host operation may still be active. Recovery will wait for it to complete or fail; no new prompt or compaction was submitted.", "warning", ); }, leaseTimeoutMs); watchdog.unref?.(); }; const failCompaction = (error: unknown) => { disarmWatchdog(); if (state.zombieOwner === owner) { state.zombieOwner = undefined; state.zombieDeadlineMs = undefined; state.activeLease?.release(); state.activeLease = undefined; const interrupted = !intent.requestBlocked && (intent.contextExhausted || intent.loopCritical); if (!interrupted) clearPending(); ctx.ui.notify( `The timed-out mid-turn compaction eventually failed: ${error instanceof Error ? error.message : String(error)}`, "warning", ); // The host has now settled, so retry recovery can safely submit input. if (!state.running && interrupted) recoverInterruptedTask(intent); return; } if (intent.generation !== state.generation || state.activeOwner !== owner) return; state.breaker = recordCompactionFailure(state.breaker, state.turnCount); state.running = false; state.activeOwner = undefined; state.activeRequestOwner = undefined; state.lastTriggerKey = undefined; state.activeLease?.release(); state.activeLease = undefined; const interrupted = !intent.requestBlocked && (intent.contextExhausted || intent.loopCritical); if (!interrupted) clearPending(); clearPressureStatus(ctx); ctx.ui.notify(`Mid-turn compaction failed: ${error instanceof Error ? error.message : String(error)}`, "error"); if (intent.requestBlocked) { state.providerPressureAttempted = false; state.providerPressureBlocked = false; state.providerPressureTerminal = false; return; } recoverInterruptedTask(intent); }; try { ctx.ui.setStatus(COMPACTION_STATUS_KEY, formatCompactionStatus({ owner: "mid-turn", trigger: midTurnTrigger, tokensBefore: intent.estimate.tokens, contextWindow: intent.linkedThreshold.contextWindow, configuredReserveTokens: intent.settings.reserveTokens, })); const instructions = buildMidTurnInstructions( intent.estimate, intent.linkedThreshold.contextWindow, intent.effectiveSettings.reserveTokens, ); ctx.compact({ customInstructions: state.activeLease?.tagInstructions(instructions) ?? instructions, onComplete: () => { disarmWatchdog(); if (state.zombieOwner === owner) { state.zombieOwner = undefined; state.zombieDeadlineMs = undefined; state.activeLease?.release(); state.activeLease = undefined; ctx.ui.notify("The timed-out mid-turn compaction eventually completed.", "info"); // The watchdog revoked ownership and recorded a failure, but the // compaction itself succeeded: restore success bookkeeping and // resume the interrupted task instead of stranding it. if (state.activeOwner !== owner) { if (state.running || (state.pendingIntent !== intent && state.pendingContinuationIntent !== intent)) return; if (state.lastTriggerKey === intent.triggerKey) state.lastTriggerKey = undefined; state.breaker = resetCompactionBreaker(); state.breakerNotified = false; clearPressureStatus(ctx); settleInterruptedContinuation(intent, ctx); return; } } if (state.activeOwner !== owner) return; state.breaker = resetCompactionBreaker(); state.breakerNotified = false; state.running = false; state.activeOwner = undefined; state.activeRequestOwner = undefined; state.lastTriggerKey = undefined; state.activeLease?.release(); state.activeLease = undefined; clearPressureStatus(ctx); settleInterruptedContinuation(intent, ctx); }, onError: failCompaction, }); armWatchdog(); } catch (error) { failCompaction(error); } } function queueOutputLimitWake( intent: PendingOutputLimitIntent, ctx: ExtensionContext, prompt: string, options: { clearAfterSend: boolean; publishCompleted: boolean }, ): boolean { if (state.pendingOutputLimitIntent !== intent) return false; if (ctx.hasPendingMessages?.()) { state.pendingOutputLimitIntent = undefined; cancelMatchingWake(intent.recoveryId, "a newer user message superseded output-limit recovery", ctx); persistPendingIntent(pi, state); publishOutputRecoveryState(intent, "cancelled", "a newer user message superseded output-limit recovery"); return true; } intent.phase = options.clearAfterSend ? "continuation" : "pending"; const promptKind = prompt === OUTPUT_LIMIT_RETRY_PROMPT ? "retry-compaction" : prompt === OUTPUT_LIMIT_DIRECT_CONTINUE_PROMPT ? "direct-continue" : "continue"; const recoveryId = outputRecoveryIdFor(intent); const producerGeneration = state.recoveryWake?.recoveryId === recoveryId ? state.recoveryWake.producerGeneration : intent.generation; const wake = prepareWake(recoveryId, "output-limit", producerGeneration, promptKind, prompt, ctx); if (!wake) { notifyBestEffort(ctx, "Output-limit continuation paused: durable recovery wake could not be saved.", "error"); return false; } if (options.publishCompleted) publishOutputRecoveryState(intent, "completed"); publishOutputRecoveryState(intent, "continuation"); const dispatched = ensureRecoveryWake(ctx); if (dispatched && options.clearAfterSend) state.outputLimitContinuations += 1; return dispatched; } async function settlePendingOutputLimit(ctx: ExtensionContext): Promise { const intent = state.pendingOutputLimitIntent; if (!intent || state.running || intent.generation !== state.generation) return false; if (state.zombieOwner !== undefined) { // Never submit a second output-limit compaction while the first host // callback is unknown. The callback (or an explicit lifecycle reset) is // the only proof that this owner can be released. return false; } if (!sameSettings(settingsFor(ctx), intent.settings)) { state.pendingOutputLimitIntent = undefined; cancelMatchingWake(intent.recoveryId, "compaction settings changed before output-limit recovery ran", ctx); persistPendingIntent(pi, state); publishOutputRecoveryState(intent, "cancelled", "compaction settings changed before output-limit recovery ran"); return false; } if (intent.phase === "continuation") { return queueOutputLimitWake(intent, ctx, OUTPUT_LIMIT_CONTINUE_PROMPT, { clearAfterSend: true, publishCompleted: true }); } const clearPending = (terminalState: "cancelled" | "failed", reason: string) => { if (state.pendingOutputLimitIntent === intent) state.pendingOutputLimitIntent = undefined; if (terminalState === "cancelled") cancelMatchingWake(intent.recoveryId, reason, ctx); else failMatchingWake(intent.recoveryId, reason, ctx); persistPendingIntent(pi, state); }; if (dependencies.arbiter?.currentOwner()) return true; let linkedThreshold: LinkedCompactionThresholdModel; try { linkedThreshold = await linkedThresholdFor(ctx, intent.settings); } catch (error) { if (intent.generation !== state.generation) return false; clearPending("failed", "output-limit compaction model resolution failed"); ctx.ui.notify(`Output-limit compaction model resolution failed: ${error instanceof Error ? error.message : String(error)}`, "warning"); return false; } if (intent.generation !== state.generation || state.running) return false; if (dependencies.arbiter?.currentOwner()) return true; const contextConstrained = isOutputLimitContextConstrained(intent.usage, ctx.model?.maxTokens); const usageTokens = intent.usage.tokens ?? (intent.usage.percent != null ? Math.floor((intent.usage.percent / 100) * intent.usage.contextWindow) : undefined); const linkedThresholdExceeded = linkedThreshold.usable && usageTokens !== undefined && usageTokens > linkedThreshold.thresholdTokens; if (contextConstrained === false && !linkedThresholdExceeded) { if (ctx.hasPendingMessages?.()) { clearPending("cancelled", "a newer user message superseded output-limit recovery"); publishOutputRecoveryState(intent, "cancelled", "a newer user message superseded output-limit recovery"); return true; } if (state.outputLimitContinuations >= MAX_OUTPUT_LIMIT_CONTINUATIONS) { clearPending("failed", "output-limit continuation budget exhausted"); publishOutputRecoveryState(intent, "failed", "output-limit continuation budget exhausted"); if (!state.outputLimitContinuationNotified) { state.outputLimitContinuationNotified = true; ctx.ui.notify( `Output-limit continuation stopped after ${state.outputLimitContinuations} attempts; the response keeps hitting the model output token limit. Raise maxTokens or reduce per-response size.`, "warning", ); } return true; } queueOutputLimitWake(intent, ctx, OUTPUT_LIMIT_DIRECT_CONTINUE_PROMPT, { clearAfterSend: true, publishCompleted: false }); return true; } if (state.outputLimitCompactions >= MAX_OUTPUT_LIMIT_COMPACTIONS) { clearPending("failed", "output-limit compaction budget exhausted"); publishOutputRecoveryState(intent, "failed", "output-limit compaction budget exhausted"); if (!state.outputLimitBreakerNotified) { state.outputLimitBreakerNotified = true; ctx.ui.notify( `Output-limit compaction stopped after ${state.outputLimitCompactions} attempts; the response keeps hitting the model output token limit. Raise maxTokens or reduce per-response size.`, "warning", ); } return true; } let internals: PiCompactionInternals | undefined; try { internals = await loadInternals(); state.internalsWarningTurn = undefined; } catch (error) { if (intent.generation !== state.generation) return false; const shouldWarn = state.internalsWarningTurn === undefined || state.turnCount - state.internalsWarningTurn >= COMPACTION_BREAKER_COOLDOWN_TURNS; if (shouldWarn) { state.internalsWarningTurn = state.turnCount; notifyBestEffort( ctx, `Output-limit compaction preflight unavailable; using host validation: ${error instanceof Error ? error.message : String(error)}`, "warning", ); } } if (intent.generation !== state.generation || state.running) return false; if (internals) { const branch = ctx.sessionManager.getBranch(); let preparation: unknown; try { preparation = internals.prepareCompaction(branch, intent.settings); } catch (error) { clearPending("failed", error instanceof Error ? error.message : String(error)); publishOutputRecoveryState(intent, "failed", error instanceof Error ? error.message : String(error)); ctx.ui.notify(`Output-limit compaction preparation failed: ${error instanceof Error ? error.message : String(error)}`, "warning"); return false; } if (!preparation) { clearPending("failed", "Pi reported no compactable history"); publishOutputRecoveryState(intent, "failed", "Pi reported no compactable history"); ctx.ui.notify( "Output-limit compaction skipped: Pi has no compactable history; the response keeps hitting the output token limit inside the recent keep window.", "warning", ); return false; } } const instructionReserve = linkedThreshold.usable ? linkedThreshold.effectiveReserveTokens : effectiveReserveTokens(intent.settings, ctx.model?.contextWindow ?? intent.usage.contextWindow, ctx.model?.maxTokens); const outputLimitTrigger: CompactionTrigger = { owner: "output-limit", usageTokens: intent.usage.tokens, contextWindow: intent.usage.contextWindow, usagePercent: intent.usage.percent, gateRatio: intent.threshold, }; const lease = dependencies.arbiter?.request("output-limit", outputLimitTrigger); if (dependencies.arbiter && !lease) return true; // Keep the output-limit intent durable until the callback has queued the // continuation. A compaction call can throw after accepting the request. intent.phase = "submitted"; if (!persistPendingIntent(pi, state)) { lease?.release(); return true; } publishOutputRecoveryState(intent, "pending"); state.outputLimitCompactions += 1; state.running = true; state.activeRequestOwner = "output-limit"; state.activeLease = lease; const owner = ++state.nextOwner; state.activeOwner = owner; let watchdog: NodeJS.Timeout | undefined; const disarmWatchdog = () => { if (watchdog) { clearTimeout(watchdog); watchdog = undefined; } }; const instructions = buildOutputLimitInstructions(intent.usage, instructionReserve); let callbackClaimed = false; const failOutputLimit = (error: unknown) => { if (callbackClaimed || (state.activeOwner !== owner && state.zombieOwner !== owner)) return; callbackClaimed = true; disarmWatchdog(); const timedOut = state.zombieOwner === owner; if (timedOut) { state.zombieOwner = undefined; state.zombieDeadlineMs = undefined; // The callback proves the host owner is settled; release the retained // lease now (a timed-out arbiter may already have rejected it). state.activeLease?.release(); state.activeLease = undefined; ctx.ui.notify( `The timed-out output-limit compaction eventually failed: ${error instanceof Error ? error.message : String(error)}`, "warning", ); } if (state.activeOwner === owner) { state.running = false; state.activeOwner = undefined; state.activeRequestOwner = undefined; state.activeLease?.release(); state.activeLease = undefined; } ctx.ui.notify(`Output-limit compaction failed: ${error instanceof Error ? error.message : String(error)}`, "error"); if (state.outputLimitCompactions >= MAX_OUTPUT_LIMIT_COMPACTIONS || ctx.hasPendingMessages?.()) { const superseded = Boolean(ctx.hasPendingMessages?.()); state.pendingOutputLimitIntent = undefined; if (superseded) cancelMatchingWake(intent.recoveryId, "a newer user message superseded output-limit recovery", ctx); else failMatchingWake(intent.recoveryId, "output-limit compaction budget exhausted", ctx); persistPendingIntent(pi, state); publishOutputRecoveryState( intent, superseded ? "cancelled" : "failed", superseded ? "a newer user message superseded output-limit recovery" : "output-limit compaction budget exhausted", ); return; } // Keep the intent pending while the retry steer is sent. The next // settled response will submit a fresh, known output-limit owner. intent.phase = "pending"; state.pendingOutputLimitIntent = intent; queueOutputLimitWake(intent, ctx, OUTPUT_LIMIT_RETRY_PROMPT, { clearAfterSend: false, publishCompleted: false }); }; try { ctx.compact({ customInstructions: lease?.tagInstructions(instructions) ?? instructions, onComplete: () => { if (callbackClaimed || (state.activeOwner !== owner && state.zombieOwner !== owner)) return; callbackClaimed = true; disarmWatchdog(); if (state.zombieOwner === owner) { state.zombieOwner = undefined; state.zombieDeadlineMs = undefined; state.activeLease?.release(); state.activeLease = undefined; ctx.ui.notify("The timed-out output-limit compaction eventually completed.", "info"); } if (state.activeOwner === owner) { state.running = false; state.activeOwner = undefined; state.activeRequestOwner = undefined; state.activeLease?.release(); state.activeLease = undefined; } // The callback is the proof that this owner settled; one staged wake // is enough even when the watchdog already sent a retry steer. intent.phase = "continuation"; queueOutputLimitWake(intent, ctx, OUTPUT_LIMIT_CONTINUE_PROMPT, { clearAfterSend: true, publishCompleted: true }); }, onError: failOutputLimit, }); // Same stuck-lock guard as the mid-turn path: release `running` if the // host never settles, so a future output-limit event can re-arm. watchdog = setTimeout(() => { if (state.activeOwner !== owner) return; state.zombieOwner = owner; state.zombieDeadlineMs = Date.now() + leaseTimeoutMs; state.running = false; state.activeOwner = undefined; state.activeRequestOwner = undefined; // Do not release the lease here: watchdog timeout does not prove the // host operation is dead. The late callback owns the release. ctx.ui.notify( "Output-limit compaction timed out; it will retry on the next truncated response.", "warning", ); }, leaseTimeoutMs); watchdog.unref?.(); } catch (error) { failOutputLimit(error); } return true; } return { onSessionStart(ctx, event) { state.generation += 1; const nextSessionId = sessionIdOf(ctx); // Session replacement normally creates a fresh guard. Alternate hosts may // reuse one, so a reversible switch must never destroy the parked owner. releaseInFlight(); state.pruneManifest.clear(); state.sessionId = nextSessionId; const loadedPrunes = loadPersistedPrunes( ctx, state.sessionId, isTeammateForkStartup(event?.reason), ); state.restoredPrunes = loadedPrunes.prunes; state.restoredPruneAges.clear(); state.persistedPrunes = new Map(loadedPrunes.byCallId); state.forcePruneCheckpoint = loadedPrunes.requiresCheckpoint; state.pruneDeltasSinceCheckpoint = loadedPrunes.deltasSinceCheckpoint; const branch = inspectPruneBranch(ctx); state.activeCheckpointId = branch.checkpointId; state.reachableCheckpoints = branch.reachable; touchRestoredPrunes(state, state.restoredPrunes.keys()); evictOffBranchRestoredPrunes(state); const restored = loadPersistedIntent(ctx, state.sessionId, state.generation); const restoredIntent = restored?.intent; state.recoveryWake = restored?.recoveryWake; if (state.recoveryWake && !wakeIsTerminal(state.recoveryWake)) { // Runtime generation fences the new executor; logical wake identity and // absolute deadline remain unchanged across restart. state.recoveryWake.runtimeGeneration = runtimeGeneration(); } consumedWakeAwaitingStart = undefined; dispatchedWakeThisLifecycle = undefined; // A provider-pressure intent belongs to the interrupted in-process request. // After a crash/fork there is no safe request identity to replay, so drop it; // the next genuinely invalid payload can derive a fresh intent. state.pendingContinuationIntent = restored?.phase === "continuation" ? restoredIntent : undefined; state.pendingIntent = restored?.phase === "continuation" || restoredIntent?.requestBlocked ? undefined : restoredIntent; state.pendingOutputLimitIntent = restored?.outputLimitIntent; state.persistedIntentKey = restored?.key ?? pendingIntentKey(undefined, "cleared", undefined, state.recoveryWake); if (restoredIntent?.requestBlocked || restored?.phase === "submitted" || restored?.outputLimitIntent?.phase === "pending") { // A fresh lifecycle proves the old in-process owner is gone. Convert an // uncertain submitted record back to pending before any new admission. persistPendingIntent(pi, state); } state.persistedPruneKey = pruneKey(state.persistedPrunes.values()); state.settingsSnapshot = undefined; state.settingsCwd = undefined; resetCycleState(); state.turnCount = 0; publishTeammateCompactionCapability(runtimeGeneration()); publishIdleStatus(ctx, settingsFor(ctx).enabled); if (state.recoveryWake) { if (!wakeIsTerminal(state.recoveryWake)) { persistPendingIntent(pi, state); } else { // Replayed terminal receipts are absorbing. If the process crashed // between receipt persistence and tombstone append, finish only that // cleanup; never recreate or dispatch the logical wake. publishWake(state.recoveryWake); if (state.recoveryWake.state === "turn-started") { if (state.recoveryWake.producer === "auto") { state.pendingContinuationIntent = undefined; state.pendingIntent = undefined; } else if (state.recoveryWake.producer === "output-limit") { state.pendingOutputLimitIntent = undefined; } persistPendingIntent(pi, state); } } } if (state.pendingContinuationIntent && (!state.recoveryWake || !wakeIsTerminal(state.recoveryWake))) { settleInterruptedContinuation(state.pendingContinuationIntent, ctx); } else if (state.pendingOutputLimitIntent?.phase === "continuation" && (!state.recoveryWake || !wakeIsTerminal(state.recoveryWake))) { queueOutputLimitWake( state.pendingOutputLimitIntent, ctx, OUTPUT_LIMIT_CONTINUE_PROMPT, { clearAfterSend: true, publishCompleted: true }, ); } }, onBeforeAgentStart(event, ctx) { const input = normalizeBeforeAgentStartInput(typeof event === "string" ? { prompt: event } : event); // Custom turns neither acknowledge nor supersede a user-input recovery wake. if (input.messageType !== "user") return; const prompt = input.text; const wake = state.recoveryWake; if (!wake || (wake.state !== "prepared" && wake.state !== "queued")) return; if (expireWakeIfNeeded(wake, ctx)) return; if (!branchMatchesWake(ctx, wake)) { transitionWakeTerminal(wake, "failed", "recovery wake branch checkpoint is no longer active", ctx); return; } const expectedPrompt = recoveryDeliveryPrompt(wake.prompt, wake.wakeId); if (prompt !== expectedPrompt || ctx.hasPendingMessages?.()) { transitionWakeTerminal(wake, "cancelled", "a newer user input superseded recovery wake", ctx); return; } const previous = { ...wake }; const recorded = branchMessageForWake(ctx, wake); wake.state = "consumed"; wake.sequence += 1; if (recorded?.id) wake.messageId = recorded.id; if (!persistPendingIntent(pi, state)) { Object.assign(wake, previous); dispatchedWakeThisLifecycle = undefined; notifyBestEffort(ctx, "Recovery input was observed but its consumed receipt could not be persisted; lifecycle completion is fail-closed.", "error"); return; } consumedWakeAwaitingStart = wake.wakeId; publishWake(wake); }, onAgentStart(ctx) { const wake = state.recoveryWake; if (!wake || wake.state !== "consumed" || consumedWakeAwaitingStart !== wake.wakeId) return; if (expireWakeIfNeeded(wake, ctx) || !branchMatchesWake(ctx, wake)) return; const previous = { ...wake }; wake.state = "turn-started"; wake.turnId = randomUUID(); wake.sequence += 1; if (!persistPendingIntent(pi, state)) { Object.assign(wake, previous); consumedWakeAwaitingStart = undefined; notifyBestEffort(ctx, "Recovery turn started but its correlated receipt could not be persisted; cleanup is fail-closed.", "error"); return; } consumedWakeAwaitingStart = undefined; publishWake(wake); const pendingContinuation = state.pendingContinuationIntent; const pendingOutput = state.pendingOutputLimitIntent; if (wake.producer === "auto") { state.pendingContinuationIntent = undefined; state.pendingIntent = undefined; } else if (wake.producer === "output-limit") { state.pendingOutputLimitIntent = undefined; } if (!persistPendingIntent(pi, state)) { state.pendingContinuationIntent = pendingContinuation; state.pendingOutputLimitIntent = pendingOutput; state.recoveryWake = wake; notifyBestEffort(ctx, "Recovery turn started, but the durable wake tombstone could not be appended; cleanup will retry fail-closed.", "error"); } }, ensureRecoveryWake(ctx) { return ensureRecoveryWake(ctx); }, async evaluate(messages, ctx) { const generation = state.generation; if (state.running) return undefined; return await withTransformLock(async () => { if (generation !== state.generation || state.running) return undefined; const transformed = await evaluateInner(messages, ctx, generation); if (generation === state.generation && !state.running) { state.lastProviderMessages = transformed ?? messages; } return transformed; }); }, onToolCall(ctx, parentToolCallId) { const abortNestedOuter = (): void => { if (parentToolCallId) ctx.abort(); }; // Once a tool-boundary gate is live, every subsequent tool in the same // batch must also block+terminate. A nested executor consumes terminate // locally, so abort its outer operation through the host lifecycle. if (state.loopCriticalBlocked && state.pendingIntent?.loopCritical) { abortNestedOuter(); return { block: true as const, terminate: true as const, reason: TOOL_BOUNDARY_BLOCK_REASON, }; } const currentSettings = settingsFor(ctx); const snapshot = currentSettings.enabled && state.latestThreshold?.generation === state.generation && sameSettings(currentSettings, state.latestThreshold.settings) ? state.latestThreshold : undefined; const usage = ctx.getContextUsage?.(); const usageTokens = usage?.tokens ?? (usage?.percent != null ? Math.floor((usage.percent / 100) * usage.contextWindow) : undefined); const measuredUsageTokens = finiteNumber(usage?.tokens); let intent = state.pendingIntent?.generation === state.generation ? state.pendingIntent : undefined; if (intent && (!currentSettings.enabled || !sameSettings(currentSettings, intent.settings))) { if (state.pendingIntent === intent) state.pendingIntent = undefined; if (state.lastTriggerKey === intent.triggerKey) state.lastTriggerKey = undefined; persistPendingIntent(pi, state); intent = undefined; } if (intent && measuredUsageTokens !== undefined && measuredUsageTokens >= 0 && measuredUsageTokens <= intent.linkedThreshold.thresholdTokens && !intent.contextExhausted && !intent.requestBlocked && !intent.loopCritical) { if (state.pendingIntent === intent) state.pendingIntent = undefined; if (state.lastTriggerKey === intent.triggerKey) state.lastTriggerKey = undefined; state.highPressureDroppedTurns = 0; state.belowEscalateStreak = 0; state.lastNoCompactableKey = undefined; persistPendingIntent(pi, state); intent = undefined; } const threshold = intent?.linkedThreshold ?? snapshot?.linkedThreshold; if (threshold && usageTokens !== undefined && usageTokens > threshold.thresholdTokens && (!intent || usageTokens > intent.estimate.tokens)) { const baseEstimate = state.lastProviderMessages ? estimateContextTokens(state.lastProviderMessages) : { tokens: usageTokens, usageTokens, trailingTokens: 0 }; const estimate = { ...baseEstimate, tokens: Math.max(baseEstimate.tokens, usageTokens), usageTokens: Math.max(baseEstimate.usageTokens, usageTokens), }; const settings = intent?.settings ?? snapshot?.settings; const effectiveSettings = intent?.effectiveSettings ?? snapshot?.effectiveSettings; if (settings && effectiveSettings) { const triggerKey = `tool-call:${estimate.tokens}:${threshold.thresholdTokens}`; intent = { generation: state.generation, triggerKey, recoveryId: newAutoRecoveryId(triggerKey), estimate, linkedThreshold: threshold, settings, effectiveSettings, contextExhausted: isContextExhausted(ctx, estimate.tokens), loopCritical: loopCriticalForReplacement(intent), }; state.pendingIntent = intent; state.lastTriggerKey = triggerKey; } } if (!intent || state.running || intent.requestBlocked || intent.loopCritical || intent.estimate.tokens <= intent.linkedThreshold.thresholdTokens) return undefined; // Block at the tool boundary, then let agent_settled submit through the // existing arbiter-owned compaction path. Calling compact() directly from // this hook would race the active agent loop and bypass recovery state. // Top-level calls prefer block+terminate so the host records a clean tool // error. Nested calls additionally require a real outer abort because // their executeTool boundary does not propagate terminate. if (state.zombieOwner !== undefined) return undefined; if (dependencies.arbiter?.currentOwner()) return undefined; if (dependencies.arbiter?.timeoutTombstone()) return undefined; if (!compactionBreakerAllows(state.breaker, state.turnCount).allowed) return undefined; intent.loopCritical = true; state.loopCriticalBlocked = true; persistPendingIntent(pi, state); publishRecoveryState(intent, "pending"); notifyPressureOnce( ctx, `tool-call-hard-threshold:${intent.linkedThreshold.thresholdTokens}`, `Context crossed the hard compaction threshold at a tool boundary (${intent.estimate.tokens.toLocaleString("en-US")}/${intent.linkedThreshold.thresholdTokens.toLocaleString("en-US")} tokens). Interrupting to compact before the tool runs; the task resumes automatically.`, ); abortNestedOuter(); return { block: true as const, terminate: true as const, reason: TOOL_BOUNDARY_BLOCK_REASON, }; }, async projectCompactionInput(event, ctx) { const generation = state.generation; return await withTransformLock(async () => { if (generation !== state.generation) return { event, prunedToolResults: 0 }; syncActivePruneBranch(state, ctx); const summaryCount = event.preparation.messagesToSummarize.length; const messages = [ ...event.preparation.messagesToSummarize, ...event.preparation.turnPrefixMessages, ]; const settings = settingsFor(ctx); const linkedThreshold = await linkedThresholdFor(ctx, settings); if (generation !== state.generation) return { event, prunedToolResults: 0 }; // A resumed session can reach compaction before its first context // evaluation; hydrate restored prunes so the projection replays the // persisted replacements instead of raw historical tool output. const downgraded = await hydrateRestoredPrunes(state, messages, generation); if (state.generation !== generation) return { event, prunedToolResults: 0 }; if (downgraded) persistPruneManifest(pi, state); const contextWindow = linkedThreshold.usable ? linkedThreshold.contextWindow : ctx.model?.contextWindow ?? Math.max(1, event.preparation.tokensBefore + event.preparation.settings.reserveTokens); const softBands: SoftPressureBands | undefined = linkedThreshold.usable && linkedThreshold.soft ? { nudgeTokens: linkedThreshold.soft.nudgeTokens, pruneTokens: linkedThreshold.soft.pruneTokens, pruneTargetTokens: linkedThreshold.soft.pruneTargetTokens, } : undefined; const pressure = applyContextPressurePolicy( messages, contextWindow, linkedThreshold.usable ? { ...settings, reserveTokens: linkedThreshold.effectiveReserveTokens } : settings, state.pruneManifest, state.velocityTracker, true, softBands, linkedThreshold.usable ? linkedThreshold.thresholdTokens : undefined, ); persistPruneManifest(pi, state); if (pressure.prunedToolResults === 0) { return { event, prunedToolResults: 0 }; } return { event: { ...event, preparation: { ...event.preparation, messagesToSummarize: pressure.messages.slice(0, summaryCount), turnPrefixMessages: pressure.messages.slice(summaryCount), }, }, estimatedInputTokens: pressure.messages.reduce( (tokens, message) => tokens + estimateMessageTokens(message), 0, ), prunedToolResults: pressure.prunedToolResults, }; }); }, contextEditDrafts(contextEntries) { const drafts: ContextEditEntryDraft[] = []; for (const [callId, entry] of state.pruneManifest) { if (entry.editCommitted) continue; // File-backed replacements (spill / minimal) embed a tmp path that the // journal re-validates on resume. Committing the text into the branch // would pin a possibly dead path with no downgrade left, so they stay // journaled and never become context edits. if (entry.spillPath !== undefined || entry.level === "minimal") continue; const content = "content" in entry.replacement ? entry.replacement.content : undefined; if (content === undefined) continue; // Resolve the call to the session entry that currently projects it. // Unresolvable targets (already omitted or compacted away) stay // uncommitted and keep their journal coverage. let targetId: string | undefined; let projectedMessage: AgentMessage | undefined; for (const projected of contextEntries) { const found = projected.messages.find((message) => toolResultCallId(message) === callId); if (found) { targetId = projected.sourceEntry.id; projectedMessage = found; break; } } if (!targetId || !projectedMessage) continue; // Committed is confirmed by observation: once the projection serves the // replacement bytes the edit is on the branch and journal coverage can // retire. Emitting the draft proves nothing — a later boundary handler // can still drop or replace the batch before commit. if (toolResultDigest(projectedMessage) === toolResultDigest(entry.replacement)) { entry.editCommitted = true; continue; } if ((entry.editDrafts ?? 0) >= MAX_CONTEXT_EDIT_DRAFTS) continue; entry.editDrafts = (entry.editDrafts ?? 0) + 1; drafts.push({ type: "context_edit", targetId, replacement: { content } }); } return drafts; }, async beforeProviderRequest(payload, ctx) { const guarded = disableInvalidBudgetThinking(payload); if (guarded === payload) { state.providerPressureAttempted = false; state.providerPressureBlocked = false; state.providerPressureTerminal = false; return undefined; } // The final payload is already known to be illegal. Abort before any // asynchronous model/auth lookup, then publish recovery state only if // this exact session lifecycle still owns the request. const generation = state.generation; const sessionId = sessionIdOf(ctx); const isCurrentLifecycle = () => generation === state.generation && (sessionId === undefined || state.sessionId === undefined || sessionId === state.sessionId); ctx.abort(); if (!isCurrentLifecycle()) return undefined; state.providerPressureBlocked = true; const abortWithError = (message: string): undefined => { if (isCurrentLifecycle()) { state.providerPressureTerminal = true; if (state.pendingIntent?.requestBlocked) { state.pendingIntent = undefined; state.lastTriggerKey = undefined; state.providerPressureAttempted = false; persistPendingIntent(pi, state); } } try { ctx.ui.notify(message, "error"); } catch { // The provider signal is already aborted; a broken UI sink must not // let the invalid payload continue to HTTP. } return undefined; }; if (state.pendingIntent?.requestBlocked) return undefined; if (state.providerPressureAttempted) { return abortWithError( "Request stopped after reactive compaction because extended thinking still has no legal output budget. Start a new session or reduce static prompt/tool scope.", ); } const messages = state.lastProviderMessages; if (!messages) { return abortWithError( "Request stopped before provider dispatch because extended thinking had no legal output budget and no context frame was available for safe compaction.", ); } try { const settings = settingsFor(ctx); if (!settings.enabled) { return abortWithError( "Request stopped before provider dispatch because extended thinking had no legal output budget and automatic compaction is disabled.", ); } const linkedThreshold = await linkedThresholdFor(ctx, settings); if (!isCurrentLifecycle()) return undefined; if (!linkedThreshold.usable) { return abortWithError( "Request stopped before provider dispatch because extended thinking had no legal output budget and the active model has no usable context window for compaction.", ); } const estimate = estimateContextTokens(messages); const effectiveSettings: CompactionSettings = { ...settings, reserveTokens: linkedThreshold.effectiveReserveTokens, }; const record = payload as Record; const thinking = record.thinking as Record; const triggerKey = `provider-pressure:${estimate.tokens}:${String(record.max_tokens)}:${String(thinking.budget_tokens)}:${messages.length}`; state.pendingIntent = { generation, triggerKey, recoveryId: newAutoRecoveryId(triggerKey), estimate, linkedThreshold, settings, effectiveSettings, contextExhausted: false, requestBlocked: true, }; state.lastTriggerKey = triggerKey; state.providerPressureAttempted = true; state.providerPressureTerminal = false; if (!persistPendingIntent(pi, state)) { return abortWithError( "Request stopped before provider dispatch because the reactive compaction intent could not be persisted safely.", ); } publishRecoveryState(state.pendingIntent, "pending"); if (!isCurrentLifecycle()) return undefined; notifyPressureOnce( ctx, `provider-pressure:${linkedThreshold.contextWindow}`, "Provider request paused because extended thinking had no legal output budget. Context will be compacted once, then the interrupted task will resume automatically.", ); return undefined; } catch (error) { return abortWithError( `Request stopped before provider dispatch because extended thinking had no legal output budget and reactive compaction could not be prepared: ${error instanceof Error ? error.message : String(error)}`, ); } }, shouldSkipStopHook() { return state.providerPressureBlocked || state.loopCriticalBlocked; }, isProviderPressureRecoveryActive() { return state.providerPressureBlocked; }, isSyntheticCompactionInterruptionActive() { return state.providerPressureBlocked || state.loopCriticalBlocked; }, async onAgentEnd(ctx) { const generation = state.generation; const sessionId = sessionIdOf(ctx); const isCurrentLifecycle = () => generation === state.generation && (sessionId === undefined || state.sessionId === undefined || sessionId === state.sessionId); if (!isCurrentLifecycle()) return; promoteDrainedWake(ctx); if (!isCurrentLifecycle()) return; state.turnCount += 1; // The run ended: any within-run critical-loop streak restarts fresh. state.criticalLoopStreak = 0; const outputLimitPending = await settlePendingOutputLimit(ctx); if (!isCurrentLifecycle()) return; if (!state.running && !outputLimitPending) { const pending = state.pendingIntent; const needsContinuation = pending?.contextExhausted || pending?.requestBlocked || pending?.loopCritical || Boolean(ctx.hasPendingMessages?.()); if (pending && !needsContinuation) { // A settled, non-exhausted loop may have completed the user's task. // Preserve its transcript once; if the session keeps running above the // threshold, run the deferred compaction at the second completed turn // so proactive compaction still happens before the provider clamps // outputs into truncation. The intent is kept (not cleared) so a pure // Q&A turn that never runs the pressure policy can still settle it. state.highPressureDroppedTurns += 1; if (state.highPressureDroppedTurns >= 2) { state.highPressureDroppedTurns = 0; await settlePendingCompaction(ctx); } else { state.lastNoCompactableKey = undefined; notifyPressureOnce( ctx, `defer:${pending.linkedThreshold.thresholdTokens}`, `Context is above the compaction threshold (${pending.estimate.tokens.toLocaleString("en-US")}/${pending.linkedThreshold.thresholdTokens.toLocaleString("en-US")} tokens). Compaction will run after the next completed turn; responses may be truncated until then.`, ); } } else { state.highPressureDroppedTurns = 0; await settlePendingCompaction(ctx); } } if (!isCurrentLifecycle()) return; if (!state.running && !state.pendingIntent && state.providerPressureBlocked) { state.providerPressureAttempted = false; state.providerPressureBlocked = false; state.providerPressureTerminal = false; } if (!state.running && !state.pendingIntent && state.loopCriticalBlocked) { state.loopCriticalBlocked = false; } if (!state.running && !state.pendingIntent && !state.pendingOutputLimitIntent) { state.settingsSnapshot = undefined; publishIdleStatus(ctx, settingsFor(ctx).enabled); if (!state.lastNoCompactableKey) clearPressureStatus(ctx); } }, async onOutputLimit(messages, ctx) { const generation = state.generation; const settings = settingsFor(ctx); const finalStopReason = finalAssistantStopReason(messages); if (!settings.enabled || !ctx.model || finalStopReason !== "length") { const superseded = state.pendingOutputLimitIntent; state.pendingOutputLimitIntent = undefined; if (superseded) { cancelMatchingWake(superseded.recoveryId, "a non-truncated turn superseded output-limit recovery", ctx); publishOutputRecoveryState(superseded, "cancelled", "a non-truncated turn superseded output-limit recovery"); } persistPendingIntent(pi, state); state.outputLimitCompactions = 0; state.outputLimitContinuations = 0; state.outputLimitBreakerNotified = false; state.outputLimitContinuationNotified = false; return; } if (generation !== state.generation) return; if (ctx.hasPendingMessages?.()) { const superseded = state.pendingOutputLimitIntent; state.pendingOutputLimitIntent = undefined; if (superseded) { cancelMatchingWake(superseded.recoveryId, "a newer user message superseded output-limit recovery", ctx); publishOutputRecoveryState(superseded, "cancelled", "a newer user message superseded output-limit recovery"); } persistPendingIntent(pi, state); return; } // A length stop means the assistant response was truncated at the model // output token limit regardless of context pressure. Recovery must NOT be // gated on high context usage: truncation is an output-side problem that // occurs at any context size, and skipping recovery here leaves the // truncated response incomplete with no continuation. settlePendingOutputLimit // still fails safely when there is no compactable history. const usage = ctx.getContextUsage?.() ?? { percent: null, tokens: null, contextWindow: ctx.model?.contextWindow ?? 0 }; const outputLimitIntent: PendingOutputLimitIntent = { generation, recoveryId: `${state.sessionId ?? "unknown"}:output-limit:${randomUUID()}`, phase: "pending", settings, usage: { ...usage }, threshold: settings.soft?.pruneRatio ?? DEFAULT_OUTPUT_LIMIT_RATIO, }; state.pendingOutputLimitIntent = outputLimitIntent; persistPendingIntent(pi, state); publishOutputRecoveryState(outputLimitIntent, "pending"); }, onCompact(completedOwner, ctx) { const eventSessionId = ctx ? sessionIdOf(ctx) : undefined; if (eventSessionId !== undefined && state.sessionId !== undefined && eventSessionId !== state.sessionId) { return false; } state.generation += 1; // session_compact fires before ctx.compact() invokes onComplete/onError. // Keep zombie ownership until that callback can reconcile a late result. const activeRequestOwner = state.activeRequestOwner; const wasPreempted = activeRequestOwner !== undefined && completedOwner !== undefined && completedOwner !== activeRequestOwner; const preserveOutputLimitBreaker = !wasPreempted && activeRequestOwner === "output-limit"; const outputLimitCompactions = state.outputLimitCompactions; const outputLimitBreakerNotified = state.outputLimitBreakerNotified; const outputLimitIntent = state.pendingOutputLimitIntent; const continuationIntent = state.pendingIntent && !state.pendingIntent.requestBlocked && (state.pendingIntent.contextExhausted || state.pendingIntent.loopCritical) ? state.pendingIntent : state.pendingContinuationIntent; // Native/foreign compaction has no owned callback, so it delivers now. // An owned callback keeps the same durable continuation until onComplete. const settledIntent = wasPreempted || (activeRequestOwner === undefined && state.zombieOwner === undefined) ? continuationIntent : undefined; if (wasPreempted) releaseInFlight(); if (state.sessionId) void cleanupSpillDir(state.sessionId, state.writerId); state.pruneManifest.clear(); state.restoredPrunes.clear(); state.restoredPruneAges.clear(); state.persistedPrunes.clear(); state.forcePruneCheckpoint = true; state.pruneDeltasSinceCheckpoint = 0; state.persistedPruneKey = undefined; state.pendingIntent = undefined; state.pendingContinuationIntent = continuationIntent && !ctx?.hasPendingMessages?.() ? continuationIntent : undefined; state.pendingOutputLimitIntent = outputLimitIntent && !ctx?.hasPendingMessages?.() ? Object.assign(outputLimitIntent, { generation: state.generation, phase: preserveOutputLimitBreaker ? "submitted" : "continuation" }) : undefined; state.lastTriggerKey = undefined; state.lastNoCompactableKey = undefined; resetCycleState(); if (preserveOutputLimitBreaker) { state.outputLimitCompactions = outputLimitCompactions; state.outputLimitBreakerNotified = outputLimitBreakerNotified; } persistPruneManifest(pi, state); if (!state.pendingContinuationIntent && continuationIntent && ctx) { cancelMatchingWake(recoveryIdFor(continuationIntent), "a newer user message superseded compaction recovery", ctx); publishRecoveryState(continuationIntent, "cancelled", "a newer user message superseded compaction recovery"); } if (!state.pendingOutputLimitIntent && outputLimitIntent) { if (ctx) cancelMatchingWake(outputLimitIntent.recoveryId, "a newer user message superseded output-limit recovery", ctx); publishOutputRecoveryState(outputLimitIntent, "cancelled", "a newer user message superseded output-limit recovery"); } persistPendingIntent(pi, state); if (settledIntent && ctx) settleInterruptedContinuation(settledIntent, ctx); return true; }, onSessionShutdown(ctx) { // Retry a receipt-driven cleanup that previously failed to append. The // terminal receipt remains absorbing while the obligation stays attached. const terminalWake = state.recoveryWake; if (terminalWake?.state === "turn-started") { const pendingContinuation = state.pendingContinuationIntent; const pendingOutput = state.pendingOutputLimitIntent; if (terminalWake.producer === "auto") { state.pendingContinuationIntent = undefined; state.pendingIntent = undefined; } else if (terminalWake.producer === "output-limit") { state.pendingOutputLimitIntent = undefined; } if (!persistPendingIntent(pi, state)) { state.pendingContinuationIntent = pendingContinuation; state.pendingOutputLimitIntent = pendingOutput; } } // Non-destructive: a normal shutdown must not tombstone the prune // manifest. Release in-flight work and persist the current state so a // resume replays the identical transformed prefix; session-scoped spill // resources stay on disk for the bounded resume lifetime. reset() and // onCompact() remain the paths that clear manifests and delete files. state.generation += 1; const parkedIntent = state.pendingIntent?.requestBlocked ? undefined : state.pendingIntent; const parkedContinuation = state.pendingContinuationIntent; const parkedOutputLimit = state.pendingOutputLimitIntent; const parkedRecoveryWake = state.recoveryWake; releaseInFlight(); state.pendingIntent = parkedIntent ? { ...parkedIntent, generation: state.generation } : undefined; state.pendingContinuationIntent = parkedContinuation ? { ...parkedContinuation, generation: state.generation } : undefined; state.pendingOutputLimitIntent = parkedOutputLimit ? { ...parkedOutputLimit, generation: state.generation, phase: parkedOutputLimit.phase === "continuation" ? "continuation" : "pending" } : undefined; state.recoveryWake = parkedRecoveryWake; persistPruneManifest(pi, state); persistPendingIntent(pi, state); if (ctx) clearPressureStatus(ctx); }, reset(ctx) { state.generation += 1; const cancelledWake = state.recoveryWake && !wakeIsTerminal(state.recoveryWake) ? Object.assign(state.recoveryWake, { state: "cancelled" as const, sequence: state.recoveryWake.sequence + 1, reason: "compaction guard reset", }) : state.recoveryWake; if (cancelledWake?.state === "cancelled" && persistPendingIntent(pi, state)) publishWake(cancelledWake); releaseInFlight(); state.recoveryWake = cancelledWake; if (state.sessionId) void cleanupSpillDir(state.sessionId, state.writerId); state.pruneManifest.clear(); state.restoredPrunes.clear(); state.restoredPruneAges.clear(); state.persistedPrunes.clear(); state.forcePruneCheckpoint = true; state.pruneDeltasSinceCheckpoint = 0; state.persistedPruneKey = undefined; state.settingsSnapshot = undefined; state.settingsCwd = undefined; resetCycleState(); state.turnCount = 0; persistPruneManifest(pi, state); persistPendingIntent(pi, state); if (ctx) { clearPressureStatus(ctx); ctx.ui.setStatus(COMPACTION_MODE_STATUS_KEY, undefined); } }, refreshSettings() { state.settingsSnapshot = undefined; }, hasPendingTakeover() { return state.pendingIntent !== undefined || state.pendingOutputLimitIntent !== undefined; }, shouldRetainNativeThreshold(ctx) { const pending = state.pendingIntent; if (!pending || pending.generation !== state.generation || pending.contextExhausted || pending.requestBlocked || pending.loopCritical) return false; const contextWindow = ctx.model?.contextWindow; if (contextWindow === undefined) return false; const usage = ctx.getContextUsage?.(); const usageTokens = usage?.tokens ?? (usage?.percent != null ? Math.floor((usage.percent / 100) * usage.contextWindow) : undefined); const currentTokens = Math.max(pending.estimate.tokens, usageTokens ?? 0); return currentTokens >= Math.max(0, contextWindow - OUTPUT_CLAMP_SAFETY_TOKENS); }, describeBreakerPause() { return describeCompactionBreakerPause(state.breaker, state.turnCount); }, describeState(): MidTurnCompactionStatus { return { turnCount: state.turnCount, running: state.running, pressureSnapshot: state.latestPressureSnapshot ? { ...state.latestPressureSnapshot } : undefined, activeRequestOwner: state.activeRequestOwner, zombieOwner: state.zombieOwner, pendingIntent: state.pendingIntent ? { tokens: state.pendingIntent.estimate.tokens, thresholdTokens: state.pendingIntent.linkedThreshold.thresholdTokens, contextExhausted: state.pendingIntent.contextExhausted, requestBlocked: state.pendingIntent.requestBlocked === true, loopCritical: state.pendingIntent.loopCritical === true, } : undefined, outputLimitIntentPending: state.pendingOutputLimitIntent !== undefined, breaker: { consecutiveFailures: state.breaker.consecutiveFailures, trippedAtTurn: state.breaker.trippedAtTurn, cooldownRemainingTurns: compactionBreakerCooldownRemaining(state.breaker, state.turnCount), }, }; }, }; } // Threshold derivation lives in compaction-threshold.ts (pure, neutral). The // runtime helpers are re-exported so existing consumers keep their imports. export { MIN_RESERVE_RATIO, deriveCompactionThreshold, deriveLinkedCompactionThreshold, effectiveReserveTokens, summaryOutputTokenLimit, } from "./compaction-threshold.ts"; export type { CompactionThresholdDerivation, CompactionThresholdInput, CompactionThresholdModel, CompactionThresholdReason, LinkedCompactionThresholdInput, LinkedCompactionThresholdModel, SoftThresholdDerivation, UnusableContextThreshold, } from "./compaction-threshold.ts"; export function isOutputLimitContextConstrained( usage: ContextUsage, modelMaxTokens: number | undefined, ): boolean | undefined { if (typeof modelMaxTokens !== "number" || !Number.isFinite(modelMaxTokens) || modelMaxTokens <= 0) { return undefined; } const contextWindow = usage.contextWindow; if (!Number.isFinite(contextWindow) || contextWindow <= 0) return undefined; const usageTokens = usage.tokens ?? (usage.percent != null ? Math.floor((usage.percent / 100) * contextWindow) : undefined); if (usageTokens === undefined || !Number.isFinite(usageTokens) || usageTokens < 0) return undefined; const availableOutputTokens = Math.max(0, contextWindow - usageTokens - OUTPUT_CLAMP_SAFETY_TOKENS); return availableOutputTokens < modelMaxTokens; } export function applyContextPressurePolicy( messages: AgentMessage[], contextWindow: number, settings: CompactionSettings, pruneManifest: PruneManifest = new Map(), velocityTracker: VelocityTracker = EMPTY_VELOCITY_TRACKER, compactionPending = false, softBands?: SoftPressureBands, /** * Derived trigger threshold (summary-reserve B1 and self-hosting B2 applied). * Undefined keeps the legacy `window - reserve` formula so direct callers and * older tests keep byte-identical behavior. */ thresholdOverrideTokens?: number, ): ContextPressureResult { const thresholdTokens = thresholdOverrideTokens ?? (contextWindow - settings.reserveTokens); const applied = applyRecordedPrunes(messages, pruneManifest); // applyRecordedPrunes hands back the caller's array untouched when nothing is // recorded. New pruning writes elements in place, so it copies first (below) // — the input array is never mutated. let transformed = applied.messages; let savedTokens = applied.savedTokens; let prunedToolResults = applied.prunedToolResults; // A prune introduced after the latest successful provider usage is still // pending acknowledgement. Its saved tokens must remain deducted until a // later provider response establishes a new usage epoch. const initial = Math.max(0, estimateContextTokens(transformed).tokens - applied.pendingSavedTokens); const soft = settings.soft ?? DEFAULT_SOFT_COMPACTION; // Payload byte limit runs independently of token pressure — a session can // sit far below the token nudge band while its base64 payload already trips // a transport cap. When configured (opt-in; undefined = no ceiling), check // it FIRST so any token-pressure path (low or high) honors the byte budget: // reclaim oldest-first, and if nothing reclaimable exists (all oversize in // the protected current user message) escalate to a full compaction instead // of letting the request hit the wire. if (settings.enabled && settings.payloadLimitBytes !== undefined && !compactionPending) { const limitBytes = settings.payloadLimitBytes; const initialBytes = estimatePayloadBytes(transformed); if (initialBytes > limitBytes) { // Candidates are toolResult messages only (enforced inside // runPayloadLimitPrune), so the live user instruction — including any // images the user just pasted — is never a prune target by construction. // No positional frontier here: in a single-turn session the only user // message sits at index 0, and a frontier at that index would shield // every tool result behind it (exactly the read-20-screenshots // accumulation this guard exists to relieve). const payloadResult = runPayloadLimitPrune({ messages: transformed, pruneManifest, frontierStart: transformed.length, limitBytes, }); const noVelocity: VelocityInfo = { slope: undefined, robustGrowth: false, epochsToCritical: undefined }; if (payloadResult.pruned) { transformed = payloadResult.transformed; prunedToolResults += payloadResult.replaced; savedTokens += payloadResult.savedTokens; const reason = `payload-bytes:${(initialBytes / (1024 * 1024)).toFixed(1)}MB>${(limitBytes / (1024 * 1024)).toFixed(1)}MB`; const band: ContextPressureBand = "normal"; const estimate = Math.max(0, estimateContextTokens(transformed).tokens); return { messages: transformed, band, estimatedTokens: estimate, thresholdTokens, prunedToolResults, savedTokens, action: "none", reasons: [reason], velocityTracker: EMPTY_VELOCITY_TRACKER, velocity: noVelocity, } as ContextPressureResult; } // Nothing reclaimable before the protected boundary: every oversized // image lives inside the live user instruction. Pruning cannot help; // trigger a full compaction (like a manual /compact) so the summarizer // folds the current instruction and the next request fits the limit. return { messages: transformed, band: "normal", estimatedTokens: initial, thresholdTokens, prunedToolResults, savedTokens, action: "compact", reasons: [`payload-protected:${(initialBytes / (1024 * 1024)).toFixed(1)}MB`], velocityTracker: EMPTY_VELOCITY_TRACKER, velocity: noVelocity, } as ContextPressureResult; } } const criticalRatio = thresholdTokens / contextWindow; const initialRatio = initial / contextWindow; const initiallyCritical = initial > thresholdTokens; const nudgeTokens = softBands?.nudgeTokens ?? Math.ceil(contextWindow * soft.nudgeRatio); const pruneTokens = softBands?.pruneTokens ?? Math.ceil(contextWindow * soft.pruneRatio); // Velocity is sampled on the pre-new-prune effective estimate so recorded // prunes stay accounted for and pruning does not create false slopes. Off by // default: the tracker is untouched and escalation never fires when disabled. let nextTracker = velocityTracker; let velocity: VelocityInfo = { slope: undefined, robustGrowth: false, epochsToCritical: undefined }; let velocityEscalate = false; if (settings.enabled && soft.velocity?.enabled) { const observed = observeVelocity(velocityTracker, { epoch: latestProviderUsageEpoch(messages), tokens: initial }); nextTracker = observed.tracker; velocity = buildVelocityInfo(observed, thresholdTokens - initial); velocityEscalate = shouldVelocityEscalate(velocity, soft, initialRatio); } if (!settings.enabled || (!compactionPending && !initiallyCritical && !velocityEscalate && (!soft.enabled || initial < nudgeTokens))) { return pressureResult({ messages: transformed, band: "normal", estimatedTokens: initial, contextWindow, thresholdTokens, prunedToolResults, savedTokens, velocityTracker: nextTracker, velocity }); } if (!compactionPending && soft.enabled && !initiallyCritical && !velocityEscalate && initial < pruneTokens) { return pressureResult({ messages: transformed, band: "nudge", estimatedTokens: initial, contextWindow, thresholdTokens, prunedToolResults, savedTokens, velocityTracker: nextTracker, velocity }); } // The soft layer gates only NEW pruning. Recorded prunes above stay applied so // the request prefix remains stable within the compaction epoch. // Invariant: .workflow/specs/architecture-constraints.md S-20260724-cbhh // (compaction prune invariants) and coding-conventions.md (same-epoch // identical-replacement rule). Three prior sessions paid for this guarantee — // see the prefix-stability tests before weakening it. let newlySavedTokens = 0; let cacheGateActive = false; // DEF-004: declared outside the `soft.enabled` block so the final return can // surface the reason. Set inside the block when relevance is enabled but // no usable query can be derived (no user message, or un-tokenizable query). let relevanceDisabledReason: string | undefined = undefined; if (soft.enabled) { if (transformed === messages) transformed = [...messages]; const frontierStart = protectedFrontierStart(transformed, settings.keepRecentTokens); const pruneTarget = Math.min(thresholdTokens, softBands?.pruneTargetTokens ?? Math.floor(contextWindow * soft.pruneTargetRatio)); const usageEpoch = latestProviderUsageEpoch(messages); const cacheHitRatioValue = latestCacheHitRatio(messages); // Time-based cache-cold detection: when the gap since the last main-loop // assistant message exceeds the threshold, the provider's prompt cache has // almost certainly expired and the whole prefix will be rewritten anyway — // pruning then costs nothing extra, so the cache gate is bypassed. const cacheCold = soft.timeBased?.enabled === true && isCacheColdByTime(messages, soft.timeBased.gapThresholdMinutes); // Cache gate: a prune kills the provider's cached prefix from its index to // the end, so below the critical band a prune run must reclaim enough tokens // to pay for the prefix it invalidates. Past critical, relieving pressure // dominates — a full compaction would invalidate everything anyway plus cost // an LLM call — so the gate is bypassed and every candidate is applied. cacheGateActive = soft.cache.enabled && !initiallyCritical && !compactionPending && !cacheCold && cacheHitRatioValue !== undefined; const suffixTokens = cacheGateActive ? suffixTokenSums(transformed) : undefined; // Dynamic savings gate: a hot cache (high hit ratio) makes every mutation // more expensive, so the minimum savings required to pay for an // invalidated prefix rises with the observed hit ratio; a cold cache // lowers it. The fixed 25% baseline is kept as the mid-point. const minPruneRatio = cacheGateActive && cacheHitRatioValue !== undefined ? dynamicPruneMinRatio(cacheHitRatioValue, soft.cache.minRatioRange) : CACHE_PRUNE_MIN_SAVINGS_RATIO; // Build the exact ungated transform plan first. When the gate is active the // plan runs on copies, then one cumulative economics decision is applied to // the real messages/manifest. This lets savings combine across lossless, // replayable, and bulk tiers without publishing a partial prefix first. const planningMessages = cacheGateActive ? [...transformed] : transformed; const planningManifest = cacheGateActive ? new Map(pruneManifest) : pruneManifest; const plannedCandidates: PruneCandidate[] = []; let planSavedTokens = 0; let planPrunedToolResults = 0; const relevanceQuery = soft.relevance?.enabled === true ? latestRelevanceQuery(messages) : ""; // DEF-004: when relevance is enabled but no usable query can be derived // (no user message in the tail, or the query is non-empty but un-tokenizable), // ranking is disabled and eviction falls back to the default newest-first walk. // Surface this as a compaction reason so the disablement is observable and not // indistinguishable from relevance being off by configuration. relevanceDisabledReason = soft.relevance?.enabled === true && !relevanceQuery.trim() ? "relevance-enabled-no-query" : (soft.relevance?.enabled === true && tokenizeRelevance(relevanceQuery, RELEVANCE_MAX_QUERY_TOKENS).length === 0 ? "relevance-enabled-untokenizable-query" : undefined); const rankCandidates = relevanceQuery ? (candidates: PruneCandidate[]) => rankPruneCandidates( candidates, relevanceQuery, soft.relevance?.mode ?? "bm25", ) : undefined; // Reference targets of recorded cross-turn dedup pointers must stay in // context (the pointer's original is recovered from them). Lossy passes // therefore skip them. const dedupProtected = new Set(); for (const entry of planningManifest.values()) { if (entry.level === "dedup" && entry.refCallId) dedupProtected.add(entry.refCallId); } // Tier 0 — lossless folding first (reversible, no information loss): it // shrinks output without a decision, and whatever it saves lowers the // pressure so later lossy passes may not even need to run. let effectiveForLossy = initial; if (soft.lossless?.enabled ?? true) { const lossless = runPrunePass({ transformed: planningMessages, pruneManifest: planningManifest, frontierStart, pruneTarget, effectiveTokens: initial, usageEpoch, selector: tryLosslessFold, rankCandidates, level: "lossless", }); plannedCandidates.push(...lossless.candidates); planSavedTokens += lossless.savedTokens; planPrunedToolResults += lossless.prunedToolResults; effectiveForLossy = lossless.effectiveTokens; } // Tier 0.5 — cross-turn verbatim de-duplication (default off). Replaces // later spans that already appeared verbatim in an earlier tool output // with an in-context pointer; reference targets are protected above. if (soft.crossTurnDedup?.enabled === true) { const dedup = runDedupPass({ transformed: planningMessages, pruneManifest: planningManifest, frontierStart, effectiveTokens: effectiveForLossy, minLines: soft.crossTurnDedup.minLines, minChars: soft.crossTurnDedup.minChars, dedupProtected, }); plannedCandidates.push(...dedup.candidates); planSavedTokens += dedup.savedTokens; planPrunedToolResults += dedup.prunedToolResults; effectiveForLossy = dedup.effectiveTokens; } // Graduated eviction, cheapest/most-reversible first: pass 1 strips replayable // tools (re-runnable); pass 2 strips bulk data tools (bash/edit/write) only if // pressure persists. Control tools (e.g. todo) are in neither set and survive. const replayable = runPrunePass({ transformed: planningMessages, pruneManifest: planningManifest, frontierStart, pruneTarget, effectiveTokens: effectiveForLossy, usageEpoch, selector: replaceableToolResult, rankCandidates, protectedCallIds: dedupProtected, }); plannedCandidates.push(...replayable.candidates); planSavedTokens += replayable.savedTokens; planPrunedToolResults += replayable.prunedToolResults; const bulk = runPrunePass({ transformed: planningMessages, pruneManifest: planningManifest, frontierStart, pruneTarget, effectiveTokens: replayable.effectiveTokens, usageEpoch, selector: evictableBulkToolResult, rankCandidates, protectedCallIds: dedupProtected, }); plannedCandidates.push(...bulk.candidates); planSavedTokens += bulk.savedTokens; planPrunedToolResults += bulk.prunedToolResults; if (cacheGateActive && suffixTokens) { const depth = cacheWorthwhileDepth(plannedCandidates, suffixTokens, minPruneRatio); const appliedPlan = applyPruneCandidates( transformed, pruneManifest, plannedCandidates, depth, initial, usageEpoch, ); newlySavedTokens += appliedPlan.savedTokens; savedTokens += appliedPlan.savedTokens; prunedToolResults += appliedPlan.prunedToolResults; } else { newlySavedTokens += planSavedTokens; savedTokens += planSavedTokens; prunedToolResults += planPrunedToolResults; } } const estimatedTokens = Math.max(0, initial - newlySavedTokens); const ratio = estimatedTokens / contextWindow; const band = derivePressureBand({ ratio, criticalRatio, prunedToolResults, soft, softBands: softBands !== undefined ? { nudgeRatio: nudgeTokens / contextWindow, pruneRatio: pruneTokens / contextWindow } : undefined, }); const result = pressureResult({ messages: transformed, band, estimatedTokens, contextWindow, thresholdTokens, prunedToolResults, savedTokens, velocityTracker: nextTracker, velocity }); const withRelevanceReason = relevanceDisabledReason !== undefined ? { ...result, reasons: [...result.reasons, relevanceDisabledReason] } : result; if (cacheGateActive && newlySavedTokens === 0 && !initiallyCritical) { return { ...withRelevanceReason, action: "none", reasons: [...withRelevanceReason.reasons, "cache-veto"] }; } return withRelevanceReason; } function applyRecordedPrunes(messages: AgentMessage[], pruneManifest: PruneManifest): AppliedPrunes { // Nothing recorded means nothing to replace; the copy would be identical and // the caller discards it. This is the common path early in a session and // immediately after every compaction or reset. if (pruneManifest.size === 0) { return { messages, prunedToolResults: 0, savedTokens: 0, pendingSavedTokens: 0 }; } const transformed = [...messages]; let savedTokens = 0; let prunedToolResults = 0; let pendingSavedTokens = 0; const usageEpoch = latestProviderUsageEpoch(messages); for (let index = 0; index < transformed.length; index++) { const callId = toolResultCallId(transformed[index]); if (!callId) continue; const recorded = getRecordedPrune(pruneManifest, callId); if (!recorded && !hasRecordedPrune(pruneManifest, callId)) continue; if (recorded?.contentDigest && recorded.contentDigest !== toolResultDigest(transformed[index])) continue; const replacement = recorded?.replacement ?? pruneToolResult(transformed[index]); if (!replacement) continue; const saved = recorded?.savedTokens ?? estimateMessageTokens(transformed[index]) - estimateMessageTokens(replacement); if (saved <= 0) continue; transformed[index] = replacement; savedTokens += saved; prunedToolResults++; if (recorded?.introducedAtUsageEpoch === usageEpoch) pendingSavedTokens += saved; } return { messages: transformed, prunedToolResults, savedTokens, pendingSavedTokens }; } /** Durable projection of a live manifest entry, shared by persist and parking. */ function persistedEntryOf(callId: string, entry: PruneManifestEntry | undefined): PersistedPruneEntry { return { callId, level: entry?.level ?? (entry?.spillPath !== undefined ? "spill" : "pruned"), ...(entry?.spillPath !== undefined ? { spillPath: entry.spillPath } : {}), ...(entry?.spillContentDigest !== undefined ? { spillContentDigest: entry.spillContentDigest } : {}), ...(entry?.introducedAtUsageEpoch !== undefined ? { introducedAtUsageEpoch: entry.introducedAtUsageEpoch } : {}), ...(entry?.contentDigest !== undefined ? { contentDigest: entry.contentDigest } : {}), ...(entry?.writerId !== undefined ? { writerId: entry.writerId } : {}), ...(entry?.checkpointId !== undefined ? { checkpointId: entry.checkpointId } : {}), ...(entry?.refCallId !== undefined ? { refCallId: entry.refCallId } : {}), ...(entry?.level === "dedup" && entry.replacement !== undefined ? { replacementText: extractTextContent(entry.replacement) } : {}), }; } /** * Drop manifest entries whose tool result is not in the current window, parking * their identity in `parked` so the prune survives. * * A tool result can be invisible for one frame and come back — a branch switch, * a rewind/fork, a resume that evaluates a partial window. Deleting outright * made that permanent: the persisted fallback has already been consumed by * hydrateRestoredPrunes, so applyRecordedPrunes finds nothing and re-emits the * ORIGINAL full text. That both blows the context back up and invalidates the * whole cached prefix at that index. */ function retainVisiblePrunes( state: AutoCompactionState, messages: AgentMessage[], ): void { if (state.pruneManifest.size === 0) return; const visible = new Set(); for (const message of messages) { const callId = toolResultCallId(message); if (callId) visible.add(callId); } for (const callId of state.pruneManifest.keys()) { if (visible.has(callId)) continue; const entry = getRecordedPrune(state.pruneManifest, callId); if (entry && !entry.checkpointId) entry.checkpointId = state.activeCheckpointId; parkRestoredPrune(state, persistedEntryOf(callId, entry)); state.pruneManifest.delete(callId); } evictOffBranchRestoredPrunes(state); } function hasRecordedPrune(manifest: PruneManifest, callId: string): boolean { return manifest.has(callId); } function getRecordedPrune(manifest: PruneManifest, callId: string): PruneManifestEntry | undefined { return manifest.get(callId); } function isPruneLevel(value: unknown): value is PruneLevel { return value === "pruned" || value === "spill" || value === "minimal" || value === "lossless" || value === "dedup" || value === "payload"; } /** * Record a NEW prune. Re-recording an existing callId is refused: the stored * replacement is what applyRecordedPrunes re-applies on every request, so * overwriting it with a freshly computed one would change the request prefix * each turn and thrash the prompt cache — with every existing test still green. * The two legitimate in-place escalations (spill, minimal) mutate the entry * they already own rather than going through here. */ function recordPrune(manifest: PruneManifest, callId: string, entry: PruneManifestEntry): void { if (manifest.has(callId)) return; manifest.set(callId, entry); } function restorePruneReplacement(message: AgentMessage, persisted: PersistedPruneEntry): AgentMessage | undefined { if (persisted.level === "pruned") return pruneToolResult(message); if (persisted.level === "dedup") { // The frozen pointer text is persisted alongside the entry so a resumed // request replays byte-identical bytes. Only when it is missing (an older // build) do we degrade: the referenced original is still in context, so // the plain placeholder loses no information, only brevity. if (persisted.replacementText) { return { ...message, content: [{ type: "text", text: persisted.replacementText }] } as AgentMessage; } return pruneToolResult(message); } if (persisted.level === "lossless") { // Folding is a deterministic pure function of the content: recompute is // recovery. If the message changed (digest mismatch is checked upstream) // or folding no longer gains, fall back to the ordinary prune path. return tryLosslessFold(message) ?? pruneToolResult(message); } if (persisted.level === "minimal") { if (!persisted.spillPath) return undefined; return minimalSpillReplacement(message, persisted.spillPath); } if (persisted.level === "payload") { // Image replacement is deterministic (imagePruneReplacement); availability // of the original does not change the text. Recompute is recovery. return imagePruneReplacement(message); } // hydrateRestoredPrunes validates spill liveness and downgrades dead paths // to level "pruned" before reaching here, so a persisted spill entry with no // path (cleaned tmpdir, failed write persisted by an older build) degrades // to the plain placeholder rather than the pathless spill text, which would // carry a 1.5K preview for no recoverability. if (!persisted.spillPath) return pruneToolResult(message); const text = extractTextContent(message); if (text.length < SPILL_THRESHOLD_CHARS) return undefined; const toolName = typeof (message as MessageRecord).toolName === "string" ? (message as MessageRecord).toolName as string : "tool"; const preview = generatePreview(text, SPILL_PREVIEW_CHARS); return { ...message, content: [{ type: "text", text: buildSpillReplacementText({ ok: true, path: persisted.spillPath, preview: preview.preview, originalChars: text.length, hasMore: preview.hasMore, }, toolName), }], } as AgentMessage; } async function hydrateRestoredPrunes( state: AutoCompactionState, messages: AgentMessage[], generation: number, ): Promise { if (state.restoredPrunes.size === 0) return false; const visible: Array<{ key: string; callId: string; message: AgentMessage; persisted: PersistedPruneEntry }> = []; for (const message of messages) { const callId = toolResultCallId(message); if (!callId || state.pruneManifest.has(callId)) continue; const found = findRestoredPrune(state, callId, message); if (!found) continue; const { key, persisted } = found; if (persisted.contentDigest && persisted.contentDigest !== toolResultDigest(message)) { state.restoredPrunes.delete(key); state.restoredPruneAges.delete(key); continue; } visible.push({ key, callId, message, persisted }); } if (visible.length === 0) return false; // Validate every referenced spill path before publishing a restored // replacement. An entry whose resource is dead or foreign is atomically // downgraded to a plain prune, so the resumed prefix only advertises files // that exist and never toggles between dead-path and recovered-path text // within an epoch. const resolved: Array<{ key: string; callId: string; message: AgentMessage; persisted: PersistedPruneEntry; downgraded?: boolean }> = await Promise.all(visible.map(async (item) => { if (!item.persisted.spillPath) return item; const live = state.sessionId !== undefined && typeof item.persisted.spillContentDigest === "string" && await validateSpillPath( state.sessionId, item.persisted.spillPath, item.persisted.writerId, item.persisted.spillContentDigest, ); if (live) return item; return { ...item, persisted: { callId: item.persisted.callId, level: "pruned" as PruneLevel, ...(item.persisted.introducedAtUsageEpoch !== undefined ? { introducedAtUsageEpoch: item.persisted.introducedAtUsageEpoch } : {}), ...(item.persisted.checkpointId !== undefined ? { checkpointId: item.persisted.checkpointId } : {}), ...(item.persisted.contentDigest !== undefined ? { contentDigest: item.persisted.contentDigest } : {}), }, downgraded: true, }; })); // reset()/onSessionStart may have run during validation, bumping the // generation. Publishing the stale view would resurrect prunes the new // lifecycle deliberately cleared. if (state.generation !== generation) return false; let downgraded = false; for (const item of resolved) { if (item.downgraded) { downgraded = true; // Persist the downgrade, not just the in-memory entry: the caller // writes this state so the next resume also sees the plain level. state.restoredPrunes.set(restoredPruneKey(item.persisted), item.persisted); } const replacement = restorePruneReplacement(item.message, item.persisted); if (!replacement) continue; const savedTokens = estimateMessageTokens(item.message) - estimateMessageTokens(replacement); if (savedTokens <= 0) continue; state.pruneManifest.set(item.callId, { replacement, savedTokens, introducedAtUsageEpoch: item.persisted.introducedAtUsageEpoch, spillPath: item.persisted.spillPath, spillContentDigest: item.persisted.spillContentDigest, contentDigest: item.persisted.contentDigest, writerId: item.persisted.writerId, level: item.persisted.level, checkpointId: item.persisted.checkpointId, refCallId: item.persisted.refCallId, }); } for (const item of resolved) { state.restoredPrunes.delete(item.key); state.restoredPruneAges.delete(item.key); } return downgraded; } /** * Periodic full checkpoints bound branch reconstruction to a short delta * suffix. The host journal is append-only, so this does not remove historical * entries or claim a bound on physical session bytes. */ function persistPruneManifest(pi: ExtensionAPI, state: AutoCompactionState): void { const byCallId = new Map(); for (const entry of state.restoredPrunes.values()) { if (!isCheckpointReachable(state, entry.checkpointId)) continue; setPreferredPrune(byCallId, entry, state.reachableCheckpoints); } for (const [callId, entry] of state.pruneManifest.entries()) { // A committed context_edit makes the prune authoritative on the branch: the // projection replays it, so the journal no longer needs to carry it. if (entry.editCommitted) continue; if (!entry.checkpointId) entry.checkpointId = state.activeCheckpointId; byCallId.set(callId, persistedEntryOf(callId, entry)); } const prunes = [...byCallId.values()].sort(comparePersistedPrunes); const upserts = prunes.filter((entry) => { const previous = state.persistedPrunes.get(entry.callId); return !previous || pruneKey([previous]) !== pruneKey([entry]); }); const removals = [...state.persistedPrunes.keys()] .filter((callId) => !byCallId.has(callId)) .sort((a, b) => a.localeCompare(b)); const checkpoint = state.forcePruneCheckpoint || (state.persistedPrunes.size === 0 && upserts.length > 0) || state.pruneDeltasSinceCheckpoint >= MAX_PRUNE_DELTAS_BETWEEN_CHECKPOINTS; if (!checkpoint && upserts.length === 0 && removals.length === 0) return; if (!pi.appendEntry) return; try { pi.appendEntry(PRUNE_STATE_ENTRY_TYPE, { version: PRUNE_STATE_VERSION, sessionId: state.sessionId, checkpointId: state.activeCheckpointId, mode: checkpoint ? "checkpoint" : "delta", ...(checkpoint ? { prunes: prunes.map(serializePersistedPrune) } : { upserts: upserts.map(serializePersistedPrune), removals }), }); } catch { return; } state.persistedPrunes = byCallId; state.persistedPruneKey = pruneKey(prunes); state.forcePruneCheckpoint = false; state.pruneDeltasSinceCheckpoint = checkpoint ? 0 : state.pruneDeltasSinceCheckpoint + 1; } type PersistedIntentPhase = "pending" | "submitted" | "continuation" | "cleared"; function pendingIntentPayload(intent: PendingCompactionIntent): Record { return { triggerKey: intent.triggerKey, ...(intent.recoveryId ? { recoveryId: intent.recoveryId } : {}), estimate: intent.estimate, linkedThreshold: intent.linkedThreshold, settings: intent.settings, effectiveSettings: intent.effectiveSettings, contextExhausted: intent.contextExhausted, requestBlocked: intent.requestBlocked === true, loopCritical: intent.loopCritical === true, }; } function outputLimitIntentPayload(intent: PendingOutputLimitIntent): Record { return { recoveryId: intent.recoveryId, phase: intent.phase ?? "pending", settings: intent.settings, usage: intent.usage, threshold: intent.threshold, }; } function recoveryWakePayload(wake: RecoveryWake): Record { return { ...wake }; } function currentPendingIntentPhase(state: AutoCompactionState): PersistedIntentPhase { if (state.pendingContinuationIntent) return "continuation"; if (state.pendingIntent) { return state.activeRequestOwner === "mid-turn" || state.zombieOwner !== undefined ? "submitted" : "pending"; } const output = state.pendingOutputLimitIntent; if (!output) return "cleared"; if (output.phase === "continuation") return "continuation"; if (output.phase === "submitted" || state.activeRequestOwner === "output-limit" || state.zombieOwner !== undefined) { return "submitted"; } return "pending"; } function pendingIntentKey( intent: PendingCompactionIntent | undefined, phase: PersistedIntentPhase, outputLimit?: PendingOutputLimitIntent, recoveryWake?: RecoveryWake, ): string { return JSON.stringify({ phase, pending: intent ? pendingIntentPayload(intent) : null, outputLimit: outputLimit ? outputLimitIntentPayload(outputLimit) : null, recoveryWake: recoveryWake ? recoveryWakePayload(recoveryWake) : null, }); } function persistPendingIntent(pi: ExtensionAPI, state: AutoCompactionState): boolean { const phase = currentPendingIntentPhase(state); const intent = phase === "cleared" ? undefined : state.pendingContinuationIntent ?? state.pendingIntent; const outputLimit = state.pendingOutputLimitIntent; const key = pendingIntentKey(intent, phase, outputLimit, state.recoveryWake); if (key === state.persistedIntentKey || !pi.appendEntry) return true; try { pi.appendEntry(PENDING_INTENT_ENTRY_TYPE, { version: PENDING_INTENT_VERSION, sessionId: state.sessionId, phase, pending: intent ? pendingIntentPayload(intent) : null, outputLimit: outputLimit ? outputLimitIntentPayload(outputLimit) : null, recoveryWake: state.recoveryWake ? recoveryWakePayload(state.recoveryWake) : null, }); state.persistedIntentKey = key; return true; } catch { // Keep persistedIntentKey unchanged so every later lifecycle boundary // retries the same pending state or tombstone instead of accepting drift. return false; } } interface LoadedPersistedIntent { intent?: PendingCompactionIntent; outputLimitIntent?: PendingOutputLimitIntent; recoveryWake?: RecoveryWake; phase: PersistedIntentPhase; key: string; } function decodeRecoveryWake(value: unknown, sessionId: string | undefined): RecoveryWake | undefined { if (!value || typeof value !== "object") return undefined; const wake = value as Record; if (wake.version !== RECOVERY_WAKE_VERSION || typeof wake.recoveryId !== "string" || typeof wake.wakeId !== "string" || !["auto", "output-limit", "new-context"].includes(String(wake.producer)) || typeof wake.sessionId !== "string" || wake.sessionId !== sessionId || typeof wake.branchCheckpointId !== "string" || (wake.branchCheckpointDepth !== undefined && (!Number.isSafeInteger(wake.branchCheckpointDepth) || (wake.branchCheckpointDepth as number) < 0)) || !Number.isSafeInteger(wake.producerGeneration) || !Number.isSafeInteger(wake.runtimeGeneration) || !["prepared", "queued", "consumed", "turn-started", "cancelled", "failed"].includes(String(wake.state)) || typeof wake.promptKind !== "string" || typeof wake.prompt !== "string" || !Number.isSafeInteger(wake.sequence) || !Number.isSafeInteger(wake.deadlineAt) || !Number.isSafeInteger(wake.wakeAttempts)) return undefined; for (const key of ["messageId", "turnId", "reason"] as const) { if (wake[key] !== undefined && typeof wake[key] !== "string") return undefined; } return { ...wake, // Early v4 writers predate the depth anchor. Real Pi entries still carry // branchCheckpointId; zero is the conservative fallback for id-less fixtures. branchCheckpointDepth: typeof wake.branchCheckpointDepth === "number" ? wake.branchCheckpointDepth : 0, } as unknown as RecoveryWake; } function loadPersistedIntent( ctx: ExtensionContext, sessionId: string | undefined, generation: number, ): LoadedPersistedIntent | undefined { const manager = ctx.sessionManager as { getBranch?: () => Array<{ type?: string; customType?: string; data?: unknown }>; getEntries?: () => Array<{ type?: string; customType?: string; data?: unknown }>; } | undefined; const entries = manager?.getBranch?.() ?? manager?.getEntries?.() ?? []; let entry: { data?: unknown } | undefined; let entryIndex = -1; let compactedAfterIntent = false; for (let index = entries.length - 1; index >= 0; index--) { const candidate = entries[index]; if (candidate.type === "compaction") compactedAfterIntent = true; if (candidate.type === "custom" && candidate.customType === PENDING_INTENT_ENTRY_TYPE) { entry = candidate; entryIndex = index; break; } } const deliveryWasRecorded = (recoveryId: string): boolean => { const marker = recoveryDeliveryMarker(recoveryId); return entries.slice(entryIndex + 1).some((candidate) => { try { return JSON.stringify(candidate).includes(marker); } catch { return false; } }); }; const data = entry?.data as { version?: unknown; sessionId?: unknown; phase?: unknown; pending?: unknown; outputLimit?: unknown; recoveryWake?: unknown; } | undefined; const version = data?.version; if (!data || (version !== 1 && version !== 2 && version !== 3 && version !== PENDING_INTENT_VERSION) || data.sessionId !== sessionId) return undefined; const phase: PersistedIntentPhase = (version === 3 || version === PENDING_INTENT_VERSION) && (data.phase === "pending" || data.phase === "submitted" || data.phase === "continuation" || data.phase === "cleared") ? data.phase : data.pending && typeof data.pending === "object" ? "pending" : "cleared"; let intent: PendingCompactionIntent | undefined; let recoveredPhase: PersistedIntentPhase = phase; if (data.pending && typeof data.pending === "object" && phase !== "cleared") { const pending = data.pending as Omit; if (typeof pending.triggerKey !== "string" || !pending.estimate || !pending.linkedThreshold || !pending.settings || !pending.effectiveSettings || typeof pending.contextExhausted !== "boolean" || (version === PENDING_INTENT_VERSION && pending.recoveryId !== undefined && typeof pending.recoveryId !== "string") || (version !== 1 && typeof pending.requestBlocked !== "boolean")) return undefined; intent = { ...pending, requestBlocked: version === 1 ? undefined : pending.requestBlocked, loopCritical: pending.loopCritical === true, generation, }; recoveredPhase = compactedAfterIntent ? !intent.requestBlocked && (intent.contextExhausted || intent.loopCritical) ? "continuation" : "cleared" : phase; if (version !== PENDING_INTENT_VERSION && recoveredPhase === "continuation" && deliveryWasRecorded(autoRecoveryId(sessionId, intent))) { intent = undefined; recoveredPhase = "cleared"; } } let outputLimitIntent: PendingOutputLimitIntent | undefined; if (data.outputLimit && typeof data.outputLimit === "object") { const output = data.outputLimit as { recoveryId?: unknown; phase?: unknown; settings?: unknown; usage?: unknown; threshold?: unknown; }; if (typeof output.recoveryId !== "string" || !output.settings || !output.usage || typeof output.threshold !== "number") return undefined; const outputPhase = compactedAfterIntent || output.phase === "continuation" ? "continuation" : "pending"; if (version === PENDING_INTENT_VERSION || !deliveryWasRecorded(output.recoveryId)) { outputLimitIntent = { recoveryId: output.recoveryId, settings: output.settings as CompactionSettings, usage: output.usage as ContextUsage, threshold: output.threshold, generation, phase: outputPhase, }; } } const recoveryWake = version === PENDING_INTENT_VERSION ? decodeRecoveryWake(data.recoveryWake, sessionId) : undefined; if (version === PENDING_INTENT_VERSION && data.recoveryWake !== null && data.recoveryWake !== undefined && !recoveryWake) { return undefined; } if (!intent && !outputLimitIntent) { return phase === "cleared" || recoveredPhase === "cleared" || recoveryWake ? { ...(recoveryWake ? { recoveryWake } : {}), phase: "cleared", key: pendingIntentKey(undefined, "cleared", undefined, recoveryWake), } : undefined; } const effectivePhase = intent ? recoveredPhase : outputLimitIntent?.phase === "continuation" ? "continuation" : "pending"; return { ...(intent ? { intent } : {}), ...(outputLimitIntent ? { outputLimitIntent } : {}), ...(recoveryWake ? { recoveryWake } : {}), phase: effectivePhase, key: pendingIntentKey(intent, effectivePhase, outputLimitIntent, recoveryWake), }; } interface LoadedPersistedPrunes { prunes: Map; byCallId: Map; requiresCheckpoint: boolean; deltasSinceCheckpoint: number; } interface PruneBranchEntry { type?: string; id?: string; customType?: string; data?: unknown; } function loadPersistedPrunes( ctx: ExtensionContext, sessionId: string | undefined, allowInheritedSession = false, ): LoadedPersistedPrunes { const manager = ctx.sessionManager as { getBranch?: () => PruneBranchEntry[]; getEntries?: () => PruneBranchEntry[]; } | undefined; const entries = manager?.getBranch?.() ?? manager?.getEntries?.() ?? []; const suffix: Array<{ entry: PruneBranchEntry; data: Record; sameSession: boolean; }> = []; let foundCheckpoint = false; // Checkpoints are emitted at a fixed cadence, so a reverse collection reads // only the latest full state and its bounded delta suffix. Reusing the // collected data below also avoids touching the same journal payload twice. for (let index = entries.length - 1; index >= 0; index--) { const entry = entries[index]; if (entry.type !== "custom" || entry.customType !== PRUNE_STATE_ENTRY_TYPE) continue; const data = entry.data as Record | undefined; if (!data) continue; const sameSession = sessionId === undefined || data.sessionId === sessionId; if (!sameSession && !allowInheritedSession) continue; const version = typeof data.version === "number" ? data.version : 0; const checkpoint = (version === PRUNE_STATE_VERSION && data.mode === "checkpoint" && Array.isArray(data.prunes)) || (version >= 3 && version <= 5 && Array.isArray(data.prunes)) || ((version === 1 || version === 2) && Array.isArray(data.toolCallIds)); const delta = version === PRUNE_STATE_VERSION && data.mode === "delta" && Array.isArray(data.upserts) && Array.isArray(data.removals); if (!checkpoint && !delta) continue; suffix.push({ entry, data, sameSession }); if (checkpoint) { foundCheckpoint = true; break; } } const byCallId = new Map(); let applied = false; let hasCurrentSessionEntry = false; let requiresCheckpoint = false; let deltasSinceCheckpoint = 0; for (const { entry, data, sameSession } of suffix.reverse()) { const version = typeof data.version === "number" ? data.version : 0; const checkpointId = typeof data.checkpointId === "string" ? data.checkpointId : typeof entry.id === "string" ? entry.id : undefined; const sourceSessionId = String(data.sessionId ?? sessionId ?? "unknown"); if (version === PRUNE_STATE_VERSION) { if (data.mode === "checkpoint" && Array.isArray(data.prunes)) { byCallId.clear(); applyPersistedPruneRecords(byCallId, data.prunes, version, sourceSessionId, checkpointId); deltasSinceCheckpoint = 0; } else if (data.mode === "delta" && Array.isArray(data.upserts) && Array.isArray(data.removals)) { for (const callId of data.removals) { if (typeof callId === "string") byCallId.delete(callId); } applyPersistedPruneRecords(byCallId, data.upserts, version, sourceSessionId, checkpointId); deltasSinceCheckpoint += 1; } } else if (version >= 3 && version <= 5 && Array.isArray(data.prunes)) { requiresCheckpoint = true; deltasSinceCheckpoint = 0; byCallId.clear(); applyPersistedPruneRecords(byCallId, data.prunes, version, sourceSessionId, checkpointId); } else if ((version === 1 || version === 2) && Array.isArray(data.toolCallIds)) { requiresCheckpoint = true; deltasSinceCheckpoint = 0; byCallId.clear(); const spillPaths = new Map(); if (version === 2 && Array.isArray(data.spillPaths)) { for (const value of data.spillPaths) { if (!value || typeof value !== "object") continue; const record = value as Record; if (typeof record.callId === "string" && typeof record.path === "string") { spillPaths.set(record.callId, record.path); } } } for (const value of data.toolCallIds) { if (typeof value !== "string") continue; const spillPath = spillPaths.get(value); byCallId.set(value, { callId: value, level: spillPath !== undefined ? "spill" : "pruned", ...(spillPath !== undefined ? { spillPath } : {}), ...(checkpointId !== undefined ? { checkpointId } : {}), }); } } applied = true; if (sameSession) hasCurrentSessionEntry = true; } const prunes = new Map(); for (const entry of byCallId.values()) prunes.set(restoredPruneKey(entry), entry); return { prunes, byCallId, deltasSinceCheckpoint, requiresCheckpoint: requiresCheckpoint || (applied && (!hasCurrentSessionEntry || !foundCheckpoint)) || deltasSinceCheckpoint > MAX_PRUNE_DELTAS_BETWEEN_CHECKPOINTS, }; } function applyPersistedPruneRecords( target: Map, values: unknown[], version: number, sessionId: string, checkpointId: string | undefined, ): void { for (const value of values) { if (!value || typeof value !== "object") continue; const record = value as Record; if (typeof record.callId !== "string" || !isPruneLevel(record.level)) continue; const ownerCheckpoint = typeof record.checkpointId === "string" ? record.checkpointId : checkpointId; target.set(record.callId, { callId: record.callId, level: record.level, ...(version >= 4 && record.hasSpill === true && typeof record.writerId === "string" ? { spillPath: resolveSpillPath(sessionId, record.callId, record.writerId) } : typeof record.spillPath === "string" ? { spillPath: record.spillPath } : {}), ...(typeof record.spillContentDigest === "string" ? { spillContentDigest: record.spillContentDigest } : {}), ...(typeof record.introducedAtUsageEpoch === "string" ? { introducedAtUsageEpoch: record.introducedAtUsageEpoch } : {}), ...(typeof record.contentDigest === "string" ? { contentDigest: record.contentDigest } : {}), ...(typeof record.writerId === "string" ? { writerId: record.writerId } : {}), ...(typeof record.refCallId === "string" ? { refCallId: record.refCallId } : {}), ...(typeof record.replacementText === "string" ? { replacementText: record.replacementText } : {}), ...(ownerCheckpoint !== undefined ? { checkpointId: ownerCheckpoint } : {}), }); } } function inspectPruneBranch(ctx: ExtensionContext): { checkpointId: string; reachable: Map; latestPruneJournalIndex: number; } { const manager = ctx.sessionManager as { getBranch?: () => PruneBranchEntry[]; getEntries?: () => PruneBranchEntry[]; } | undefined; const entries = manager?.getBranch?.() ?? manager?.getEntries?.() ?? []; const reachable = new Map(); let checkpointId: string | undefined; let latestPruneJournalIndex = -1; for (let index = 0; index < entries.length; index++) { const entry = entries[index]; if (typeof entry.id === "string") reachable.set(entry.id, index); if (entry.type === "custom" && entry.customType === PRUNE_STATE_ENTRY_TYPE) { latestPruneJournalIndex = index; } const internalStateEntry = entry.type === "custom" && (entry.customType === PRUNE_STATE_ENTRY_TYPE || entry.customType === PENDING_INTENT_ENTRY_TYPE); if (!internalStateEntry && typeof entry.id === "string") checkpointId = entry.id; } const rootId = `session:${sessionIdOf(ctx) ?? "unknown"}:root`; reachable.set(rootId, -1); return { checkpointId: checkpointId ?? rootId, reachable, latestPruneJournalIndex }; } function syncActivePruneBranch(state: AutoCompactionState, ctx: ExtensionContext): void { const branch = inspectPruneBranch(ctx); const previousCheckpointId = state.activeCheckpointId; const previousRank = previousCheckpointId === undefined ? undefined : branch.reachable.get(previousCheckpointId); const switched = previousCheckpointId !== undefined && previousRank === undefined; const advancedAcrossPersistedState = previousCheckpointId !== undefined && previousCheckpointId !== branch.checkpointId && previousRank !== undefined && branch.latestPruneJournalIndex > previousRank; if (switched || advancedAcrossPersistedState) { if (switched) { for (const [callId, entry] of state.pruneManifest) { if (!entry.checkpointId) entry.checkpointId = previousCheckpointId; if (entry.checkpointId && branch.reachable.has(entry.checkpointId)) continue; parkRestoredPrune(state, persistedEntryOf(callId, entry)); state.pruneManifest.delete(callId); } } const loaded = loadPersistedPrunes(ctx, state.sessionId); for (const entry of loaded.byCallId.values()) parkRestoredPrune(state, entry); state.persistedPrunes = new Map(loaded.byCallId); state.persistedPruneKey = pruneKey(loaded.byCallId.values()); state.forcePruneCheckpoint = loaded.requiresCheckpoint; state.pruneDeltasSinceCheckpoint = loaded.deltasSinceCheckpoint; } state.activeCheckpointId = branch.checkpointId; state.reachableCheckpoints = branch.reachable; evictOffBranchRestoredPrunes(state); } function restoredPruneKey(entry: PersistedPruneEntry): string { return `${entry.checkpointId ?? "legacy"}\u0000${entry.callId}`; } function parkRestoredPrune(state: AutoCompactionState, entry: PersistedPruneEntry): void { const key = restoredPruneKey(entry); state.restoredPrunes.set(key, entry); state.restoredPruneAges.set(key, ++state.restoredPruneSequence); } function touchRestoredPrunes(state: AutoCompactionState, keys: Iterable): void { const age = ++state.restoredPruneSequence; for (const key of keys) state.restoredPruneAges.set(key, age); } function isCheckpointReachable(state: AutoCompactionState, checkpointId: string | undefined): boolean { return checkpointId === undefined || state.reachableCheckpoints.has(checkpointId); } function findRestoredPrune( state: AutoCompactionState, callId: string, message: AgentMessage, ): { key: string; persisted: PersistedPruneEntry } | undefined { const digest = toolResultDigest(message); return [...state.restoredPrunes.entries()] .filter(([, entry]) => entry.callId === callId && isCheckpointReachable(state, entry.checkpointId)) .sort(([leftKey, left], [rightKey, right]) => { const leftDigest = left.contentDigest === undefined || left.contentDigest === digest ? 1 : 0; const rightDigest = right.contentDigest === undefined || right.contentDigest === digest ? 1 : 0; if (leftDigest !== rightDigest) return rightDigest - leftDigest; const leftRank = state.reachableCheckpoints.get(left.checkpointId ?? "") ?? -1; const rightRank = state.reachableCheckpoints.get(right.checkpointId ?? "") ?? -1; return rightRank - leftRank || leftKey.localeCompare(rightKey); }) .map(([key, persisted]) => ({ key, persisted }))[0]; } /** * Evict whole off-branch checkpoints, oldest touch first. Reachable checkpoints * are never candidates; an evicted branch is reconstructed from its own journal * when it becomes active again. */ function evictOffBranchRestoredPrunes(state: AutoCompactionState): void { const groups = new Map(); for (const [key, entry] of state.restoredPrunes) { if (isCheckpointReachable(state, entry.checkpointId)) continue; const checkpointId = entry.checkpointId ?? "legacy"; const group = groups.get(checkpointId) ?? { keys: [], entries: 0, bytes: 0, age: 0 }; group.keys.push(key); group.entries += 1; group.bytes += serializedPruneBytes(entry); group.age = Math.max(group.age, state.restoredPruneAges.get(key) ?? 0); groups.set(checkpointId, group); } let entries = [...groups.values()].reduce((total, group) => total + group.entries, 0); let bytes = [...groups.values()].reduce((total, group) => total + group.bytes, 0); const victims = [...groups.entries()].sort(([leftId, left], [rightId, right]) => left.age - right.age || leftId.localeCompare(rightId)); for (const [, group] of victims) { if (entries <= MAX_OFF_BRANCH_PRUNE_ENTRIES && bytes <= MAX_OFF_BRANCH_PRUNE_BYTES) break; for (const key of group.keys) { state.restoredPrunes.delete(key); state.restoredPruneAges.delete(key); } entries -= group.entries; bytes -= group.bytes; } } function serializePersistedPrune(entry: PersistedPruneEntry): Record { const { spillPath: _spillPath, ...serialized } = entry; return { ...serialized, hasSpill: _spillPath !== undefined }; } function serializedPruneBytes(entry: PersistedPruneEntry): number { return Buffer.byteLength(JSON.stringify(serializePersistedPrune(entry)), "utf8"); } function comparePersistedPrunes(left: PersistedPruneEntry, right: PersistedPruneEntry): number { return left.callId.localeCompare(right.callId) || String(left.checkpointId ?? "").localeCompare(String(right.checkpointId ?? "")); } function setPreferredPrune( target: Map, entry: PersistedPruneEntry, reachable: Map, ): void { const previous = target.get(entry.callId); if (!previous) { target.set(entry.callId, entry); return; } const previousRank = reachable.get(previous.checkpointId ?? "") ?? -1; const nextRank = reachable.get(entry.checkpointId ?? "") ?? -1; if (nextRank > previousRank || (nextRank === previousRank && restoredPruneKey(entry).localeCompare(restoredPruneKey(previous)) < 0)) { target.set(entry.callId, entry); } } function sessionIdOf(ctx: ExtensionContext): string | undefined { const manager = ctx.sessionManager as { getSessionId?: () => string } | undefined; return manager?.getSessionId?.(); } function pruneKey(entries: Iterable): string { return JSON.stringify([...entries] .map((entry) => [ entry.callId, entry.level, entry.spillPath ?? null, entry.spillContentDigest ?? null, entry.introducedAtUsageEpoch ?? null, entry.contentDigest ?? null, entry.writerId ?? null, entry.checkpointId ?? null, entry.refCallId ?? null, entry.replacementText ?? null, ]) .sort(([a], [b]) => String(a).localeCompare(String(b)))); } export function shouldCompactMidTurn(input: { messages: AgentMessage[]; contextWindow: number; settings: CompactionSettings; modelMaxTokens?: number; /** Per-model trigger override for the active session model; see {@link deriveCompactionThreshold}. */ thresholdTokensOverride?: number; }): boolean { if (!input.settings.enabled || input.contextWindow <= input.settings.reserveTokens) return false; if (!endsWithCompleteToolResultBatch(input.messages)) return false; const effectiveSettings = { ...input.settings, reserveTokens: effectiveReserveTokens(input.settings, input.contextWindow, input.modelMaxTokens) }; const derived = deriveCompactionThreshold({ reserveTokens: input.settings.reserveTokens, contextWindow: input.contextWindow, modelMaxTokens: input.modelMaxTokens, soft: input.settings.soft, thresholdTokensOverride: input.thresholdTokensOverride, }); const softBands = derived.usable && derived.soft ? { nudgeTokens: derived.soft.nudgeTokens, pruneTokens: derived.soft.pruneTokens, pruneTargetTokens: derived.soft.pruneTargetTokens, } : undefined; return applyContextPressurePolicy(input.messages, input.contextWindow, effectiveSettings, undefined, undefined, false, softBands, derived.usable ? derived.thresholdTokens : undefined).band === "critical"; } export function estimateContextTokens(messages: AgentMessage[]): ContextEstimate { const source = latestProviderUsageSource(messages); const lastUsageIndex = source?.index ?? -1; const usageTokens = source ? source.usage.totalTokens ?? source.usage.input + source.usage.output + source.usage.cacheRead + source.usage.cacheWrite : 0; let trailingTokens = 0; for (let index = lastUsageIndex + 1; index < messages.length; index++) { trailingTokens += estimateMessageTokens(messages[index]); } return { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens }; } export function endsWithCompleteToolResultBatch(messages: AgentMessage[]): boolean { let endIndex = messages.length - 1; while (endIndex >= 0 && roleOf(messages[endIndex]) === "custom") endIndex--; if (roleOf(messages[endIndex]) !== "toolResult") return false; let assistantIndex = endIndex; while (assistantIndex >= 0 && roleOf(messages[assistantIndex]) === "toolResult") assistantIndex--; if (roleOf(messages[assistantIndex]) !== "assistant") return false; const callIds = assistantToolCallIds(messages[assistantIndex]); const resultIds = messages.slice(assistantIndex + 1, endIndex + 1).map(toolResultCallId); if (!callIds || callIds.length === 0 || resultIds.some((id) => !id)) return false; return callIds.length === resultIds.length && new Set(callIds).size === callIds.length && callIds.every((id) => resultIds.includes(id)); } export function assistantUsage(message: AgentMessage): { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens?: number } | undefined { const record = message as MessageRecord; if (record.role !== "assistant" || record.stopReason === "aborted" || record.stopReason === "error") return undefined; if (!record.usage || typeof record.usage !== "object") return undefined; const usage = record.usage as Record; const input = finiteNumber(usage.input); const output = finiteNumber(usage.output); const cacheRead = finiteNumber(usage.cacheRead); const cacheWrite = finiteNumber(usage.cacheWrite); if ([input, output, cacheRead, cacheWrite].some((value) => value === undefined)) return undefined; return { input: input!, output: output!, cacheRead: cacheRead!, cacheWrite: cacheWrite!, ...(finiteNumber(usage.totalTokens) !== undefined ? { totalTokens: finiteNumber(usage.totalTokens) } : {}), }; } interface ProviderUsageSource { index: number; message: AgentMessage; usage: NonNullable>; } function messageTimestamp(message: AgentMessage): number | undefined { const value = (message as MessageRecord).timestamp; if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value !== "string") return undefined; const trimmed = value.trim(); if (trimmed.length === 0) return undefined; const numeric = Number(trimmed); if (Number.isFinite(numeric)) return numeric; const parsed = Date.parse(trimmed); return Number.isFinite(parsed) ? parsed : undefined; } function latestProviderUsageSource(messages: AgentMessage[]): ProviderUsageSource | undefined { let hasCompactionBoundary = false; let compactionTimestamp: number | undefined; for (const message of messages) { if ((message as MessageRecord).role !== "compactionSummary") continue; hasCompactionBoundary = true; const timestamp = messageTimestamp(message); if (timestamp !== undefined && (compactionTimestamp === undefined || timestamp > compactionTimestamp)) { compactionTimestamp = timestamp; } } for (let index = messages.length - 1; index >= 0; index--) { const message = messages[index]; const usage = assistantUsage(message); if (!usage) continue; if (hasCompactionBoundary) { const timestamp = messageTimestamp(message); if (compactionTimestamp === undefined || timestamp === undefined || timestamp <= compactionTimestamp) continue; } return { index, message, usage }; } return undefined; } export function latestProviderUsageEpoch(messages: AgentMessage[]): string | undefined { const source = latestProviderUsageSource(messages); if (!source) return undefined; const record = source.message as MessageRecord; const usageKey = JSON.stringify(source.usage); const timestamp = record.timestamp; return (typeof timestamp === "number" || (typeof timestamp === "string" && timestamp.length > 0)) ? `timestamp:${String(timestamp)}:${usageKey}` : `message:${createHash("sha256").update(JSON.stringify({ content: record.content, stopReason: record.stopReason, usage: source.usage, })).digest("hex")}`; } function finiteNumber(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? value : undefined; } /** * Token estimates are pure functions of an immutable message object, and the * same array is estimate-scanned by up to five passes per request (context * estimate, frontier, both prune passes, telemetry). Keying the memo on object * identity is safe because prunes replace whole array elements — a message is * never mutated in place — so a replacement is a new object and misses * correctly. */ const messageTokenMemo = new WeakMap(); export function estimateMessageTokens(message: AgentMessage): number { const key = message as unknown as object; const memoized = messageTokenMemo.get(key); if (memoized !== undefined) return memoized; const content = (message as MessageRecord).content; const imageSizes: number[] = []; let documentCount = 0; if (Array.isArray(content)) { for (const block of content) { if (block && typeof block === "object") { const type = (block as { type?: unknown }).type; if (type === "image") { const data = (block as { data?: unknown }).data; imageSizes.push(typeof data === "string" ? Buffer.byteLength(data, "utf8") : 0); } else if (type === "document") documentCount++; } } } let tokens: number; if (imageSizes.length > 0 || documentCount > 0) { // Base64 image data must not be counted as text. Build a lightweight copy // with empty data fields so the ratio-based estimator only sees the // textual payload; per-image cost comes from the size-aware estimator // (see the byte-budget design doc). Document blocks (e.g. base64 PDFs) are // collapsed to a bare type marker for the same reason and are charged a // fixed per-document cost. const lightweight = { ...message, content: (content as Array>).map((block) => block?.type === "image" ? { type: "image", mimeType: block.mimeType, data: "" } : block?.type === "document" ? { type: "document" } : block, ), }; const serialized = JSON.stringify(lightweight); tokens = Math.ceil(serialized.length / tokenCharsPerToken(serialized)) + estimateImageTokens(imageSizes) + documentCount * ESTIMATED_DOCUMENT_TOKENS; } else { const serialized = JSON.stringify(message); tokens = Math.ceil(serialized.length / tokenCharsPerToken(serialized)); } messageTokenMemo.set(key, tokens); return tokens; } /** * Size-aware per-image token heuristic for local pressure accounting. Each * image is floored independently so a small thumbnail in a mixed message is * never subsidized by a large screenshot. The estimate grows monotonically * with decoded bytes; transport-body limits (e.g. a 16MiB proxy cap) are an * external constraint carried by `payloadLimitBytes`, NOT by this estimator — * no hard cap is baked in here. * * This is a *pressure estimate*, not a provider billing contract. */ export function estimateImageTokens(imageSizes: readonly number[]): number { if (imageSizes.length === 0) return 0; let total = 0; for (const bytes of imageSizes) { if (bytes <= 0) continue; const decodedApprox = Math.ceil(bytes * 3 / 4); total += Math.max(ESTIMATED_IMAGE_TOKENS, Math.ceil(decodedApprox / IMAGE_TOKEN_BYTES_PER_TOKEN)); } return total; } function tokenCharsPerToken(serialized: string): number { if (serialized.includes("```")) return TOKEN_RATIO_CODE; let whitespace = 0; for (let index = 0; index < serialized.length; index++) { const ch = serialized[index]; if (ch === " " || ch === "\n" || ch === "\t" || ch === "\r") whitespace++; } const whitespaceRatio = serialized.length > 0 ? whitespace / serialized.length : 0; if (whitespaceRatio > 0.3) return TOKEN_RATIO_WHITESPACE_HEAVY; return TOKEN_RATIO_DEFAULT; } function protectedFrontierStart(messages: AgentMessage[], keepRecentTokens: number): number { let tokens = 0; let start = messages.length; while (start > 0 && tokens < keepRecentTokens) { start--; tokens += estimateMessageTokens(messages[start]); } while (start > 0 && roleOf(messages[start]) === "toolResult") start--; return start; } /** * Minimum ratio of reclaimed tokens to invalidated cached-prefix tokens for a * prune depth to be worth taking. * * A prune replaces the message at index i, so the provider's cached prompt * prefix dies from i to the end. Re-establishing that suffix costs roughly * (cacheWrite - cacheRead) ≈ 1.15x base input per token one time, while the * reclaimed tokens save ≈ 0.1x per later request. Break-even is therefore about * 11.5 / ratio subsequent requests: 0.25 pays for itself within ~46 requests, * while the pathological shapes this gate exists to stop sit near 0.03 (~400 * requests — never). The band is measured, not tuned: observed healthy prune * shapes land at 0.66 and pathological ones at 0.027, so any cut in 0.1–0.4 * separates them. */ export const CACHE_PRUNE_MIN_SAVINGS_RATIO = 0.25; interface PrunePassInput { transformed: AgentMessage[]; pruneManifest: PruneManifest; frontierStart: number; pruneTarget: number; effectiveTokens: number; usageEpoch: string | undefined; selector: (message: AgentMessage) => AgentMessage | undefined; rankCandidates?: (candidates: PruneCandidate[]) => PruneCandidate[]; /** Call IDs that must not be lossy-pruned (dedup pointer targets). */ protectedCallIds?: ReadonlySet; /** Manifest level recorded for candidates this pass applies. */ level?: PruneLevel; } interface PruneCandidate { index: number; callId: string; replacement: AgentMessage; saved: number; contentDigest: string; level: PruneLevel; toolName: string; relevanceText: string; refCallId?: string; } interface PrunePassResult { savedTokens: number; prunedToolResults: number; effectiveTokens: number; candidates: PruneCandidate[]; } /** * Suffix sums of per-message token estimates. suffix[i] is the cost of * invalidating the cached prefix at index i — everything from i to the end must * be reprocessed. Computed once per policy run; estimates are memoized. */ export function suffixTokenSums(messages: AgentMessage[]): number[] { const suffix = new Array(messages.length + 1); suffix[messages.length] = 0; for (let index = messages.length - 1; index >= 0; index--) { suffix[index] = suffix[index + 1] + estimateMessageTokens(messages[index]); } return suffix; } /** * Dynamic prune-savings gate: the minimum fraction of the invalidated suffix * a prune run must reclaim rises with the observed cache hit ratio (a hot * cache makes every mutation more expensive). Baseline is the fixed 25%; the * observed ratio pivots it within `range` (default [0.1, 0.5]). */ export function dynamicPruneMinRatio( hitRatio: number, range: [number, number] = [0.1, 0.5], ): number { const [lo, hi] = range; const clampedHit = Math.min(1, Math.max(0, hitRatio)); // pivot: hit 0.5 -> baseline 0.25; hit 1.0 -> 0.375; hit 0.0 -> 0.125 const pivoted = CACHE_PRUNE_MIN_SAVINGS_RATIO * (1 + (clampedHit - 0.5)); return Math.min(hi, Math.max(lo, pivoted)); } /** Milliseconds-since-epoch of the latest assistant message, if trustworthy. */ function lastAssistantTimestamp(messages: AgentMessage[]): number | undefined { for (let index = messages.length - 1; index >= 0; index--) { const record = messages[index] as MessageRecord & { timestamp?: unknown }; if (record.role !== "assistant") continue; const ts = record.timestamp; if (typeof ts === "number" && Number.isFinite(ts)) return ts; if (typeof ts === "string" && ts.length > 0) { const parsed = Date.parse(ts); if (!Number.isNaN(parsed)) return parsed; } // The latest assistant message owns cache freshness. Falling back to an // older timestamp could mark a recently active cache as cold. return undefined; } return undefined; } /** * True when the gap since the last assistant message exceeds * `gapThresholdMinutes` (cache-cold heuristic). Missing/parsable-false * timestamps conservatively return false so pruning never runs un-gated on * unknown age. */ export function isCacheColdByTime( messages: AgentMessage[], gapThresholdMinutes: number, now: number = Date.now(), ): boolean { if (!Number.isFinite(gapThresholdMinutes) || gapThresholdMinutes <= 0) return false; const lastTs = lastAssistantTimestamp(messages); if (lastTs === undefined) return false; const gapMinutes = (now - lastTs) / 60_000; return Number.isFinite(gapMinutes) && gapMinutes > gapThresholdMinutes; } /** * Deepest prune depth whose CUMULATIVE savings still justify the cached prefix * it invalidates, or 0 to prune nothing. * * Selection must look at the whole candidate run, not each candidate in * isolation: within a tier candidates are visited newest-first, and the first * one always looks worst because it already pays for the entire suffix while * contributing only its own savings. A complete plan may concatenate tiers, * so the invalidation boundary is the earliest index seen in the prefix rather * than the current candidate's index. Measured on a uniform tool-loop transcript * the ratio climbs 0.12 -> 0.66 across 15 candidates, so a greedy per-candidate * test would reject the whole (profitable) run on its first element. */ export function cacheWorthwhileDepth( candidates: Array<{ index: number; saved: number }>, suffixTokens: number[], minRatio: number = CACHE_PRUNE_MIN_SAVINGS_RATIO, initialSavings: number = 0, ): number { let cumulative = initialSavings; let earliestIndex = Number.POSITIVE_INFINITY; let depth = 0; for (let position = 0; position < candidates.length; position++) { const candidate = candidates[position]; cumulative += candidate.saved; earliestIndex = Math.min(earliestIndex, candidate.index); const invalidated = suffixTokens[earliestIndex] ?? 0; if (invalidated <= 0) continue; if (cumulative >= invalidated * minRatio) depth = position + 1; } return depth; } function applyPruneCandidates( transformed: AgentMessage[], pruneManifest: PruneManifest, candidates: PruneCandidate[], depth: number, effectiveTokens: number, usageEpoch: string | undefined, ): PrunePassResult { let savedTokens = 0; let prunedToolResults = 0; const appliedCandidates = candidates.slice(0, depth); for (const candidate of appliedCandidates) { transformed[candidate.index] = candidate.replacement; recordPrune(pruneManifest, candidate.callId, { replacement: candidate.replacement, savedTokens: candidate.saved, introducedAtUsageEpoch: usageEpoch, contentDigest: candidate.contentDigest, level: candidate.level, ...(candidate.refCallId !== undefined ? { refCallId: candidate.refCallId } : {}), }); savedTokens += candidate.saved; prunedToolResults++; effectiveTokens -= candidate.saved; } return { savedTokens, prunedToolResults, effectiveTokens, candidates: appliedCandidates }; } function rankPruneCandidates( candidates: PruneCandidate[], query: string, mode: RelevanceMode, ): PruneCandidate[] { if (candidates.length < 2 || !query.trim()) return candidates; // DEF-003: a non-empty query that tokenizes to zero terms (punctuation, // symbol, or emoji only — e.g. "???" or "....") must NOT be scored as a // uniform-zero result. That masquerades as a valid tie and sorts newest-first, // silently inverting the relevance feature's intent. Treat it as ranking // disabled (same as an empty query) so eviction falls back to the default // newest-first walk explicitly, not via a fabricated uniform-zero score. if (tokenizeRelevance(query, RELEVANCE_MAX_QUERY_TOKENS).length === 0) { return candidates; } const documents = candidates.map((candidate) => `${candidate.toolName}\n${candidate.relevanceText}`); const scores = scoreRelevanceBatch(documents, query, mode); return candidates .map((candidate, position) => ({ candidate, position, score: scores[position] ?? 0 })) .sort((left, right) => left.score - right.score || right.candidate.index - left.candidate.index || left.position - right.position) .map((item) => item.candidate); } function sampleRelevanceText(text: string, maxChars: number): string { if (text.length <= maxChars) return text; const half = Math.floor(maxChars / 2); return `${text.slice(0, half)}\n${text.slice(-half)}`; } function latestRelevanceQuery(messages: AgentMessage[]): string { let userIndex = -1; for (let index = messages.length - 1; index >= 0; index--) { if ((messages[index] as MessageRecord).role === "user") { userIndex = index; break; } } if (userIndex < 0) return ""; const userText = sampleRelevanceText( extractTextContent(messages[userIndex]).trim(), RELEVANCE_QUERY_SAMPLE_CHARS, ); const parts = [userText]; const toolNames = new Set(); for (let index = userIndex + 1; index < messages.length; index++) { const content = (messages[index] as MessageRecord).content; if (!Array.isArray(content)) continue; for (const block of content) { if (!block || typeof block !== "object") continue; const call = block as { type?: unknown; name?: unknown }; if (call.type === "toolCall" && typeof call.name === "string" && call.name.trim()) { toolNames.add(call.name.trim()); } } } parts.push(...toolNames); return parts.filter(Boolean).join("\n"); } function runPrunePass(input: PrunePassInput): PrunePassResult { const { transformed, pruneManifest, frontierStart, pruneTarget, usageEpoch, selector } = input; const effectiveTokens = input.effectiveTokens; // Default mode preserves the historical newest-first walk and stops once the // target is projected. Relevance mode must collect the whole eligible tier // before sorting, otherwise older low-relevance candidates are invisible. const candidates: PruneCandidate[] = []; const claimedCallIds = new Set(pruneManifest.keys()); let projectedTokens = effectiveTokens; let relevanceSampleChars = 0; const collectWholeTier = input.rankCandidates !== undefined && projectedTokens > pruneTarget; for (let index = frontierStart - 1; index >= 0 && (collectWholeTier || projectedTokens > pruneTarget); index--) { const original = transformed[index]; const callId = toolResultCallId(original); if (!callId || claimedCallIds.has(callId)) continue; const replacement = selector(original); if (!replacement) continue; const before = estimateMessageTokens(original); const after = estimateMessageTokens(replacement); if (after >= before) continue; const saved = before - after; const record = original as MessageRecord; if (input.protectedCallIds?.has(callId)) continue; const relevanceText = collectWholeTier ? sampleRelevanceText(extractTextContent(original), RELEVANCE_DOCUMENT_SAMPLE_CHARS) : ""; if (collectWholeTier && (candidates.length >= RELEVANCE_MAX_CANDIDATES || relevanceSampleChars + relevanceText.length > RELEVANCE_TOTAL_SAMPLE_CHARS)) { break; } candidates.push({ index, callId, replacement, saved, contentDigest: toolResultDigest(original), level: input.level ?? "pruned", toolName: typeof record.toolName === "string" ? record.toolName : "tool", relevanceText, }); claimedCallIds.add(callId); relevanceSampleChars += relevanceText.length; if (!collectWholeTier) projectedTokens -= saved; } let selected = candidates; if (collectWholeTier && input.rankCandidates) { const ranked = input.rankCandidates(candidates); projectedTokens = effectiveTokens; let depth = 0; while (depth < ranked.length && projectedTokens > pruneTarget) { projectedTokens -= ranked[depth].saved; depth++; } selected = ranked.slice(0, depth); } // A pass always applies its complete ungated candidate run. When cache // economics are active, callers execute passes against a copy and apply one // cumulatively qualified prefix to the real context afterward. return applyPruneCandidates( transformed, pruneManifest, selected, selected.length, effectiveTokens, usageEpoch, ); } /** * Tier 0.5 — cross-turn verbatim de-duplication (default off). Builds a * prefix-ordered list of eligible tool outputs and folds later spans that * already appeared verbatim in an earlier output into an in-context pointer * (ported from headroom cross_turn_dedup). Folds are recorded in the manifest * at level "dedup" with their reference target, and every target is added to * `dedupProtected` so lossy passes keep the pointer's original in context. */ function runDedupPass(input: { transformed: AgentMessage[]; pruneManifest: PruneManifest; frontierStart: number; effectiveTokens: number; minLines: number; minChars: number; dedupProtected: Set; }): PrunePassResult { const { transformed, pruneManifest, frontierStart, minLines, minChars } = input; const claimed = new Set(pruneManifest.keys()); const blocks: DedupBlock[] = []; const indexByCallId = new Map(); for (let index = 0; index < frontierStart; index++) { const original = transformed[index]; const record = original as MessageRecord; if (record.role !== "toolResult") continue; const callId = toolResultCallId(original); if (!callId || claimed.has(callId)) continue; if (record.isError === true) continue; const text = extractTextContent(original); if (text.length < 8) continue; // Dedup only rewrites pure text outputs. A result carrying image or other // non-text blocks is skipped so those blocks can never be dropped. if (!isTextOnlyContent(original)) continue; blocks.push({ text, callId }); indexByCallId.set(callId, index); } if (blocks.length < 2) { return { savedTokens: 0, prunedToolResults: 0, effectiveTokens: input.effectiveTokens, candidates: [] }; } const folded = dedupBlocks(blocks, { minLines, minChars }); const candidates: PruneCandidate[] = []; let effectiveTokens = input.effectiveTokens; let savedTokens = 0; for (let position = 0; position < blocks.length; position++) { const original = blocks[position]; const foldedText = folded.blocks[position]?.text; if (foldedText === undefined || foldedText === original.text) continue; const index = indexByCallId.get(original.callId); if (index === undefined) continue; const before = estimateMessageTokens(transformed[index]); const replacement = { ...transformed[index], content: [{ type: "text", text: foldedText }], } as AgentMessage; const after = estimateMessageTokens(replacement); if (after >= before) continue; const record = transformed[index] as MessageRecord; const targets = folded.refs.get(original.callId); const refCallId = targets !== undefined && targets.size > 0 ? [...targets][0] : undefined; candidates.push({ index, callId: original.callId, replacement, saved: before - after, contentDigest: toolResultDigest(transformed[index]), level: "dedup", toolName: typeof record.toolName === "string" ? record.toolName : "tool", relevanceText: original.text, ...(refCallId !== undefined ? { refCallId } : {}), }); savedTokens += before - after; effectiveTokens -= before - after; if (targets !== undefined) { for (const target of targets) input.dedupProtected.add(target); } } if (candidates.length > 0) { // Record folds in the planning manifest so later lossy passes skip them // (recordPrune refuses overwrites) and apply them to the plan's messages. applyPruneCandidates(transformed, pruneManifest, candidates, candidates.length, effectiveTokens, undefined); } return { savedTokens, prunedToolResults: candidates.length, effectiveTokens, candidates }; } /** True when every content block of a message is a text block. */ function isTextOnlyContent(message: AgentMessage): boolean { const content = (message as MessageRecord).content; if (typeof content === "string") return true; if (!Array.isArray(content)) return false; return content.every((block) => !!block && typeof block === "object" && block.type === "text"); } function isProtectedToolResult(message: AgentMessage): boolean { const record = message as MessageRecord; if (record.role !== "toolResult") return false; if (record.isError === true) return true; if (typeof record.toolName === "string" && PROTECTED_TOOL_NAMES.has(record.toolName.toLowerCase())) return true; return extractTextContent(message).length < PROTECTED_THRESHOLD_CHARS; } /** * Content-aware kind selection: when the output's shape is unambiguous the * detector wins over the tool name (e.g. `bash` running `git diff`), otherwise * fall back to the tool-name mapping. */ function losslessKindForContent(text: string): LosslessKind | undefined { const detected = detectContentType(text); switch (detected.contentType) { case "search": return "search"; case "diff": return "diff"; case "build": return "log"; case "source_code": return "text"; default: return undefined; } } /** Map a tool name to the lossless compaction kind it most resembles. */ function losslessKindForTool(toolName: string | undefined): LosslessKind { switch (toolName?.toLowerCase()) { case "grep": case "ripgrep": case "search": return "search"; case "bash": case "shell": case "exec": return "log"; case "diff": case "git-diff": return "diff"; case "find": case "ls": return "paths"; default: return "text"; } } /** * Tier-0 lossless folding (algorithm ported from headroom lossless_compaction, * Apache-2.0). Reversibly folds format-native redundancy — log run collapse, * grep heading folding, diff index stripping — with a runtime round-trip * self-check inside compactLossless, so it never loses information and never * inflates. Returns undefined when nothing is gained; the caller falls through * to the lossy prune path. */ function tryLosslessFold(message: AgentMessage): AgentMessage | undefined { if (isProtectedToolResult(message)) return undefined; const record = message as MessageRecord; const text = extractTextContent(message); if (text.length < PROTECTED_THRESHOLD_CHARS) return undefined; const folded = compactLossless( text, losslessKindForContent(text) ?? losslessKindForTool( typeof record.toolName === "string" ? record.toolName : undefined, ), ); if (folded === text || folded.length >= text.length) return undefined; const replacement = { ...message, content: [{ type: "text", text: folded }], } as AgentMessage; // Token guard in addition to the char guard: a shorter string can still // tokenize larger (marker words vs. raw data). runPrunePass re-checks, but // failing here keeps lossless entries out of the manifest entirely. const before = estimateMessageTokens(message); const after = estimateMessageTokens(replacement); if (after >= before) return undefined; return replacement; } function replaceableToolResult(message: AgentMessage): AgentMessage | undefined { if (isProtectedToolResult(message)) return undefined; const record = message as MessageRecord; if (record.role !== "toolResult" || record.isError === true) return undefined; if (typeof record.toolName !== "string" || !REPLAYABLE_TOOL_NAMES.has(record.toolName.toLowerCase())) return undefined; if (extractTextContent(message).length < SPILL_THRESHOLD_CHARS) return undefined; return { ...message, content: [{ type: "text", text: replayablePruneText(record) }], } as AgentMessage; } /** * Placeholder for a pruned replayable result. Derived only from stable message * fields (toolName + details.uri), so recomputing it for a restored prune * yields byte-identical text. resource results name the exact URI to re-read — * self-describing even when the paired toolCall is no longer nearby; other * replayable tools keep the generic re-run hint. */ function replayablePruneText(record: MessageRecord): string { const toolName = typeof record.toolName === "string" ? record.toolName : "tool"; const uri = toolName.toLowerCase() === "resource" ? prunedResourceUri(record) : undefined; if (uri) { return `[Maestro context pressure: stale large output from resource was pruned. Re-read ${uri} with the resource tool if the full payload is needed; pr:// and issue:// re-reads may return updated state.]`; } return `[Maestro context pressure: stale large output from ${toolName} was pruned. Re-run the tool if the full payload is needed.]`; } /** details.uri of a resource tool result, when present and placeholder-safe. */ function prunedResourceUri(record: MessageRecord): string | undefined { const details = record.details; if (typeof details !== "object" || details === null || Array.isArray(details)) return undefined; const uri = (details as { uri?: unknown }).uri; if (typeof uri !== "string") return undefined; const trimmed = uri.trim(); // Whitespace or brackets would break the one-line bracketed placeholder; // real resource URIs contain neither. Fall back to the generic hint then. if (!trimmed || trimmed.length > 256 || /[\s\][]/.test(trimmed)) return undefined; return trimmed; } function evictableBulkToolResult(message: AgentMessage): AgentMessage | undefined { if (isProtectedToolResult(message)) return undefined; const record = message as MessageRecord; if (record.role !== "toolResult" || record.isError === true) return undefined; if (typeof record.toolName !== "string" || !EVICTABLE_BULK_TOOL_NAMES.has(record.toolName.toLowerCase())) return undefined; if (extractTextContent(message).length < SPILL_THRESHOLD_CHARS) return undefined; const toolName = record.toolName; return { ...message, content: [{ type: "text", text: `[Maestro context pressure: stale large output from ${toolName} was evicted to reclaim context. The original payload is no longer available; re-derive it from the affected files or commands if still needed.]`, }], } as AgentMessage; } export function pruneToolResult(message: AgentMessage): AgentMessage | undefined { return replaceableToolResult(message) ?? evictableBulkToolResult(message); } /** * Deterministic replacement for a tool result whose image blocks are pruned by * the image byte budget. Text blocks and metadata survive; each image block is * replaced with a structured marker carrying mime and byte size so the model * still knows an image was present and roughly how heavy it was. * * Purely derived from stable message fields, so recomputing it for a restored * prune (imagePruneReplacement) yields byte-identical placeholder text. */ /** * Deterministic replacement for a message whose image blocks are evicted by * the payload limit. Text blocks and metadata survive; each image block is * replaced with a structured marker carrying mime and byte size so the model * still knows an image was present and roughly how heavy it was. * * Purely derived from stable message fields, so recomputing it for a restored * prune yields byte-identical placeholder text. */ export function imagePruneReplacement(message: AgentMessage): AgentMessage | undefined { const record = message as MessageRecord; const content = record.content; if (!Array.isArray(content)) return undefined; let hasImage = false; const replaced: Array> = []; for (const block of content) { if (!block || typeof block !== "object") continue; if (block.type === "image") { hasImage = true; const data = block.data; const bytes = typeof data === "string" ? Buffer.byteLength(data, "utf8") : 0; const mime = typeof block.mimeType === "string" && block.mimeType.length > 0 ? block.mimeType : "image"; replaced.push({ type: "text", text: `[image:${mime} (${bytes}B, evicted by payload limit)]`, }); } else { replaced.push({ ...block }); } } if (!hasImage) return undefined; return { ...message, content: replaced } as unknown as AgentMessage; } /** Total UTF-8 bytes of image payloads in a single message. */ export function imageBytesOfMessage(message: AgentMessage): number { let total = 0; const content = (message as MessageRecord).content; if (!Array.isArray(content)) return 0; for (const block of content) { if (!block || typeof block !== "object") continue; if (block.type !== "image") continue; const data = block.data; if (typeof data === "string") total += Buffer.byteLength(data, "utf8"); } return total; } /** * Estimated UTF-8 byte size of the request body a message array would produce. * * This is byte accounting, deliberately separate from token estimation: an * image costs a bounded number of tokens but its base64 occupies bytes on the * wire and can trip a transport cap long before token pressure matters. The * estimate serializes without image data duplication and adds the base64 * payloads back in, so it stays linear and cheap. */ export function estimatePayloadBytes(messages: AgentMessage[]): number { let total = 0; for (const message of messages) { const content = (message as MessageRecord).content; if (!Array.isArray(content)) { total += Buffer.byteLength(JSON.stringify(message), "utf8"); continue; } const lightweight: Array> = []; for (const block of content) { if (!block || typeof block !== "object") continue; if (block.type === "image") { const data = block.data; const bytes = typeof data === "string" ? Buffer.byteLength(data, "utf8") : 0; lightweight.push({ type: "image", mimeType: block.mimeType }); total += bytes; } else { lightweight.push({ ...block }); } } total += Buffer.byteLength(JSON.stringify(lightweight), "utf8"); } return total; } /** Result of the payload-limit prune pass. */ export interface PayloadLimitPruneResult { pruned: boolean; replaced: number; savedTokens: number; savedBytes: number; effectiveTokens: number; candidates: PruneCandidate[]; /** The (possibly copied) message array with applied replacements. */ transformed: AgentMessage[]; } /** * Payload-limit driven prune pass. * * Runs independently of token pressure and reclaims bytes OLDEST-FIRST: the * earliest eligible message is reduced first (image blocks replaced with a * metadata marker, large text outputs folded to a replayable/evictable * placeholder), then the next, until the estimated request body drops below * the configured ceiling. Later, more likely relevant context therefore * survives longest. * * Eligibility: toolResult messages only — user messages (including any * images the user just pasted) are never candidates by construction, nor are * error results, protected control outputs, or entries already claimed by * the manifest. Candidates are scanned across the WHOLE message array: * in a single-turn session the only user message sits at index 0 and every * tool result lives after it, so a positional frontier would shield exactly * the read-many-screenshots accumulation this pass exists to relieve. */ export function runPayloadLimitPrune(input: { messages: AgentMessage[]; pruneManifest: PruneManifest; frontierStart: number; protectedCallIds?: ReadonlySet; limitBytes: number; }): PayloadLimitPruneResult { const { messages, pruneManifest, frontierStart, protectedCallIds, limitBytes } = input; const empty: PayloadLimitPruneResult = { pruned: false, replaced: 0, savedTokens: 0, savedBytes: 0, effectiveTokens: estimateContextTokens(messages).tokens, candidates: [], transformed: messages, }; if (!Number.isFinite(limitBytes) || limitBytes <= 0) return empty; const initialBytes = estimatePayloadBytes(messages); if (initialBytes <= limitBytes) return empty; const claimed = new Set(pruneManifest.keys()); // Oldest -> newest eligible reductions. const eligible: Array<{ index: number; callId: string; byteCount: number; replacement: AgentMessage; candidate: PruneCandidate }> = []; for (let index = 0; index < frontierStart; index++) { const message = messages[index]; const callId = toolResultCallId(message); if (!callId || claimed.has(callId) || protectedCallIds?.has(callId)) continue; const record = message as MessageRecord; if (record.isError === true || (typeof record.toolName === "string" && PROTECTED_TOOL_NAMES.has(record.toolName.toLowerCase()))) { continue; } const before = estimateMessageTokens(message); // Prefer image eviction (largest byte win per message); otherwise fall back // to the existing replayable/bulk tool-result placeholder for large text. const replacement = imagePruneReplacement(message) ?? pruneToolResult(message); if (!replacement) continue; const after = estimateMessageTokens(replacement); if (after >= before) continue; eligible.push({ index, callId, byteCount: estimatePayloadBytes([message]), replacement, candidate: { index, callId, replacement, saved: before - after, contentDigest: toolResultDigest(message), level: "payload" as PruneLevel, toolName: typeof record.toolName === "string" ? record.toolName : "tool", relevanceText: "", }, }); } if (eligible.length === 0) return empty; // Apply oldest-first until the estimated payload fits the ceiling. const transformed = [...messages]; let remainingBytes = initialBytes; let reclaimedBytes = 0; let reclaimedTokens = 0; let applied = 0; const appliedCandidates: PruneCandidate[] = []; for (const entry of eligible) { transformed[entry.index] = entry.replacement; recordPrune(pruneManifest, entry.callId, { replacement: entry.replacement, savedTokens: entry.candidate.saved, introducedAtUsageEpoch: undefined, contentDigest: entry.candidate.contentDigest, level: "payload" as PruneLevel, }); remainingBytes -= entry.byteCount; reclaimedBytes += entry.byteCount; reclaimedTokens += entry.candidate.saved; applied++; appliedCandidates.push(entry.candidate); if (remainingBytes <= limitBytes) break; } return { pruned: applied > 0, replaced: applied, savedTokens: reclaimedTokens, savedBytes: reclaimedBytes, effectiveTokens: estimateContextTokens(transformed).tokens, candidates: appliedCandidates, transformed, }; } const PERSISTED_OUTPUT_TAG = ""; function minimalSpillReplacement(message: AgentMessage, spillPath: string): AgentMessage { return { ...message, content: [{ type: "text", text: `[Maestro context pressure: pruned. File: ${spillPath}. Use read to reload.]` }], } as AgentMessage; } function compactNewSpilledPrunes( transformed: AgentMessage[], pruneManifest: Map, newSpillCallIds: Set, ): { tokenDelta: number } { let tokenDelta = 0; for (let index = 0; index < transformed.length; index++) { const callId = toolResultCallId(transformed[index]); if (!callId || !newSpillCallIds.has(callId)) continue; const entry = pruneManifest.get(callId); if (!entry?.spillPath || (entry.level ?? "spill") !== "spill") continue; const text = extractTextContent(transformed[index]); if (!text.startsWith(PERSISTED_OUTPUT_TAG)) continue; const minimal = minimalSpillReplacement(transformed[index], entry.spillPath); const delta = estimateMessageTokens(minimal) - estimateMessageTokens(transformed[index]); if (delta >= 0) continue; transformed[index] = minimal; entry.replacement = minimal; entry.savedTokens -= delta; entry.level = "minimal"; tokenDelta += delta; } return { tokenDelta }; } // Tool-result messages are immutable in the compaction pipeline: every prune // replaces an array element with a new object. Cache the full-content digest by // object identity so applyRecordedPrunes does not re-hash every retained large // output on every context hook; a replayed or branch-replaced message naturally // gets a new identity and is verified again. const toolResultDigestMemo = new WeakMap(); function toolResultDigest(message: AgentMessage): string { const key = message as unknown as object; const memoized = toolResultDigestMemo.get(key); if (memoized !== undefined) return memoized; const record = message as MessageRecord; const digest = createHash("sha256").update(JSON.stringify({ toolCallId: record.toolCallId, toolName: record.toolName, isError: record.isError === true, content: record.content, })).digest("hex"); toolResultDigestMemo.set(key, digest); return digest; } // Tool-result messages are immutable in the compaction pipeline, so text // extraction can share the same object-identity cache discipline as token and // digest estimation. Replacements use new objects and automatically miss. const extractedTextMemo = new WeakMap(); function extractTextContent(message: AgentMessage): string { const key = message as unknown as object; const memoized = extractedTextMemo.get(key); if (memoized !== undefined) return memoized; const content = (message as MessageRecord).content; let text = ""; if (typeof content === "string") { text = content; } else if (Array.isArray(content)) { for (const block of content) { const blockText = (block as { text?: unknown } | null)?.text; if (typeof blockText === "string") text += blockText; } } extractedTextMemo.set(key, text); return text; } async function upgradeNewPrunesWithSpill( originalMessages: AgentMessage[], transformedMessages: AgentMessage[], pruneManifest: PruneManifest, sessionId: string, newCallIds: Set, writerId?: string, ): Promise<{ tokenDelta: number; callIds: Set }> { const callIds = new Set(); let tokenDelta = 0; if (!(pruneManifest instanceof Map)) return { tokenDelta, callIds }; const originalsByCallId = new Map(); for (const msg of originalMessages) { const callId = toolResultCallId(msg); if (callId) originalsByCallId.set(callId, msg); } for (let index = 0; index < transformedMessages.length; index++) { const callId = toolResultCallId(transformedMessages[index]); if (!callId || !newCallIds.has(callId)) continue; const entry = pruneManifest.get(callId); if (!entry || entry.spillPath !== undefined) continue; // Lossless folds carry no recoverability need — the folded text IS the // full content, reversibly so. Spilling it would replace a complete // (folded) payload with a 1.5K preview for no gain. Dedup pointers name an // in-context original; spilling would replace the pointer with a preview // after the cache gate already approved pointer economics. if ((entry.level ?? "pruned") === "lossless" || (entry.level ?? "pruned") === "dedup") continue; const original = originalsByCallId.get(callId); if (!original) continue; const text = extractTextContent(original); if (text.length < SPILL_THRESHOLD_CHARS) continue; const toolName = typeof (original as MessageRecord).toolName === "string" ? (original as MessageRecord).toolName as string : "tool"; const spill = await spillToolResult(sessionId, callId, text, writerId); if (!spill.ok) { // The write failed, so the payload is not recoverable from disk and the // no-path replacement text is a 1.5K preview — strictly worse than the // pruned placeholder already in place. Keep the placeholder and leave // spillPath unset. This entry is NOT retried on a later pass: only // callIds recorded by the current policy pass are eligible above, so the // plain replacement stays frozen for the rest of the compaction epoch // even if the disk recovers. A background persist may still land the // bytes, but provider-visible bytes never change mid-epoch. continue; } const replacementText = buildSpillReplacementText(spill, toolName); const upgraded: AgentMessage = { ...transformedMessages[index], content: [{ type: "text", text: replacementText }], } as AgentMessage; tokenDelta += estimateMessageTokens(upgraded) - estimateMessageTokens(transformedMessages[index]); transformedMessages[index] = upgraded; entry.replacement = upgraded; entry.spillPath = spill.path; entry.spillContentDigest = spill.contentDigest; entry.writerId = writerId; entry.savedTokens = estimateMessageTokens(original) - estimateMessageTokens(upgraded); entry.level = "spill"; callIds.add(callId); } return { tokenDelta, callIds }; } // --- Extracted to pressure-telemetry.ts --- import { adjustPressureAfterReplacementChange, assistantToolCallIds, buildMidTurnInstructions, buildMidTurnTrigger, buildOutputLimitInstructions, buildVelocityInfo, cacheHitRatio, clearPressureStatus, compactionBreakerAllows, COMPACTION_BREAKER_COOLDOWN_TURNS, compactionBreakerCooldownRemaining, computeContextSignals, decideContextAction, derivePressureBand, describeCompactionBreakerPause, EMPTY_VELOCITY_TRACKER, finalAssistantStopReason, formatTokens, latestCacheHitRatio, MAX_CONSECUTIVE_COMPACTION_FAILURES, observeCacheAttribution, observeVelocity, pressureResult, publishIdleStatus, recordCompactionFailure, redundantToolResultCallIds, resetCompactionBreaker, roleOf, scanContextTokens, shouldCancelCompletedTurnThreshold, shouldPreserveCompletedTurn, shouldVelocityEscalate, toolResultCallId, toolResultPatternKey, updatePressureStatus, type CompactionBreakerState, } from "./pressure-telemetry.ts"; export { cacheHitRatio, compactionBreakerAllows, COMPACTION_BREAKER_COOLDOWN_TURNS, compactionBreakerCooldownRemaining, computeContextSignals, decideContextAction, derivePressureBand, describeCompactionBreakerPause, EMPTY_VELOCITY_TRACKER, finalAssistantStopReason, MAX_CONSECUTIVE_COMPACTION_FAILURES, observeVelocity, buildVelocityInfo, recordCompactionFailure, redundantToolResultCallIds, resetCompactionBreaker, shouldPreserveCompletedTurn, shouldCancelCompletedTurnThreshold, shouldVelocityEscalate, toolResultPatternKey, }; export type { CompactionBreakerState } from "./pressure-telemetry.ts";