Repository navigation
[Bug]: auto-compaction sends an empty conversation at the LIVE call site — full.length > summary.messageCount is always false there (works on resume) #878
Description
Summary
When --compact-threshold fires, the compacted transcript handed to the provider is [system, summary] with no live turn at all, so the model has nothing to answer and the run dies with provider returned an empty completion (no assistant text or tool call), exit 1.
Please read the call-site distinction below before testing. The tail-preservation logic in buildCompactedTranscript is correct on the resume path and unreachable on the live path. Testing with --continue will show it working.
Reproduced on main @ 8dce012, v0.1.0, built from source.
Repro
oh-my-cli --compact-threshold 1400 "read the four .txt files in this directory and report each sentinel value"Observed, 3/3 identical runs, to the token (3 701 prompt tokens, 2 requests):
Context compacted: summarized 6 message(s) (2 completed-action receipt(s)) after 2442 prompt tokens
... 3 attempts ...
provider returned an empty completion (no assistant text or tool call)
exit 1
Negative control — the same task, same backend, without --compact-threshold: succeeds, 5 requests, 18 125 prompt tokens, all four sentinels returned correctly. So this is not the provider.
Root cause
compactMessages stamps the summary with the length of the array it was given (src/compaction.ts:258):
messageCount: messages.length,buildCompactedTranscript preserves the tail only when the full transcript is longer than that (src/compaction.ts:324):
if (full.length > summary.messageCount) {
out.push(...full.slice(summary.messageCount));
}That condition depends on which array full is:
-
Resume path —
loadSessionMessages(src/compaction.ts:437):fullis the on-disk transcript, which has grown pastmessageCount. Condition true, tail preserved, works as intended. -
Live path —
src/agent.ts:618-619:const { summary } = compactMessages(messages); const compacted = buildCompactedTranscript(messages, summary);
fullis the very array that producedmessageCount. The condition evaluates tofull.length > full.length— always false. The branch is unreachable here.
Only the live path runs during auto-compaction. So the compacted transcript is always [system, renderSummaryMessage(summary)] — and renderSummaryMessage emits role system too. The provider receives two system messages and zero user/assistant turns.
The comment directly above buildCompactedTranscript states the opposite intent — "Preserving the tail is what keeps continuation honest (Issue #719)" — which is accurate for resume and not for the live call site.
Suggested fix
Give the live path a messageCount that reflects what was summarized rather than the whole array, or have buildCompactedTranscript take the summarized slice explicitly instead of inferring it from a length comparison that only holds on one of its two callers.
Secondary observation (same flag, much smaller)
--compact-threshold can never fire on the first provider call of a process: lastPromptTokens is initialized to 0 (src/agent.ts:492) and the guard tests lastPromptTokens >= compactThreshold before the call (src/agent.ts:616). A session resumed with --continue therefore sends its full history at least once before compaction is even considered.
Verified: a 1 941-token session resumed with --compact-threshold 1500 issued a 1 984-token request with no compaction.
Context
Found while benchmarking terminal agents against a local MLX server whose usable history ceiling is ~24k tokens. oh-my-cli's own overhead is excellent for that constraint — 1 231 tokens fixed, ~237 per turn — which is why the compaction path mattered enough to chase down.
Activity
github-actions commentedon Aug 16, 2026
Automated intake triage
bugpriority:p10.95Auto-compaction on the live call site builds a transcript with only system and summary messages, causing the provider to receive no user turn and fail with an empty completion.
Reported evidence (unverified)
Repository verification
Pending. Repository verification is still required before this Issue can become agent-ready.
This bounded pass classifies intake only. It cannot execute code, close the Issue, or grant governance authority.