Skip to content

Commit 740c2b4

Browse files
authored
Merge pull request #9344 from Kilo-Org/fix/infinite-compact
fix(cli): cap per-turn compaction attempts
2 parents d147f78 + c032fc2 commit 740c2b4

4 files changed

Lines changed: 484 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@kilocode/cli": patch
3+
---
4+
5+
Fix an infinite "busy" loop that could occur when a model kept reporting context overflow after every compaction. Each turn now caps compactions at three attempts and closes the turn with a visible context-overflow error instead of silently looping forever.

packages/opencode/src/kilocode/session/prompt.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,37 @@ export namespace KiloSessionPrompt {
165165
}
166166
return "completed"
167167
}
168+
169+
/**
170+
* Maximum number of compactions attempted within a single turn before we
171+
* surface an exhaustion error. Three is enough to cover a normal overflow
172+
* compaction plus a summary-self-overflow retry without spinning forever.
173+
*/
174+
export const MAX_COMPACTION_ATTEMPTS = 3
175+
176+
/**
177+
* Guards a compaction attempt. When the attempt count has already reached
178+
* `MAX_COMPACTION_ATTEMPTS`, marks the close reason as `"error"`, attaches a
179+
* `ContextOverflowError` to the assistant message (if provided), and returns
180+
* `{ exhausted: true }` so callers can break out of the loop. Otherwise
181+
* returns `{ exhausted: false }`.
182+
*/
183+
export function guardCompactionAttempt(input: {
184+
sessionID: string
185+
attempts: number
186+
closeReasons: Map<string, KiloSession.CloseReason>
187+
message?: MessageV2.Assistant
188+
}) {
189+
if (input.attempts < MAX_COMPACTION_ATTEMPTS) return { exhausted: false as const }
190+
const error = new MessageV2.ContextOverflowError({
191+
message: `Compaction exhausted: context still exceeds model limits after ${MAX_COMPACTION_ATTEMPTS} attempts`,
192+
}).toObject()
193+
input.closeReasons.set(input.sessionID, "error")
194+
if (input.message) {
195+
// Preserve any pre-existing error/finish the caller already set; only fill in blanks.
196+
input.message.error ??= error
197+
input.message.finish ??= "error"
198+
}
199+
return { exhausted: true as const, error }
200+
}
168201
}

packages/opencode/src/session/prompt.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1343,6 +1343,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
13431343
// kilocode_change — cache environment details per turn (prompt caching)
13441344
const envCache: KiloSessionPrompt.EnvCache = {}
13451345
closeReasons.delete(sessionID) // kilocode_change
1346+
let compactionAttempts = 0 // kilocode_change - cap compaction attempts per turn to avoid infinite loops
13461347
const ctx = yield* InstanceState.context
13471348
const slog = elog.with({ sessionID })
13481349
let structured: unknown | undefined
@@ -1424,7 +1425,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the
14241425
auto: task.auto,
14251426
overflow: task.overflow,
14261427
})
1427-
if (result === "stop") break
1428+
// kilocode_change start - compaction.process only returns "stop" after
1429+
// setting ContextOverflowError on the summary message; surface as turn error
1430+
if (result === "stop") {
1431+
closeReasons.set(sessionID, "error")
1432+
break
1433+
}
1434+
// kilocode_change end
14281435
continue
14291436
}
14301437

@@ -1433,6 +1440,22 @@ NOTE: At any point in time through this workflow you should feel free to ask the
14331440
lastFinished.summary !== true &&
14341441
(yield* compaction.isOverflow({ tokens: lastFinished.tokens, model }))
14351442
) {
1443+
// kilocode_change start
1444+
const guard = KiloSessionPrompt.guardCompactionAttempt({
1445+
sessionID,
1446+
attempts: compactionAttempts,
1447+
closeReasons,
1448+
message: lastFinished,
1449+
})
1450+
if (guard.exhausted) {
1451+
// lastFinished is a prior turn's assistant — record exhaustion on the
1452+
// message whose size tipped us past the compaction cap.
1453+
yield* sessions.updateMessage(lastFinished)
1454+
yield* bus.publish(Session.Event.Error, { sessionID, error: guard.error })
1455+
break
1456+
}
1457+
compactionAttempts++
1458+
// kilocode_change end
14361459
yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true })
14371460
continue
14381461
}
@@ -1570,6 +1593,20 @@ NOTE: At any point in time through this workflow you should feel free to ask the
15701593
}
15711594
// kilocode_change end
15721595
if (result === "compact") {
1596+
// kilocode_change start
1597+
const guard = KiloSessionPrompt.guardCompactionAttempt({
1598+
sessionID,
1599+
attempts: compactionAttempts,
1600+
closeReasons,
1601+
message: handle.message,
1602+
})
1603+
if (guard.exhausted) {
1604+
yield* sessions.updateMessage(handle.message)
1605+
yield* bus.publish(Session.Event.Error, { sessionID, error: guard.error })
1606+
return "break" as const
1607+
}
1608+
compactionAttempts++
1609+
// kilocode_change end
15731610
yield* compaction.create({
15741611
sessionID,
15751612
agent: lastUser.agent,

0 commit comments

Comments
 (0)