feat: wire context-window-recovery callers to async SDK/HTTP variants on SQLite
- empty-content-recovery: isSqliteBackend() branch delegating to extracted empty-content-recovery-sdk.ts with SDK message scanning - message-builder: sanitizeEmptyMessagesBeforeSummarize now async with SDK path using replaceEmptyTextPartsAsync/injectTextPartAsync - target-token-truncation: truncateUntilTargetTokens now async with SDK path using findToolResultsBySizeFromSDK/truncateToolResultAsync - aggressive-truncation-strategy: passes client to truncateUntilTargetTokens - summarize-retry-strategy: await sanitizeEmptyMessagesBeforeSummarize - client.ts: derive Client from PluginInput['client'] instead of manual defs - executor.test.ts: .mockReturnValue() → .mockResolvedValue() for async fns - storage.test.ts: add await for async truncateUntilTargetTokens
This commit is contained in:
parent
dff3a551d8
commit
62e4e57455
@ -25,12 +25,13 @@ export async function runAggressiveTruncationStrategy(params: {
|
|||||||
targetRatio: TRUNCATE_CONFIG.targetTokenRatio,
|
targetRatio: TRUNCATE_CONFIG.targetTokenRatio,
|
||||||
})
|
})
|
||||||
|
|
||||||
const aggressiveResult = truncateUntilTargetTokens(
|
const aggressiveResult = await truncateUntilTargetTokens(
|
||||||
params.sessionID,
|
params.sessionID,
|
||||||
params.currentTokens,
|
params.currentTokens,
|
||||||
params.maxTokens,
|
params.maxTokens,
|
||||||
TRUNCATE_CONFIG.targetTokenRatio,
|
TRUNCATE_CONFIG.targetTokenRatio,
|
||||||
TRUNCATE_CONFIG.charsPerToken,
|
TRUNCATE_CONFIG.charsPerToken,
|
||||||
|
params.client,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (aggressiveResult.truncatedCount <= 0) {
|
if (aggressiveResult.truncatedCount <= 0) {
|
||||||
|
|||||||
@ -1,19 +1,7 @@
|
|||||||
export type Client = {
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
|
||||||
|
export type Client = PluginInput["client"] & {
|
||||||
session: {
|
session: {
|
||||||
messages: (opts: {
|
|
||||||
path: { id: string }
|
|
||||||
query?: { directory?: string }
|
|
||||||
}) => Promise<unknown>
|
|
||||||
summarize: (opts: {
|
|
||||||
path: { id: string }
|
|
||||||
body: { providerID: string; modelID: string }
|
|
||||||
query: { directory: string }
|
|
||||||
}) => Promise<unknown>
|
|
||||||
revert: (opts: {
|
|
||||||
path: { id: string }
|
|
||||||
body: { messageID: string; partID?: string }
|
|
||||||
query: { directory: string }
|
|
||||||
}) => Promise<unknown>
|
|
||||||
prompt_async: (opts: {
|
prompt_async: (opts: {
|
||||||
path: { id: string }
|
path: { id: string }
|
||||||
body: { parts: Array<{ type: string; text: string }> }
|
body: { parts: Array<{ type: string; text: string }> }
|
||||||
|
|||||||
@ -0,0 +1,185 @@
|
|||||||
|
import { replaceEmptyTextPartsAsync } from "../session-recovery/storage/empty-text"
|
||||||
|
import { injectTextPartAsync } from "../session-recovery/storage/text-part-injector"
|
||||||
|
import type { Client } from "./client"
|
||||||
|
|
||||||
|
interface SDKPart {
|
||||||
|
id?: string
|
||||||
|
type?: string
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SDKMessage {
|
||||||
|
info?: { id?: string }
|
||||||
|
parts?: SDKPart[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const IGNORE_TYPES = new Set(["thinking", "redacted_thinking", "meta"])
|
||||||
|
const TOOL_TYPES = new Set(["tool", "tool_use", "tool_result"])
|
||||||
|
|
||||||
|
function messageHasContentFromSDK(message: SDKMessage): boolean {
|
||||||
|
const parts = message.parts
|
||||||
|
if (!parts || parts.length === 0) return false
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const type = part.type
|
||||||
|
if (!type) continue
|
||||||
|
if (IGNORE_TYPES.has(type)) continue
|
||||||
|
|
||||||
|
if (type === "text") {
|
||||||
|
if (part.text?.trim()) return true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TOOL_TYPES.has(type)) return true
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSdkMessages(response: unknown): SDKMessage[] {
|
||||||
|
if (typeof response !== "object" || response === null) return []
|
||||||
|
const record = response as Record<string, unknown>
|
||||||
|
const data = record["data"]
|
||||||
|
return Array.isArray(data) ? (data as SDKMessage[]) : []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findEmptyMessagesFromSDK(client: Client, sessionID: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const response = await client.session.messages({ path: { id: sessionID } })
|
||||||
|
const messages = getSdkMessages(response)
|
||||||
|
|
||||||
|
const emptyIds: string[] = []
|
||||||
|
for (const message of messages) {
|
||||||
|
const messageID = message.info?.id
|
||||||
|
if (!messageID) continue
|
||||||
|
if (!messageHasContentFromSDK(message)) {
|
||||||
|
emptyIds.push(messageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyIds
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findEmptyMessageByIndexFromSDK(
|
||||||
|
client: Client,
|
||||||
|
sessionID: string,
|
||||||
|
targetIndex: number,
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const response = await client.session.messages({ path: { id: sessionID } })
|
||||||
|
const messages = getSdkMessages(response)
|
||||||
|
|
||||||
|
const indicesToTry = [
|
||||||
|
targetIndex,
|
||||||
|
targetIndex - 1,
|
||||||
|
targetIndex + 1,
|
||||||
|
targetIndex - 2,
|
||||||
|
targetIndex + 2,
|
||||||
|
targetIndex - 3,
|
||||||
|
targetIndex - 4,
|
||||||
|
targetIndex - 5,
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const index of indicesToTry) {
|
||||||
|
if (index < 0 || index >= messages.length) continue
|
||||||
|
|
||||||
|
const targetMessage = messages[index]
|
||||||
|
const targetMessageId = targetMessage?.info?.id
|
||||||
|
if (!targetMessageId) continue
|
||||||
|
|
||||||
|
if (!messageHasContentFromSDK(targetMessage)) {
|
||||||
|
return targetMessageId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fixEmptyMessagesWithSDK(params: {
|
||||||
|
sessionID: string
|
||||||
|
client: Client
|
||||||
|
placeholderText: string
|
||||||
|
messageIndex?: number
|
||||||
|
}): Promise<{ fixed: boolean; fixedMessageIds: string[]; scannedEmptyCount: number }> {
|
||||||
|
let fixed = false
|
||||||
|
const fixedMessageIds: string[] = []
|
||||||
|
|
||||||
|
if (params.messageIndex !== undefined) {
|
||||||
|
const targetMessageId = await findEmptyMessageByIndexFromSDK(
|
||||||
|
params.client,
|
||||||
|
params.sessionID,
|
||||||
|
params.messageIndex,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (targetMessageId) {
|
||||||
|
const replaced = await replaceEmptyTextPartsAsync(
|
||||||
|
params.client,
|
||||||
|
params.sessionID,
|
||||||
|
targetMessageId,
|
||||||
|
params.placeholderText,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (replaced) {
|
||||||
|
fixed = true
|
||||||
|
fixedMessageIds.push(targetMessageId)
|
||||||
|
} else {
|
||||||
|
const injected = await injectTextPartAsync(
|
||||||
|
params.client,
|
||||||
|
params.sessionID,
|
||||||
|
targetMessageId,
|
||||||
|
params.placeholderText,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (injected) {
|
||||||
|
fixed = true
|
||||||
|
fixedMessageIds.push(targetMessageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fixed) {
|
||||||
|
return { fixed, fixedMessageIds, scannedEmptyCount: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyMessageIds = await findEmptyMessagesFromSDK(params.client, params.sessionID)
|
||||||
|
if (emptyMessageIds.length === 0) {
|
||||||
|
return { fixed: false, fixedMessageIds: [], scannedEmptyCount: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const messageID of emptyMessageIds) {
|
||||||
|
const replaced = await replaceEmptyTextPartsAsync(
|
||||||
|
params.client,
|
||||||
|
params.sessionID,
|
||||||
|
messageID,
|
||||||
|
params.placeholderText,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (replaced) {
|
||||||
|
fixed = true
|
||||||
|
fixedMessageIds.push(messageID)
|
||||||
|
} else {
|
||||||
|
const injected = await injectTextPartAsync(
|
||||||
|
params.client,
|
||||||
|
params.sessionID,
|
||||||
|
messageID,
|
||||||
|
params.placeholderText,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (injected) {
|
||||||
|
fixed = true
|
||||||
|
fixedMessageIds.push(messageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fixed, fixedMessageIds, scannedEmptyCount: emptyMessageIds.length }
|
||||||
|
}
|
||||||
@ -4,10 +4,12 @@ import {
|
|||||||
injectTextPart,
|
injectTextPart,
|
||||||
replaceEmptyTextParts,
|
replaceEmptyTextParts,
|
||||||
} from "../session-recovery/storage"
|
} from "../session-recovery/storage"
|
||||||
|
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
|
||||||
import type { AutoCompactState } from "./types"
|
import type { AutoCompactState } from "./types"
|
||||||
import type { Client } from "./client"
|
import type { Client } from "./client"
|
||||||
import { PLACEHOLDER_TEXT } from "./message-builder"
|
import { PLACEHOLDER_TEXT } from "./message-builder"
|
||||||
import { incrementEmptyContentAttempt } from "./state"
|
import { incrementEmptyContentAttempt } from "./state"
|
||||||
|
import { fixEmptyMessagesWithSDK } from "./empty-content-recovery-sdk"
|
||||||
|
|
||||||
export async function fixEmptyMessages(params: {
|
export async function fixEmptyMessages(params: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
@ -20,6 +22,44 @@ export async function fixEmptyMessages(params: {
|
|||||||
let fixed = false
|
let fixed = false
|
||||||
const fixedMessageIds: string[] = []
|
const fixedMessageIds: string[] = []
|
||||||
|
|
||||||
|
if (isSqliteBackend()) {
|
||||||
|
const result = await fixEmptyMessagesWithSDK({
|
||||||
|
sessionID: params.sessionID,
|
||||||
|
client: params.client,
|
||||||
|
placeholderText: PLACEHOLDER_TEXT,
|
||||||
|
messageIndex: params.messageIndex,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.fixed && result.scannedEmptyCount === 0) {
|
||||||
|
await params.client.tui
|
||||||
|
.showToast({
|
||||||
|
body: {
|
||||||
|
title: "Empty Content Error",
|
||||||
|
message: "No empty messages found in storage. Cannot auto-recover.",
|
||||||
|
variant: "error",
|
||||||
|
duration: 5000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.fixed) {
|
||||||
|
await params.client.tui
|
||||||
|
.showToast({
|
||||||
|
body: {
|
||||||
|
title: "Session Recovery",
|
||||||
|
message: `Fixed ${result.fixedMessageIds.length} empty message(s). Retrying...`,
|
||||||
|
variant: "warning",
|
||||||
|
duration: 3000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.fixed
|
||||||
|
}
|
||||||
|
|
||||||
if (params.messageIndex !== undefined) {
|
if (params.messageIndex !== undefined) {
|
||||||
const targetMessageId = findEmptyMessageByIndex(params.sessionID, params.messageIndex)
|
const targetMessageId = findEmptyMessageByIndex(params.sessionID, params.messageIndex)
|
||||||
if (targetMessageId) {
|
if (targetMessageId) {
|
||||||
|
|||||||
@ -313,7 +313,7 @@ describe("executeCompact lock management", () => {
|
|||||||
maxTokens: 200000,
|
maxTokens: 200000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const truncateSpy = spyOn(storage, "truncateUntilTargetTokens").mockReturnValue({
|
const truncateSpy = spyOn(storage, "truncateUntilTargetTokens").mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
sufficient: false,
|
sufficient: false,
|
||||||
truncatedCount: 3,
|
truncatedCount: 3,
|
||||||
@ -354,7 +354,7 @@ describe("executeCompact lock management", () => {
|
|||||||
maxTokens: 200000,
|
maxTokens: 200000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const truncateSpy = spyOn(storage, "truncateUntilTargetTokens").mockReturnValue({
|
const truncateSpy = spyOn(storage, "truncateUntilTargetTokens").mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
sufficient: true,
|
sufficient: true,
|
||||||
truncatedCount: 5,
|
truncatedCount: 5,
|
||||||
|
|||||||
@ -1,14 +1,113 @@
|
|||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
|
||||||
import {
|
import {
|
||||||
findEmptyMessages,
|
findEmptyMessages,
|
||||||
injectTextPart,
|
injectTextPart,
|
||||||
replaceEmptyTextParts,
|
replaceEmptyTextParts,
|
||||||
} from "../session-recovery/storage"
|
} from "../session-recovery/storage"
|
||||||
|
import { replaceEmptyTextPartsAsync } from "../session-recovery/storage/empty-text"
|
||||||
|
import { injectTextPartAsync } from "../session-recovery/storage/text-part-injector"
|
||||||
import type { Client } from "./client"
|
import type { Client } from "./client"
|
||||||
|
|
||||||
export const PLACEHOLDER_TEXT = "[user interrupted]"
|
export const PLACEHOLDER_TEXT = "[user interrupted]"
|
||||||
|
|
||||||
export function sanitizeEmptyMessagesBeforeSummarize(sessionID: string): number {
|
type OpencodeClient = PluginInput["client"]
|
||||||
|
|
||||||
|
interface SDKPart {
|
||||||
|
type?: string
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SDKMessage {
|
||||||
|
info?: { id?: string }
|
||||||
|
parts?: SDKPart[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const IGNORE_TYPES = new Set(["thinking", "redacted_thinking", "meta"])
|
||||||
|
const TOOL_TYPES = new Set(["tool", "tool_use", "tool_result"])
|
||||||
|
|
||||||
|
function messageHasContentFromSDK(message: SDKMessage): boolean {
|
||||||
|
const parts = message.parts
|
||||||
|
if (!parts || parts.length === 0) return false
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const type = part.type
|
||||||
|
if (!type) continue
|
||||||
|
if (IGNORE_TYPES.has(type)) continue
|
||||||
|
|
||||||
|
if (type === "text") {
|
||||||
|
if (part.text?.trim()) return true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TOOL_TYPES.has(type)) return true
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findEmptyMessageIdsFromSDK(
|
||||||
|
client: OpencodeClient,
|
||||||
|
sessionID: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const response = (await client.session.messages({
|
||||||
|
path: { id: sessionID },
|
||||||
|
})) as { data?: SDKMessage[] }
|
||||||
|
const messages = response.data ?? []
|
||||||
|
|
||||||
|
const emptyIds: string[] = []
|
||||||
|
for (const message of messages) {
|
||||||
|
const messageID = message.info?.id
|
||||||
|
if (!messageID) continue
|
||||||
|
if (!messageHasContentFromSDK(message)) {
|
||||||
|
emptyIds.push(messageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyIds
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sanitizeEmptyMessagesBeforeSummarize(
|
||||||
|
sessionID: string,
|
||||||
|
client?: OpencodeClient,
|
||||||
|
): Promise<number> {
|
||||||
|
if (client && isSqliteBackend()) {
|
||||||
|
const emptyMessageIds = await findEmptyMessageIdsFromSDK(client, sessionID)
|
||||||
|
if (emptyMessageIds.length === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let fixedCount = 0
|
||||||
|
for (const messageID of emptyMessageIds) {
|
||||||
|
const replaced = await replaceEmptyTextPartsAsync(client, sessionID, messageID, PLACEHOLDER_TEXT)
|
||||||
|
if (replaced) {
|
||||||
|
fixedCount++
|
||||||
|
} else {
|
||||||
|
const injected = await injectTextPartAsync(client, sessionID, messageID, PLACEHOLDER_TEXT)
|
||||||
|
if (injected) {
|
||||||
|
fixedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fixedCount > 0) {
|
||||||
|
log("[auto-compact] pre-summarize sanitization fixed empty messages", {
|
||||||
|
sessionID,
|
||||||
|
fixedCount,
|
||||||
|
totalEmpty: emptyMessageIds.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixedCount
|
||||||
|
}
|
||||||
|
|
||||||
const emptyMessageIds = findEmptyMessages(sessionID)
|
const emptyMessageIds = findEmptyMessages(sessionID)
|
||||||
if (emptyMessageIds.length === 0) {
|
if (emptyMessageIds.length === 0) {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@ -21,7 +21,7 @@ describe("truncateUntilTargetTokens", () => {
|
|||||||
truncateToolResult.mockReset()
|
truncateToolResult.mockReset()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("truncates only until target is reached", () => {
|
test("truncates only until target is reached", async () => {
|
||||||
const { findToolResultsBySize, truncateToolResult } = require("./storage")
|
const { findToolResultsBySize, truncateToolResult } = require("./storage")
|
||||||
|
|
||||||
// given: Two tool results, each 1000 chars. Target reduction is 500 chars.
|
// given: Two tool results, each 1000 chars. Target reduction is 500 chars.
|
||||||
@ -39,7 +39,7 @@ describe("truncateUntilTargetTokens", () => {
|
|||||||
|
|
||||||
// when: currentTokens=1000, maxTokens=1000, targetRatio=0.5 (target=500, reduce=500)
|
// when: currentTokens=1000, maxTokens=1000, targetRatio=0.5 (target=500, reduce=500)
|
||||||
// charsPerToken=1 for simplicity in test
|
// charsPerToken=1 for simplicity in test
|
||||||
const result = truncateUntilTargetTokens(sessionID, 1000, 1000, 0.5, 1)
|
const result = await truncateUntilTargetTokens(sessionID, 1000, 1000, 0.5, 1)
|
||||||
|
|
||||||
// then: Should only truncate the first tool
|
// then: Should only truncate the first tool
|
||||||
expect(result.truncatedCount).toBe(1)
|
expect(result.truncatedCount).toBe(1)
|
||||||
@ -49,7 +49,7 @@ describe("truncateUntilTargetTokens", () => {
|
|||||||
expect(result.sufficient).toBe(true)
|
expect(result.sufficient).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("truncates all if target not reached", () => {
|
test("truncates all if target not reached", async () => {
|
||||||
const { findToolResultsBySize, truncateToolResult } = require("./storage")
|
const { findToolResultsBySize, truncateToolResult } = require("./storage")
|
||||||
|
|
||||||
// given: Two tool results, each 100 chars. Target reduction is 500 chars.
|
// given: Two tool results, each 100 chars. Target reduction is 500 chars.
|
||||||
@ -66,7 +66,7 @@ describe("truncateUntilTargetTokens", () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// when: reduce 500 chars
|
// when: reduce 500 chars
|
||||||
const result = truncateUntilTargetTokens(sessionID, 1000, 1000, 0.5, 1)
|
const result = await truncateUntilTargetTokens(sessionID, 1000, 1000, 0.5, 1)
|
||||||
|
|
||||||
// then: Should truncate both
|
// then: Should truncate both
|
||||||
expect(result.truncatedCount).toBe(2)
|
expect(result.truncatedCount).toBe(2)
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export async function runSummarizeRetryStrategy(params: {
|
|||||||
|
|
||||||
if (providerID && modelID) {
|
if (providerID && modelID) {
|
||||||
try {
|
try {
|
||||||
sanitizeEmptyMessagesBeforeSummarize(params.sessionID)
|
await sanitizeEmptyMessagesBeforeSummarize(params.sessionID, params.client)
|
||||||
|
|
||||||
await params.client.tui
|
await params.client.tui
|
||||||
.showToast({
|
.showToast({
|
||||||
|
|||||||
@ -1,5 +1,24 @@
|
|||||||
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import type { AggressiveTruncateResult } from "./tool-part-types"
|
import type { AggressiveTruncateResult } from "./tool-part-types"
|
||||||
import { findToolResultsBySize, truncateToolResult } from "./tool-result-storage"
|
import { findToolResultsBySize, truncateToolResult } from "./tool-result-storage"
|
||||||
|
import { findToolResultsBySizeFromSDK, truncateToolResultAsync } from "./tool-result-storage-sdk"
|
||||||
|
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
|
||||||
|
|
||||||
|
type OpencodeClient = PluginInput["client"]
|
||||||
|
|
||||||
|
interface SDKToolPart {
|
||||||
|
id: string
|
||||||
|
type: string
|
||||||
|
tool?: string
|
||||||
|
state?: { output?: string }
|
||||||
|
truncated?: boolean
|
||||||
|
originalSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SDKMessage {
|
||||||
|
info?: { id?: string }
|
||||||
|
parts?: SDKToolPart[]
|
||||||
|
}
|
||||||
|
|
||||||
function calculateTargetBytesToRemove(
|
function calculateTargetBytesToRemove(
|
||||||
currentTokens: number,
|
currentTokens: number,
|
||||||
@ -13,13 +32,14 @@ function calculateTargetBytesToRemove(
|
|||||||
return { tokensToReduce, targetBytesToRemove }
|
return { tokensToReduce, targetBytesToRemove }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function truncateUntilTargetTokens(
|
export async function truncateUntilTargetTokens(
|
||||||
sessionID: string,
|
sessionID: string,
|
||||||
currentTokens: number,
|
currentTokens: number,
|
||||||
maxTokens: number,
|
maxTokens: number,
|
||||||
targetRatio: number = 0.8,
|
targetRatio: number = 0.8,
|
||||||
charsPerToken: number = 4
|
charsPerToken: number = 4,
|
||||||
): AggressiveTruncateResult {
|
client?: OpencodeClient
|
||||||
|
): Promise<AggressiveTruncateResult> {
|
||||||
const { tokensToReduce, targetBytesToRemove } = calculateTargetBytesToRemove(
|
const { tokensToReduce, targetBytesToRemove } = calculateTargetBytesToRemove(
|
||||||
currentTokens,
|
currentTokens,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
@ -38,6 +58,82 @@ export function truncateUntilTargetTokens(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client && isSqliteBackend()) {
|
||||||
|
let toolPartsByKey = new Map<string, SDKToolPart>()
|
||||||
|
try {
|
||||||
|
const response = (await client.session.messages({
|
||||||
|
path: { id: sessionID },
|
||||||
|
})) as { data?: SDKMessage[] }
|
||||||
|
const messages = response.data ?? []
|
||||||
|
toolPartsByKey = new Map<string, SDKToolPart>()
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
const messageID = message.info?.id
|
||||||
|
if (!messageID || !message.parts) continue
|
||||||
|
for (const part of message.parts) {
|
||||||
|
if (part.type !== "tool") continue
|
||||||
|
toolPartsByKey.set(`${messageID}:${part.id}`, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toolPartsByKey = new Map<string, SDKToolPart>()
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await findToolResultsBySizeFromSDK(client, sessionID)
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
sufficient: false,
|
||||||
|
truncatedCount: 0,
|
||||||
|
totalBytesRemoved: 0,
|
||||||
|
targetBytesToRemove,
|
||||||
|
truncatedTools: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalRemoved = 0
|
||||||
|
let truncatedCount = 0
|
||||||
|
const truncatedTools: Array<{ toolName: string; originalSize: number }> = []
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const part = toolPartsByKey.get(`${result.messageID}:${result.partId}`)
|
||||||
|
if (!part) continue
|
||||||
|
|
||||||
|
const truncateResult = await truncateToolResultAsync(
|
||||||
|
client,
|
||||||
|
sessionID,
|
||||||
|
result.messageID,
|
||||||
|
result.partId,
|
||||||
|
part
|
||||||
|
)
|
||||||
|
if (truncateResult.success) {
|
||||||
|
truncatedCount++
|
||||||
|
const removedSize = truncateResult.originalSize ?? result.outputSize
|
||||||
|
totalRemoved += removedSize
|
||||||
|
truncatedTools.push({
|
||||||
|
toolName: truncateResult.toolName ?? result.toolName,
|
||||||
|
originalSize: removedSize,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (totalRemoved >= targetBytesToRemove) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sufficient = totalRemoved >= targetBytesToRemove
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: truncatedCount > 0,
|
||||||
|
sufficient,
|
||||||
|
truncatedCount,
|
||||||
|
totalBytesRemoved: totalRemoved,
|
||||||
|
targetBytesToRemove,
|
||||||
|
truncatedTools,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const results = findToolResultsBySize(sessionID)
|
const results = findToolResultsBySize(sessionID)
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user