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).
| Package | v3 line | v4 release |
|---|---|---|
@context-chef/core | 3.9.0 | 4.0.0 |
@context-chef/ai-sdk-middleware | 2.1.0 | 3.0.0 |
@context-chef/tanstack-ai | 0.6.0 | 1.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.
| Removed | Replacement |
|---|---|
TokenUtils | estimate, estimateObject |
XmlGenerator | objectToXml |
AdapterFactory | getAdapter, adapterRegistry |
JanitorConfig.onBudgetExceeded | JanitorConfig.onBeforeCompress |
// 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');// 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.
// 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.
// 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++).
// 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-independent —
withGuardrailsbefore or aftersetDynamicStategives 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.
- Persisted —
ChefSnapshotgainsguardrailOptions; snapshot/restore round-trips it.
// 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.contextManagementand 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:tscreateContextMiddleware({ ..., allowDoubleCompression: true })Inherits core v4 compression behavior, including the
triggerRatio0.7 default — middleware compression now fires at 70% ofcontextWindow. PasstriggerRatio: 1to 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.contextWindowis now optional — but required as soon as you configurecompress,onCompress, oronBeforeCompress(throws otherwise).Session keying by
ctx.threadId(conversationIdaccepted as deprecated alias). There is no explicitsessionIdoption; callers without athreadIdget per-run isolation only.compactwindow semantics now mirror AI SDKpruneMessages;reasoningsupports'all' | 'before-last-message' | 'none',toolCallsaccepts array form with per-tool granularity.emptyMessagesdefaults to'remove'(v0.6 kept empty messages).clear: ['thinking']now works — in v0.6 it was a warned no-op.transformContextgained a thirdctxparam andsystemPromptswidened toSystemPrompt[]: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
| Feature | Config / API |
|---|---|
| Constraint pinning (pinned messages survive compression verbatim, turn-scoped) | Message.pinned: true |
| Reversible compression archive + recall tool | janitor.archive: 'vfs' | CompressionArchiveConfig, getRecallToolDefinition(), chef.resolveRecall(uri) |
| Server-side context management (Anthropic compaction, betas auto-derived) | contextManagement: { strategy: 'server' } |
| Domain guidelines in the compression prompt | janitor.compressionGuidelines: string[] |
| Incremental-anchored compression (persistent anchor doc) | janitor.compressionMode: 'incremental-anchored', janitor.getAnchorDoc() |
| Background (non-blocking) compression | janitor.compressionScheduling: 'background' |
| Post-summarization validation gate | janitor.validateCompression(summary, { compressed, kept }) |
| Uniform tool-result rewrite (offload, PII redaction) before compression | transformToolResult(content, { toolName, toolCallId }) |
| Granular events | compress: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 text | new OpenAIAdapter({ preserveThinkingAsText: true }), same on GeminiAdapter |
| Anthropic Tool Search annotation | ToolDefinition.deferLoading: true |
New compact() targets | clear: ['reasoning-tags']; { target: 'tool-result', toolFilter, exemptTools } |
| Single memory-store scan per compile (was 4) | Memory.compileArtifacts() (public) |
Migration checklist
- Bump the packages: core
^4.0.0, ai-sdk-middleware^3.0.0, tanstack-ai^1.0.0(plus@tanstack/ai ^0.44if applicable). - Replace removed imports:
TokenUtils→estimate/estimateObject,XmlGenerator→objectToXml,AdapterFactory→getAdapter/adapterRegistry. - Rename
janitor.onBudgetExceeded→onBeforeCompress(signature unchanged). - Decide on compression timing: keep the new 0.7 pre-rot trigger, or set
triggerRatio: 1for v3 timing. Re-check any tunedpreserveRatio. - If you depended on placeholder truncation when the compression model failed, add your own fallback in
onBeforeCompress— v4 leaves history unchanged on failure. - If your summarizer legitimately produces low-shrink output on large spans, lower or disable
minShrinkRatio. - Audit
withGuardrailsusage: remove any ordering workarounds, and addwithGuardrails(null)where you relied on it being forgotten. Expect the guardrail as its own tail message in payload diffs. - Move any control-flow logic out of event handlers (they no longer fail
compile()). - ai-sdk-middleware: if you intentionally combine client compression with Anthropic server context management, set
allowDoubleCompression: true; otherwise delete your own de-duplication guards. - tanstack-ai: upgrade
@tanstack/aito ^0.44, passthreadIdfor cross-run session continuity, addcontextWindowwherever compression is configured, updatetransformContextsignatures, and re-check flows that relied onemptyMessages: 'keep'or theclear: ['thinking']no-op. - Then adopt the new features where they pay off — pin your policy messages (
pinned: true), considerarchive: 'vfs'+ the recall tool, and evaluatecontextManagement: { strategy: 'server' }on Anthropic.