refactor: remove dcp_for_compaction and preemptive_compaction features
- Delete src/hooks/preemptive-compaction/ entirely - Remove dcp_for_compaction from schema and executor - Clean up related imports, options, and test code - Update READMEs to remove experimental options docs
This commit is contained in:
parent
bf28b3e711
commit
b933992e36
13
README.ja.md
13
README.ja.md
@ -1047,7 +1047,6 @@ OpenCode でサポートされるすべての LSP 構成およびカスタム設
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"preemptive_compaction_threshold": 0.85,
|
|
||||||
"truncate_all_tool_outputs": true,
|
"truncate_all_tool_outputs": true,
|
||||||
"aggressive_truncation": true,
|
"aggressive_truncation": true,
|
||||||
"auto_resume": true
|
"auto_resume": true
|
||||||
@ -1055,13 +1054,11 @@ OpenCode でサポートされるすべての LSP 構成およびカスタム設
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| オプション | デフォルト | 説明 |
|
| オプション | デフォルト | 説明 |
|
||||||
| --------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `preemptive_compaction_threshold` | `0.85` | プリエンプティブコンパクションをトリガーする閾値(0.5-0.95)。`preemptive-compaction` フックはデフォルトで有効です。このオプションで閾値をカスタマイズできます。 |
|
| `truncate_all_tool_outputs` | `false` | ホワイトリストのツール(Grep、Glob、LSP、AST-grep)だけでなく、すべてのツール出力を切り詰めます。Tool output truncator はデフォルトで有効です - `disabled_hooks`で無効化できます。 |
|
||||||
| `truncate_all_tool_outputs` | `false` | ホワイトリストのツール(Grep、Glob、LSP、AST-grep)だけでなく、すべてのツール出力を切り詰めます。Tool output truncator はデフォルトで有効です - `disabled_hooks`で無効化できます。 |
|
| `aggressive_truncation` | `false` | トークン制限を超えた場合、ツール出力を積極的に切り詰めて制限内に収めます。デフォルトの切り詰めより積極的です。不十分な場合は要約/復元にフォールバックします。 |
|
||||||
| `aggressive_truncation` | `false` | トークン制限を超えた場合、ツール出力を積極的に切り詰めて制限内に収めます。デフォルトの切り詰めより積極的です。不十分な場合は要約/復元にフォールバックします。 |
|
| `auto_resume` | `false` | thinking block エラーや thinking disabled violation からの回復成功後、自動的にセッションを再開します。最後のユーザーメッセージを抽出して続行します。 |
|
||||||
| `auto_resume` | `false` | thinking block エラーや thinking disabled violation からの回復成功後、自動的にセッションを再開します。最後のユーザーメッセージを抽出して続行します。 |
|
|
||||||
| `dcp_for_compaction` | `false` | コンパクション用DCP(動的コンテキスト整理)を有効化 - トークン制限超過時に最初に実行されます。コンパクション前に重複したツール呼び出しと古いツール出力を整理します。 |
|
|
||||||
|
|
||||||
**警告**:これらの機能は実験的であり、予期しない動作を引き起こす可能性があります。影響を理解した場合にのみ有効にしてください。
|
**警告**:これらの機能は実験的であり、予期しない動作を引き起こす可能性があります。影響を理解した場合にのみ有効にしてください。
|
||||||
|
|
||||||
|
|||||||
13
README.md
13
README.md
@ -1165,7 +1165,6 @@ Opt-in experimental features that may change or be removed in future versions. U
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"preemptive_compaction_threshold": 0.85,
|
|
||||||
"truncate_all_tool_outputs": true,
|
"truncate_all_tool_outputs": true,
|
||||||
"aggressive_truncation": true,
|
"aggressive_truncation": true,
|
||||||
"auto_resume": true
|
"auto_resume": true
|
||||||
@ -1173,13 +1172,11 @@ Opt-in experimental features that may change or be removed in future versions. U
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| --------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `preemptive_compaction_threshold` | `0.85` | Threshold percentage (0.5-0.95) to trigger preemptive compaction. The `preemptive-compaction` hook is enabled by default; this option customizes the threshold. |
|
| `truncate_all_tool_outputs` | `false` | Truncates ALL tool outputs instead of just whitelisted tools (Grep, Glob, LSP, AST-grep). Tool output truncator is enabled by default - disable via `disabled_hooks`. |
|
||||||
| `truncate_all_tool_outputs` | `false` | Truncates ALL tool outputs instead of just whitelisted tools (Grep, Glob, LSP, AST-grep). Tool output truncator is enabled by default - disable via `disabled_hooks`. |
|
| `aggressive_truncation` | `false` | When token limit is exceeded, aggressively truncates tool outputs to fit within limits. More aggressive than the default truncation behavior. Falls back to summarize/revert if insufficient. |
|
||||||
| `aggressive_truncation` | `false` | When token limit is exceeded, aggressively truncates tool outputs to fit within limits. More aggressive than the default truncation behavior. Falls back to summarize/revert if insufficient. |
|
| `auto_resume` | `false` | Automatically resumes session after successful recovery from thinking block errors or thinking disabled violations. Extracts the last user message and continues. |
|
||||||
| `auto_resume` | `false` | Automatically resumes session after successful recovery from thinking block errors or thinking disabled violations. Extracts the last user message and continues. |
|
|
||||||
| `dcp_for_compaction` | `false` | Enable DCP (Dynamic Context Pruning) for compaction - runs first when token limit exceeded. Prunes duplicate tool calls and old tool outputs before running compaction. |
|
|
||||||
|
|
||||||
**Warning**: These features are experimental and may cause unexpected behavior. Enable only if you understand the implications.
|
**Warning**: These features are experimental and may cause unexpected behavior. Enable only if you understand the implications.
|
||||||
|
|
||||||
|
|||||||
@ -1174,7 +1174,6 @@ Oh My OpenCode 添加了重构工具(重命名、代码操作)。
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"preemptive_compaction_threshold": 0.85,
|
|
||||||
"truncate_all_tool_outputs": true,
|
"truncate_all_tool_outputs": true,
|
||||||
"aggressive_truncation": true,
|
"aggressive_truncation": true,
|
||||||
"auto_resume": true
|
"auto_resume": true
|
||||||
@ -1182,13 +1181,11 @@ Oh My OpenCode 添加了重构工具(重命名、代码操作)。
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| 选项 | 默认 | 描述 |
|
| 选项 | 默认 | 描述 |
|
||||||
| --------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `preemptive_compaction_threshold` | `0.85` | 触发预防性压缩的阈值百分比(0.5-0.95)。`preemptive-compaction` 钩子默认启用;此选项自定义阈值。 |
|
| `truncate_all_tool_outputs` | `false` | 截断所有工具输出而不仅仅是白名单工具(Grep、Glob、LSP、AST-grep)。工具输出截断器默认启用——通过 `disabled_hooks` 禁用。 |
|
||||||
| `truncate_all_tool_outputs` | `false` | 截断所有工具输出而不仅仅是白名单工具(Grep、Glob、LSP、AST-grep)。工具输出截断器默认启用——通过 `disabled_hooks` 禁用。 |
|
| `aggressive_truncation` | `false` | 当超过 token 限制时,积极截断工具输出以适应限制。比默认截断行为更激进。如果不足以满足,则回退到总结/恢复。 |
|
||||||
| `aggressive_truncation` | `false` | 当超过 token 限制时,积极截断工具输出以适应限制。比默认截断行为更激进。如果不足以满足,则回退到总结/恢复。 |
|
| `auto_resume` | `false` | 从思考块错误或禁用思考违规成功恢复后自动恢复会话。提取最后一条用户消息并继续。 |
|
||||||
| `auto_resume` | `false` | 从思考块错误或禁用思考违规成功恢复后自动恢复会话。提取最后一条用户消息并继续。 |
|
|
||||||
| `dcp_for_compaction` | `false` | 为压缩启用 DCP(动态上下文修剪)——当超过 token 限制时首先运行。在运行压缩之前修剪重复的工具调用和旧的工具输出。 |
|
|
||||||
|
|
||||||
**警告**:这些功能是实验性的,可能导致意外行为。只有在理解其影响后才启用。
|
**警告**:这些功能是实验性的,可能导致意外行为。只有在理解其影响后才启用。
|
||||||
|
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export const HookNameSchema = z.enum([
|
|||||||
"empty-message-sanitizer",
|
"empty-message-sanitizer",
|
||||||
"thinking-block-validator",
|
"thinking-block-validator",
|
||||||
"ralph-loop",
|
"ralph-loop",
|
||||||
"preemptive-compaction",
|
|
||||||
"compaction-context-injector",
|
"compaction-context-injector",
|
||||||
"claude-code-hooks",
|
"claude-code-hooks",
|
||||||
"auto-slash-command",
|
"auto-slash-command",
|
||||||
@ -225,16 +225,10 @@ export const DynamicContextPruningConfigSchema = z.object({
|
|||||||
export const ExperimentalConfigSchema = z.object({
|
export const ExperimentalConfigSchema = z.object({
|
||||||
aggressive_truncation: z.boolean().optional(),
|
aggressive_truncation: z.boolean().optional(),
|
||||||
auto_resume: z.boolean().optional(),
|
auto_resume: z.boolean().optional(),
|
||||||
/** Enable preemptive compaction at threshold (default: true since v2.9.0) */
|
|
||||||
preemptive_compaction: z.boolean().optional(),
|
|
||||||
/** Threshold percentage to trigger preemptive compaction (default: 0.80) */
|
|
||||||
preemptive_compaction_threshold: z.number().min(0.5).max(0.95).optional(),
|
|
||||||
/** Truncate all tool outputs, not just whitelisted tools (default: false). Tool output truncator is enabled by default - disable via disabled_hooks. */
|
/** Truncate all tool outputs, not just whitelisted tools (default: false). Tool output truncator is enabled by default - disable via disabled_hooks. */
|
||||||
truncate_all_tool_outputs: z.boolean().optional(),
|
truncate_all_tool_outputs: z.boolean().optional(),
|
||||||
/** Dynamic context pruning configuration */
|
/** Dynamic context pruning configuration */
|
||||||
dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(),
|
dynamic_context_pruning: DynamicContextPruningConfigSchema.optional(),
|
||||||
/** Enable DCP (Dynamic Context Pruning) for compaction - runs first when token limit exceeded (default: false) */
|
|
||||||
dcp_for_compaction: z.boolean().optional(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const SkillSourceSchema = z.union([
|
export const SkillSourceSchema = z.union([
|
||||||
|
|||||||
@ -17,7 +17,6 @@ describe("executeCompact lock management", () => {
|
|||||||
errorDataBySession: new Map(),
|
errorDataBySession: new Map(),
|
||||||
retryStateBySession: new Map(),
|
retryStateBySession: new Map(),
|
||||||
truncateStateBySession: new Map(),
|
truncateStateBySession: new Map(),
|
||||||
dcpStateBySession: new Map(),
|
|
||||||
emptyContentAttemptBySession: new Map(),
|
emptyContentAttemptBySession: new Map(),
|
||||||
compactionInProgress: new Set<string>(),
|
compactionInProgress: new Set<string>(),
|
||||||
}
|
}
|
||||||
@ -119,7 +118,6 @@ describe("executeCompact lock management", () => {
|
|||||||
truncate_all_tool_outputs: false,
|
truncate_all_tool_outputs: false,
|
||||||
aggressive_truncation: true,
|
aggressive_truncation: true,
|
||||||
}
|
}
|
||||||
const dcpForCompaction = true
|
|
||||||
|
|
||||||
// #when: Execute compaction with experimental flag
|
// #when: Execute compaction with experimental flag
|
||||||
await executeCompact(
|
await executeCompact(
|
||||||
@ -129,7 +127,6 @@ describe("executeCompact lock management", () => {
|
|||||||
mockClient,
|
mockClient,
|
||||||
directory,
|
directory,
|
||||||
experimental,
|
experimental,
|
||||||
dcpForCompaction,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// #then: Lock should be cleared even on early return
|
// #then: Lock should be cleared even on early return
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import type {
|
import type {
|
||||||
AutoCompactState,
|
AutoCompactState,
|
||||||
DcpState,
|
|
||||||
RetryState,
|
RetryState,
|
||||||
TruncateState,
|
TruncateState,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { ExperimentalConfig } from "../../config";
|
import type { ExperimentalConfig } from "../../config";
|
||||||
import { RETRY_CONFIG, TRUNCATE_CONFIG } from "./types";
|
import { RETRY_CONFIG, TRUNCATE_CONFIG } from "./types";
|
||||||
import { executeDynamicContextPruning } from "./pruning-executor";
|
|
||||||
import {
|
import {
|
||||||
findLargestToolResult,
|
findLargestToolResult,
|
||||||
truncateToolResult,
|
truncateToolResult,
|
||||||
@ -82,17 +81,7 @@ function getOrCreateTruncateState(
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateDcpState(
|
|
||||||
autoCompactState: AutoCompactState,
|
|
||||||
sessionID: string,
|
|
||||||
): DcpState {
|
|
||||||
let state = autoCompactState.dcpStateBySession.get(sessionID);
|
|
||||||
if (!state) {
|
|
||||||
state = { attempted: false, itemsPruned: 0 };
|
|
||||||
autoCompactState.dcpStateBySession.set(sessionID, state);
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeEmptyMessagesBeforeSummarize(sessionID: string): number {
|
function sanitizeEmptyMessagesBeforeSummarize(sessionID: string): number {
|
||||||
const emptyMessageIds = findEmptyMessages(sessionID);
|
const emptyMessageIds = findEmptyMessages(sessionID);
|
||||||
@ -168,7 +157,6 @@ function clearSessionState(
|
|||||||
autoCompactState.errorDataBySession.delete(sessionID);
|
autoCompactState.errorDataBySession.delete(sessionID);
|
||||||
autoCompactState.retryStateBySession.delete(sessionID);
|
autoCompactState.retryStateBySession.delete(sessionID);
|
||||||
autoCompactState.truncateStateBySession.delete(sessionID);
|
autoCompactState.truncateStateBySession.delete(sessionID);
|
||||||
autoCompactState.dcpStateBySession.delete(sessionID);
|
|
||||||
autoCompactState.emptyContentAttemptBySession.delete(sessionID);
|
autoCompactState.emptyContentAttemptBySession.delete(sessionID);
|
||||||
autoCompactState.compactionInProgress.delete(sessionID);
|
autoCompactState.compactionInProgress.delete(sessionID);
|
||||||
}
|
}
|
||||||
@ -275,7 +263,6 @@ export async function executeCompact(
|
|||||||
client: any,
|
client: any,
|
||||||
directory: string,
|
directory: string,
|
||||||
experimental?: ExperimentalConfig,
|
experimental?: ExperimentalConfig,
|
||||||
dcpForCompaction?: boolean,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (autoCompactState.compactionInProgress.has(sessionID)) {
|
if (autoCompactState.compactionInProgress.has(sessionID)) {
|
||||||
await (client as Client).tui
|
await (client as Client).tui
|
||||||
@ -302,61 +289,7 @@ export async function executeCompact(
|
|||||||
errorData?.maxTokens &&
|
errorData?.maxTokens &&
|
||||||
errorData.currentTokens > errorData.maxTokens;
|
errorData.currentTokens > errorData.maxTokens;
|
||||||
|
|
||||||
// PHASE 1: DCP (Dynamic Context Pruning) - prune duplicate tool calls first
|
// Aggressive Truncation - always try when over limit
|
||||||
const dcpState = getOrCreateDcpState(autoCompactState, sessionID);
|
|
||||||
if (dcpForCompaction !== false && !dcpState.attempted && isOverLimit) {
|
|
||||||
dcpState.attempted = true;
|
|
||||||
log("[auto-compact] PHASE 1: DCP triggered on token limit error", {
|
|
||||||
sessionID,
|
|
||||||
currentTokens: errorData.currentTokens,
|
|
||||||
maxTokens: errorData.maxTokens,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dcpConfig = experimental?.dynamic_context_pruning ?? {
|
|
||||||
enabled: true,
|
|
||||||
notification: "detailed" as const,
|
|
||||||
protected_tools: [
|
|
||||||
"task",
|
|
||||||
"todowrite",
|
|
||||||
"todoread",
|
|
||||||
"lsp_rename",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pruningResult = await executeDynamicContextPruning(
|
|
||||||
sessionID,
|
|
||||||
dcpConfig,
|
|
||||||
client,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pruningResult.itemsPruned > 0) {
|
|
||||||
dcpState.itemsPruned = pruningResult.itemsPruned;
|
|
||||||
log("[auto-compact] DCP successful, proceeding to truncation", {
|
|
||||||
itemsPruned: pruningResult.itemsPruned,
|
|
||||||
tokensSaved: pruningResult.totalTokensSaved,
|
|
||||||
});
|
|
||||||
|
|
||||||
await (client as Client).tui
|
|
||||||
.showToast({
|
|
||||||
body: {
|
|
||||||
title: "Dynamic Context Pruning",
|
|
||||||
message: `Pruned ${pruningResult.itemsPruned} items (~${Math.round(pruningResult.totalTokensSaved / 1000)}k tokens). Proceeding to truncation...`,
|
|
||||||
variant: "success",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
// Continue to PHASE 2 (truncation) instead of summarizing immediately
|
|
||||||
} else {
|
|
||||||
log("[auto-compact] DCP did not prune any items", { sessionID });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log("[auto-compact] DCP failed", { error: String(error) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PHASE 2: Aggressive Truncation - always try when over limit (not experimental-only)
|
|
||||||
if (
|
if (
|
||||||
isOverLimit &&
|
isOverLimit &&
|
||||||
truncateState.truncateAttempt < TRUNCATE_CONFIG.maxTruncateAttempts
|
truncateState.truncateAttempt < TRUNCATE_CONFIG.maxTruncateAttempts
|
||||||
@ -448,7 +381,6 @@ export async function executeCompact(
|
|||||||
client,
|
client,
|
||||||
directory,
|
directory,
|
||||||
experimental,
|
experimental,
|
||||||
dcpForCompaction,
|
|
||||||
);
|
);
|
||||||
}, 500);
|
}, 500);
|
||||||
return;
|
return;
|
||||||
@ -517,7 +449,6 @@ export async function executeCompact(
|
|||||||
client,
|
client,
|
||||||
directory,
|
directory,
|
||||||
experimental,
|
experimental,
|
||||||
dcpForCompaction,
|
|
||||||
);
|
);
|
||||||
}, cappedDelay);
|
}, cappedDelay);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { log } from "../../shared/logger"
|
|||||||
|
|
||||||
export interface AnthropicContextWindowLimitRecoveryOptions {
|
export interface AnthropicContextWindowLimitRecoveryOptions {
|
||||||
experimental?: ExperimentalConfig
|
experimental?: ExperimentalConfig
|
||||||
dcpForCompaction?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRecoveryState(): AutoCompactState {
|
function createRecoveryState(): AutoCompactState {
|
||||||
@ -16,7 +15,6 @@ function createRecoveryState(): AutoCompactState {
|
|||||||
errorDataBySession: new Map<string, ParsedTokenLimitError>(),
|
errorDataBySession: new Map<string, ParsedTokenLimitError>(),
|
||||||
retryStateBySession: new Map(),
|
retryStateBySession: new Map(),
|
||||||
truncateStateBySession: new Map(),
|
truncateStateBySession: new Map(),
|
||||||
dcpStateBySession: new Map(),
|
|
||||||
emptyContentAttemptBySession: new Map(),
|
emptyContentAttemptBySession: new Map(),
|
||||||
compactionInProgress: new Set<string>(),
|
compactionInProgress: new Set<string>(),
|
||||||
}
|
}
|
||||||
@ -25,7 +23,6 @@ function createRecoveryState(): AutoCompactState {
|
|||||||
export function createAnthropicContextWindowLimitRecoveryHook(ctx: PluginInput, options?: AnthropicContextWindowLimitRecoveryOptions) {
|
export function createAnthropicContextWindowLimitRecoveryHook(ctx: PluginInput, options?: AnthropicContextWindowLimitRecoveryOptions) {
|
||||||
const autoCompactState = createRecoveryState()
|
const autoCompactState = createRecoveryState()
|
||||||
const experimental = options?.experimental
|
const experimental = options?.experimental
|
||||||
const dcpForCompaction = options?.dcpForCompaction
|
|
||||||
|
|
||||||
const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
||||||
const props = event.properties as Record<string, unknown> | undefined
|
const props = event.properties as Record<string, unknown> | undefined
|
||||||
@ -37,7 +34,6 @@ export function createAnthropicContextWindowLimitRecoveryHook(ctx: PluginInput,
|
|||||||
autoCompactState.errorDataBySession.delete(sessionInfo.id)
|
autoCompactState.errorDataBySession.delete(sessionInfo.id)
|
||||||
autoCompactState.retryStateBySession.delete(sessionInfo.id)
|
autoCompactState.retryStateBySession.delete(sessionInfo.id)
|
||||||
autoCompactState.truncateStateBySession.delete(sessionInfo.id)
|
autoCompactState.truncateStateBySession.delete(sessionInfo.id)
|
||||||
autoCompactState.dcpStateBySession.delete(sessionInfo.id)
|
|
||||||
autoCompactState.emptyContentAttemptBySession.delete(sessionInfo.id)
|
autoCompactState.emptyContentAttemptBySession.delete(sessionInfo.id)
|
||||||
autoCompactState.compactionInProgress.delete(sessionInfo.id)
|
autoCompactState.compactionInProgress.delete(sessionInfo.id)
|
||||||
}
|
}
|
||||||
@ -81,8 +77,7 @@ export function createAnthropicContextWindowLimitRecoveryHook(ctx: PluginInput,
|
|||||||
autoCompactState,
|
autoCompactState,
|
||||||
ctx.client,
|
ctx.client,
|
||||||
ctx.directory,
|
ctx.directory,
|
||||||
experimental,
|
experimental
|
||||||
dcpForCompaction
|
|
||||||
)
|
)
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
@ -141,8 +136,7 @@ export function createAnthropicContextWindowLimitRecoveryHook(ctx: PluginInput,
|
|||||||
autoCompactState,
|
autoCompactState,
|
||||||
ctx.client,
|
ctx.client,
|
||||||
ctx.directory,
|
ctx.directory,
|
||||||
experimental,
|
experimental
|
||||||
dcpForCompaction
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,6 +146,6 @@ export function createAnthropicContextWindowLimitRecoveryHook(ctx: PluginInput,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { AutoCompactState, DcpState, ParsedTokenLimitError, TruncateState } from "./types"
|
export type { AutoCompactState, ParsedTokenLimitError, TruncateState } from "./types"
|
||||||
export { parseAnthropicTokenLimitError } from "./parser"
|
export { parseAnthropicTokenLimitError } from "./parser"
|
||||||
export { executeCompact, getLastAssistant } from "./executor"
|
export { executeCompact, getLastAssistant } from "./executor"
|
||||||
|
|||||||
@ -18,17 +18,11 @@ export interface TruncateState {
|
|||||||
lastTruncatedPartId?: string
|
lastTruncatedPartId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DcpState {
|
|
||||||
attempted: boolean
|
|
||||||
itemsPruned: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutoCompactState {
|
export interface AutoCompactState {
|
||||||
pendingCompact: Set<string>
|
pendingCompact: Set<string>
|
||||||
errorDataBySession: Map<string, ParsedTokenLimitError>
|
errorDataBySession: Map<string, ParsedTokenLimitError>
|
||||||
retryStateBySession: Map<string, RetryState>
|
retryStateBySession: Map<string, RetryState>
|
||||||
truncateStateBySession: Map<string, TruncateState>
|
truncateStateBySession: Map<string, TruncateState>
|
||||||
dcpStateBySession: Map<string, DcpState>
|
|
||||||
emptyContentAttemptBySession: Map<string, number>
|
emptyContentAttemptBySession: Map<string, number>
|
||||||
compactionInProgress: Set<string>
|
compactionInProgress: Set<string>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
import type { SummarizeContext } from "../preemptive-compaction"
|
|
||||||
import { injectHookMessage } from "../../features/hook-message-injector"
|
import { injectHookMessage } from "../../features/hook-message-injector"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
|
|
||||||
|
export interface SummarizeContext {
|
||||||
|
sessionID: string
|
||||||
|
providerID: string
|
||||||
|
modelID: string
|
||||||
|
usageRatio: number
|
||||||
|
directory: string
|
||||||
|
}
|
||||||
|
|
||||||
const SUMMARIZE_CONTEXT_PROMPT = `[COMPACTION CONTEXT INJECTION]
|
const SUMMARIZE_CONTEXT_PROMPT = `[COMPACTION CONTEXT INJECTION]
|
||||||
|
|
||||||
When summarizing this session, you MUST include the following sections in your summary:
|
When summarizing this session, you MUST include the following sections in your summary:
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export { createDirectoryAgentsInjectorHook } from "./directory-agents-injector";
|
|||||||
export { createDirectoryReadmeInjectorHook } from "./directory-readme-injector";
|
export { createDirectoryReadmeInjectorHook } from "./directory-readme-injector";
|
||||||
export { createEmptyTaskResponseDetectorHook } from "./empty-task-response-detector";
|
export { createEmptyTaskResponseDetectorHook } from "./empty-task-response-detector";
|
||||||
export { createAnthropicContextWindowLimitRecoveryHook, type AnthropicContextWindowLimitRecoveryOptions } from "./anthropic-context-window-limit-recovery";
|
export { createAnthropicContextWindowLimitRecoveryHook, type AnthropicContextWindowLimitRecoveryOptions } from "./anthropic-context-window-limit-recovery";
|
||||||
export { createPreemptiveCompactionHook, type PreemptiveCompactionOptions, type SummarizeContext, type BeforeSummarizeCallback } from "./preemptive-compaction";
|
|
||||||
export { createCompactionContextInjector } from "./compaction-context-injector";
|
export { createCompactionContextInjector } from "./compaction-context-injector";
|
||||||
export { createThinkModeHook } from "./think-mode";
|
export { createThinkModeHook } from "./think-mode";
|
||||||
export { createClaudeCodeHooksHook } from "./claude-code-hooks";
|
export { createClaudeCodeHooksHook } from "./claude-code-hooks";
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export const DEFAULT_THRESHOLD = 0.85
|
|
||||||
export const MIN_TOKENS_FOR_COMPACTION = 50_000
|
|
||||||
export const COMPACTION_COOLDOWN_MS = 60_000
|
|
||||||
@ -1,265 +0,0 @@
|
|||||||
import { existsSync, readdirSync } from "node:fs"
|
|
||||||
import { join } from "node:path"
|
|
||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
|
||||||
import type { ExperimentalConfig } from "../../config"
|
|
||||||
import type { PreemptiveCompactionState, TokenInfo } from "./types"
|
|
||||||
import {
|
|
||||||
DEFAULT_THRESHOLD,
|
|
||||||
MIN_TOKENS_FOR_COMPACTION,
|
|
||||||
COMPACTION_COOLDOWN_MS,
|
|
||||||
} from "./constants"
|
|
||||||
import {
|
|
||||||
findNearestMessageWithFields,
|
|
||||||
MESSAGE_STORAGE,
|
|
||||||
} from "../../features/hook-message-injector"
|
|
||||||
import { log } from "../../shared/logger"
|
|
||||||
|
|
||||||
export interface SummarizeContext {
|
|
||||||
sessionID: string
|
|
||||||
providerID: string
|
|
||||||
modelID: string
|
|
||||||
usageRatio: number
|
|
||||||
directory: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BeforeSummarizeCallback = (ctx: SummarizeContext) => Promise<void> | void
|
|
||||||
|
|
||||||
export type GetModelLimitCallback = (providerID: string, modelID: string) => number | undefined
|
|
||||||
|
|
||||||
export interface PreemptiveCompactionOptions {
|
|
||||||
experimental?: ExperimentalConfig
|
|
||||||
onBeforeSummarize?: BeforeSummarizeCallback
|
|
||||||
getModelLimit?: GetModelLimitCallback
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MessageInfo {
|
|
||||||
id: string
|
|
||||||
role: string
|
|
||||||
sessionID: string
|
|
||||||
providerID?: string
|
|
||||||
modelID?: string
|
|
||||||
tokens?: TokenInfo
|
|
||||||
summary?: boolean
|
|
||||||
finish?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MessageWrapper {
|
|
||||||
info: MessageInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
const CLAUDE_MODEL_PATTERN = /claude-(opus|sonnet|haiku)/i
|
|
||||||
const CLAUDE_DEFAULT_CONTEXT_LIMIT =
|
|
||||||
process.env.ANTHROPIC_1M_CONTEXT === "true" ||
|
|
||||||
process.env.VERTEX_ANTHROPIC_1M_CONTEXT === "true"
|
|
||||||
? 1_000_000
|
|
||||||
: 200_000
|
|
||||||
|
|
||||||
function isSupportedModel(modelID: string): boolean {
|
|
||||||
return CLAUDE_MODEL_PATTERN.test(modelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMessageDir(sessionID: string): string | null {
|
|
||||||
if (!existsSync(MESSAGE_STORAGE)) return null
|
|
||||||
|
|
||||||
const directPath = join(MESSAGE_STORAGE, sessionID)
|
|
||||||
if (existsSync(directPath)) return directPath
|
|
||||||
|
|
||||||
for (const dir of readdirSync(MESSAGE_STORAGE)) {
|
|
||||||
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
|
|
||||||
if (existsSync(sessionPath)) return sessionPath
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function createState(): PreemptiveCompactionState {
|
|
||||||
return {
|
|
||||||
lastCompactionTime: new Map(),
|
|
||||||
compactionInProgress: new Set(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPreemptiveCompactionHook(
|
|
||||||
ctx: PluginInput,
|
|
||||||
options?: PreemptiveCompactionOptions
|
|
||||||
) {
|
|
||||||
const experimental = options?.experimental
|
|
||||||
const onBeforeSummarize = options?.onBeforeSummarize
|
|
||||||
const getModelLimit = options?.getModelLimit
|
|
||||||
// Preemptive compaction is now enabled by default.
|
|
||||||
// Backward compatibility: explicit false in experimental config disables the hook.
|
|
||||||
const explicitlyDisabled = experimental?.preemptive_compaction === false
|
|
||||||
const threshold = experimental?.preemptive_compaction_threshold ?? DEFAULT_THRESHOLD
|
|
||||||
|
|
||||||
if (explicitlyDisabled) {
|
|
||||||
return { event: async () => {} }
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = createState()
|
|
||||||
|
|
||||||
const checkAndTriggerCompaction = async (
|
|
||||||
sessionID: string,
|
|
||||||
lastAssistant: MessageInfo
|
|
||||||
): Promise<void> => {
|
|
||||||
if (state.compactionInProgress.has(sessionID)) return
|
|
||||||
|
|
||||||
const lastCompaction = state.lastCompactionTime.get(sessionID) ?? 0
|
|
||||||
if (Date.now() - lastCompaction < COMPACTION_COOLDOWN_MS) return
|
|
||||||
|
|
||||||
if (lastAssistant.summary === true) return
|
|
||||||
|
|
||||||
const tokens = lastAssistant.tokens
|
|
||||||
if (!tokens) return
|
|
||||||
|
|
||||||
const modelID = lastAssistant.modelID ?? ""
|
|
||||||
const providerID = lastAssistant.providerID ?? ""
|
|
||||||
|
|
||||||
if (!isSupportedModel(modelID)) {
|
|
||||||
log("[preemptive-compaction] skipping unsupported model", { modelID })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const configLimit = getModelLimit?.(providerID, modelID)
|
|
||||||
const contextLimit = configLimit ?? CLAUDE_DEFAULT_CONTEXT_LIMIT
|
|
||||||
const totalUsed = tokens.input + tokens.cache.read + tokens.output
|
|
||||||
|
|
||||||
if (totalUsed < MIN_TOKENS_FOR_COMPACTION) return
|
|
||||||
|
|
||||||
const usageRatio = totalUsed / contextLimit
|
|
||||||
|
|
||||||
log("[preemptive-compaction] checking", {
|
|
||||||
sessionID,
|
|
||||||
totalUsed,
|
|
||||||
contextLimit,
|
|
||||||
usageRatio: usageRatio.toFixed(2),
|
|
||||||
threshold,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (usageRatio < threshold) return
|
|
||||||
|
|
||||||
state.compactionInProgress.add(sessionID)
|
|
||||||
state.lastCompactionTime.set(sessionID, Date.now())
|
|
||||||
|
|
||||||
if (!providerID || !modelID) {
|
|
||||||
state.compactionInProgress.delete(sessionID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.client.tui
|
|
||||||
.showToast({
|
|
||||||
body: {
|
|
||||||
title: "Preemptive Compaction",
|
|
||||||
message: `Context at ${(usageRatio * 100).toFixed(0)}% - compacting to prevent overflow...`,
|
|
||||||
variant: "warning",
|
|
||||||
duration: 3000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
|
|
||||||
log("[preemptive-compaction] triggering compaction", { sessionID, usageRatio })
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (onBeforeSummarize) {
|
|
||||||
await onBeforeSummarize({
|
|
||||||
sessionID,
|
|
||||||
providerID,
|
|
||||||
modelID,
|
|
||||||
usageRatio,
|
|
||||||
directory: ctx.directory,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const summarizeBody = { providerID, modelID, auto: true }
|
|
||||||
await ctx.client.session.summarize({
|
|
||||||
path: { id: sessionID },
|
|
||||||
body: summarizeBody as never,
|
|
||||||
query: { directory: ctx.directory },
|
|
||||||
})
|
|
||||||
|
|
||||||
await ctx.client.tui
|
|
||||||
.showToast({
|
|
||||||
body: {
|
|
||||||
title: "Compaction Complete",
|
|
||||||
message: "Session compacted successfully. Resuming...",
|
|
||||||
variant: "success",
|
|
||||||
duration: 2000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
|
|
||||||
state.compactionInProgress.delete(sessionID)
|
|
||||||
return
|
|
||||||
} catch (err) {
|
|
||||||
log("[preemptive-compaction] compaction failed", { sessionID, error: err })
|
|
||||||
} finally {
|
|
||||||
state.compactionInProgress.delete(sessionID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
|
||||||
const props = event.properties as Record<string, unknown> | undefined
|
|
||||||
|
|
||||||
if (event.type === "session.deleted") {
|
|
||||||
const sessionInfo = props?.info as { id?: string } | undefined
|
|
||||||
if (sessionInfo?.id) {
|
|
||||||
state.lastCompactionTime.delete(sessionInfo.id)
|
|
||||||
state.compactionInProgress.delete(sessionInfo.id)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "message.updated") {
|
|
||||||
const info = props?.info as MessageInfo | undefined
|
|
||||||
if (!info) return
|
|
||||||
|
|
||||||
if (info.role !== "assistant" || !info.finish) return
|
|
||||||
|
|
||||||
const sessionID = info.sessionID
|
|
||||||
if (!sessionID) return
|
|
||||||
|
|
||||||
await checkAndTriggerCompaction(sessionID, info)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "session.idle") {
|
|
||||||
const sessionID = props?.sessionID as string | undefined
|
|
||||||
if (!sessionID) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await ctx.client.session.messages({
|
|
||||||
path: { id: sessionID },
|
|
||||||
query: { directory: ctx.directory },
|
|
||||||
})
|
|
||||||
|
|
||||||
const messages = (resp.data ?? resp) as MessageWrapper[]
|
|
||||||
const assistants = messages
|
|
||||||
.filter((m) => m.info.role === "assistant")
|
|
||||||
.map((m) => m.info)
|
|
||||||
|
|
||||||
if (assistants.length === 0) return
|
|
||||||
|
|
||||||
const lastAssistant = assistants[assistants.length - 1]
|
|
||||||
|
|
||||||
if (!lastAssistant.providerID || !lastAssistant.modelID) {
|
|
||||||
const messageDir = getMessageDir(sessionID)
|
|
||||||
const storedMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
|
||||||
if (storedMessage?.model?.providerID && storedMessage?.model?.modelID) {
|
|
||||||
lastAssistant.providerID = storedMessage.model.providerID
|
|
||||||
lastAssistant.modelID = storedMessage.model.modelID
|
|
||||||
log("[preemptive-compaction] using stored message model info", {
|
|
||||||
sessionID,
|
|
||||||
providerID: lastAssistant.providerID,
|
|
||||||
modelID: lastAssistant.modelID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await checkAndTriggerCompaction(sessionID, lastAssistant)
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
event: eventHandler,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
export interface PreemptiveCompactionState {
|
|
||||||
lastCompactionTime: Map<string, number>
|
|
||||||
compactionInProgress: Set<string>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TokenInfo {
|
|
||||||
input: number
|
|
||||||
output: number
|
|
||||||
reasoning: number
|
|
||||||
cache: { read: number; write: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelLimits {
|
|
||||||
context: number
|
|
||||||
output: number
|
|
||||||
}
|
|
||||||
12
src/index.ts
12
src/index.ts
@ -12,7 +12,7 @@ import {
|
|||||||
createThinkModeHook,
|
createThinkModeHook,
|
||||||
createClaudeCodeHooksHook,
|
createClaudeCodeHooksHook,
|
||||||
createAnthropicContextWindowLimitRecoveryHook,
|
createAnthropicContextWindowLimitRecoveryHook,
|
||||||
createPreemptiveCompactionHook,
|
|
||||||
createCompactionContextInjector,
|
createCompactionContextInjector,
|
||||||
createRulesInjectorHook,
|
createRulesInjectorHook,
|
||||||
createBackgroundNotificationHook,
|
createBackgroundNotificationHook,
|
||||||
@ -145,20 +145,11 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
)
|
)
|
||||||
? createAnthropicContextWindowLimitRecoveryHook(ctx, {
|
? createAnthropicContextWindowLimitRecoveryHook(ctx, {
|
||||||
experimental: pluginConfig.experimental,
|
experimental: pluginConfig.experimental,
|
||||||
dcpForCompaction: pluginConfig.experimental?.dcp_for_compaction,
|
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
const compactionContextInjector = isHookEnabled("compaction-context-injector")
|
const compactionContextInjector = isHookEnabled("compaction-context-injector")
|
||||||
? createCompactionContextInjector()
|
? createCompactionContextInjector()
|
||||||
: undefined;
|
: undefined;
|
||||||
const preemptiveCompaction = isHookEnabled("preemptive-compaction")
|
|
||||||
? createPreemptiveCompactionHook(ctx, {
|
|
||||||
experimental: pluginConfig.experimental,
|
|
||||||
onBeforeSummarize: compactionContextInjector,
|
|
||||||
getModelLimit: (providerID, modelID) =>
|
|
||||||
getModelLimit(modelCacheState, providerID, modelID),
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
const rulesInjector = isHookEnabled("rules-injector")
|
const rulesInjector = isHookEnabled("rules-injector")
|
||||||
? createRulesInjectorHook(ctx)
|
? createRulesInjectorHook(ctx)
|
||||||
: null;
|
: null;
|
||||||
@ -420,7 +411,6 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
await rulesInjector?.event(input);
|
await rulesInjector?.event(input);
|
||||||
await thinkMode?.event(input);
|
await thinkMode?.event(input);
|
||||||
await anthropicContextWindowLimitRecovery?.event(input);
|
await anthropicContextWindowLimitRecovery?.event(input);
|
||||||
await preemptiveCompaction?.event(input);
|
|
||||||
await agentUsageReminder?.event(input);
|
await agentUsageReminder?.event(input);
|
||||||
await interactiveBashSession?.event(input);
|
await interactiveBashSession?.event(input);
|
||||||
await ralphLoop?.event(input);
|
await ralphLoop?.event(input);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user