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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -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<TValue>(
|
||||
promise: Promise<TValue>,
|
||||
timeoutMs: number,
|
||||
errorMessage: string,
|
||||
): Promise<TValue> {
|
||||
let timeoutID: unknown
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, 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<string, CachedCompactionState>
|
||||
compactionInProgress: Set<string>
|
||||
compactedSessions: Set<string>
|
||||
lastCompactionTime: Map<string, number>
|
||||
}): Promise<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<unknown>
|
||||
summarize: (input: {
|
||||
path: { id: string }
|
||||
body: { providerID: string; modelID: string; auto?: boolean }
|
||||
query: { directory: string }
|
||||
}) => Promise<unknown>
|
||||
}
|
||||
tui: {
|
||||
showToast: (input: {
|
||||
body: {
|
||||
title: string
|
||||
message: string
|
||||
variant: "warning"
|
||||
duration: number
|
||||
}
|
||||
}) => Promise<unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface PreemptiveCompactionContext {
|
||||
client: PreemptiveCompactionClient
|
||||
directory: string
|
||||
}
|
||||
@@ -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<TValue>(
|
||||
promise: Promise<TValue>,
|
||||
timeoutMs: number,
|
||||
errorMessage: string,
|
||||
): Promise<TValue> {
|
||||
let timeoutID: unknown
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, 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 } }) => {
|
||||
|
||||
Reference in New Issue
Block a user