Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/soft-compaction-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Recover compaction when large tool results or media attachments exceed provider payload limits.
111 changes: 111 additions & 0 deletions packages/opencode/src/kilocode/session/compaction-payload-recovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Effect } from "effect"
import type { Agent } from "@/agent/agent"
import type { Provider } from "@/provider/provider"
import type { LLM } from "@/session/llm"
import { MessageV2 } from "@/session/message-v2"
import type { SessionProcessor } from "@/session/processor"
import type { MessageID, SessionID } from "@/session/schema"

type Update = <T extends MessageV2.Part>(part: T) => Effect.Effect<T>
type UpdateMessage = <T extends MessageV2.Info>(msg: T) => Effect.Effect<T>

const pattern = /request entity too large|function_payload_too_large/i

export namespace KiloCompactionPayloadRecovery {
export function matches(error: MessageV2.Assistant["error"]) {
if (!error) return false
if (error.name !== "ContextOverflowError" && error.name !== "APIError") return false
return pattern.test([error.data.message, error.data.responseBody].filter(Boolean).join("\n"))
}

export function prompt(text: string) {
return [
"The previous compaction request exceeded the provider's 4MB payload limit.",
"Older tool outputs and media attachments were removed from this compaction request.",
text,
].join("\n\n")
}

export function strip(input: {
messages: MessageV2.WithParts[]
update: Update
}) {
return Effect.forEach(
input.messages,
(msg) =>
Effect.forEach(msg.parts, (part) => {
if (part.type === "tool" && part.state.status === "completed" && !part.state.time.compacted) {
part.state.time.compacted = Date.now()
return input.update(part)
}
if (part.type === "file" && MessageV2.isMedia(part.mime)) {
return input.update({
id: part.id,
messageID: part.messageID,
sessionID: part.sessionID,
type: "text",
text: `[Attached ${part.mime}: ${part.filename ?? "file"}]`,
})
}
return Effect.void
}),
{ concurrency: 1 },
)
}

export function process(input: {
processor: SessionProcessor.Handle
user: MessageV2.User
agent: Agent.Info
sessionID: SessionID
model: Provider.Model
messages: LLM.StreamInput["messages"]
prompt: string
recovery: MessageV2.WithParts[]
updateMessage: UpdateMessage
updatePart: Update
}) {
const run = Effect.fn("KiloCompactionPayloadRecovery.process")(function* (
messages: LLM.StreamInput["messages"],
text: string,
) {
return yield* input.processor.process({
user: input.user,
agent: input.agent,
sessionID: input.sessionID,
tools: {},
system: [],
messages: [
...messages,
{
role: "user",
content: [{ type: "text", text }],
},
],
model: input.model,
})
})

return run(input.messages, input.prompt).pipe(
Effect.flatMap((result) => {
if (result !== "compact" && (result !== "stop" || !matches(input.processor.message.error))) {
return Effect.succeed(result)
}
if (result === "compact" && !matches(input.processor.compactError?.())) {
return Effect.succeed(result)
}
return Effect.gen(function* () {
input.processor.message.error = undefined
input.processor.message.finish = undefined
yield* input.updateMessage(input.processor.message)
yield* strip({ messages: input.recovery, update: input.updatePart })
const stripped = yield* MessageV2.toModelMessagesEffect(input.recovery, input.model, {
stripMedia: true,
toolOutputMaxChars: 0,
})
return yield* run(stripped, prompt(input.prompt))
})
}),
)
}
}
20 changes: 10 additions & 10 deletions packages/opencode/src/session/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { isOverflow as overflow, usable } from "./overflow"
import { makeRuntime } from "@/effect/run-service"
import { fn } from "@/util/fn"
import { KiloSessionPromptQueue } from "@/kilocode/session/prompt-queue" // kilocode_change
import { KiloCompactionPayloadRecovery } from "@/kilocode/session/compaction-payload-recovery" // kilocode_change

const log = Log.create({ service: "session.compaction" })

Expand Down Expand Up @@ -440,21 +441,20 @@ export const layer: Layer.Layer<
sessionID: input.sessionID,
model,
})
const result = yield* processor.process({
// kilocode_change start
const result = yield* KiloCompactionPayloadRecovery.process({
processor,
user: userMessage,
agent,
sessionID: input.sessionID,
tools: {},
system: [],
messages: [
...modelMessages,
{
role: "user",
content: [{ type: "text", text: nextPrompt }],
},
],
model,
messages: modelMessages,
prompt: nextPrompt,
recovery: selected.head,
updateMessage: session.updateMessage,
updatePart: session.updatePart,
})
// kilocode_change end

if (result === "compact") {
processor.message.error = new MessageV2.ContextOverflowError({
Expand Down
21 changes: 20 additions & 1 deletion packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface Handle {
},
) => Effect.Effect<void>
readonly process: (streamInput: LLM.StreamInput) => Effect.Effect<Result>
readonly compactError?: () => ReturnType<typeof MessageV2.ContextOverflowError.prototype.toObject> | undefined // kilocode_change
}

type Input = {
Expand All @@ -73,6 +74,7 @@ interface ProcessorContext extends Input {
snapshot: string | undefined
blocked: boolean
needsCompaction: boolean
compactionError: ReturnType<typeof MessageV2.ContextOverflowError.prototype.toObject> | undefined // kilocode_change
currentText: MessageV2.TextPart | undefined
reasoningMap: Record<string, MessageV2.ReasoningPart>
stepStart: number // kilocode_change
Expand Down Expand Up @@ -130,6 +132,7 @@ export const layer: Layer.Layer<
snapshot: initialSnapshot,
blocked: false,
needsCompaction: false,
compactionError: undefined, // kilocode_change
currentText: undefined,
reasoningMap: {},
telemetry: input.telemetry, // kilocode_change
Expand Down Expand Up @@ -505,6 +508,11 @@ export const layer: Layer.Layer<
isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model })
) {
ctx.needsCompaction = true
// kilocode_change start
ctx.compactionError = new MessageV2.ContextOverflowError({
message: "Input exceeds context window of this model",
}).toObject()
// kilocode_change end
}
return
}
Expand Down Expand Up @@ -635,6 +643,9 @@ export const layer: Layer.Layer<
const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) {
slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined })
const error = parse(e)
// kilocode_change start
ctx.compactionError = MessageV2.ContextOverflowError.isInstance(error) ? error : ctx.compactionError
// kilocode_change end
if (MessageV2.ContextOverflowError.isInstance(error)) {
ctx.needsCompaction = true
yield* bus.publish(Session.Event.Error, { sessionID: ctx.sessionID, error })
Expand All @@ -648,9 +659,16 @@ export const layer: Layer.Layer<
yield* status.set(ctx.sessionID, { type: "idle" })
})

// kilocode_change start
const output = {
compactError: () => ctx.compactionError,
}
// kilocode_change end

const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
slog.info("process")
ctx.needsCompaction = false
ctx.compactionError = undefined // kilocode_change
ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true

return yield* Effect.gen(function* () {
Expand Down Expand Up @@ -700,7 +718,7 @@ export const layer: Layer.Layer<

if (ctx.needsCompaction) return "compact"
if (ctx.blocked || ctx.assistantMessage.error) return "stop"
return "continue"
return "continue" // kilocode_change - remove once compactError is no longer Kilo-specific
})
})

Expand All @@ -710,6 +728,7 @@ export const layer: Layer.Layer<
},
updateToolCall,
completeToolCall,
...output, // kilocode_change
process,
} satisfies Handle
})
Expand Down
Loading
Loading