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): full is the on-disk transcript, which has grown past messageCount. Condition true, tail preserved, works as intended.
-
Live path — src/agent.ts:618-619:
const { summary } = compactMessages(messages);
const compacted = buildCompactedTranscript(messages, summary);
full is the very array that produced messageCount. The condition evaluates to full.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.
Summary
When
--compact-thresholdfires, 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 withprovider 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
buildCompactedTranscriptis correct on the resume path and unreachable on the live path. Testing with--continuewill 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):
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
compactMessagesstamps the summary with the length of the array it was given (src/compaction.ts:258):buildCompactedTranscriptpreserves the tail only when the full transcript is longer than that (src/compaction.ts:324):That condition depends on which array
fullis: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: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)]— andrenderSummaryMessageemits rolesystemtoo. The provider receives two system messages and zero user/assistant turns.The comment directly above
buildCompactedTranscriptstates 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
messageCountthat reflects what was summarized rather than the whole array, or havebuildCompactedTranscripttake 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-thresholdcan never fire on the first provider call of a process:lastPromptTokensis initialized to0(src/agent.ts:492) and the guard testslastPromptTokens >= compactThresholdbefore the call (src/agent.ts:616). A session resumed with--continuetherefore sends its full history at least once before compaction is even considered.Verified: a 1 941-token session resumed with
--compact-threshold 1500issued 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.