Skip to content

Migrating to ContextChef v4

v4 is the compression pipeline v2 release: compression now triggers early ("pre-rot"), can be pinned, validated, archived and reversed, run in the background, or delegated entirely to the provider's server-side context management (Anthropic compact_20260112, OpenAI /responses/compact). It also ships a new openai-responses target adapter, a full rewrite of @context-chef/tanstack-ai for @tanstack/ai ^0.44, and structural cleanup: deprecated namespace wrappers are gone and src/index.ts is now a pure barrel (the facade lives in src/chef.ts — the public API surface is otherwise unchanged).

Packagev3 linev4 release
@context-chef/core3.9.04.0.0
@context-chef/ai-sdk-middleware2.1.03.0.0
@context-chef/tanstack-ai0.6.01.0.0

Breaking changes

1. Removed APIs

The deprecated namespace wrappers are deleted. The underlying functions were already exported directly; imports are the only thing that changes.

RemovedReplacement
TokenUtilsestimate, estimateObject
XmlGeneratorobjectToXml
AdapterFactorygetAdapter, adapterRegistry
JanitorConfig.onBudgetExceededJanitorConfig.onBeforeCompress
ts
// v3
import { TokenUtils, XmlGenerator, AdapterFactory } from '@context-chef/core';
TokenUtils.estimate(text);
XmlGenerator.objectToXml(obj);
AdapterFactory.getAdapter('anthropic');

// v4
import { estimate, objectToXml, getAdapter } from '@context-chef/core';
estimate(text);
objectToXml(obj);
getAdapter('anthropic');
ts
// v3
janitor: { onBudgetExceeded: (history, info) => firstPassCompact(history) }

// v4 — same signature, same semantics, new name
janitor: { onBeforeCompress: (history, info) => firstPassCompact(history) }

2. Compression triggers at 70% of the window (triggerRatio default 0.7)

Why: model quality degrades well before the hard window limit, and compressing at 100% invites death loops (Gemini CLI moved its trigger from 0.7 to 0.5 after exactly this). v4 triggers compression at contextWindow * triggerRatio, default 0.7. preserveRatio now applies to that effective budget, not the raw window.

ts
// v3 behavior: compress only when contextWindow itself is exceeded
janitor: { contextWindow: 200_000 }

// v4: compression now fires at 140k tokens. To restore v3 behavior:
janitor: { contextWindow: 200_000, triggerRatio: 1 }

If you tuned preserveRatio against the raw window in v3, re-check it — it is now a fraction of contextWindow * triggerRatio.

3. Compression failure leaves history unchanged

Why: v3 truncated history with a placeholder when the compression model failed, silently destroying context. In v4 a model failure, a shrink-guard trip (see next item), or a validateCompression rejection all return the history unchanged and increment the circuit breaker. Failures also no longer set the post-compression suppression flag (E10) — that flag now only suppresses the re-check after a successful compression.

ts
// v3: compressionModel throws → history replaced with a truncation placeholder
// v4: compressionModel throws → history returned as-is, breaker++ (3 strikes → compress() no-ops)

If you relied on the lossy fallback to keep payloads under the window, add your own first-pass onBeforeCompress / compact() fallback.

4. Shrink guard: minShrinkRatio default 0.5

Why: a summarizer that echoes its input used to "succeed" and loop forever. In v4, for compressed spans ≥ 2000 characters, the summary must shrink the span by at least 50% or the compression is treated as failed (history unchanged, breaker++).

ts
// A summarizer that returns near-verbatim output now trips the breaker.
// To disable the guard:
janitor: { contextWindow: 200_000, minShrinkRatio: 0 }

5. withGuardrails is deferred, replace-semantics, and its own message

Why: in v3 the guardrail was applied eagerly and merged into the dynamic-state message — calling setDynamicState() after withGuardrails() silently discarded the guardrail. In v4 the options are stored and applied during compile():

  • Order-independentwithGuardrails before or after setDynamicState gives the same output.
  • Replace semantics — each call replaces the previous options (no accumulation).
  • withGuardrails(null) clears the stored options.
  • Own tail message — the guardrail lands at the sandwich end as its own message, no longer merged into the dynamic-state message. Byte-level payload diffs are expected.
  • PersistedChefSnapshot gains guardrailOptions; snapshot/restore round-trips it.
ts
// v3 — order mattered, this silently dropped the guardrail:
chef.withGuardrails({ enforceXML: { outputTag: 'answer' } });
chef.setDynamicState(state); // guardrail gone

// v4 — same code works in any order; to remove a guardrail, be explicit:
chef.withGuardrails(null);

6. Event handlers are error-isolated

Why: an observer should never break compilation. In v3 a throwing event handler propagated out of compile(). In v4 the error is logged and compile() continues. If you (ab)used a throwing handler as a control-flow gate, move that logic into a real hook (onBeforeCompile, validateCompression, transformContext).

7. compile() calls are serialized

Concurrent compile() calls on one instance now queue (snapshot + serialize) instead of interleaving and corrupting shared state (turn counter, circuit breaker, signal stash). Inputs are snapshotted at call time — setHistory() between two queued calls means the first compile sees the old history, the second sees the new. A rejected compile does not poison the queue. The canonical pattern is still one chef per concurrent caller; the queue exists to make accidental sharing safe, not fast.

