feat: implement SDK/HTTP pruning for deduplication and tool-output truncation on SQLite

- executeDeduplication: now async, reads messages from SDK on SQLite via
  client.session.messages() instead of JSON file reads
- truncateToolOutputsByCallId: now async, uses truncateToolResultAsync()
  HTTP PATCH on SQLite instead of file-based truncateToolResult()
- deduplication-recovery: passes client through to both functions
- recovery-hook: passes ctx.client to attemptDeduplicationRecovery

Removes the last intentional feature gap on SQLite backend — dynamic
context pruning (dedup + tool-output truncation) now works on both
JSON and SQLite storage backends.
This commit is contained in:
YeonGyu-Kim
2026-02-15 19:06:57 +09:00
parent a25b35c380
commit 3bbe0cbb1d
4 changed files with 93 additions and 22 deletions
@@ -1,11 +1,14 @@
import { readdirSync, readFileSync } from "node:fs"
import { join } from "node:path"
import type { PluginInput } from "@opencode-ai/plugin"
import type { PruningState, ToolCallSignature } from "./pruning-types"
import { estimateTokens } from "./pruning-types"
import { log } from "../../shared/logger"
import { getMessageDir } from "../../shared/opencode-message-dir"
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
type OpencodeClient = PluginInput["client"]
export interface DeduplicationConfig {
enabled: boolean
protectedTools?: string[]
@@ -45,7 +48,6 @@ function sortObject(obj: unknown): unknown {
}
function readMessages(sessionID: string): MessagePart[] {
if (isSqliteBackend()) return []
const messageDir = getMessageDir(sessionID)
if (!messageDir) return []
@@ -67,20 +69,29 @@ function readMessages(sessionID: string): MessagePart[] {
return messages
}
export function executeDeduplication(
async function readMessagesFromSDK(client: OpencodeClient, sessionID: string): Promise<MessagePart[]> {
try {
const response = await client.session.messages({ path: { id: sessionID } })
const rawMessages = (response.data ?? []) as Array<{ parts?: ToolPart[] }>
return rawMessages.filter((m) => m.parts) as MessagePart[]
} catch {
return []
}
}
export async function executeDeduplication(
sessionID: string,
state: PruningState,
config: DeduplicationConfig,
protectedTools: Set<string>
): number {
if (isSqliteBackend()) {
log("[pruning-deduplication] Skipping deduplication on SQLite backend")
return 0
}
protectedTools: Set<string>,
client?: OpencodeClient,
): Promise<number> {
if (!config.enabled) return 0
const messages = readMessages(sessionID)
const messages = (client && isSqliteBackend())
? await readMessagesFromSDK(client, sessionID)
: readMessages(sessionID)
const signatures = new Map<string, ToolCallSignature[]>()
let currentTurn = 0