Skip to content

fix(router): port compaction handover to OpenAI path - #792

Open
rohith500 wants to merge 2 commits into
workweave:mainfrom
rohith500:fix/791-openai-compaction-handover
Open

fix(router): port compaction handover to OpenAI path#792
rohith500 wants to merge 2 commits into
workweave:mainfrom
rohith500:fix/791-openai-compaction-handover

Conversation

@rohith500

Copy link
Copy Markdown
Contributor

Fixes #791.

Summary

ProxyOpenAIChatCompletion never called runCompactionHandover, even
though the shared runTurnLoop already detects the exact trigger condition
(PrefixTrimmed) that ProxyMessages uses to fire it. This PR ports the
same consumer, plus its billing, to the OpenAI path.

Background / root cause

runTurnLoop's compactionTracker.checkAndRecord
(internal/proxy/no_progress.go) detects two shapes of client-side history
trimming by comparing each turn's message count and tool-call count to the
last observation for the same session: full compaction (message count drops
sharply from a substantial conversation, gated at
compactionMinHistoryMessages = 8) and rolling-window trimming (message
count flat, tool-call count shrinks). Either sets routeRes.PrefixTrimmed,
computed pre-routing and shared by both ProxyMessages and
ProxyOpenAIChatCompletion — both already read it for planner cold-pin /
free-switch pricing.

Only ProxyMessages (pre-fix) consumed it post-routing to protect
correctness, not just cost. PR #298 introduced this after a real production
incident: Claude Code's context compaction dropped a completed Edit and its
tool_result from history, and the non-Anthropic model serving that
session (DeepSeek) had no record the edit had happened — it re-applied the
same edit. runCompactionHandover rewrites the envelope with a summary of
the elided work before dispatch, specifically for the STAY-on-non-Anthropic
case (a SWITCH turn gets its own handover summarizer via the turn loop
already).

ProxyOpenAIChatCompletion had zero references to PrefixTrimmed or
runCompactionHandover post-routing. It got maybeCompact (the separate,
proactive, pre-routing feature that prevents context-window overflow) via
PR #644 — which is a different mechanism entirely and doesn't address this.
For an OpenAI-wire session pinned to a non-Anthropic model that compacts
its own history mid-session, the router forwarded the already-trimmed
history raw, with no injected "prior work" summary — the same failure
shape #298 fixed for Messages, just unaddressed on this sibling surface.

What changed

In ProxyOpenAIChatCompletion (internal/proxy/service.go):

  1. New guard, mirroring ProxyMessages exactly, with one addition:
compactionHandoverRan := false
var compactionHandoverOutcome handoverOutcome
if !responsesPassthrough && decision.Provider != providers.ProviderAnthropic &&
   !routeRes.HardPinned && !routeRes.Handover.Invoked && !compResOAI.Applied &&
   routeRes.PrefixTrimmed {
    log.Info("Context trimming detected on non-Anthropic route; rewriting context with handover summary", ...)
    compactionHandoverOutcome = s.runCompactionHandover(ctx, env, r.Header, decision.Model)
    compactionHandoverRan = true
}

The !responsesPassthrough clause is new relative to the Messages guard —
Codex Responses passthrough bodies are forwarded verbatim by design
elsewhere in this function, and should stay that way rather than getting
rewritten with a handover summary.

  1. cacheEligible updated to exclude compactionHandoverRan, matching
    the existing Messages-path comment/rationale: a handover rewrite means
    the routing decision's embedding predates the rewrite, so caching under
    that embedding would be wrong.

  2. Billing wired, verified as a structural match (not just an
    equivalent outcome) against the Messages-path block at
    service.go:3023 — same DebitInferenceParams construction, same
    _compaction_summary request-ID suffix, same guard shape:

if compactionHandoverOutcome.Invoked && !compactionHandoverOutcome.FallbackToFullHistory {
    sumUsage := compactionHandoverOutcome.SummaryUsage
    if sumUsage.Model != "" && (sumUsage.InputTokens > 0 || sumUsage.OutputTokens > 0) {
        // ... identical to ProxyMessages' block
    }
}
  1. runCompactionHandover itself is untouched. ProxyOpenAIChatCompletion never calls runCompactionHandover after PrefixTrimmed — OpenAI client-trim stays lose prior-work summary #791's investigation
    confirmed the summarizer already accepts OpenAI-format envelopes end to
    end:

    • buildSummaryRequestBodyenv.PrepareAnthropic (handover.go:221-222)
    • PrepareAnthropic handles FormatOpenAI via buildAnthropicFromOpenAI
      (emit_anthropic.go:21-25)
    • RewriteForHandover already has an OpenAI case (translate/handover.go)

    This confirmed it as a call-site wiring gap, not a missing translation
    layer — so this PR is scoped to service.go only.

Commits

  1. 5b705edTestService_CompactionHandover_OpenAIParityWithMessages,
    asserting the desired post-fix behavior (summarizer invoked on OpenAI,
    matching Messages). Confirmed failing against pre-fix code for the right
    reason: the trim was correctly detected
    ("turnloop detected client history trim" in logs) but the summarizer
    was never called (sz.calls == 0).
  2. 9c5f0c7 — the fix described above. Test passes after.

Testing

  • New test sends an identical 9-message → 3-message trim sequence (same
    session: same API key, same first user message, per DeriveSessionKey)
    through both ProxyMessages and ProxyOpenAIChatCompletion against a
    real *Service with stub providers and a counting stub summarizer (no
    network calls). Turn 1 (first observation, nothing to compare against)
    correctly shows zero invocations on both surfaces — confirms the trigger
    is genuinely the turn-2 count drop, not incidental to message shape or
    turn count alone. Turn 2 now shows sz.calls >= 1 on both surfaces.
  • go test ./internal/proxy/... -count=1 — clean, including all existing
    OpenAI-path and compaction-related tests (no regressions).
  • make check — clean.

Known, deliberately unaddressed

Related (from #791)

rohith500 and others added 2 commits July 19, 2026 16:18
Signed-off-by: N Rohith Reddy <rohithreddy2202@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Mirrors ProxyMessages' PrefixTrimmed consumer: ProxyOpenAIChatCompletion
now calls runCompactionHandover when staying on a non-Anthropic model after
a client-side history trim, matching the protection PR workweave#298 added for
Messages. Without this, an OpenAI-wire session pinned to a non-Anthropic
model that compacts its own history got no 'prior work' summary injected,
risking the same duplicate-edit failure workweave#298 fixed for Messages.

Also wires the corresponding _compaction_summary billing, matching the
Messages path.

Fixes workweave#791

Signed-off-by: N Rohith Reddy <rohithreddy2202@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Thanks for this — reviewed against the repo's AGENTS.md conventions and it conforms nicely, no changes needed.

  • Correct layer: the consumer sits on proxy.Service post-routing, mirroring the ProxyMessages guard, with the sensible !responsesPassthrough addition for Codex passthrough bodies.
  • cacheEligible correctly excludes the handover-rewritten turn (embedding predates the rewrite), matching the Messages-path rationale.
  • Billing block is a structural match to the Messages _compaction_summary debit.
  • Tests use providers.Provider* constants (no magic provider strings) and are non-tautological — asserting the summarizer fires on the 9→3 trim on both surfaces, with a first-observation negative check. The scoping to service.go only (verified runCompactionHandover already handles OpenAI envelopes end-to-end) is exactly right.

Nice parity fix — appreciated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ProxyOpenAIChatCompletion never calls runCompactionHandover after PrefixTrimmed — OpenAI client-trim stays lose prior-work summary

1 participant