8. @context-chef/ai-sdk-middleware 3.0.0

  • Server context management skips middleware compression. When a call carries providerOptions.anthropic.contextManagement and the middleware also has compression configured, middleware compression is skipped for that call (one-time warning) — otherwise both you and Anthropic would compress the same history. Escape hatch:

    ts
    createContextMiddleware({ ..., allowDoubleCompression: true })
  • Inherits core v4 compression behavior, including the triggerRatio 0.7 default — middleware compression now fires at 70% of contextWindow. Pass triggerRatio: 1 to restore v2 timing.

9. @context-chef/tanstack-ai 1.0.0 (rewrite for @tanstack/ai ^0.44)

The old ^0.10 upstream API no longer exists; the package is rewritten around 0.44's ChatMiddleware with a return-based onConfig (fires at init + every iteration; injections are idempotent, hooks never throw into the host).

  • Peer dependency: @tanstack/ai ^0.44.0.

  • contextWindow is now optional — but required as soon as you configure compress, onCompress, or onBeforeCompress (throws otherwise).

  • Session keying by ctx.threadId (conversationId accepted as deprecated alias). There is no explicit sessionId option; callers without a threadId get per-run isolation only.

  • compact window semantics now mirror AI SDK pruneMessages; reasoning supports 'all' | 'before-last-message' | 'none', toolCalls accepts array form with per-tool granularity.

  • emptyMessages defaults to 'remove' (v0.6 kept empty messages).

  • clear: ['thinking'] now works — in v0.6 it was a warned no-op.

  • transformContext gained a third ctx param and systemPrompts widened to SystemPrompt[]:

    ts
    // v0.6
    transformContext?: (messages, systemPrompts: string[]) => {...}
    
    // 1.0
    transformContext?: (messages, systemPrompts: SystemPrompt[], ctx: ChatMiddlewareContext) => {...}
  • Compression adapter no longer caps summarizer output tokens — 0.44 removed top-level maxTokens. Bound your summarizer's output in your own model call if you need a cap.

New in v4

FeatureConfig / API
Constraint pinning (pinned messages survive compression verbatim, turn-scoped)Message.pinned: true
Reversible compression archive + recall tooljanitor.archive: 'vfs' | CompressionArchiveConfig, getRecallToolDefinition(), chef.resolveRecall(uri)
Server-side context management (Anthropic compaction, betas auto-derived)contextManagement: { strategy: 'server' }
Domain guidelines in the compression promptjanitor.compressionGuidelines: string[]
Incremental-anchored compression (persistent anchor doc)janitor.compressionMode: 'incremental-anchored', janitor.getAnchorDoc()
Background (non-blocking) compressionjanitor.compressionScheduling: 'background'
Post-summarization validation gatejanitor.validateCompression(summary, { compressed, kept })
Uniform tool-result rewrite (offload, PII redaction) before compressiontransformToolResult(content, { toolName, toolCallId })
Granular eventscompress:start, compress:end, offload:created, pruner:tool-blocked
OpenAI Responses API target (reasoning items preserved byte-identically)compile({ target: 'openai-responses' }), fromOpenAIResponses()
Gemini 3.x thought signatures (round-trip, survives compact(['thinking']))ToolCall.thoughtSignature (automatic)
Cross-provider thinking replay as textnew OpenAIAdapter({ preserveThinkingAsText: true }), same on GeminiAdapter
Anthropic Tool Search annotationToolDefinition.deferLoading: true
New compact() targetsclear: ['reasoning-tags']; { target: 'tool-result', toolFilter, exemptTools }
Single memory-store scan per compile (was 4)Memory.compileArtifacts() (public)

Migration checklist

  1. Bump the packages: core ^4.0.0, ai-sdk-middleware ^3.0.0, tanstack-ai ^1.0.0 (plus @tanstack/ai ^0.44 if applicable).
  2. Replace removed imports: TokenUtilsestimate/estimateObject, XmlGeneratorobjectToXml, AdapterFactorygetAdapter/adapterRegistry.
  3. Rename janitor.onBudgetExceededonBeforeCompress (signature unchanged).
  4. Decide on compression timing: keep the new 0.7 pre-rot trigger, or set triggerRatio: 1 for v3 timing. Re-check any tuned preserveRatio.
  5. If you depended on placeholder truncation when the compression model failed, add your own fallback in onBeforeCompress — v4 leaves history unchanged on failure.
  6. If your summarizer legitimately produces low-shrink output on large spans, lower or disable minShrinkRatio.
  7. Audit withGuardrails usage: remove any ordering workarounds, and add withGuardrails(null) where you relied on it being forgotten. Expect the guardrail as its own tail message in payload diffs.
  8. Move any control-flow logic out of event handlers (they no longer fail compile()).
  9. ai-sdk-middleware: if you intentionally combine client compression with Anthropic server context management, set allowDoubleCompression: true; otherwise delete your own de-duplication guards.
  10. tanstack-ai: upgrade @tanstack/ai to ^0.44, pass threadId for cross-run session continuity, add contextWindow wherever compression is configured, update transformContext signatures, and re-check flows that relied on emptyMessages: 'keep' or the clear: ['thinking'] no-op.
  11. Then adopt the new features where they pay off — pin your policy messages (pinned: true), consider archive: 'vfs' + the recall tool, and evaluate contextManagement: { strategy: 'server' } on Anthropic.