From 1aebf39d23d6131a3389b7c5e57c87098f0ddcf7 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 18 Apr 2026 01:50:41 +0900 Subject: [PATCH] refactor(hooks): split preemptive-compaction.ts to comply with 200 LOC module rule Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/preemptive-compaction-trigger.ts | 131 ++++++++++++++++++ src/hooks/preemptive-compaction-types.ts | 41 ++++++ src/hooks/preemptive-compaction.ts | 149 +++------------------ 3 files changed, 189 insertions(+), 132 deletions(-) create mode 100644 src/hooks/preemptive-compaction-trigger.ts create mode 100644 src/hooks/preemptive-compaction-types.ts diff --git a/src/hooks/preemptive-compaction-trigger.ts b/src/hooks/preemptive-compaction-trigger.ts new file mode 100644 index 000000000..bbab74f76 --- /dev/null +++ b/src/hooks/preemptive-compaction-trigger.ts @@ -0,0 +1,131 @@ +import type { OhMyOpenCodeConfig } from "../config" +import { + resolveActualContextLimit, + type ContextLimitModelCacheState, +} from "../shared/context-limit-resolver" +import { log } from "../shared/logger" + +import { resolveCompactionModel } from "./shared/compaction-model-resolver" +import type { + CachedCompactionState, + PreemptiveCompactionContext, +} from "./preemptive-compaction-types" + +const PREEMPTIVE_COMPACTION_TIMEOUT_MS = 60_000 +const PREEMPTIVE_COMPACTION_THRESHOLD = 0.78 +const PREEMPTIVE_COMPACTION_COOLDOWN_MS = 60_000 + +declare function setTimeout(handler: () => void, timeout?: number): unknown +declare function clearTimeout(timeoutID: unknown): void + +async function withTimeout( + promise: Promise, + timeoutMs: number, + errorMessage: string, +): Promise { + let timeoutID: unknown + + const timeoutPromise = new Promise((_, reject) => { + timeoutID = setTimeout(() => { + reject(new Error(errorMessage)) + }, timeoutMs) + }) + + return await Promise.race([promise, timeoutPromise]).finally(() => { + clearTimeout(timeoutID) + }) +} + +export async function runPreemptiveCompactionIfNeeded(args: { + ctx: PreemptiveCompactionContext + pluginConfig: OhMyOpenCodeConfig + modelCacheState?: ContextLimitModelCacheState + sessionID: string + tokenCache: Map + compactionInProgress: Set + compactedSessions: Set + lastCompactionTime: Map +}): Promise { + const { + ctx, + pluginConfig, + modelCacheState, + sessionID, + tokenCache, + compactionInProgress, + compactedSessions, + lastCompactionTime, + } = args + + if (compactedSessions.has(sessionID) || compactionInProgress.has(sessionID)) return + + const lastTime = lastCompactionTime.get(sessionID) + if (lastTime && Date.now() - lastTime < PREEMPTIVE_COMPACTION_COOLDOWN_MS) return + + const cached = tokenCache.get(sessionID) + if (!cached) return + + const actualLimit = resolveActualContextLimit( + cached.providerID, + cached.modelID, + modelCacheState, + ) + + if (actualLimit === null) { + log("[preemptive-compaction] Skipping preemptive compaction: unknown context limit for model", { + providerID: cached.providerID, + modelID: cached.modelID, + }) + return + } + + const totalInputTokens = (cached.tokens.input ?? 0) + (cached.tokens.cache?.read ?? 0) + const usageRatio = totalInputTokens / actualLimit + if (usageRatio < PREEMPTIVE_COMPACTION_THRESHOLD || !cached.modelID) return + + compactionInProgress.add(sessionID) + lastCompactionTime.set(sessionID, Date.now()) + + try { + const { providerID: targetProviderID, modelID: targetModelID } = resolveCompactionModel( + pluginConfig, + sessionID, + cached.providerID, + cached.modelID, + ) + + await withTimeout( + ctx.client.session.summarize({ + path: { id: sessionID }, + body: { providerID: targetProviderID, modelID: targetModelID, auto: true }, + query: { directory: ctx.directory }, + }), + PREEMPTIVE_COMPACTION_TIMEOUT_MS, + `Compaction summarize timed out after ${PREEMPTIVE_COMPACTION_TIMEOUT_MS}ms`, + ) + + compactedSessions.add(sessionID) + } catch (error) { + log("[preemptive-compaction] Compaction failed", { + sessionID, + providerID: cached.providerID, + modelID: cached.modelID, + error: String(error), + }) + ctx.client.tui.showToast({ + body: { + title: "Preemptive compaction failed", + message: `Context window is above ${Math.round(PREEMPTIVE_COMPACTION_THRESHOLD * 100)}% and auto-compaction could not run. The session may grow large. Error: ${String(error)}`, + variant: "warning", + duration: 10000, + }, + }).catch((toastError: unknown) => { + log("[preemptive-compaction] Failed to show toast", { + sessionID, + toastError: String(toastError), + }) + }) + } finally { + compactionInProgress.delete(sessionID) + } +} diff --git a/src/hooks/preemptive-compaction-types.ts b/src/hooks/preemptive-compaction-types.ts new file mode 100644 index 000000000..77efed575 --- /dev/null +++ b/src/hooks/preemptive-compaction-types.ts @@ -0,0 +1,41 @@ +export interface TokenInfo { + input: number + output: number + reasoning: number + cache: { read: number; write: number } +} + +export interface CachedCompactionState { + providerID: string + modelID: string + tokens: TokenInfo +} + +export interface PreemptiveCompactionClient { + session: { + messages: (input: { + path: { id: string } + query?: { directory: string } + }) => Promise + summarize: (input: { + path: { id: string } + body: { providerID: string; modelID: string; auto?: boolean } + query: { directory: string } + }) => Promise + } + tui: { + showToast: (input: { + body: { + title: string + message: string + variant: "warning" + duration: number + } + }) => Promise + } +} + +export interface PreemptiveCompactionContext { + client: PreemptiveCompactionClient + directory: string +} diff --git a/src/hooks/preemptive-compaction.ts b/src/hooks/preemptive-compaction.ts index ecab70676..7b4828dcb 100644 --- a/src/hooks/preemptive-compaction.ts +++ b/src/hooks/preemptive-compaction.ts @@ -1,69 +1,16 @@ -import { log } from "../shared/logger" import type { OhMyOpenCodeConfig } from "../config" -import { - resolveActualContextLimit, - type ContextLimitModelCacheState, -} from "../shared/context-limit-resolver" +import type { ContextLimitModelCacheState } from "../shared/context-limit-resolver" -import { resolveCompactionModel } from "./shared/compaction-model-resolver" import { createPostCompactionDegradationMonitor } from "./preemptive-compaction-degradation-monitor" - -const PREEMPTIVE_COMPACTION_TIMEOUT_MS = 60_000 -const PREEMPTIVE_COMPACTION_THRESHOLD = 0.78 -const PREEMPTIVE_COMPACTION_COOLDOWN_MS = 60_000 - -declare function setTimeout(handler: () => void, timeout?: number): unknown -declare function clearTimeout(timeoutID: unknown): void - -interface TokenInfo { - input: number - output: number - reasoning: number - cache: { read: number; write: number } -} - -interface CachedCompactionState { - providerID: string - modelID: string - tokens: TokenInfo -} - -async function withTimeout( - promise: Promise, - timeoutMs: number, - errorMessage: string, -): Promise { - let timeoutID: unknown - - const timeoutPromise = new Promise((_, reject) => { - timeoutID = setTimeout(() => { - reject(new Error(errorMessage)) - }, timeoutMs) - }) - - return await Promise.race([promise, timeoutPromise]).finally(() => { - clearTimeout(timeoutID) - }) -} - -type PluginInput = { - client: { - session: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - messages: (...args: any[]) => any - // eslint-disable-next-line @typescript-eslint/no-explicit-any - summarize: (...args: any[]) => any - } - tui: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - showToast: (...args: any[]) => any - } - } - directory: string -} +import { runPreemptiveCompactionIfNeeded } from "./preemptive-compaction-trigger" +import type { + CachedCompactionState, + PreemptiveCompactionContext, + TokenInfo, +} from "./preemptive-compaction-types" export function createPreemptiveCompactionHook( - ctx: PluginInput, + ctx: PreemptiveCompactionContext, pluginConfig: OhMyOpenCodeConfig, modelCacheState?: ContextLimitModelCacheState, ) { @@ -84,78 +31,16 @@ export function createPreemptiveCompactionHook( input: { tool: string; sessionID: string; callID: string }, _output: { title: string; output: string; metadata: unknown } ) => { - const { sessionID } = input - if (compactedSessions.has(sessionID) || compactionInProgress.has(sessionID)) return - - const lastTime = lastCompactionTime.get(sessionID) - if (lastTime && Date.now() - lastTime < PREEMPTIVE_COMPACTION_COOLDOWN_MS) return - - const cached = tokenCache.get(sessionID) - if (!cached) return - - const actualLimit = resolveActualContextLimit( - cached.providerID, - cached.modelID, + await runPreemptiveCompactionIfNeeded({ + ctx, + pluginConfig, modelCacheState, - ) - - if (actualLimit === null) { - log("[preemptive-compaction] Skipping preemptive compaction: unknown context limit for model", { - providerID: cached.providerID, - modelID: cached.modelID, - }) - return - } - - const totalInputTokens = (cached.tokens.input ?? 0) + (cached.tokens.cache?.read ?? 0) - const usageRatio = totalInputTokens / actualLimit - if (usageRatio < PREEMPTIVE_COMPACTION_THRESHOLD || !cached.modelID) return - - compactionInProgress.add(sessionID) - lastCompactionTime.set(sessionID, Date.now()) - - try { - const { providerID: targetProviderID, modelID: targetModelID } = resolveCompactionModel( - pluginConfig, - sessionID, - cached.providerID, - cached.modelID, - ) - - await withTimeout( - ctx.client.session.summarize({ - path: { id: sessionID }, - body: { providerID: targetProviderID, modelID: targetModelID, auto: true } as never, - query: { directory: ctx.directory }, - }), - PREEMPTIVE_COMPACTION_TIMEOUT_MS, - `Compaction summarize timed out after ${PREEMPTIVE_COMPACTION_TIMEOUT_MS}ms`, - ) - - compactedSessions.add(sessionID) - } catch (error) { - log("[preemptive-compaction] Compaction failed", { - sessionID, - providerID: cached.providerID, - modelID: cached.modelID, - error: String(error), - }) - ctx.client.tui.showToast({ - body: { - title: "Preemptive compaction failed", - message: `Context window is above ${Math.round(PREEMPTIVE_COMPACTION_THRESHOLD * 100)}% and auto-compaction could not run. The session may grow large. Error: ${String(error)}`, - variant: "warning", - duration: 10000, - }, - }).catch((toastError: unknown) => { - log("[preemptive-compaction] Failed to show toast", { - sessionID, - toastError: String(toastError), - }) - }) - } finally { - compactionInProgress.delete(sessionID) - } + sessionID: input.sessionID, + tokenCache, + compactionInProgress, + compactedSessions, + lastCompactionTime, + }) } const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => {