@bastani/atomic 0.9.5-alpha.4 → 0.9.5-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +7 -0
- package/dist/builtin/subagents/agents/debugger.md +2 -2
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +11 -0
- package/dist/builtin/workflows/builtin/deep-research-codebase-utils.ts +5 -5
- package/dist/builtin/workflows/builtin/goal-runner.ts +5 -5
- package/dist/builtin/workflows/builtin/goal-types.ts +1 -0
- package/dist/builtin/workflows/builtin/goal.d.ts +2 -0
- package/dist/builtin/workflows/builtin/goal.ts +9 -0
- package/dist/builtin/workflows/builtin/index.d.ts +2 -0
- package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +5 -5
- package/dist/builtin/workflows/builtin/ralph-models.ts +25 -25
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-auto-compaction.js +3 -2
- package/dist/core/agent-session-auto-compaction.js.map +1 -1
- package/dist/core/agent-session-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-compaction.js +25 -4
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/compaction/context-compaction-critical.d.ts +15 -0
- package/dist/core/compaction/context-compaction-critical.d.ts.map +1 -0
- package/dist/core/compaction/context-compaction-critical.js +57 -0
- package/dist/core/compaction/context-compaction-critical.js.map +1 -0
- package/dist/core/compaction/context-compaction-eviction.d.ts +4 -0
- package/dist/core/compaction/context-compaction-eviction.d.ts.map +1 -0
- package/dist/core/compaction/context-compaction-eviction.js +172 -0
- package/dist/core/compaction/context-compaction-eviction.js.map +1 -0
- package/dist/core/compaction/context-compaction-runner.d.ts +7 -1
- package/dist/core/compaction/context-compaction-runner.d.ts.map +1 -1
- package/dist/core/compaction/context-compaction-runner.js +97 -21
- package/dist/core/compaction/context-compaction-runner.js.map +1 -1
- package/dist/core/compaction/context-transcript-analysis.d.ts +3 -0
- package/dist/core/compaction/context-transcript-analysis.d.ts.map +1 -1
- package/dist/core/compaction/context-transcript-analysis.js +3 -3
- package/dist/core/compaction/context-transcript-analysis.js.map +1 -1
- package/docs/compaction.md +45 -27
- package/docs/workflows.md +5 -1
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session-compaction.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-compaction.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,2BAA2B,IAAI,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC9F,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAChF,OAAO,EAAE,KAAK,2BAA2B,EAAqC,KAAK,uBAAuB,EAAmK,MAAM,uBAAuB,CAAC;AAc3S,wBAAsB,+BAA+B,CACpD,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,6BAA6B,GACpC,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC,CAsJ9C;AAED;;;GAGG;AAEH,wBAAsB,OAAO,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,OAAO,CAAC,2BAA2B,CAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAmDtI;AAED;;;GAGG;AAEH,wBAAsB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAgDzF;AAED;;GAEG;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAGxD;AAED;;GAEG;AAEH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAE3D;AAED;;;;;;;;;;GAUG;AAEH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAEnF;AAED,yCAAyC;AAEzC,eAAO,MAAM,6BAA6B;;;;;;;CAOzC,CAAC","sourcesContent":["import { formatNoModelSelectedMessage } from \"./auth-guidance.ts\";\nimport type { AgentSessionInternalSurface as AgentSession } from \"./agent-session-methods.ts\";\nimport type { ContextCompactionApplyOptions } from \"./agent-session-methods.ts\";\nimport { type ContextCompactionParameters, type ContextCompactionPreparation, type ContextCompactionResult, type ContextDeletionRequest, type ValidatedContextDeletionResult, contextCompact as runContextCompact, prepareContextCompaction, validateContextDeletionRequest } from \"./compaction/index.ts\";\nimport type { SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionCompactEvent } from \"./extensions/index.ts\";\nimport type { ContextCompactionEntry } from \"./session-manager.ts\";\n\nfunction deepFreeze<T>(value: T): T {\n\tif (value && typeof value === \"object\") {\n\t\tObject.freeze(value);\n\t\tfor (const nested of Object.values(value)) {\n\t\t\tdeepFreeze(nested);\n\t\t}\n\t}\n\treturn value;\n}\n\nexport async function _applyContextVerbatimCompaction(\n\tthis: AgentSession,\n\toptions: ContextCompactionApplyOptions,\n): Promise<ContextCompactionResult | undefined> {\n\tif (!this.model) {\n\t\tthrow new Error(formatNoModelSelectedMessage());\n\t}\n\t// Capture the narrowed model now (control-flow narrowing holds immediately after the\n\t// guard) so the lazy planner-fallback closure below can use a non-undefined model.\n\tconst model = this.model;\n\tconst compactionThinkingLevel = this.thinkingLevel;\n\n\tconst pathEntries = this.sessionManager.getBranch();\n\tconst settings = this.settingsManager.getCompactionSettings();\n\tconst preparation = prepareContextCompaction(pathEntries, settings, {\n\t\t...(options.compression_ratio === undefined ? {} : { compression_ratio: options.compression_ratio }),\n\t\t...(options.preserve_recent === undefined ? {} : { preserve_recent: options.preserve_recent }),\n\t\t...(options.query === undefined ? {} : { query: options.query }),\n\t});\n\tif (!preparation) {\n\t\treturn undefined;\n\t}\n\tconst parameters: ContextCompactionParameters = preparation.parameters;\n\n\t// Planner fallback used when no extension supplies a deletionRequest. Auth is resolved\n\t// lazily here so extension-provided deletion requests keep working offline. Returns\n\t// undefined when auth is unavailable (auto-mode resolvers), signaling a no-op compaction.\n\tconst runPlanner = async (): Promise<ValidatedContextDeletionResult | undefined> => {\n\t\tconst auth = await options.resolvePlannerAuth();\n\t\tif (!auth) return undefined;\n\t\treturn runContextCompact(\n\t\t\tpreparation,\n\t\t\tmodel,\n\t\t\tauth.apiKey,\n\t\t\tauth.headers,\n\t\t\toptions.abortController.signal,\n\t\t\tcompactionThinkingLevel,\n\t\t);\n\t};\n\n\t// Emit session_before_compact to allow extensions to cancel or provide a deletion request.\n\t// This happens BEFORE any auth resolution so local extension deletion requests work\n\t// without configured API credentials.\n\tlet fromExtension = false;\n\tlet validated: ValidatedContextDeletionResult | undefined;\n\n\tif (this._extensionRunner.hasHandlers(\"session_before_compact\")) {\n\t\t// Deep-clone the preparation only when a before-compact handler actually exists. Extensions\n\t\t// receive an isolated, frozen snapshot so they cannot mutate protection metadata\n\t\t// (protectedEntryIds, entry .protected flags, etc.) on the internal preparation used for\n\t\t// validation. Building it lazily avoids deep-cloning the transcript — largest exactly when\n\t\t// compaction fires — on the common no-extension path.\n\t\tlet extensionPreparation: ContextCompactionPreparation;\n\t\ttry {\n\t\t\textensionPreparation = deepFreeze(structuredClone(preparation));\n\t\t} catch (error) {\n\t\t\t// structuredClone only throws if an entry carries a non-cloneable value (a function or a\n\t\t\t// class instance). Transcript entries are plain data today, so this guards a latent\n\t\t\t// invariant: surface a clear error instead of letting a raw DataCloneError abort an\n\t\t\t// otherwise-viable compaction.\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to snapshot transcript for compaction extensions: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\tconst hookResult = (await this._extensionRunner.emit({\n\t\t\ttype: \"session_before_compact\",\n\t\t\treason: options.reason,\n\t\t\tparameters,\n\t\t\tpreparation: extensionPreparation,\n\t\t\tbranchEntries: pathEntries,\n\t\t\tsignal: options.abortController.signal,\n\t\t} satisfies SessionBeforeCompactEvent)) as SessionBeforeCompactResult | undefined;\n\n\t\tif (hookResult?.cancel) {\n\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t}\n\n\t\tif (hookResult?.deletionRequest) {\n\t\t\tconst extensionDeletionRequest = hookResult.deletionRequest as ContextDeletionRequest;\n\t\t\t// Reject empty deletion requests before any side effects (backup, append, rebuild).\n\t\t\tif (!Array.isArray(extensionDeletionRequest.deletions) || extensionDeletionRequest.deletions.length === 0) {\n\t\t\t\tthrow new Error(\"No safe context deletions proposed by extension\");\n\t\t\t}\n\t\t\t// Validate against the internal transcript snapshot, not the extension-facing clone.\n\t\t\t// Auth is NOT resolved here — local extension deletion requests work offline.\n\t\t\tvalidated = validateContextDeletionRequest(extensionDeletionRequest, preparation.transcript);\n\t\t\t// Reject if reconciliation reduced deletions to zero.\n\t\t\tif (validated.deletedTargets.length === 0) {\n\t\t\t\tthrow new Error(\"No safe context deletions proposed by extension\");\n\t\t\t}\n\t\t\tfromExtension = true;\n\t\t}\n\t}\n\n\t// Planner fallback shared by both paths: no before-compact handler at all, or a handler that\n\t// observed without supplying a deletionRequest. Resolves auth lazily; undefined means auth is\n\t// unavailable (auto-mode resolvers), so compaction is a no-op.\n\tif (!validated) {\n\t\tconst plannerResult = await runPlanner();\n\t\tif (!plannerResult) {\n\t\t\treturn undefined;\n\t\t}\n\t\tvalidated = plannerResult;\n\t}\n\n\tif (options.abortController.signal.aborted) {\n\t\tthrow new Error(\"Compaction cancelled\");\n\t}\n\n\tconst backupPath = this.sessionManager.writeBackupSnapshot(options.backupLabel);\n\tconst compactionEntryId = this.sessionManager.appendContextCompaction(\n\t\tvalidated.deletedTargets,\n\t\tvalidated.protectedEntryIds,\n\t\tvalidated.stats,\n\t\tbackupPath,\n\t);\n\tconst sessionContext = this.sessionManager.buildSessionContext();\n\tthis.agent.state.messages = sessionContext.messages;\n\n\tconst result: ContextCompactionResult = {\n\t\t...validated,\n\t\tpromptVersion: 1,\n\t\tparameters,\n\t\t...(backupPath ? { backupPath } : {}),\n\t};\n\n\t// Emit session_compact so extensions can observe the validated result. This is a pure\n\t// observation hook fired AFTER the compaction has been committed (backup written,\n\t// context_compaction entry persisted, active context rebuilt). A misbehaving observer must\n\t// never turn a successful, already-persisted compaction into a reported failure, so any\n\t// throw is routed to the non-fatal extension-error channel and compaction still reports\n\t// success.\n\tconst contextCompactionEntry = this.sessionManager.getEntry(compactionEntryId) as ContextCompactionEntry;\n\ttry {\n\t\tawait this._extensionRunner.emit({\n\t\t\ttype: \"session_compact\",\n\t\t\treason: options.reason,\n\t\t\tparameters,\n\t\t\tresult,\n\t\t\tcontextCompactionEntry,\n\t\t\tfromExtension,\n\t\t} satisfies SessionCompactEvent);\n\t} catch (error) {\n\t\tthis._extensionRunner.emitError({\n\t\t\textensionPath: \"<session_compact>\",\n\t\t\tevent: \"session_compact\",\n\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\tstack: error instanceof Error ? error.stack : undefined,\n\t\t});\n\t}\n\n\treturn result;\n}\n\n/**\n * Manually compact the session context using deletion-only verbatim context compaction.\n * Aborts current agent operation first.\n */\n\nexport async function compact(this: AgentSession, options: Partial<ContextCompactionParameters> = {}): Promise<ContextCompactionResult> {\n\tthis._disconnectFromAgent();\n\tawait this.abort();\n\tthis._compactionAbortController = new AbortController();\n\tthis._emit({ type: \"compaction_start\", reason: \"manual\" });\n\n\ttry {\n\t\tif (!this.model) {\n\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t}\n\n\t\t// Auth is resolved lazily: only called when the planner fallback is needed.\n\t\t// Extensions that provide a deletionRequest work without configured credentials.\n\t\tconst model = this.model;\n\t\tconst result = await this._applyContextVerbatimCompaction({\n\t\t\tresolvePlannerAuth: () => this._getRequiredRequestAuth(model),\n\t\t\tabortController: this._compactionAbortController,\n\t\t\tbackupLabel: \"compact\",\n\t\t\treason: \"manual\",\n\t\t\t...(options.compression_ratio === undefined ? {} : { compression_ratio: options.compression_ratio }),\n\t\t\t...(options.preserve_recent === undefined ? {} : { preserve_recent: options.preserve_recent }),\n\t\t\t...(options.query === undefined ? {} : { query: options.query }),\n\t\t});\n\t\tif (!result) {\n\t\t\tthrow new Error(\"Nothing to compact (session too small)\");\n\t\t}\n\n\t\tthis._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t});\n\t\treturn result;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\tthis._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: undefined,\n\t\t\taborted,\n\t\t\twillRetry: false,\n\t\t\terrorMessage: aborted ? undefined : `Compaction failed: ${message}`,\n\t\t});\n\t\tthrow error;\n\t} finally {\n\t\tthis._compactionAbortController = undefined;\n\t\tthis._reconnectToAgent();\n\t}\n}\n\n/**\n * Manually compact the session context by applying validated logical deletions.\n * Retained transcript entries/content blocks stay verbatim; no user prompt text is accepted.\n */\n\nexport async function contextCompact(this: AgentSession): Promise<ContextCompactionResult> {\n\tthis._disconnectFromAgent();\n\tawait this.abort();\n\tthis._compactionAbortController = new AbortController();\n\tthis._emit({ type: \"context_compaction_start\", reason: \"manual\" });\n\n\ttry {\n\t\tif (!this.model) {\n\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t}\n\n\t\t// Auth is resolved lazily: only called when the planner fallback is needed.\n\t\t// Extensions that provide a deletionRequest work without configured credentials.\n\t\tconst model = this.model;\n\t\tconst result = await this._applyContextVerbatimCompaction({\n\t\t\tresolvePlannerAuth: () => this._getRequiredRequestAuth(model),\n\t\t\tabortController: this._compactionAbortController,\n\t\t\tbackupLabel: \"context-compact\",\n\t\t\treason: \"manual\",\n\t\t});\n\t\tif (!result) {\n\t\t\tthrow new Error(\"Nothing to context-compact (session too small)\");\n\t\t}\n\n\t\tthis._emit({\n\t\t\ttype: \"context_compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t});\n\t\treturn result;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\tthis._emit({\n\t\t\ttype: \"context_compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: undefined,\n\t\t\taborted,\n\t\t\twillRetry: false,\n\t\t\terrorMessage: aborted ? undefined : `Context compaction failed: ${message}`,\n\t\t});\n\t\tthrow error;\n\t} finally {\n\t\tthis._compactionAbortController = undefined;\n\t\tthis._reconnectToAgent();\n\t}\n}\n\n/**\n * Cancel in-progress compaction (manual or auto).\n */\n\nexport function abortCompaction(this: AgentSession): void {\n\tthis._compactionAbortController?.abort();\n\tthis._autoCompactionAbortController?.abort();\n}\n\n/**\n * Cancel in-progress branch summarization.\n */\n\nexport function abortBranchSummary(this: AgentSession): void {\n\tthis._branchSummaryAbortController?.abort();\n}\n\n/**\n * Check if compaction is needed and run it.\n * Called after agent_end and before prompt submission.\n *\n * Two cases:\n * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry\n * 2. Threshold: Context over threshold, compact, resume queued active-turn work if present; otherwise wait for user\n *\n * @param assistantMessage The assistant message to check\n * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true\n */\n\nexport function setAutoCompactionEnabled(this: AgentSession, enabled: boolean): void {\n\tthis.settingsManager.setCompactionEnabled(enabled);\n}\n\n/** Whether auto-compaction is enabled */\n\nexport const agentSessionCompactionMethods = {\n\t_applyContextVerbatimCompaction,\n\tcompact,\n\tcontextCompact,\n\tabortCompaction,\n\tabortBranchSummary,\n\tsetAutoCompactionEnabled,\n};\n"]}
|
|
1
|
+
{"version":3,"file":"agent-session-compaction.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-compaction.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,2BAA2B,IAAI,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC9F,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAEhF,OAAO,EAAE,KAAK,2BAA2B,EAAqC,KAAK,uBAAuB,EAAmK,MAAM,uBAAuB,CAAC;AAiB3S,wBAAsB,+BAA+B,CACpD,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,6BAA6B,GACpC,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC,CA2K9C;AAED;;;GAGG;AAEH,wBAAsB,OAAO,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,OAAO,CAAC,2BAA2B,CAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAmDtI;AAED;;;GAGG;AAEH,wBAAsB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAgDzF;AAED;;GAEG;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAGxD;AAED;;GAEG;AAEH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAE3D;AAED;;;;;;;;;;GAUG;AAEH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAEnF;AAED,yCAAyC;AAEzC,eAAO,MAAM,6BAA6B;;;;;;;CAOzC,CAAC","sourcesContent":["import { formatNoModelSelectedMessage } from \"./auth-guidance.ts\";\nimport type { AgentSessionInternalSurface as AgentSession } from \"./agent-session-methods.ts\";\nimport type { ContextCompactionApplyOptions } from \"./agent-session-methods.ts\";\nimport { getEffectiveInputBudget } from \"./context-window.ts\";\nimport { type ContextCompactionParameters, type ContextCompactionPreparation, type ContextCompactionResult, type ContextDeletionRequest, type ValidatedContextDeletionResult, contextCompact as runContextCompact, prepareContextCompaction, validateContextDeletionRequest } from \"./compaction/index.ts\";\nimport { runDeterministicContextEviction } from \"./compaction/context-compaction-eviction.ts\";\n// Type-only import: erased at runtime so module mocks of the compaction barrel stay authoritative.\nimport type { ContextCompactionLadderOptions } from \"./compaction/context-compaction-runner.ts\";\nimport type { SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionCompactEvent } from \"./extensions/index.ts\";\nimport type { ContextCompactionEntry } from \"./session-manager.ts\";\n\nfunction deepFreeze<T>(value: T): T {\n\tif (value && typeof value === \"object\") {\n\t\tObject.freeze(value);\n\t\tfor (const nested of Object.values(value)) {\n\t\t\tdeepFreeze(nested);\n\t\t}\n\t}\n\treturn value;\n}\n\nexport async function _applyContextVerbatimCompaction(\n\tthis: AgentSession,\n\toptions: ContextCompactionApplyOptions,\n): Promise<ContextCompactionResult | undefined> {\n\tif (!this.model) {\n\t\tthrow new Error(formatNoModelSelectedMessage());\n\t}\n\t// Capture the narrowed model now (control-flow narrowing holds immediately after the\n\t// guard) so the lazy planner-fallback closure below can use a non-undefined model.\n\tconst model = this.model;\n\tconst compactionThinkingLevel = this.thinkingLevel;\n\n\tconst pathEntries = this.sessionManager.getBranch();\n\tconst settings = this.settingsManager.getCompactionSettings();\n\tconst preparation = prepareContextCompaction(pathEntries, settings, {\n\t\t...(options.compression_ratio === undefined ? {} : { compression_ratio: options.compression_ratio }),\n\t\t...(options.preserve_recent === undefined ? {} : { preserve_recent: options.preserve_recent }),\n\t\t...(options.query === undefined ? {} : { query: options.query }),\n\t});\n\tif (!preparation) {\n\t\t// Overflow recovery must be loud when there is no transcript the ladder can compact:\n\t\t// a silent no-op would leave the session overflowing without proving safe deletion is exhausted.\n\t\tif (options.reason === \"overflow\") {\n\t\t\tthrow new Error(\"Context compaction found no compactable transcript entries; nothing more was safely deletable\");\n\t\t}\n\t\treturn undefined;\n\t}\n\tconst parameters: ContextCompactionParameters = preparation.parameters;\n\tconst effectiveBudget = getEffectiveInputBudget(model);\n\tconst ladderOptions: ContextCompactionLadderOptions | undefined =\n\t\toptions.reason === \"overflow\"\n\t\t\t? { acceptanceTokenBudget: effectiveBudget, criticalEvictionTokenBudget: effectiveBudget }\n\t\t\t: options.reason === \"threshold\"\n\t\t\t\t// Match the shouldCompact trigger boundary in transcript-estimate space:\n\t\t\t\t// tokensAfter <= effectiveBudget - reserveTokens is equivalent to\n\t\t\t\t// !shouldCompact(tokensAfter, effectiveBudget, settings). The live post-retry\n\t\t\t\t// trigger uses provider usage (system/tools included), so this is a best-effort\n\t\t\t\t// boundary; a negative boundary correctly means no estimated result fits.\n\t\t\t\t? { acceptanceTokenBudget: effectiveBudget - settings.reserveTokens }\n\t\t\t\t: undefined;\n\n\t// Planner fallback used when no extension supplies a deletionRequest. Auth is resolved\n\t// lazily here so extension-provided deletion requests keep working offline. Overflow with\n\t// missing auth uses deterministic eviction directly instead of silently no-oping.\n\tconst runPlanner = async (): Promise<ValidatedContextDeletionResult | undefined> => {\n\t\tconst auth = await options.resolvePlannerAuth();\n\t\tif (!auth) {\n\t\t\tif (options.reason === \"overflow\") return runDeterministicContextEviction(preparation.transcript, effectiveBudget);\n\t\t\treturn undefined;\n\t\t}\n\t\treturn runContextCompact(\n\t\t\tpreparation,\n\t\t\tmodel,\n\t\t\tauth.apiKey,\n\t\t\tauth.headers,\n\t\t\toptions.abortController.signal,\n\t\t\tcompactionThinkingLevel,\n\t\t\tladderOptions,\n\t\t);\n\t};\n\n\t// Emit session_before_compact to allow extensions to cancel or provide a deletion request.\n\t// This happens BEFORE any auth resolution so local extension deletion requests work\n\t// without configured API credentials.\n\tlet fromExtension = false;\n\tlet validated: ValidatedContextDeletionResult | undefined;\n\n\tif (this._extensionRunner.hasHandlers(\"session_before_compact\")) {\n\t\t// Deep-clone the preparation only when a before-compact handler actually exists. Extensions\n\t\t// receive an isolated, frozen snapshot so they cannot mutate protection metadata\n\t\t// (protectedEntryIds, entry .protected flags, etc.) on the internal preparation used for\n\t\t// validation. Building it lazily avoids deep-cloning the transcript — largest exactly when\n\t\t// compaction fires — on the common no-extension path.\n\t\tlet extensionPreparation: ContextCompactionPreparation;\n\t\ttry {\n\t\t\textensionPreparation = deepFreeze(structuredClone(preparation));\n\t\t} catch (error) {\n\t\t\t// structuredClone only throws if an entry carries a non-cloneable value (a function or a\n\t\t\t// class instance). Transcript entries are plain data today, so this guards a latent\n\t\t\t// invariant: surface a clear error instead of letting a raw DataCloneError abort an\n\t\t\t// otherwise-viable compaction.\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to snapshot transcript for compaction extensions: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\tconst hookResult = (await this._extensionRunner.emit({\n\t\t\ttype: \"session_before_compact\",\n\t\t\treason: options.reason,\n\t\t\tparameters,\n\t\t\tpreparation: extensionPreparation,\n\t\t\tbranchEntries: pathEntries,\n\t\t\tsignal: options.abortController.signal,\n\t\t} satisfies SessionBeforeCompactEvent)) as SessionBeforeCompactResult | undefined;\n\n\t\tif (hookResult?.cancel) {\n\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t}\n\n\t\tif (hookResult?.deletionRequest) {\n\t\t\tconst extensionDeletionRequest = hookResult.deletionRequest as ContextDeletionRequest;\n\t\t\t// Reject empty deletion requests before any side effects (backup, append, rebuild).\n\t\t\tif (!Array.isArray(extensionDeletionRequest.deletions) || extensionDeletionRequest.deletions.length === 0) {\n\t\t\t\tthrow new Error(\"No safe context deletions proposed by extension\");\n\t\t\t}\n\t\t\t// Validate against the internal transcript snapshot, not the extension-facing clone.\n\t\t\t// Auth is NOT resolved here — local extension deletion requests work offline.\n\t\t\tvalidated = validateContextDeletionRequest(extensionDeletionRequest, preparation.transcript);\n\t\t\t// Reject if reconciliation reduced deletions to zero.\n\t\t\tif (validated.deletedTargets.length === 0) {\n\t\t\t\tthrow new Error(\"No safe context deletions proposed by extension\");\n\t\t\t}\n\t\t\tfromExtension = true;\n\t\t}\n\t}\n\n\t// Planner fallback shared by both paths: no before-compact handler at all, or a handler that\n\t// observed without supplying a deletionRequest. Resolves auth lazily; undefined means auth is\n\t// unavailable (auto-mode resolvers), so compaction is a no-op.\n\tif (!validated) {\n\t\tconst plannerResult = await runPlanner();\n\t\tif (!plannerResult) {\n\t\t\treturn undefined;\n\t\t}\n\t\tvalidated = plannerResult;\n\t}\n\n\tif (options.abortController.signal.aborted) {\n\t\tthrow new Error(\"Compaction cancelled\");\n\t}\n\n\tconst backupPath = this.sessionManager.writeBackupSnapshot(options.backupLabel);\n\tconst compactionEntryId = this.sessionManager.appendContextCompaction(\n\t\tvalidated.deletedTargets,\n\t\tvalidated.protectedEntryIds,\n\t\tvalidated.stats,\n\t\tbackupPath,\n\t);\n\tconst sessionContext = this.sessionManager.buildSessionContext();\n\tthis.agent.state.messages = sessionContext.messages;\n\n\tconst result: ContextCompactionResult = {\n\t\t...validated,\n\t\tpromptVersion: 1,\n\t\tparameters,\n\t\t...(backupPath ? { backupPath } : {}),\n\t};\n\n\t// Emit session_compact so extensions can observe the validated result. This is a pure\n\t// observation hook fired AFTER the compaction has been committed (backup written,\n\t// context_compaction entry persisted, active context rebuilt). A misbehaving observer must\n\t// never turn a successful, already-persisted compaction into a reported failure, so any\n\t// throw is routed to the non-fatal extension-error channel and compaction still reports\n\t// success.\n\tconst contextCompactionEntry = this.sessionManager.getEntry(compactionEntryId) as ContextCompactionEntry;\n\ttry {\n\t\tawait this._extensionRunner.emit({\n\t\t\ttype: \"session_compact\",\n\t\t\treason: options.reason,\n\t\t\tparameters,\n\t\t\tresult,\n\t\t\tcontextCompactionEntry,\n\t\t\tfromExtension,\n\t\t} satisfies SessionCompactEvent);\n\t} catch (error) {\n\t\tthis._extensionRunner.emitError({\n\t\t\textensionPath: \"<session_compact>\",\n\t\t\tevent: \"session_compact\",\n\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\tstack: error instanceof Error ? error.stack : undefined,\n\t\t});\n\t}\n\n\treturn result;\n}\n\n/**\n * Manually compact the session context using deletion-only verbatim context compaction.\n * Aborts current agent operation first.\n */\n\nexport async function compact(this: AgentSession, options: Partial<ContextCompactionParameters> = {}): Promise<ContextCompactionResult> {\n\tthis._disconnectFromAgent();\n\tawait this.abort();\n\tthis._compactionAbortController = new AbortController();\n\tthis._emit({ type: \"compaction_start\", reason: \"manual\" });\n\n\ttry {\n\t\tif (!this.model) {\n\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t}\n\n\t\t// Auth is resolved lazily: only called when the planner fallback is needed.\n\t\t// Extensions that provide a deletionRequest work without configured credentials.\n\t\tconst model = this.model;\n\t\tconst result = await this._applyContextVerbatimCompaction({\n\t\t\tresolvePlannerAuth: () => this._getRequiredRequestAuth(model),\n\t\t\tabortController: this._compactionAbortController,\n\t\t\tbackupLabel: \"compact\",\n\t\t\treason: \"manual\",\n\t\t\t...(options.compression_ratio === undefined ? {} : { compression_ratio: options.compression_ratio }),\n\t\t\t...(options.preserve_recent === undefined ? {} : { preserve_recent: options.preserve_recent }),\n\t\t\t...(options.query === undefined ? {} : { query: options.query }),\n\t\t});\n\t\tif (!result) {\n\t\t\tthrow new Error(\"Nothing to compact (session too small)\");\n\t\t}\n\n\t\tthis._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t});\n\t\treturn result;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\tthis._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: undefined,\n\t\t\taborted,\n\t\t\twillRetry: false,\n\t\t\terrorMessage: aborted ? undefined : `Compaction failed: ${message}`,\n\t\t});\n\t\tthrow error;\n\t} finally {\n\t\tthis._compactionAbortController = undefined;\n\t\tthis._reconnectToAgent();\n\t}\n}\n\n/**\n * Manually compact the session context by applying validated logical deletions.\n * Retained transcript entries/content blocks stay verbatim; no user prompt text is accepted.\n */\n\nexport async function contextCompact(this: AgentSession): Promise<ContextCompactionResult> {\n\tthis._disconnectFromAgent();\n\tawait this.abort();\n\tthis._compactionAbortController = new AbortController();\n\tthis._emit({ type: \"context_compaction_start\", reason: \"manual\" });\n\n\ttry {\n\t\tif (!this.model) {\n\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t}\n\n\t\t// Auth is resolved lazily: only called when the planner fallback is needed.\n\t\t// Extensions that provide a deletionRequest work without configured credentials.\n\t\tconst model = this.model;\n\t\tconst result = await this._applyContextVerbatimCompaction({\n\t\t\tresolvePlannerAuth: () => this._getRequiredRequestAuth(model),\n\t\t\tabortController: this._compactionAbortController,\n\t\t\tbackupLabel: \"context-compact\",\n\t\t\treason: \"manual\",\n\t\t});\n\t\tif (!result) {\n\t\t\tthrow new Error(\"Nothing to context-compact (session too small)\");\n\t\t}\n\n\t\tthis._emit({\n\t\t\ttype: \"context_compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t});\n\t\treturn result;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\tthis._emit({\n\t\t\ttype: \"context_compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: undefined,\n\t\t\taborted,\n\t\t\twillRetry: false,\n\t\t\terrorMessage: aborted ? undefined : `Context compaction failed: ${message}`,\n\t\t});\n\t\tthrow error;\n\t} finally {\n\t\tthis._compactionAbortController = undefined;\n\t\tthis._reconnectToAgent();\n\t}\n}\n\n/**\n * Cancel in-progress compaction (manual or auto).\n */\n\nexport function abortCompaction(this: AgentSession): void {\n\tthis._compactionAbortController?.abort();\n\tthis._autoCompactionAbortController?.abort();\n}\n\n/**\n * Cancel in-progress branch summarization.\n */\n\nexport function abortBranchSummary(this: AgentSession): void {\n\tthis._branchSummaryAbortController?.abort();\n}\n\n/**\n * Check if compaction is needed and run it.\n * Called after agent_end and before prompt submission.\n *\n * Two cases:\n * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry\n * 2. Threshold: Context over threshold, compact, resume queued active-turn work if present; otherwise wait for user\n *\n * @param assistantMessage The assistant message to check\n * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true\n */\n\nexport function setAutoCompactionEnabled(this: AgentSession, enabled: boolean): void {\n\tthis.settingsManager.setCompactionEnabled(enabled);\n}\n\n/** Whether auto-compaction is enabled */\n\nexport const agentSessionCompactionMethods = {\n\t_applyContextVerbatimCompaction,\n\tcompact,\n\tcontextCompact,\n\tabortCompaction,\n\tabortBranchSummary,\n\tsetAutoCompactionEnabled,\n};\n"]}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
2
|
+
import { getEffectiveInputBudget } from "./context-window.js";
|
|
2
3
|
import { contextCompact as runContextCompact, prepareContextCompaction, validateContextDeletionRequest } from "./compaction/index.js";
|
|
4
|
+
import { runDeterministicContextEviction } from "./compaction/context-compaction-eviction.js";
|
|
3
5
|
function deepFreeze(value) {
|
|
4
6
|
if (value && typeof value === "object") {
|
|
5
7
|
Object.freeze(value);
|
|
@@ -25,17 +27,36 @@ export async function _applyContextVerbatimCompaction(options) {
|
|
|
25
27
|
...(options.query === undefined ? {} : { query: options.query }),
|
|
26
28
|
});
|
|
27
29
|
if (!preparation) {
|
|
30
|
+
// Overflow recovery must be loud when there is no transcript the ladder can compact:
|
|
31
|
+
// a silent no-op would leave the session overflowing without proving safe deletion is exhausted.
|
|
32
|
+
if (options.reason === "overflow") {
|
|
33
|
+
throw new Error("Context compaction found no compactable transcript entries; nothing more was safely deletable");
|
|
34
|
+
}
|
|
28
35
|
return undefined;
|
|
29
36
|
}
|
|
30
37
|
const parameters = preparation.parameters;
|
|
38
|
+
const effectiveBudget = getEffectiveInputBudget(model);
|
|
39
|
+
const ladderOptions = options.reason === "overflow"
|
|
40
|
+
? { acceptanceTokenBudget: effectiveBudget, criticalEvictionTokenBudget: effectiveBudget }
|
|
41
|
+
: options.reason === "threshold"
|
|
42
|
+
// Match the shouldCompact trigger boundary in transcript-estimate space:
|
|
43
|
+
// tokensAfter <= effectiveBudget - reserveTokens is equivalent to
|
|
44
|
+
// !shouldCompact(tokensAfter, effectiveBudget, settings). The live post-retry
|
|
45
|
+
// trigger uses provider usage (system/tools included), so this is a best-effort
|
|
46
|
+
// boundary; a negative boundary correctly means no estimated result fits.
|
|
47
|
+
? { acceptanceTokenBudget: effectiveBudget - settings.reserveTokens }
|
|
48
|
+
: undefined;
|
|
31
49
|
// Planner fallback used when no extension supplies a deletionRequest. Auth is resolved
|
|
32
|
-
// lazily here so extension-provided deletion requests keep working offline.
|
|
33
|
-
//
|
|
50
|
+
// lazily here so extension-provided deletion requests keep working offline. Overflow with
|
|
51
|
+
// missing auth uses deterministic eviction directly instead of silently no-oping.
|
|
34
52
|
const runPlanner = async () => {
|
|
35
53
|
const auth = await options.resolvePlannerAuth();
|
|
36
|
-
if (!auth)
|
|
54
|
+
if (!auth) {
|
|
55
|
+
if (options.reason === "overflow")
|
|
56
|
+
return runDeterministicContextEviction(preparation.transcript, effectiveBudget);
|
|
37
57
|
return undefined;
|
|
38
|
-
|
|
58
|
+
}
|
|
59
|
+
return runContextCompact(preparation, model, auth.apiKey, auth.headers, options.abortController.signal, compactionThinkingLevel, ladderOptions);
|
|
39
60
|
};
|
|
40
61
|
// Emit session_before_compact to allow extensions to cancel or provide a deletion request.
|
|
41
62
|
// This happens BEFORE any auth resolution so local extension deletion requests work
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session-compaction.js","sourceRoot":"","sources":["../../src/core/agent-session-compaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { formatNoModelSelectedMessage } from \"./auth-guidance.ts\";\nimport type { AgentSessionInternalSurface as AgentSession } from \"./agent-session-methods.ts\";\nimport type { ContextCompactionApplyOptions } from \"./agent-session-methods.ts\";\nimport { type ContextCompactionParameters, type ContextCompactionPreparation, type ContextCompactionResult, type ContextDeletionRequest, type ValidatedContextDeletionResult, contextCompact as runContextCompact, prepareContextCompaction, validateContextDeletionRequest } from \"./compaction/index.ts\";\nimport type { SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionCompactEvent } from \"./extensions/index.ts\";\nimport type { ContextCompactionEntry } from \"./session-manager.ts\";\n\nfunction deepFreeze<T>(value: T): T {\n\tif (value && typeof value === \"object\") {\n\t\tObject.freeze(value);\n\t\tfor (const nested of Object.values(value)) {\n\t\t\tdeepFreeze(nested);\n\t\t}\n\t}\n\treturn value;\n}\n\nexport async function _applyContextVerbatimCompaction(\n\tthis: AgentSession,\n\toptions: ContextCompactionApplyOptions,\n): Promise<ContextCompactionResult | undefined> {\n\tif (!this.model) {\n\t\tthrow new Error(formatNoModelSelectedMessage());\n\t}\n\t// Capture the narrowed model now (control-flow narrowing holds immediately after the\n\t// guard) so the lazy planner-fallback closure below can use a non-undefined model.\n\tconst model = this.model;\n\tconst compactionThinkingLevel = this.thinkingLevel;\n\n\tconst pathEntries = this.sessionManager.getBranch();\n\tconst settings = this.settingsManager.getCompactionSettings();\n\tconst preparation = prepareContextCompaction(pathEntries, settings, {\n\t\t...(options.compression_ratio === undefined ? {} : { compression_ratio: options.compression_ratio }),\n\t\t...(options.preserve_recent === undefined ? {} : { preserve_recent: options.preserve_recent }),\n\t\t...(options.query === undefined ? {} : { query: options.query }),\n\t});\n\tif (!preparation) {\n\t\treturn undefined;\n\t}\n\tconst parameters: ContextCompactionParameters = preparation.parameters;\n\n\t// Planner fallback used when no extension supplies a deletionRequest. Auth is resolved\n\t// lazily here so extension-provided deletion requests keep working offline. Returns\n\t// undefined when auth is unavailable (auto-mode resolvers), signaling a no-op compaction.\n\tconst runPlanner = async (): Promise<ValidatedContextDeletionResult | undefined> => {\n\t\tconst auth = await options.resolvePlannerAuth();\n\t\tif (!auth) return undefined;\n\t\treturn runContextCompact(\n\t\t\tpreparation,\n\t\t\tmodel,\n\t\t\tauth.apiKey,\n\t\t\tauth.headers,\n\t\t\toptions.abortController.signal,\n\t\t\tcompactionThinkingLevel,\n\t\t);\n\t};\n\n\t// Emit session_before_compact to allow extensions to cancel or provide a deletion request.\n\t// This happens BEFORE any auth resolution so local extension deletion requests work\n\t// without configured API credentials.\n\tlet fromExtension = false;\n\tlet validated: ValidatedContextDeletionResult | undefined;\n\n\tif (this._extensionRunner.hasHandlers(\"session_before_compact\")) {\n\t\t// Deep-clone the preparation only when a before-compact handler actually exists. Extensions\n\t\t// receive an isolated, frozen snapshot so they cannot mutate protection metadata\n\t\t// (protectedEntryIds, entry .protected flags, etc.) on the internal preparation used for\n\t\t// validation. Building it lazily avoids deep-cloning the transcript — largest exactly when\n\t\t// compaction fires — on the common no-extension path.\n\t\tlet extensionPreparation: ContextCompactionPreparation;\n\t\ttry {\n\t\t\textensionPreparation = deepFreeze(structuredClone(preparation));\n\t\t} catch (error) {\n\t\t\t// structuredClone only throws if an entry carries a non-cloneable value (a function or a\n\t\t\t// class instance). Transcript entries are plain data today, so this guards a latent\n\t\t\t// invariant: surface a clear error instead of letting a raw DataCloneError abort an\n\t\t\t// otherwise-viable compaction.\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to snapshot transcript for compaction extensions: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\tconst hookResult = (await this._extensionRunner.emit({\n\t\t\ttype: \"session_before_compact\",\n\t\t\treason: options.reason,\n\t\t\tparameters,\n\t\t\tpreparation: extensionPreparation,\n\t\t\tbranchEntries: pathEntries,\n\t\t\tsignal: options.abortController.signal,\n\t\t} satisfies SessionBeforeCompactEvent)) as SessionBeforeCompactResult | undefined;\n\n\t\tif (hookResult?.cancel) {\n\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t}\n\n\t\tif (hookResult?.deletionRequest) {\n\t\t\tconst extensionDeletionRequest = hookResult.deletionRequest as ContextDeletionRequest;\n\t\t\t// Reject empty deletion requests before any side effects (backup, append, rebuild).\n\t\t\tif (!Array.isArray(extensionDeletionRequest.deletions) || extensionDeletionRequest.deletions.length === 0) {\n\t\t\t\tthrow new Error(\"No safe context deletions proposed by extension\");\n\t\t\t}\n\t\t\t// Validate against the internal transcript snapshot, not the extension-facing clone.\n\t\t\t// Auth is NOT resolved here — local extension deletion requests work offline.\n\t\t\tvalidated = validateContextDeletionRequest(extensionDeletionRequest, preparation.transcript);\n\t\t\t// Reject if reconciliation reduced deletions to zero.\n\t\t\tif (validated.deletedTargets.length === 0) {\n\t\t\t\tthrow new Error(\"No safe context deletions proposed by extension\");\n\t\t\t}\n\t\t\tfromExtension = true;\n\t\t}\n\t}\n\n\t// Planner fallback shared by both paths: no before-compact handler at all, or a handler that\n\t// observed without supplying a deletionRequest. Resolves auth lazily; undefined means auth is\n\t// unavailable (auto-mode resolvers), so compaction is a no-op.\n\tif (!validated) {\n\t\tconst plannerResult = await runPlanner();\n\t\tif (!plannerResult) {\n\t\t\treturn undefined;\n\t\t}\n\t\tvalidated = plannerResult;\n\t}\n\n\tif (options.abortController.signal.aborted) {\n\t\tthrow new Error(\"Compaction cancelled\");\n\t}\n\n\tconst backupPath = this.sessionManager.writeBackupSnapshot(options.backupLabel);\n\tconst compactionEntryId = this.sessionManager.appendContextCompaction(\n\t\tvalidated.deletedTargets,\n\t\tvalidated.protectedEntryIds,\n\t\tvalidated.stats,\n\t\tbackupPath,\n\t);\n\tconst sessionContext = this.sessionManager.buildSessionContext();\n\tthis.agent.state.messages = sessionContext.messages;\n\n\tconst result: ContextCompactionResult = {\n\t\t...validated,\n\t\tpromptVersion: 1,\n\t\tparameters,\n\t\t...(backupPath ? { backupPath } : {}),\n\t};\n\n\t// Emit session_compact so extensions can observe the validated result. This is a pure\n\t// observation hook fired AFTER the compaction has been committed (backup written,\n\t// context_compaction entry persisted, active context rebuilt). A misbehaving observer must\n\t// never turn a successful, already-persisted compaction into a reported failure, so any\n\t// throw is routed to the non-fatal extension-error channel and compaction still reports\n\t// success.\n\tconst contextCompactionEntry = this.sessionManager.getEntry(compactionEntryId) as ContextCompactionEntry;\n\ttry {\n\t\tawait this._extensionRunner.emit({\n\t\t\ttype: \"session_compact\",\n\t\t\treason: options.reason,\n\t\t\tparameters,\n\t\t\tresult,\n\t\t\tcontextCompactionEntry,\n\t\t\tfromExtension,\n\t\t} satisfies SessionCompactEvent);\n\t} catch (error) {\n\t\tthis._extensionRunner.emitError({\n\t\t\textensionPath: \"<session_compact>\",\n\t\t\tevent: \"session_compact\",\n\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\tstack: error instanceof Error ? error.stack : undefined,\n\t\t});\n\t}\n\n\treturn result;\n}\n\n/**\n * Manually compact the session context using deletion-only verbatim context compaction.\n * Aborts current agent operation first.\n */\n\nexport async function compact(this: AgentSession, options: Partial<ContextCompactionParameters> = {}): Promise<ContextCompactionResult> {\n\tthis._disconnectFromAgent();\n\tawait this.abort();\n\tthis._compactionAbortController = new AbortController();\n\tthis._emit({ type: \"compaction_start\", reason: \"manual\" });\n\n\ttry {\n\t\tif (!this.model) {\n\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t}\n\n\t\t// Auth is resolved lazily: only called when the planner fallback is needed.\n\t\t// Extensions that provide a deletionRequest work without configured credentials.\n\t\tconst model = this.model;\n\t\tconst result = await this._applyContextVerbatimCompaction({\n\t\t\tresolvePlannerAuth: () => this._getRequiredRequestAuth(model),\n\t\t\tabortController: this._compactionAbortController,\n\t\t\tbackupLabel: \"compact\",\n\t\t\treason: \"manual\",\n\t\t\t...(options.compression_ratio === undefined ? {} : { compression_ratio: options.compression_ratio }),\n\t\t\t...(options.preserve_recent === undefined ? {} : { preserve_recent: options.preserve_recent }),\n\t\t\t...(options.query === undefined ? {} : { query: options.query }),\n\t\t});\n\t\tif (!result) {\n\t\t\tthrow new Error(\"Nothing to compact (session too small)\");\n\t\t}\n\n\t\tthis._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t});\n\t\treturn result;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\tthis._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: undefined,\n\t\t\taborted,\n\t\t\twillRetry: false,\n\t\t\terrorMessage: aborted ? undefined : `Compaction failed: ${message}`,\n\t\t});\n\t\tthrow error;\n\t} finally {\n\t\tthis._compactionAbortController = undefined;\n\t\tthis._reconnectToAgent();\n\t}\n}\n\n/**\n * Manually compact the session context by applying validated logical deletions.\n * Retained transcript entries/content blocks stay verbatim; no user prompt text is accepted.\n */\n\nexport async function contextCompact(this: AgentSession): Promise<ContextCompactionResult> {\n\tthis._disconnectFromAgent();\n\tawait this.abort();\n\tthis._compactionAbortController = new AbortController();\n\tthis._emit({ type: \"context_compaction_start\", reason: \"manual\" });\n\n\ttry {\n\t\tif (!this.model) {\n\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t}\n\n\t\t// Auth is resolved lazily: only called when the planner fallback is needed.\n\t\t// Extensions that provide a deletionRequest work without configured credentials.\n\t\tconst model = this.model;\n\t\tconst result = await this._applyContextVerbatimCompaction({\n\t\t\tresolvePlannerAuth: () => this._getRequiredRequestAuth(model),\n\t\t\tabortController: this._compactionAbortController,\n\t\t\tbackupLabel: \"context-compact\",\n\t\t\treason: \"manual\",\n\t\t});\n\t\tif (!result) {\n\t\t\tthrow new Error(\"Nothing to context-compact (session too small)\");\n\t\t}\n\n\t\tthis._emit({\n\t\t\ttype: \"context_compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t});\n\t\treturn result;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\tthis._emit({\n\t\t\ttype: \"context_compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: undefined,\n\t\t\taborted,\n\t\t\twillRetry: false,\n\t\t\terrorMessage: aborted ? undefined : `Context compaction failed: ${message}`,\n\t\t});\n\t\tthrow error;\n\t} finally {\n\t\tthis._compactionAbortController = undefined;\n\t\tthis._reconnectToAgent();\n\t}\n}\n\n/**\n * Cancel in-progress compaction (manual or auto).\n */\n\nexport function abortCompaction(this: AgentSession): void {\n\tthis._compactionAbortController?.abort();\n\tthis._autoCompactionAbortController?.abort();\n}\n\n/**\n * Cancel in-progress branch summarization.\n */\n\nexport function abortBranchSummary(this: AgentSession): void {\n\tthis._branchSummaryAbortController?.abort();\n}\n\n/**\n * Check if compaction is needed and run it.\n * Called after agent_end and before prompt submission.\n *\n * Two cases:\n * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry\n * 2. Threshold: Context over threshold, compact, resume queued active-turn work if present; otherwise wait for user\n *\n * @param assistantMessage The assistant message to check\n * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true\n */\n\nexport function setAutoCompactionEnabled(this: AgentSession, enabled: boolean): void {\n\tthis.settingsManager.setCompactionEnabled(enabled);\n}\n\n/** Whether auto-compaction is enabled */\n\nexport const agentSessionCompactionMethods = {\n\t_applyContextVerbatimCompaction,\n\tcompact,\n\tcontextCompact,\n\tabortCompaction,\n\tabortBranchSummary,\n\tsetAutoCompactionEnabled,\n};\n"]}
|
|
1
|
+
{"version":3,"file":"agent-session-compaction.js","sourceRoot":"","sources":["../../src/core/agent-session-compaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { formatNoModelSelectedMessage } from \"./auth-guidance.ts\";\nimport type { AgentSessionInternalSurface as AgentSession } from \"./agent-session-methods.ts\";\nimport type { ContextCompactionApplyOptions } from \"./agent-session-methods.ts\";\nimport { getEffectiveInputBudget } from \"./context-window.ts\";\nimport { type ContextCompactionParameters, type ContextCompactionPreparation, type ContextCompactionResult, type ContextDeletionRequest, type ValidatedContextDeletionResult, contextCompact as runContextCompact, prepareContextCompaction, validateContextDeletionRequest } from \"./compaction/index.ts\";\nimport { runDeterministicContextEviction } from \"./compaction/context-compaction-eviction.ts\";\n// Type-only import: erased at runtime so module mocks of the compaction barrel stay authoritative.\nimport type { ContextCompactionLadderOptions } from \"./compaction/context-compaction-runner.ts\";\nimport type { SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionCompactEvent } from \"./extensions/index.ts\";\nimport type { ContextCompactionEntry } from \"./session-manager.ts\";\n\nfunction deepFreeze<T>(value: T): T {\n\tif (value && typeof value === \"object\") {\n\t\tObject.freeze(value);\n\t\tfor (const nested of Object.values(value)) {\n\t\t\tdeepFreeze(nested);\n\t\t}\n\t}\n\treturn value;\n}\n\nexport async function _applyContextVerbatimCompaction(\n\tthis: AgentSession,\n\toptions: ContextCompactionApplyOptions,\n): Promise<ContextCompactionResult | undefined> {\n\tif (!this.model) {\n\t\tthrow new Error(formatNoModelSelectedMessage());\n\t}\n\t// Capture the narrowed model now (control-flow narrowing holds immediately after the\n\t// guard) so the lazy planner-fallback closure below can use a non-undefined model.\n\tconst model = this.model;\n\tconst compactionThinkingLevel = this.thinkingLevel;\n\n\tconst pathEntries = this.sessionManager.getBranch();\n\tconst settings = this.settingsManager.getCompactionSettings();\n\tconst preparation = prepareContextCompaction(pathEntries, settings, {\n\t\t...(options.compression_ratio === undefined ? {} : { compression_ratio: options.compression_ratio }),\n\t\t...(options.preserve_recent === undefined ? {} : { preserve_recent: options.preserve_recent }),\n\t\t...(options.query === undefined ? {} : { query: options.query }),\n\t});\n\tif (!preparation) {\n\t\t// Overflow recovery must be loud when there is no transcript the ladder can compact:\n\t\t// a silent no-op would leave the session overflowing without proving safe deletion is exhausted.\n\t\tif (options.reason === \"overflow\") {\n\t\t\tthrow new Error(\"Context compaction found no compactable transcript entries; nothing more was safely deletable\");\n\t\t}\n\t\treturn undefined;\n\t}\n\tconst parameters: ContextCompactionParameters = preparation.parameters;\n\tconst effectiveBudget = getEffectiveInputBudget(model);\n\tconst ladderOptions: ContextCompactionLadderOptions | undefined =\n\t\toptions.reason === \"overflow\"\n\t\t\t? { acceptanceTokenBudget: effectiveBudget, criticalEvictionTokenBudget: effectiveBudget }\n\t\t\t: options.reason === \"threshold\"\n\t\t\t\t// Match the shouldCompact trigger boundary in transcript-estimate space:\n\t\t\t\t// tokensAfter <= effectiveBudget - reserveTokens is equivalent to\n\t\t\t\t// !shouldCompact(tokensAfter, effectiveBudget, settings). The live post-retry\n\t\t\t\t// trigger uses provider usage (system/tools included), so this is a best-effort\n\t\t\t\t// boundary; a negative boundary correctly means no estimated result fits.\n\t\t\t\t? { acceptanceTokenBudget: effectiveBudget - settings.reserveTokens }\n\t\t\t\t: undefined;\n\n\t// Planner fallback used when no extension supplies a deletionRequest. Auth is resolved\n\t// lazily here so extension-provided deletion requests keep working offline. Overflow with\n\t// missing auth uses deterministic eviction directly instead of silently no-oping.\n\tconst runPlanner = async (): Promise<ValidatedContextDeletionResult | undefined> => {\n\t\tconst auth = await options.resolvePlannerAuth();\n\t\tif (!auth) {\n\t\t\tif (options.reason === \"overflow\") return runDeterministicContextEviction(preparation.transcript, effectiveBudget);\n\t\t\treturn undefined;\n\t\t}\n\t\treturn runContextCompact(\n\t\t\tpreparation,\n\t\t\tmodel,\n\t\t\tauth.apiKey,\n\t\t\tauth.headers,\n\t\t\toptions.abortController.signal,\n\t\t\tcompactionThinkingLevel,\n\t\t\tladderOptions,\n\t\t);\n\t};\n\n\t// Emit session_before_compact to allow extensions to cancel or provide a deletion request.\n\t// This happens BEFORE any auth resolution so local extension deletion requests work\n\t// without configured API credentials.\n\tlet fromExtension = false;\n\tlet validated: ValidatedContextDeletionResult | undefined;\n\n\tif (this._extensionRunner.hasHandlers(\"session_before_compact\")) {\n\t\t// Deep-clone the preparation only when a before-compact handler actually exists. Extensions\n\t\t// receive an isolated, frozen snapshot so they cannot mutate protection metadata\n\t\t// (protectedEntryIds, entry .protected flags, etc.) on the internal preparation used for\n\t\t// validation. Building it lazily avoids deep-cloning the transcript — largest exactly when\n\t\t// compaction fires — on the common no-extension path.\n\t\tlet extensionPreparation: ContextCompactionPreparation;\n\t\ttry {\n\t\t\textensionPreparation = deepFreeze(structuredClone(preparation));\n\t\t} catch (error) {\n\t\t\t// structuredClone only throws if an entry carries a non-cloneable value (a function or a\n\t\t\t// class instance). Transcript entries are plain data today, so this guards a latent\n\t\t\t// invariant: surface a clear error instead of letting a raw DataCloneError abort an\n\t\t\t// otherwise-viable compaction.\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to snapshot transcript for compaction extensions: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t}\n\n\t\tconst hookResult = (await this._extensionRunner.emit({\n\t\t\ttype: \"session_before_compact\",\n\t\t\treason: options.reason,\n\t\t\tparameters,\n\t\t\tpreparation: extensionPreparation,\n\t\t\tbranchEntries: pathEntries,\n\t\t\tsignal: options.abortController.signal,\n\t\t} satisfies SessionBeforeCompactEvent)) as SessionBeforeCompactResult | undefined;\n\n\t\tif (hookResult?.cancel) {\n\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t}\n\n\t\tif (hookResult?.deletionRequest) {\n\t\t\tconst extensionDeletionRequest = hookResult.deletionRequest as ContextDeletionRequest;\n\t\t\t// Reject empty deletion requests before any side effects (backup, append, rebuild).\n\t\t\tif (!Array.isArray(extensionDeletionRequest.deletions) || extensionDeletionRequest.deletions.length === 0) {\n\t\t\t\tthrow new Error(\"No safe context deletions proposed by extension\");\n\t\t\t}\n\t\t\t// Validate against the internal transcript snapshot, not the extension-facing clone.\n\t\t\t// Auth is NOT resolved here — local extension deletion requests work offline.\n\t\t\tvalidated = validateContextDeletionRequest(extensionDeletionRequest, preparation.transcript);\n\t\t\t// Reject if reconciliation reduced deletions to zero.\n\t\t\tif (validated.deletedTargets.length === 0) {\n\t\t\t\tthrow new Error(\"No safe context deletions proposed by extension\");\n\t\t\t}\n\t\t\tfromExtension = true;\n\t\t}\n\t}\n\n\t// Planner fallback shared by both paths: no before-compact handler at all, or a handler that\n\t// observed without supplying a deletionRequest. Resolves auth lazily; undefined means auth is\n\t// unavailable (auto-mode resolvers), so compaction is a no-op.\n\tif (!validated) {\n\t\tconst plannerResult = await runPlanner();\n\t\tif (!plannerResult) {\n\t\t\treturn undefined;\n\t\t}\n\t\tvalidated = plannerResult;\n\t}\n\n\tif (options.abortController.signal.aborted) {\n\t\tthrow new Error(\"Compaction cancelled\");\n\t}\n\n\tconst backupPath = this.sessionManager.writeBackupSnapshot(options.backupLabel);\n\tconst compactionEntryId = this.sessionManager.appendContextCompaction(\n\t\tvalidated.deletedTargets,\n\t\tvalidated.protectedEntryIds,\n\t\tvalidated.stats,\n\t\tbackupPath,\n\t);\n\tconst sessionContext = this.sessionManager.buildSessionContext();\n\tthis.agent.state.messages = sessionContext.messages;\n\n\tconst result: ContextCompactionResult = {\n\t\t...validated,\n\t\tpromptVersion: 1,\n\t\tparameters,\n\t\t...(backupPath ? { backupPath } : {}),\n\t};\n\n\t// Emit session_compact so extensions can observe the validated result. This is a pure\n\t// observation hook fired AFTER the compaction has been committed (backup written,\n\t// context_compaction entry persisted, active context rebuilt). A misbehaving observer must\n\t// never turn a successful, already-persisted compaction into a reported failure, so any\n\t// throw is routed to the non-fatal extension-error channel and compaction still reports\n\t// success.\n\tconst contextCompactionEntry = this.sessionManager.getEntry(compactionEntryId) as ContextCompactionEntry;\n\ttry {\n\t\tawait this._extensionRunner.emit({\n\t\t\ttype: \"session_compact\",\n\t\t\treason: options.reason,\n\t\t\tparameters,\n\t\t\tresult,\n\t\t\tcontextCompactionEntry,\n\t\t\tfromExtension,\n\t\t} satisfies SessionCompactEvent);\n\t} catch (error) {\n\t\tthis._extensionRunner.emitError({\n\t\t\textensionPath: \"<session_compact>\",\n\t\t\tevent: \"session_compact\",\n\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\tstack: error instanceof Error ? error.stack : undefined,\n\t\t});\n\t}\n\n\treturn result;\n}\n\n/**\n * Manually compact the session context using deletion-only verbatim context compaction.\n * Aborts current agent operation first.\n */\n\nexport async function compact(this: AgentSession, options: Partial<ContextCompactionParameters> = {}): Promise<ContextCompactionResult> {\n\tthis._disconnectFromAgent();\n\tawait this.abort();\n\tthis._compactionAbortController = new AbortController();\n\tthis._emit({ type: \"compaction_start\", reason: \"manual\" });\n\n\ttry {\n\t\tif (!this.model) {\n\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t}\n\n\t\t// Auth is resolved lazily: only called when the planner fallback is needed.\n\t\t// Extensions that provide a deletionRequest work without configured credentials.\n\t\tconst model = this.model;\n\t\tconst result = await this._applyContextVerbatimCompaction({\n\t\t\tresolvePlannerAuth: () => this._getRequiredRequestAuth(model),\n\t\t\tabortController: this._compactionAbortController,\n\t\t\tbackupLabel: \"compact\",\n\t\t\treason: \"manual\",\n\t\t\t...(options.compression_ratio === undefined ? {} : { compression_ratio: options.compression_ratio }),\n\t\t\t...(options.preserve_recent === undefined ? {} : { preserve_recent: options.preserve_recent }),\n\t\t\t...(options.query === undefined ? {} : { query: options.query }),\n\t\t});\n\t\tif (!result) {\n\t\t\tthrow new Error(\"Nothing to compact (session too small)\");\n\t\t}\n\n\t\tthis._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t});\n\t\treturn result;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\tthis._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: undefined,\n\t\t\taborted,\n\t\t\twillRetry: false,\n\t\t\terrorMessage: aborted ? undefined : `Compaction failed: ${message}`,\n\t\t});\n\t\tthrow error;\n\t} finally {\n\t\tthis._compactionAbortController = undefined;\n\t\tthis._reconnectToAgent();\n\t}\n}\n\n/**\n * Manually compact the session context by applying validated logical deletions.\n * Retained transcript entries/content blocks stay verbatim; no user prompt text is accepted.\n */\n\nexport async function contextCompact(this: AgentSession): Promise<ContextCompactionResult> {\n\tthis._disconnectFromAgent();\n\tawait this.abort();\n\tthis._compactionAbortController = new AbortController();\n\tthis._emit({ type: \"context_compaction_start\", reason: \"manual\" });\n\n\ttry {\n\t\tif (!this.model) {\n\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t}\n\n\t\t// Auth is resolved lazily: only called when the planner fallback is needed.\n\t\t// Extensions that provide a deletionRequest work without configured credentials.\n\t\tconst model = this.model;\n\t\tconst result = await this._applyContextVerbatimCompaction({\n\t\t\tresolvePlannerAuth: () => this._getRequiredRequestAuth(model),\n\t\t\tabortController: this._compactionAbortController,\n\t\t\tbackupLabel: \"context-compact\",\n\t\t\treason: \"manual\",\n\t\t});\n\t\tif (!result) {\n\t\t\tthrow new Error(\"Nothing to context-compact (session too small)\");\n\t\t}\n\n\t\tthis._emit({\n\t\t\ttype: \"context_compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult,\n\t\t\taborted: false,\n\t\t\twillRetry: false,\n\t\t});\n\t\treturn result;\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\tthis._emit({\n\t\t\ttype: \"context_compaction_end\",\n\t\t\treason: \"manual\",\n\t\t\tresult: undefined,\n\t\t\taborted,\n\t\t\twillRetry: false,\n\t\t\terrorMessage: aborted ? undefined : `Context compaction failed: ${message}`,\n\t\t});\n\t\tthrow error;\n\t} finally {\n\t\tthis._compactionAbortController = undefined;\n\t\tthis._reconnectToAgent();\n\t}\n}\n\n/**\n * Cancel in-progress compaction (manual or auto).\n */\n\nexport function abortCompaction(this: AgentSession): void {\n\tthis._compactionAbortController?.abort();\n\tthis._autoCompactionAbortController?.abort();\n}\n\n/**\n * Cancel in-progress branch summarization.\n */\n\nexport function abortBranchSummary(this: AgentSession): void {\n\tthis._branchSummaryAbortController?.abort();\n}\n\n/**\n * Check if compaction is needed and run it.\n * Called after agent_end and before prompt submission.\n *\n * Two cases:\n * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry\n * 2. Threshold: Context over threshold, compact, resume queued active-turn work if present; otherwise wait for user\n *\n * @param assistantMessage The assistant message to check\n * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true\n */\n\nexport function setAutoCompactionEnabled(this: AgentSession, enabled: boolean): void {\n\tthis.settingsManager.setCompactionEnabled(enabled);\n}\n\n/** Whether auto-compaction is enabled */\n\nexport const agentSessionCompactionMethods = {\n\t_applyContextVerbatimCompaction,\n\tcompact,\n\tcontextCompact,\n\tabortCompaction,\n\tabortBranchSummary,\n\tsetAutoCompactionEnabled,\n};\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CompactableTranscript, CompactableTranscriptEntry, ContextCompactionParameters } from "./context-compaction-types.ts";
|
|
2
|
+
export declare const CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT = 5;
|
|
3
|
+
export declare function criticalCompactionParameters(parameters: ContextCompactionParameters): ContextCompactionParameters;
|
|
4
|
+
export declare function isCriticalOverflowProtectedEntryDeletable(entry: CompactableTranscriptEntry, transcript: CompactableTranscript): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Return a transcript for critical overflow passes. The effective recent guard is
|
|
7
|
+
* widened to max(configured preserve_recent, 5) over all entries, restoring the
|
|
8
|
+
* pre-#1399 critical-overflow last-5 floor through the existing recent-target
|
|
9
|
+
* validation path. Only stale protected task-bearing entries outside that floor
|
|
10
|
+
* are relaxed; `protectedEntryIds` is rebuilt from still-protected entries so
|
|
11
|
+
* persisted results/events stay disjoint from deleted targets.
|
|
12
|
+
*/
|
|
13
|
+
export declare function relaxTranscriptForCriticalEviction(transcript: CompactableTranscript): CompactableTranscript;
|
|
14
|
+
export declare const CONTEXT_COMPACTION_CRITICAL_OVERFLOW_PROMPT = "\n<critical-overflow-mode>\nThe previous model request overflowed its context window. This is a critical LRU-style compaction pass. First delete stale unprotected context. If that is not enough, you may also delete the earliest formerly-protected entries or protected content shown in the manifest. Evict old low-signal context first, including old reasoning/thinking traces when they are not part of the latest retained assistant message, then older user/custom/summary context while preserving recent entries, unresolved errors, failed commands, and enough task-bearing context for the assistant to continue.\n\nSafety invariant: the latest retained assistant message cannot be modified when it contains content blocks with type \"thinking\" or \"redacted_thinking\". Do not delete, target, or suggest any content block in that latest thinking-bearing assistant message, including sibling text or tool-call blocks. Older non-latest thinking/redacted_thinking blocks may be deleted during critical overflow when validation allows it.\n</critical-overflow-mode>";
|
|
15
|
+
//# sourceMappingURL=context-compaction-critical.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-compaction-critical.d.ts","sourceRoot":"","sources":["../../../src/core/compaction/context-compaction-critical.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAKpI,eAAO,MAAM,4CAA4C,IAAI,CAAC;AAE9D,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,2BAA2B,GAAG,2BAA2B,CAKjH;AAED,wBAAgB,yCAAyC,CACxD,KAAK,EAAE,0BAA0B,EACjC,UAAU,EAAE,qBAAqB,GAC/B,OAAO,CAUT;AAED;;;;;;;GAOG;AACH,wBAAgB,kCAAkC,CAAC,UAAU,EAAE,qBAAqB,GAAG,qBAAqB,CAgB3G;AAED,eAAO,MAAM,2CAA2C,0iCAK9B,CAAC","sourcesContent":["import type { CompactableTranscript, CompactableTranscriptEntry, ContextCompactionParameters } from \"./context-compaction-types.ts\";\nimport { getTranscriptCompactionParameters } from \"./context-compaction-strategy.ts\";\nimport { isTaskBearingEntry } from \"./context-deletion-targets.ts\";\nimport { hasAssistantError, hasFailedBashExecution, hasToolResultError } from \"./context-transcript-analysis.ts\";\n\nexport const CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT = 5;\n\nexport function criticalCompactionParameters(parameters: ContextCompactionParameters): ContextCompactionParameters {\n\treturn {\n\t\t...parameters,\n\t\tpreserve_recent: Math.max(parameters.preserve_recent, CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT),\n\t};\n}\n\nexport function isCriticalOverflowProtectedEntryDeletable(\n\tentry: CompactableTranscriptEntry,\n\ttranscript: CompactableTranscript,\n): boolean {\n\tif (!entry.protected) return true;\n\tconst entryIndex = transcript.entries.findIndex((candidate) => candidate.entryId === entry.entryId);\n\tif (entryIndex < 0) return false;\n\tconst recentBoundary = Math.max(0, transcript.entries.length - CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT);\n\tif (entryIndex >= recentBoundary) return false;\n\tif (hasAssistantError(entry.message) || hasToolResultError(entry.message) || hasFailedBashExecution(entry.message)) {\n\t\treturn false;\n\t}\n\treturn isTaskBearingEntry(entry);\n}\n\n/**\n * Return a transcript for critical overflow passes. The effective recent guard is\n * widened to max(configured preserve_recent, 5) over all entries, restoring the\n * pre-#1399 critical-overflow last-5 floor through the existing recent-target\n * validation path. Only stale protected task-bearing entries outside that floor\n * are relaxed; `protectedEntryIds` is rebuilt from still-protected entries so\n * persisted results/events stay disjoint from deleted targets.\n */\nexport function relaxTranscriptForCriticalEviction(transcript: CompactableTranscript): CompactableTranscript {\n\tconst entries = transcript.entries.map((entry) => {\n\t\tif (!entry.protected || !isCriticalOverflowProtectedEntryDeletable(entry, transcript)) return entry;\n\t\treturn {\n\t\t\t...entry,\n\t\t\tprotected: false,\n\t\t\tcontentBlocks: entry.contentBlocks.map((block) => ({ ...block, protected: false })),\n\t\t};\n\t});\n\tconst parameters = criticalCompactionParameters(getTranscriptCompactionParameters(transcript));\n\treturn {\n\t\t...transcript,\n\t\tentries,\n\t\tparameters,\n\t\tprotectedEntryIds: entries.filter((entry) => entry.protected).map((entry) => entry.entryId),\n\t};\n}\n\nexport const CONTEXT_COMPACTION_CRITICAL_OVERFLOW_PROMPT = `\n<critical-overflow-mode>\nThe previous model request overflowed its context window. This is a critical LRU-style compaction pass. First delete stale unprotected context. If that is not enough, you may also delete the earliest formerly-protected entries or protected content shown in the manifest. Evict old low-signal context first, including old reasoning/thinking traces when they are not part of the latest retained assistant message, then older user/custom/summary context while preserving recent entries, unresolved errors, failed commands, and enough task-bearing context for the assistant to continue.\n\nSafety invariant: the latest retained assistant message cannot be modified when it contains content blocks with type \"thinking\" or \"redacted_thinking\". Do not delete, target, or suggest any content block in that latest thinking-bearing assistant message, including sibling text or tool-call blocks. Older non-latest thinking/redacted_thinking blocks may be deleted during critical overflow when validation allows it.\n</critical-overflow-mode>`;\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getTranscriptCompactionParameters } from "./context-compaction-strategy.js";
|
|
2
|
+
import { isTaskBearingEntry } from "./context-deletion-targets.js";
|
|
3
|
+
import { hasAssistantError, hasFailedBashExecution, hasToolResultError } from "./context-transcript-analysis.js";
|
|
4
|
+
export const CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT = 5;
|
|
5
|
+
export function criticalCompactionParameters(parameters) {
|
|
6
|
+
return {
|
|
7
|
+
...parameters,
|
|
8
|
+
preserve_recent: Math.max(parameters.preserve_recent, CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function isCriticalOverflowProtectedEntryDeletable(entry, transcript) {
|
|
12
|
+
if (!entry.protected)
|
|
13
|
+
return true;
|
|
14
|
+
const entryIndex = transcript.entries.findIndex((candidate) => candidate.entryId === entry.entryId);
|
|
15
|
+
if (entryIndex < 0)
|
|
16
|
+
return false;
|
|
17
|
+
const recentBoundary = Math.max(0, transcript.entries.length - CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT);
|
|
18
|
+
if (entryIndex >= recentBoundary)
|
|
19
|
+
return false;
|
|
20
|
+
if (hasAssistantError(entry.message) || hasToolResultError(entry.message) || hasFailedBashExecution(entry.message)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return isTaskBearingEntry(entry);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Return a transcript for critical overflow passes. The effective recent guard is
|
|
27
|
+
* widened to max(configured preserve_recent, 5) over all entries, restoring the
|
|
28
|
+
* pre-#1399 critical-overflow last-5 floor through the existing recent-target
|
|
29
|
+
* validation path. Only stale protected task-bearing entries outside that floor
|
|
30
|
+
* are relaxed; `protectedEntryIds` is rebuilt from still-protected entries so
|
|
31
|
+
* persisted results/events stay disjoint from deleted targets.
|
|
32
|
+
*/
|
|
33
|
+
export function relaxTranscriptForCriticalEviction(transcript) {
|
|
34
|
+
const entries = transcript.entries.map((entry) => {
|
|
35
|
+
if (!entry.protected || !isCriticalOverflowProtectedEntryDeletable(entry, transcript))
|
|
36
|
+
return entry;
|
|
37
|
+
return {
|
|
38
|
+
...entry,
|
|
39
|
+
protected: false,
|
|
40
|
+
contentBlocks: entry.contentBlocks.map((block) => ({ ...block, protected: false })),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
const parameters = criticalCompactionParameters(getTranscriptCompactionParameters(transcript));
|
|
44
|
+
return {
|
|
45
|
+
...transcript,
|
|
46
|
+
entries,
|
|
47
|
+
parameters,
|
|
48
|
+
protectedEntryIds: entries.filter((entry) => entry.protected).map((entry) => entry.entryId),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export const CONTEXT_COMPACTION_CRITICAL_OVERFLOW_PROMPT = `
|
|
52
|
+
<critical-overflow-mode>
|
|
53
|
+
The previous model request overflowed its context window. This is a critical LRU-style compaction pass. First delete stale unprotected context. If that is not enough, you may also delete the earliest formerly-protected entries or protected content shown in the manifest. Evict old low-signal context first, including old reasoning/thinking traces when they are not part of the latest retained assistant message, then older user/custom/summary context while preserving recent entries, unresolved errors, failed commands, and enough task-bearing context for the assistant to continue.
|
|
54
|
+
|
|
55
|
+
Safety invariant: the latest retained assistant message cannot be modified when it contains content blocks with type "thinking" or "redacted_thinking". Do not delete, target, or suggest any content block in that latest thinking-bearing assistant message, including sibling text or tool-call blocks. Older non-latest thinking/redacted_thinking blocks may be deleted during critical overflow when validation allows it.
|
|
56
|
+
</critical-overflow-mode>`;
|
|
57
|
+
//# sourceMappingURL=context-compaction-critical.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-compaction-critical.js","sourceRoot":"","sources":["../../../src/core/compaction/context-compaction-critical.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iCAAiC,EAAE,MAAM,kCAAkC,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEjH,MAAM,CAAC,MAAM,4CAA4C,GAAG,CAAC,CAAC;AAE9D,MAAM,UAAU,4BAA4B,CAAC,UAAuC;IACnF,OAAO;QACN,GAAG,UAAU;QACb,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,eAAe,EAAE,4CAA4C,CAAC;KACnG,CAAC;AACH,CAAC;AAED,MAAM,UAAU,yCAAyC,CACxD,KAAiC,EACjC,UAAiC;IAEjC,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAClC,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC;IACpG,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACjC,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,MAAM,GAAG,4CAA4C,CAAC,CAAC;IAC7G,IAAI,UAAU,IAAI,cAAc;QAAE,OAAO,KAAK,CAAC;IAC/C,IAAI,iBAAiB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,sBAAsB,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACpH,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kCAAkC,CAAC,UAAiC;IACnF,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAChD,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,yCAAyC,CAAC,KAAK,EAAE,UAAU,CAAC;YAAE,OAAO,KAAK,CAAC;QACpG,OAAO;YACN,GAAG,KAAK;YACR,SAAS,EAAE,KAAK;YAChB,aAAa,EAAE,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;SACnF,CAAC;IACH,CAAC,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,4BAA4B,CAAC,iCAAiC,CAAC,UAAU,CAAC,CAAC,CAAC;IAC/F,OAAO;QACN,GAAG,UAAU;QACb,OAAO;QACP,UAAU;QACV,iBAAiB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC;KAC3F,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,2CAA2C,GAAG;;;;;0BAKjC,CAAC","sourcesContent":["import type { CompactableTranscript, CompactableTranscriptEntry, ContextCompactionParameters } from \"./context-compaction-types.ts\";\nimport { getTranscriptCompactionParameters } from \"./context-compaction-strategy.ts\";\nimport { isTaskBearingEntry } from \"./context-deletion-targets.ts\";\nimport { hasAssistantError, hasFailedBashExecution, hasToolResultError } from \"./context-transcript-analysis.ts\";\n\nexport const CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT = 5;\n\nexport function criticalCompactionParameters(parameters: ContextCompactionParameters): ContextCompactionParameters {\n\treturn {\n\t\t...parameters,\n\t\tpreserve_recent: Math.max(parameters.preserve_recent, CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT),\n\t};\n}\n\nexport function isCriticalOverflowProtectedEntryDeletable(\n\tentry: CompactableTranscriptEntry,\n\ttranscript: CompactableTranscript,\n): boolean {\n\tif (!entry.protected) return true;\n\tconst entryIndex = transcript.entries.findIndex((candidate) => candidate.entryId === entry.entryId);\n\tif (entryIndex < 0) return false;\n\tconst recentBoundary = Math.max(0, transcript.entries.length - CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT);\n\tif (entryIndex >= recentBoundary) return false;\n\tif (hasAssistantError(entry.message) || hasToolResultError(entry.message) || hasFailedBashExecution(entry.message)) {\n\t\treturn false;\n\t}\n\treturn isTaskBearingEntry(entry);\n}\n\n/**\n * Return a transcript for critical overflow passes. The effective recent guard is\n * widened to max(configured preserve_recent, 5) over all entries, restoring the\n * pre-#1399 critical-overflow last-5 floor through the existing recent-target\n * validation path. Only stale protected task-bearing entries outside that floor\n * are relaxed; `protectedEntryIds` is rebuilt from still-protected entries so\n * persisted results/events stay disjoint from deleted targets.\n */\nexport function relaxTranscriptForCriticalEviction(transcript: CompactableTranscript): CompactableTranscript {\n\tconst entries = transcript.entries.map((entry) => {\n\t\tif (!entry.protected || !isCriticalOverflowProtectedEntryDeletable(entry, transcript)) return entry;\n\t\treturn {\n\t\t\t...entry,\n\t\t\tprotected: false,\n\t\t\tcontentBlocks: entry.contentBlocks.map((block) => ({ ...block, protected: false })),\n\t\t};\n\t});\n\tconst parameters = criticalCompactionParameters(getTranscriptCompactionParameters(transcript));\n\treturn {\n\t\t...transcript,\n\t\tentries,\n\t\tparameters,\n\t\tprotectedEntryIds: entries.filter((entry) => entry.protected).map((entry) => entry.entryId),\n\t};\n}\n\nexport const CONTEXT_COMPACTION_CRITICAL_OVERFLOW_PROMPT = `\n<critical-overflow-mode>\nThe previous model request overflowed its context window. This is a critical LRU-style compaction pass. First delete stale unprotected context. If that is not enough, you may also delete the earliest formerly-protected entries or protected content shown in the manifest. Evict old low-signal context first, including old reasoning/thinking traces when they are not part of the latest retained assistant message, then older user/custom/summary context while preserving recent entries, unresolved errors, failed commands, and enough task-bearing context for the assistant to continue.\n\nSafety invariant: the latest retained assistant message cannot be modified when it contains content blocks with type \"thinking\" or \"redacted_thinking\". Do not delete, target, or suggest any content block in that latest thinking-bearing assistant message, including sibling text or tool-call blocks. Older non-latest thinking/redacted_thinking blocks may be deleted during critical overflow when validation allows it.\n</critical-overflow-mode>`;\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CompactableTranscript, ValidatedContextDeletionResult } from "./context-compaction-types.ts";
|
|
2
|
+
export declare const CONTEXT_COMPACTION_MAX_EVICTION_PASSES = 50;
|
|
3
|
+
export declare function runDeterministicContextEviction(transcript: CompactableTranscript, tokenBudget: number): ValidatedContextDeletionResult;
|
|
4
|
+
//# sourceMappingURL=context-compaction-eviction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-compaction-eviction.d.ts","sourceRoot":"","sources":["../../../src/core/compaction/context-compaction-eviction.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,8BAA8B,EAAE,MAAM,+BAA+B,CAAC;AAM3G,eAAO,MAAM,sCAAsC,KAAK,CAAC;AAgKzD,wBAAgB,+BAA+B,CAC9C,UAAU,EAAE,qBAAqB,EACjC,WAAW,EAAE,MAAM,GACjB,8BAA8B,CAmDhC","sourcesContent":["import type { ContextDeletionTarget } from \"../session-manager.ts\";\nimport type { CompactableTranscript, ValidatedContextDeletionResult } from \"./context-compaction-types.ts\";\nimport { validateContextDeletionRequest } from \"./context-deletion-application.ts\";\nimport { canDeleteTarget, deletionRequestFromTargets, isTaskBearingEntry } from \"./context-deletion-targets.ts\";\nimport { relaxTranscriptForCriticalEviction } from \"./context-compaction-critical.ts\";\nimport { assistantEntryHasThinkingContentBlock } from \"./context-transcript-analysis.ts\";\n\nexport const CONTEXT_COMPACTION_MAX_EVICTION_PASSES = 50;\n\nfunction terminalDeterministicEvictionError(\n\treason: string,\n\tlastValidated: ValidatedContextDeletionResult | undefined,\n\ttokenBudget: number,\n): Error {\n\tconst statsText = lastValidated\n\t\t? `achieved tokensAfter=${lastValidated.stats.tokensAfter}, reduction=${lastValidated.stats.percentReduction}%, deletionTargets=${lastValidated.deletedTargets.length}`\n\t\t: \"achieved no validated deletion targets\";\n\treturn new Error(\n\t\t`Context deterministic overflow eviction failed: ${reason}; ${statsText}; budget=${tokenBudget}; nothing more was safely deletable`,\n\t);\n}\n\n\nfunction hasFitBudget(result: ValidatedContextDeletionResult | undefined, tokenBudget: number): result is ValidatedContextDeletionResult {\n\treturn result !== undefined && result.deletedTargets.length > 0 && result.stats.tokensAfter <= tokenBudget;\n}\n\nfunction isThinkingAssistantEntryTarget(transcript: CompactableTranscript, target: ContextDeletionTarget): boolean {\n\tif (target.kind !== \"entry\") return false;\n\tconst entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);\n\treturn entry?.role === \"assistant\" && assistantEntryHasThinkingContentBlock(entry);\n}\n\nfunction isTaskBearingEntryTarget(transcript: CompactableTranscript, target: ContextDeletionTarget): boolean {\n\tif (target.kind !== \"entry\") return false;\n\tconst entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);\n\treturn entry !== undefined && isTaskBearingEntry(entry);\n}\n\nfunction targetSignature(targets: readonly ContextDeletionTarget[]): string {\n\treturn targets\n\t\t.map((target) => `${target.kind}:${target.entryId}:${target.kind === \"content_block\" ? target.blockIndex : \"\"}`)\n\t\t.join(\"|\");\n}\n\nfunction entryOrder(transcript: CompactableTranscript, entryId: string): number {\n\tconst index = transcript.entries.findIndex((entry) => entry.entryId === entryId);\n\treturn index < 0 ? Number.MAX_SAFE_INTEGER : index;\n}\n\nfunction entryTokenEstimate(transcript: CompactableTranscript, entryId: string): number {\n\treturn transcript.entries.find((entry) => entry.entryId === entryId)?.tokenEstimate ?? 0;\n}\n\nfunction smallestTaskTarget(transcript: CompactableTranscript, targets: readonly ContextDeletionTarget[]): ContextDeletionTarget | undefined {\n\treturn [...targets]\n\t\t.filter((target) => isTaskBearingEntryTarget(transcript, target))\n\t\t.sort((left: ContextDeletionTarget, right: ContextDeletionTarget) => {\n\t\t\tconst tokenDelta = entryTokenEstimate(transcript, left.entryId) - entryTokenEstimate(transcript, right.entryId);\n\t\t\tif (tokenDelta !== 0) return tokenDelta;\n\t\t\treturn entryOrder(transcript, left.entryId) - entryOrder(transcript, right.entryId);\n\t\t})[0];\n}\n\nfunction newestThinkingTarget(transcript: CompactableTranscript, targets: readonly ContextDeletionTarget[]): ContextDeletionTarget | undefined {\n\treturn [...targets]\n\t\t.filter((target) => isThinkingAssistantEntryTarget(transcript, target))\n\t\t.sort((left: ContextDeletionTarget, right: ContextDeletionTarget) => entryOrder(transcript, right.entryId) - entryOrder(transcript, left.entryId))[0];\n}\n\ntype ThinkingExchange = \"keep-all\" | \"drop-all\" | \"retain-newest\";\ntype TaskExchange = \"keep-all\" | \"drop-all\" | \"retain-smallest\";\n\nconst THINKING_EXCHANGES: readonly ThinkingExchange[] = [\"keep-all\", \"drop-all\", \"retain-newest\"];\nconst TASK_EXCHANGES: readonly TaskExchange[] = [\"keep-all\", \"drop-all\", \"retain-smallest\"];\n\nfunction shouldDeleteTarget(\n\ttranscript: CompactableTranscript,\n\ttarget: ContextDeletionTarget,\n\tthinkingExchange: ThinkingExchange,\n\ttaskExchange: TaskExchange,\n\tretainedThinking: ContextDeletionTarget | undefined,\n\tretainedTask: ContextDeletionTarget | undefined,\n): boolean {\n\tif (thinkingExchange === \"drop-all\" && isThinkingAssistantEntryTarget(transcript, target)) return false;\n\tif (thinkingExchange === \"retain-newest\" && retainedThinking !== undefined && targetSignature([target]) === targetSignature([retainedThinking])) return false;\n\tif (taskExchange === \"drop-all\" && isTaskBearingEntryTarget(transcript, target)) return false;\n\tif (taskExchange === \"retain-smallest\" && retainedTask !== undefined && targetSignature([target]) === targetSignature([retainedTask])) return false;\n\treturn true;\n}\n\nfunction buildExchangePlan(\n\ttranscript: CompactableTranscript,\n\twithCandidate: readonly ContextDeletionTarget[],\n\tthinkingExchange: ThinkingExchange,\n\ttaskExchange: TaskExchange,\n): ContextDeletionTarget[] {\n\tconst retainedThinking = thinkingExchange === \"retain-newest\" ? newestThinkingTarget(transcript, withCandidate) : undefined;\n\tconst retainedTask = taskExchange === \"retain-smallest\" ? smallestTaskTarget(transcript, withCandidate) : undefined;\n\treturn withCandidate.filter((target) =>\n\t\tshouldDeleteTarget(transcript, target, thinkingExchange, taskExchange, retainedThinking, retainedTask),\n\t);\n}\n\nfunction shouldBuildExchangePlan(\n\twithCandidate: readonly ContextDeletionTarget[],\n\tthinkingExchange: ThinkingExchange,\n\ttaskExchange: TaskExchange,\n): boolean {\n\tif (thinkingExchange === \"keep-all\" && taskExchange === \"keep-all\") return false;\n\tif (thinkingExchange === \"retain-newest\" && withCandidate.filter((target) => target.kind === \"entry\").length === 0) return false;\n\treturn true;\n}\n\nfunction addExchangePlan(\n\tplans: ContextDeletionTarget[][],\n\tseen: Set<string>,\n\toriginalTargets: readonly ContextDeletionTarget[],\n\ttargets: readonly ContextDeletionTarget[],\n): void {\n\tif (targetSignature(targets) === targetSignature(originalTargets)) return;\n\tconst signature = targetSignature(targets);\n\tif (seen.has(signature)) return;\n\tseen.add(signature);\n\tplans.push([...targets]);\n}\n\nfunction exchangePlans(\n\ttranscript: CompactableTranscript,\n\tplanned: readonly ContextDeletionTarget[],\n\tentryId: string,\n): ContextDeletionTarget[][] {\n\tconst currentTarget: ContextDeletionTarget = { kind: \"entry\", entryId };\n\tconst withCandidate = [...planned, currentTarget];\n\tconst plans: ContextDeletionTarget[][] = [];\n\tconst seen = new Set<string>();\n\t// Deterministic cross-product order: thinking handling outermost, then task handling.\n\t// Variants are keep/drop-all/retain-one for each non-monotone category, minus the\n\t// keep-all/keep-all no-op; signature dedup collapses degenerate variants.\n\tfor (const thinkingExchange of THINKING_EXCHANGES) {\n\t\tfor (const taskExchange of TASK_EXCHANGES) {\n\t\t\tif (!shouldBuildExchangePlan(withCandidate, thinkingExchange, taskExchange)) continue;\n\t\t\taddExchangePlan(plans, seen, withCandidate, buildExchangePlan(transcript, withCandidate, thinkingExchange, taskExchange));\n\t\t}\n\t}\n\treturn plans;\n}\n\nfunction validatedExchange(\n\ttranscript: CompactableTranscript,\n\tplanned: readonly ContextDeletionTarget[],\n\tentryId: string,\n\tlastValidated: ValidatedContextDeletionResult | undefined,\n): ValidatedContextDeletionResult | undefined {\n\tlet best: ValidatedContextDeletionResult | undefined;\n\tfor (const retryTargets of exchangePlans(transcript, planned, entryId)) {\n\t\ttry {\n\t\t\tconst result = validateContextDeletionRequest(deletionRequestFromTargets(retryTargets), transcript);\n\t\t\tif (lastValidated !== undefined && result.stats.tokensAfter >= lastValidated.stats.tokensAfter) continue;\n\t\t\tif (best === undefined || result.stats.tokensAfter < best.stats.tokensAfter) best = result;\n\t\t} catch {\n\t\t\t// Try the next deterministic exchange variant.\n\t\t}\n\t}\n\treturn best;\n}\n\nexport function runDeterministicContextEviction(\n\ttranscript: CompactableTranscript,\n\ttokenBudget: number,\n): ValidatedContextDeletionResult {\n\tconst relaxed = relaxTranscriptForCriticalEviction(transcript);\n\tconst latestAssistant = [...relaxed.entries].reverse().find((entry) => entry.role === \"assistant\");\n\tconst protectedThinkingAssistant = latestAssistant && assistantEntryHasThinkingContentBlock(latestAssistant) ? latestAssistant.entryId : undefined;\n\tconst candidates = relaxed.entries\n\t\t.filter((entry) => entry.entryId !== protectedThinkingAssistant)\n\t\t.filter((entry) => canDeleteTarget(relaxed, { kind: \"entry\", entryId: entry.entryId }))\n\t\t.map((entry) => entry.entryId);\n\tconst planned: ContextDeletionTarget[] = [];\n\tconst excluded = new Set<string>();\n\tlet lastValidated: ValidatedContextDeletionResult | undefined;\n\n\t// Hard iteration cap required by the compaction-ladder design: every compaction loop must be\n\t// bounded (max 50) so no tier can spin indefinitely. Non-monotone validation rules can require\n\t// local exchange: a prior thinking-bearing or task-bearing deletion can make a newer candidate\n\t// invalid, even though replacing the prior deletion with that candidate is safe and fits better.\n\t// Exchanges are deterministic and adopted only when validation succeeds and tokensAfter strictly\n\t// decreases; exclusions grow monotonically, so no oscillation is possible, and the pass cap remains\n\t// the terminal safety belt.\n\tfor (let pass = 0; pass < CONTEXT_COMPACTION_MAX_EVICTION_PASSES; pass++) {\n\t\tif (hasFitBudget(lastValidated, tokenBudget)) return lastValidated;\n\t\tlet changed = false;\n\t\tfor (const entryId of candidates) {\n\t\t\tif (hasFitBudget(lastValidated, tokenBudget)) return lastValidated;\n\t\t\tif (planned.some((target) => target.entryId === entryId) || excluded.has(entryId)) continue;\n\t\t\tconst nextPlanned: ContextDeletionTarget[] = [...planned, { kind: \"entry\", entryId }];\n\t\t\ttry {\n\t\t\t\tlastValidated = validateContextDeletionRequest(deletionRequestFromTargets(nextPlanned), relaxed);\n\t\t\t\tplanned.splice(0, planned.length, ...lastValidated.deletedTargets);\n\t\t\t\tchanged = true;\n\t\t\t} catch {\n\t\t\t\tconst exchanged = validatedExchange(relaxed, planned, entryId, lastValidated);\n\t\t\t\tif (exchanged !== undefined) {\n\t\t\t\t\tlastValidated = exchanged;\n\t\t\t\t\tplanned.splice(0, planned.length, ...lastValidated.deletedTargets);\n\t\t\t\t\tchanged = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\texcluded.add(entryId);\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\tif (!changed) {\n\t\t\tthrow terminalDeterministicEvictionError(\"candidate sweep exhausted\", lastValidated, tokenBudget);\n\t\t}\n\t}\n\tthrow terminalDeterministicEvictionError(\n\t\t`reached ${CONTEXT_COMPACTION_MAX_EVICTION_PASSES} eviction pass cap`,\n\t\tlastValidated,\n\t\ttokenBudget,\n\t);\n}\n"]}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { validateContextDeletionRequest } from "./context-deletion-application.js";
|
|
2
|
+
import { canDeleteTarget, deletionRequestFromTargets, isTaskBearingEntry } from "./context-deletion-targets.js";
|
|
3
|
+
import { relaxTranscriptForCriticalEviction } from "./context-compaction-critical.js";
|
|
4
|
+
import { assistantEntryHasThinkingContentBlock } from "./context-transcript-analysis.js";
|
|
5
|
+
export const CONTEXT_COMPACTION_MAX_EVICTION_PASSES = 50;
|
|
6
|
+
function terminalDeterministicEvictionError(reason, lastValidated, tokenBudget) {
|
|
7
|
+
const statsText = lastValidated
|
|
8
|
+
? `achieved tokensAfter=${lastValidated.stats.tokensAfter}, reduction=${lastValidated.stats.percentReduction}%, deletionTargets=${lastValidated.deletedTargets.length}`
|
|
9
|
+
: "achieved no validated deletion targets";
|
|
10
|
+
return new Error(`Context deterministic overflow eviction failed: ${reason}; ${statsText}; budget=${tokenBudget}; nothing more was safely deletable`);
|
|
11
|
+
}
|
|
12
|
+
function hasFitBudget(result, tokenBudget) {
|
|
13
|
+
return result !== undefined && result.deletedTargets.length > 0 && result.stats.tokensAfter <= tokenBudget;
|
|
14
|
+
}
|
|
15
|
+
function isThinkingAssistantEntryTarget(transcript, target) {
|
|
16
|
+
if (target.kind !== "entry")
|
|
17
|
+
return false;
|
|
18
|
+
const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
|
|
19
|
+
return entry?.role === "assistant" && assistantEntryHasThinkingContentBlock(entry);
|
|
20
|
+
}
|
|
21
|
+
function isTaskBearingEntryTarget(transcript, target) {
|
|
22
|
+
if (target.kind !== "entry")
|
|
23
|
+
return false;
|
|
24
|
+
const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
|
|
25
|
+
return entry !== undefined && isTaskBearingEntry(entry);
|
|
26
|
+
}
|
|
27
|
+
function targetSignature(targets) {
|
|
28
|
+
return targets
|
|
29
|
+
.map((target) => `${target.kind}:${target.entryId}:${target.kind === "content_block" ? target.blockIndex : ""}`)
|
|
30
|
+
.join("|");
|
|
31
|
+
}
|
|
32
|
+
function entryOrder(transcript, entryId) {
|
|
33
|
+
const index = transcript.entries.findIndex((entry) => entry.entryId === entryId);
|
|
34
|
+
return index < 0 ? Number.MAX_SAFE_INTEGER : index;
|
|
35
|
+
}
|
|
36
|
+
function entryTokenEstimate(transcript, entryId) {
|
|
37
|
+
return transcript.entries.find((entry) => entry.entryId === entryId)?.tokenEstimate ?? 0;
|
|
38
|
+
}
|
|
39
|
+
function smallestTaskTarget(transcript, targets) {
|
|
40
|
+
return [...targets]
|
|
41
|
+
.filter((target) => isTaskBearingEntryTarget(transcript, target))
|
|
42
|
+
.sort((left, right) => {
|
|
43
|
+
const tokenDelta = entryTokenEstimate(transcript, left.entryId) - entryTokenEstimate(transcript, right.entryId);
|
|
44
|
+
if (tokenDelta !== 0)
|
|
45
|
+
return tokenDelta;
|
|
46
|
+
return entryOrder(transcript, left.entryId) - entryOrder(transcript, right.entryId);
|
|
47
|
+
})[0];
|
|
48
|
+
}
|
|
49
|
+
function newestThinkingTarget(transcript, targets) {
|
|
50
|
+
return [...targets]
|
|
51
|
+
.filter((target) => isThinkingAssistantEntryTarget(transcript, target))
|
|
52
|
+
.sort((left, right) => entryOrder(transcript, right.entryId) - entryOrder(transcript, left.entryId))[0];
|
|
53
|
+
}
|
|
54
|
+
const THINKING_EXCHANGES = ["keep-all", "drop-all", "retain-newest"];
|
|
55
|
+
const TASK_EXCHANGES = ["keep-all", "drop-all", "retain-smallest"];
|
|
56
|
+
function shouldDeleteTarget(transcript, target, thinkingExchange, taskExchange, retainedThinking, retainedTask) {
|
|
57
|
+
if (thinkingExchange === "drop-all" && isThinkingAssistantEntryTarget(transcript, target))
|
|
58
|
+
return false;
|
|
59
|
+
if (thinkingExchange === "retain-newest" && retainedThinking !== undefined && targetSignature([target]) === targetSignature([retainedThinking]))
|
|
60
|
+
return false;
|
|
61
|
+
if (taskExchange === "drop-all" && isTaskBearingEntryTarget(transcript, target))
|
|
62
|
+
return false;
|
|
63
|
+
if (taskExchange === "retain-smallest" && retainedTask !== undefined && targetSignature([target]) === targetSignature([retainedTask]))
|
|
64
|
+
return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
function buildExchangePlan(transcript, withCandidate, thinkingExchange, taskExchange) {
|
|
68
|
+
const retainedThinking = thinkingExchange === "retain-newest" ? newestThinkingTarget(transcript, withCandidate) : undefined;
|
|
69
|
+
const retainedTask = taskExchange === "retain-smallest" ? smallestTaskTarget(transcript, withCandidate) : undefined;
|
|
70
|
+
return withCandidate.filter((target) => shouldDeleteTarget(transcript, target, thinkingExchange, taskExchange, retainedThinking, retainedTask));
|
|
71
|
+
}
|
|
72
|
+
function shouldBuildExchangePlan(withCandidate, thinkingExchange, taskExchange) {
|
|
73
|
+
if (thinkingExchange === "keep-all" && taskExchange === "keep-all")
|
|
74
|
+
return false;
|
|
75
|
+
if (thinkingExchange === "retain-newest" && withCandidate.filter((target) => target.kind === "entry").length === 0)
|
|
76
|
+
return false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
function addExchangePlan(plans, seen, originalTargets, targets) {
|
|
80
|
+
if (targetSignature(targets) === targetSignature(originalTargets))
|
|
81
|
+
return;
|
|
82
|
+
const signature = targetSignature(targets);
|
|
83
|
+
if (seen.has(signature))
|
|
84
|
+
return;
|
|
85
|
+
seen.add(signature);
|
|
86
|
+
plans.push([...targets]);
|
|
87
|
+
}
|
|
88
|
+
function exchangePlans(transcript, planned, entryId) {
|
|
89
|
+
const currentTarget = { kind: "entry", entryId };
|
|
90
|
+
const withCandidate = [...planned, currentTarget];
|
|
91
|
+
const plans = [];
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
// Deterministic cross-product order: thinking handling outermost, then task handling.
|
|
94
|
+
// Variants are keep/drop-all/retain-one for each non-monotone category, minus the
|
|
95
|
+
// keep-all/keep-all no-op; signature dedup collapses degenerate variants.
|
|
96
|
+
for (const thinkingExchange of THINKING_EXCHANGES) {
|
|
97
|
+
for (const taskExchange of TASK_EXCHANGES) {
|
|
98
|
+
if (!shouldBuildExchangePlan(withCandidate, thinkingExchange, taskExchange))
|
|
99
|
+
continue;
|
|
100
|
+
addExchangePlan(plans, seen, withCandidate, buildExchangePlan(transcript, withCandidate, thinkingExchange, taskExchange));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return plans;
|
|
104
|
+
}
|
|
105
|
+
function validatedExchange(transcript, planned, entryId, lastValidated) {
|
|
106
|
+
let best;
|
|
107
|
+
for (const retryTargets of exchangePlans(transcript, planned, entryId)) {
|
|
108
|
+
try {
|
|
109
|
+
const result = validateContextDeletionRequest(deletionRequestFromTargets(retryTargets), transcript);
|
|
110
|
+
if (lastValidated !== undefined && result.stats.tokensAfter >= lastValidated.stats.tokensAfter)
|
|
111
|
+
continue;
|
|
112
|
+
if (best === undefined || result.stats.tokensAfter < best.stats.tokensAfter)
|
|
113
|
+
best = result;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Try the next deterministic exchange variant.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return best;
|
|
120
|
+
}
|
|
121
|
+
export function runDeterministicContextEviction(transcript, tokenBudget) {
|
|
122
|
+
const relaxed = relaxTranscriptForCriticalEviction(transcript);
|
|
123
|
+
const latestAssistant = [...relaxed.entries].reverse().find((entry) => entry.role === "assistant");
|
|
124
|
+
const protectedThinkingAssistant = latestAssistant && assistantEntryHasThinkingContentBlock(latestAssistant) ? latestAssistant.entryId : undefined;
|
|
125
|
+
const candidates = relaxed.entries
|
|
126
|
+
.filter((entry) => entry.entryId !== protectedThinkingAssistant)
|
|
127
|
+
.filter((entry) => canDeleteTarget(relaxed, { kind: "entry", entryId: entry.entryId }))
|
|
128
|
+
.map((entry) => entry.entryId);
|
|
129
|
+
const planned = [];
|
|
130
|
+
const excluded = new Set();
|
|
131
|
+
let lastValidated;
|
|
132
|
+
// Hard iteration cap required by the compaction-ladder design: every compaction loop must be
|
|
133
|
+
// bounded (max 50) so no tier can spin indefinitely. Non-monotone validation rules can require
|
|
134
|
+
// local exchange: a prior thinking-bearing or task-bearing deletion can make a newer candidate
|
|
135
|
+
// invalid, even though replacing the prior deletion with that candidate is safe and fits better.
|
|
136
|
+
// Exchanges are deterministic and adopted only when validation succeeds and tokensAfter strictly
|
|
137
|
+
// decreases; exclusions grow monotonically, so no oscillation is possible, and the pass cap remains
|
|
138
|
+
// the terminal safety belt.
|
|
139
|
+
for (let pass = 0; pass < CONTEXT_COMPACTION_MAX_EVICTION_PASSES; pass++) {
|
|
140
|
+
if (hasFitBudget(lastValidated, tokenBudget))
|
|
141
|
+
return lastValidated;
|
|
142
|
+
let changed = false;
|
|
143
|
+
for (const entryId of candidates) {
|
|
144
|
+
if (hasFitBudget(lastValidated, tokenBudget))
|
|
145
|
+
return lastValidated;
|
|
146
|
+
if (planned.some((target) => target.entryId === entryId) || excluded.has(entryId))
|
|
147
|
+
continue;
|
|
148
|
+
const nextPlanned = [...planned, { kind: "entry", entryId }];
|
|
149
|
+
try {
|
|
150
|
+
lastValidated = validateContextDeletionRequest(deletionRequestFromTargets(nextPlanned), relaxed);
|
|
151
|
+
planned.splice(0, planned.length, ...lastValidated.deletedTargets);
|
|
152
|
+
changed = true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
const exchanged = validatedExchange(relaxed, planned, entryId, lastValidated);
|
|
156
|
+
if (exchanged !== undefined) {
|
|
157
|
+
lastValidated = exchanged;
|
|
158
|
+
planned.splice(0, planned.length, ...lastValidated.deletedTargets);
|
|
159
|
+
changed = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
excluded.add(entryId);
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!changed) {
|
|
167
|
+
throw terminalDeterministicEvictionError("candidate sweep exhausted", lastValidated, tokenBudget);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw terminalDeterministicEvictionError(`reached ${CONTEXT_COMPACTION_MAX_EVICTION_PASSES} eviction pass cap`, lastValidated, tokenBudget);
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=context-compaction-eviction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-compaction-eviction.js","sourceRoot":"","sources":["../../../src/core/compaction/context-compaction-eviction.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ContextDeletionTarget } from \"../session-manager.ts\";\nimport type { CompactableTranscript, ValidatedContextDeletionResult } from \"./context-compaction-types.ts\";\nimport { validateContextDeletionRequest } from \"./context-deletion-application.ts\";\nimport { canDeleteTarget, deletionRequestFromTargets, isTaskBearingEntry } from \"./context-deletion-targets.ts\";\nimport { relaxTranscriptForCriticalEviction } from \"./context-compaction-critical.ts\";\nimport { assistantEntryHasThinkingContentBlock } from \"./context-transcript-analysis.ts\";\n\nexport const CONTEXT_COMPACTION_MAX_EVICTION_PASSES = 50;\n\nfunction terminalDeterministicEvictionError(\n\treason: string,\n\tlastValidated: ValidatedContextDeletionResult | undefined,\n\ttokenBudget: number,\n): Error {\n\tconst statsText = lastValidated\n\t\t? `achieved tokensAfter=${lastValidated.stats.tokensAfter}, reduction=${lastValidated.stats.percentReduction}%, deletionTargets=${lastValidated.deletedTargets.length}`\n\t\t: \"achieved no validated deletion targets\";\n\treturn new Error(\n\t\t`Context deterministic overflow eviction failed: ${reason}; ${statsText}; budget=${tokenBudget}; nothing more was safely deletable`,\n\t);\n}\n\n\nfunction hasFitBudget(result: ValidatedContextDeletionResult | undefined, tokenBudget: number): result is ValidatedContextDeletionResult {\n\treturn result !== undefined && result.deletedTargets.length > 0 && result.stats.tokensAfter <= tokenBudget;\n}\n\nfunction isThinkingAssistantEntryTarget(transcript: CompactableTranscript, target: ContextDeletionTarget): boolean {\n\tif (target.kind !== \"entry\") return false;\n\tconst entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);\n\treturn entry?.role === \"assistant\" && assistantEntryHasThinkingContentBlock(entry);\n}\n\nfunction isTaskBearingEntryTarget(transcript: CompactableTranscript, target: ContextDeletionTarget): boolean {\n\tif (target.kind !== \"entry\") return false;\n\tconst entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);\n\treturn entry !== undefined && isTaskBearingEntry(entry);\n}\n\nfunction targetSignature(targets: readonly ContextDeletionTarget[]): string {\n\treturn targets\n\t\t.map((target) => `${target.kind}:${target.entryId}:${target.kind === \"content_block\" ? target.blockIndex : \"\"}`)\n\t\t.join(\"|\");\n}\n\nfunction entryOrder(transcript: CompactableTranscript, entryId: string): number {\n\tconst index = transcript.entries.findIndex((entry) => entry.entryId === entryId);\n\treturn index < 0 ? Number.MAX_SAFE_INTEGER : index;\n}\n\nfunction entryTokenEstimate(transcript: CompactableTranscript, entryId: string): number {\n\treturn transcript.entries.find((entry) => entry.entryId === entryId)?.tokenEstimate ?? 0;\n}\n\nfunction smallestTaskTarget(transcript: CompactableTranscript, targets: readonly ContextDeletionTarget[]): ContextDeletionTarget | undefined {\n\treturn [...targets]\n\t\t.filter((target) => isTaskBearingEntryTarget(transcript, target))\n\t\t.sort((left: ContextDeletionTarget, right: ContextDeletionTarget) => {\n\t\t\tconst tokenDelta = entryTokenEstimate(transcript, left.entryId) - entryTokenEstimate(transcript, right.entryId);\n\t\t\tif (tokenDelta !== 0) return tokenDelta;\n\t\t\treturn entryOrder(transcript, left.entryId) - entryOrder(transcript, right.entryId);\n\t\t})[0];\n}\n\nfunction newestThinkingTarget(transcript: CompactableTranscript, targets: readonly ContextDeletionTarget[]): ContextDeletionTarget | undefined {\n\treturn [...targets]\n\t\t.filter((target) => isThinkingAssistantEntryTarget(transcript, target))\n\t\t.sort((left: ContextDeletionTarget, right: ContextDeletionTarget) => entryOrder(transcript, right.entryId) - entryOrder(transcript, left.entryId))[0];\n}\n\ntype ThinkingExchange = \"keep-all\" | \"drop-all\" | \"retain-newest\";\ntype TaskExchange = \"keep-all\" | \"drop-all\" | \"retain-smallest\";\n\nconst THINKING_EXCHANGES: readonly ThinkingExchange[] = [\"keep-all\", \"drop-all\", \"retain-newest\"];\nconst TASK_EXCHANGES: readonly TaskExchange[] = [\"keep-all\", \"drop-all\", \"retain-smallest\"];\n\nfunction shouldDeleteTarget(\n\ttranscript: CompactableTranscript,\n\ttarget: ContextDeletionTarget,\n\tthinkingExchange: ThinkingExchange,\n\ttaskExchange: TaskExchange,\n\tretainedThinking: ContextDeletionTarget | undefined,\n\tretainedTask: ContextDeletionTarget | undefined,\n): boolean {\n\tif (thinkingExchange === \"drop-all\" && isThinkingAssistantEntryTarget(transcript, target)) return false;\n\tif (thinkingExchange === \"retain-newest\" && retainedThinking !== undefined && targetSignature([target]) === targetSignature([retainedThinking])) return false;\n\tif (taskExchange === \"drop-all\" && isTaskBearingEntryTarget(transcript, target)) return false;\n\tif (taskExchange === \"retain-smallest\" && retainedTask !== undefined && targetSignature([target]) === targetSignature([retainedTask])) return false;\n\treturn true;\n}\n\nfunction buildExchangePlan(\n\ttranscript: CompactableTranscript,\n\twithCandidate: readonly ContextDeletionTarget[],\n\tthinkingExchange: ThinkingExchange,\n\ttaskExchange: TaskExchange,\n): ContextDeletionTarget[] {\n\tconst retainedThinking = thinkingExchange === \"retain-newest\" ? newestThinkingTarget(transcript, withCandidate) : undefined;\n\tconst retainedTask = taskExchange === \"retain-smallest\" ? smallestTaskTarget(transcript, withCandidate) : undefined;\n\treturn withCandidate.filter((target) =>\n\t\tshouldDeleteTarget(transcript, target, thinkingExchange, taskExchange, retainedThinking, retainedTask),\n\t);\n}\n\nfunction shouldBuildExchangePlan(\n\twithCandidate: readonly ContextDeletionTarget[],\n\tthinkingExchange: ThinkingExchange,\n\ttaskExchange: TaskExchange,\n): boolean {\n\tif (thinkingExchange === \"keep-all\" && taskExchange === \"keep-all\") return false;\n\tif (thinkingExchange === \"retain-newest\" && withCandidate.filter((target) => target.kind === \"entry\").length === 0) return false;\n\treturn true;\n}\n\nfunction addExchangePlan(\n\tplans: ContextDeletionTarget[][],\n\tseen: Set<string>,\n\toriginalTargets: readonly ContextDeletionTarget[],\n\ttargets: readonly ContextDeletionTarget[],\n): void {\n\tif (targetSignature(targets) === targetSignature(originalTargets)) return;\n\tconst signature = targetSignature(targets);\n\tif (seen.has(signature)) return;\n\tseen.add(signature);\n\tplans.push([...targets]);\n}\n\nfunction exchangePlans(\n\ttranscript: CompactableTranscript,\n\tplanned: readonly ContextDeletionTarget[],\n\tentryId: string,\n): ContextDeletionTarget[][] {\n\tconst currentTarget: ContextDeletionTarget = { kind: \"entry\", entryId };\n\tconst withCandidate = [...planned, currentTarget];\n\tconst plans: ContextDeletionTarget[][] = [];\n\tconst seen = new Set<string>();\n\t// Deterministic cross-product order: thinking handling outermost, then task handling.\n\t// Variants are keep/drop-all/retain-one for each non-monotone category, minus the\n\t// keep-all/keep-all no-op; signature dedup collapses degenerate variants.\n\tfor (const thinkingExchange of THINKING_EXCHANGES) {\n\t\tfor (const taskExchange of TASK_EXCHANGES) {\n\t\t\tif (!shouldBuildExchangePlan(withCandidate, thinkingExchange, taskExchange)) continue;\n\t\t\taddExchangePlan(plans, seen, withCandidate, buildExchangePlan(transcript, withCandidate, thinkingExchange, taskExchange));\n\t\t}\n\t}\n\treturn plans;\n}\n\nfunction validatedExchange(\n\ttranscript: CompactableTranscript,\n\tplanned: readonly ContextDeletionTarget[],\n\tentryId: string,\n\tlastValidated: ValidatedContextDeletionResult | undefined,\n): ValidatedContextDeletionResult | undefined {\n\tlet best: ValidatedContextDeletionResult | undefined;\n\tfor (const retryTargets of exchangePlans(transcript, planned, entryId)) {\n\t\ttry {\n\t\t\tconst result = validateContextDeletionRequest(deletionRequestFromTargets(retryTargets), transcript);\n\t\t\tif (lastValidated !== undefined && result.stats.tokensAfter >= lastValidated.stats.tokensAfter) continue;\n\t\t\tif (best === undefined || result.stats.tokensAfter < best.stats.tokensAfter) best = result;\n\t\t} catch {\n\t\t\t// Try the next deterministic exchange variant.\n\t\t}\n\t}\n\treturn best;\n}\n\nexport function runDeterministicContextEviction(\n\ttranscript: CompactableTranscript,\n\ttokenBudget: number,\n): ValidatedContextDeletionResult {\n\tconst relaxed = relaxTranscriptForCriticalEviction(transcript);\n\tconst latestAssistant = [...relaxed.entries].reverse().find((entry) => entry.role === \"assistant\");\n\tconst protectedThinkingAssistant = latestAssistant && assistantEntryHasThinkingContentBlock(latestAssistant) ? latestAssistant.entryId : undefined;\n\tconst candidates = relaxed.entries\n\t\t.filter((entry) => entry.entryId !== protectedThinkingAssistant)\n\t\t.filter((entry) => canDeleteTarget(relaxed, { kind: \"entry\", entryId: entry.entryId }))\n\t\t.map((entry) => entry.entryId);\n\tconst planned: ContextDeletionTarget[] = [];\n\tconst excluded = new Set<string>();\n\tlet lastValidated: ValidatedContextDeletionResult | undefined;\n\n\t// Hard iteration cap required by the compaction-ladder design: every compaction loop must be\n\t// bounded (max 50) so no tier can spin indefinitely. Non-monotone validation rules can require\n\t// local exchange: a prior thinking-bearing or task-bearing deletion can make a newer candidate\n\t// invalid, even though replacing the prior deletion with that candidate is safe and fits better.\n\t// Exchanges are deterministic and adopted only when validation succeeds and tokensAfter strictly\n\t// decreases; exclusions grow monotonically, so no oscillation is possible, and the pass cap remains\n\t// the terminal safety belt.\n\tfor (let pass = 0; pass < CONTEXT_COMPACTION_MAX_EVICTION_PASSES; pass++) {\n\t\tif (hasFitBudget(lastValidated, tokenBudget)) return lastValidated;\n\t\tlet changed = false;\n\t\tfor (const entryId of candidates) {\n\t\t\tif (hasFitBudget(lastValidated, tokenBudget)) return lastValidated;\n\t\t\tif (planned.some((target) => target.entryId === entryId) || excluded.has(entryId)) continue;\n\t\t\tconst nextPlanned: ContextDeletionTarget[] = [...planned, { kind: \"entry\", entryId }];\n\t\t\ttry {\n\t\t\t\tlastValidated = validateContextDeletionRequest(deletionRequestFromTargets(nextPlanned), relaxed);\n\t\t\t\tplanned.splice(0, planned.length, ...lastValidated.deletedTargets);\n\t\t\t\tchanged = true;\n\t\t\t} catch {\n\t\t\t\tconst exchanged = validatedExchange(relaxed, planned, entryId, lastValidated);\n\t\t\t\tif (exchanged !== undefined) {\n\t\t\t\t\tlastValidated = exchanged;\n\t\t\t\t\tplanned.splice(0, planned.length, ...lastValidated.deletedTargets);\n\t\t\t\t\tchanged = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\texcluded.add(entryId);\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\t\tif (!changed) {\n\t\t\tthrow terminalDeterministicEvictionError(\"candidate sweep exhausted\", lastValidated, tokenBudget);\n\t\t}\n\t}\n\tthrow terminalDeterministicEvictionError(\n\t\t`reached ${CONTEXT_COMPACTION_MAX_EVICTION_PASSES} eviction pass cap`,\n\t\tlastValidated,\n\t\ttokenBudget,\n\t);\n}\n"]}
|