Repository navigation
Architecture v5: five-axis model — pipeline phases, overflow strategies, one store, one context tool (4.2) - #53
Merged
Merged
Conversation
The spec every refactor phase implements against: pipeline phases + slots, OverflowStrategy, handoff budget / new_context / window lineage, one StorageBackend + Store, one model-facing context tool, vocabulary alignment, and the 4.2 → 5.0 versioning plan.
Behavior-preserving prep for the pipeline/overflow/store refactor.
Golden fixtures: tests/golden/ — 10 configurations × 3 targets, payloads
snapshotted on the untouched code before any cleanup edit. This is the
byte-stability guard every later phase must keep green.
Core altitude: computeAnthropicBetas + default server edits move into the
Anthropic adapter (anthropicServerContextManagement); _anthropic_compaction /
_gemini_thought_signature / _openai_reasoning become typed Message fields;
resolveRecall gains { format: 'text' } rendering (default unchanged).
Janitor: BackgroundCompressionJob derives splitIndex/first/last from
toCompress; _summarize's three breaker-warn paths share _compressionFailed.
Cross-package: createJanitorPool (core/integrations) replaces the duplicated
budgeting/persistence-warn/Janitor assembly in ai-sdk-middleware and
tanstack-ai; createThinkingTextifier dedupes the <thinking> textifier and
redacted-thinking warn-once between the OpenAI and Gemini adapters (the TODO
misattributed that duplication to the middleware packages); tanstack drops
the derivable _originalText/_originalThinkingText projections;
detectServerContextManagement collapses to serverContextManagementWarning.
All relocated warning strings verified byte-identical.
Dead-code sweep found only two orphaned JSDoc blocks in chef.ts (reattached).
Persistence collapses to a single substrate: addressed content + meta.
- src/store/: StorageBackend (read/write/delete/list + optional readAll/
append/search/snapshot/restore/getPhysicalPath, capability-queried),
Store + NamespaceView (namespace routing, context:// URIs, auto-id put,
read-modify-write append fallback, per-namespace LRU eviction moved out of
Offloader), InMemoryBackend, FileSystemBackend (port of FileSystemAdapter
with a per-namespace on-disk layout so both legacy formats survive), and
Store.fromMemoryStore / Store.fromVfsAdapter legacy wrappers.
- Memory accepts MemoryStore | StorageBackend | Store and speaks only to
namespace('memory'); readAll keeps getAll() at 1+N reads. VFSMemoryStore
is rebuilt on FileSystemBackend (same on-disk format, no _index.json).
- Offloader accepts Store | StorageBackend | VFSStorageAdapter; offload /
resolve / cleanup / reconcile delegate to namespace('vfs'). Archive spans
keep landing in vfs (byte-identical context://vfs URIs); the archive
namespace is ready for a later switch.
- MemoryStore, VFSStorageAdapter, VFSMemoryStore, FileSystemAdapter marked
@deprecated. Public APIs unchanged; golden suite byte-identical.
_compileInner is now 43 lines: build a CompileContext, run COMPILE_PHASES in order with the same six abort checks as before. Each phase lives in src/pipeline/phases/<name>.ts and reaches chef state through a PipelineHost closure (fields stay private). Slots (chef.use / unuse, registration order): before-overflow (return false to skip), after-overflow, before-assemble (ctx.inject), after- assemble (messages → messages), before-adapt, after-adapt. Legacy onBeforeCompile / transformContext register on the same registry and are @deprecated; alias-equivalence tests assert stringifyPayload-identical output across all three targets. JanitorConfig.onBeforeCompress stays in Janitor until Phase 2. ChefConfig.pipelineChecks (default off): pinned survival + tool-pair integrity after after-assemble, pre-tail byte-identity after tail — warn + 'pipeline:invariant' event, never throw. Golden suite byte-identical. Barrel gains the pipeline types and export * from './store'.
…ack semantics; strategy snapshot hooks
Janitor is now the runner (budget evaluation, circuit breaker, compress:*
events, onCompress/onBeforeCompress, durable compaction) and calls
strategy.apply(); the compression logic lives in src/overflow/:
- summarize() = compressionMode 'rewrite'
- anchored() = compressionMode 'incremental-anchored' (owns the anchor doc)
- server(cfg, { fallback }) = contextManagement.strategy 'server' — Anthropic
target server-managed; other targets run the fallback (v4 behavior kept)
- reset() new: keeps pinned only, everything else evicted
- chain(...) escalates when changed:false or still over budget
- background() = compressionScheduling 'background' (owns the job)
OverflowStrategy gains optional commit() (anchor moves only when a result
lands — a speculative apply must not pollute it), attach() (runner breaker
+ logger), snapshot()/restore() (anchorDoc keeps round-tripping; snapshots
add an opaque `strategy` field, pre-4.2 snapshots still restore).
ChefConfig.overflow { strategy, archive, handoff (Phase 3 placeholder) };
overflow.strategy wins over the deprecated janitor.compressionMode /
compressionScheduling / archive and contextManagement aliases (warn-once on
conflict). Archive is a strategy-agnostic post-step over evicted, still
writing vfs URIs byte-identically. before-overflow handlers now receive the
runner's real budget (Janitor.readBudget).
Janitor 1294 → 870 lines. Golden suite untouched.
Window lineage { first, previous?, current } is owned by the Janitor
runner and advances at commit time only, so a stale background result
never moves it. OverflowResult.meta.windowId is the apply-time id;
CompileMeta.windowId is the window the payload belongs to. anchored() keys
its anchor document per window (capped map; pre-4.2 flat anchorDoc
adopted by the first window). Lineage rides JanitorSnapshot / chef
snapshots; clearHistory() starts fresh.
Handoff budget: overflow.handoff { budgetTokens, prompt? } (validated at
construction; {n_remaining} template; default Prompts.HANDOFF_NOTICE_TEMPLATE).
A new `handoff` phase before `overflow` renders the notice once per window
when remaining <= budgetTokens, delivered through the announcement tail
channel via CompileContext.handoffNotice — never persisted, never in
getAnnouncements(), skipped on server-managed compiles.
new_context: getNewContextToolDefinition() (static, no params) and
chef.requestNewContext() — a one-shot force that makes the next compile
run strategy.apply under budget (before-overflow veto and the circuit
breaker still apply). Dispatch wiring lands in Phase 4b.
reset() summary names the closed window. Golden suite untouched.
src/tools/: getContextToolDefinition() — a single static, frozen,
reference-stable tool named `context`, memory_20250818-shaped (view /
create / str_replace / insert / delete / rename) plus `search`, addressed
by context://<ns>/<path>. The schema has exactly one enum (`command`), so
no live key list can enter the cached prefix.
dispatchContextTool(host, call): memory/ routes through the Memory module
(allowedKeys, onMemoryUpdate veto, onMemoryChanged, TTL, updateCount all
apply; create-exists and rename = create+delete handled at the dispatcher);
notes/ goes straight to store.namespace('notes') (line-numbered view,
single-occurrence str_replace, 0-based insert, search with a list+get
fallback when the backend lacks search); vfs/ and archive/ are view-only
by default and render through the recall path. Model-facing mistakes
return `Error: …` text (dispatch-gate), programmer errors throw.
ContextChef: ownsTool(name), handleTool(call), getStore();
ChefConfig.tools 'legacy' (default, byte-identical payloads) | 'unified'
(pruner tools + context, + new_context when overflow.handoff is set;
legacy memory tools never co-emitted); ChefConfig.contextTool.writable
(default memory + notes); ChefConfig.store wires one backend into Memory
and Offloader with explicit per-module stores taking precedence.
Golden suite untouched. core 1144 → 1226.
src/vocabulary.ts: Vocabulary { memoryInstruction, memoryBlockHeader,
memoryBlock(), offloadPlaceholder(), summaryWrapper(lineage?),
handoffNotice }. LEGACY_VOCABULARY delegates verbatim to the existing
Prompts strings (byte-identity guarantee); UNIFIED_VOCABULARY speaks
context:// + the context tool + new_context. Resolved once in the chef
constructor from `tools` mode and injected into memory shaping, the
Offloader placeholder (VFSConfig.vocabulary), the Janitor runner's summary
rendering (JanitorConfig.vocabulary; the lineage line
"Context window: w_n (previous: w_m)" lives here) and the handoff default.
Standalone modules without a vocabulary behave exactly as before.
Unified constants: CONTEXT_STORE_INSTRUCTION, CONTEXT_MEMORY_BLOCK_HEADER,
getContextMemoryBlock, getContextOffloadReminder, getContextSummaryWrapper,
CONTEXT_HANDOFF_NOTICE_TEMPLATE. cacheAudit derives its memory markers from
both vocabularies' memoryBlockHeader. Memory.getToolDefinitions marked
@deprecated (the inline <update_core_memory> path the TODO described no
longer exists in code).
Middleware packages: TruncateOptions.store and ContextChefOptions.overflow
pass through to Offloader / Janitor (neither package constructs a chef or
dispatches library tools, so `tools` mode has no target there).
One-vocabulary invariant under `tools: 'unified'` is asserted by test: no
legacy tool name anywhere in the payload. Golden suite untouched.
core 1226 → 1248, middleware 132 → 138, tanstack 114 → 120.
…ill docs READMEs (root en/zh, core, ai-sdk-middleware en/zh, tanstack-ai) and the docs-site (en/zh) are restructured around the five axes: a "How it fits together" map, Overflow (strategies, handoff, new_context, lineage, deprecation table), Context store (backend, Store, namespaces, context://, ChefConfig.store, tools mode, the context tool, handleTool), Pipeline slots. New docs-site pages guide/architecture and guide/context-store; the compression page is retitled Overflow (URL kept); sidebar regrouped in both locales. Every new snippet was typechecked against the built 4.2 types. Release artifacts: three changesets (core 4.2.0, ai-sdk-middleware 3.1.0, tanstack-ai 1.1.0); MIGRATION-5.md with the full deprecated → replacement table and the 5.0 flip list (tools default → unified, alias removals, archive/ namespace switch, durableCompaction vocabulary); TODO.md pruned of landed follow-ups (each verified in code) and given a 5.0 removal list; both agent-facing skill docs refreshed to the 4.2 API; @deprecated audit (one missing tag added: getRecallToolDefinition). Also: createCompressionAdapter is now exported from ai-sdk-middleware (tanstack already exported its equivalent); spec §3/§8 corrected — onBeforeCompress stays a runner concern, the inline memory-tag write path no longer exists.
Store / dispatcher: validate every context:// namespace and segment, confine FileSystemBackend to its namespace directory, list nested paths recursively, keep readAll/entries()/getAll() in backend key order, apply meta on native append, route recall/view through the offloader's own URI scheme, and write VFSMemoryStore's _index.json through again. Overflow / runner / chef: derive contextManagement from the resolved strategy so janitor.strategy = server() is server-managed, report a dropped forced overflow via pipeline:invariant + warn-once, carry handoffNoticedWindow in ChefSnapshot, restore the construction-time compressionModel warning, add OverflowResult.span so onCompress/archive/citation see the 4.1 numbers, make forced overflow fold every turn but the last, add OverflowStrategy.pending() so a finished background job lands regardless of budget, and stop advertising archive/ in 4.x model-facing text. Docs: onBeforeCompress is not deprecated, only three JanitorConfig aliases are, handoff runs before overflow, pipelineChecks runs once per chain, FileSystemBackend does not read legacy .mem files (migrate through Store.fromMemoryStore), integrations pass through store/overflow only.
MyPrototypeWhat
marked this pull request as ready for review
September 6, 2026 06:35
Four places where a new contract shipped with a hedge that only made sense for an already-published API. OverflowResult.span is required. Every strategy declares what its result stands for: span is the compressed span (pinned turns it re-inserted included), evicted is what left the window, a result that changed nothing declares both empty. The three span ?? evicted fallbacks are gone, so a future consumer cannot forget one. StoredEntries is a Map. The Record half of the union existed for backends written against a signature that has never been released; asEntryMap and the two-shapes contract go with it. resolveContextManagement is a pure projection of the resolved strategy. It no longer re-reads the config to answer a question _resolveOverflowStrategy has already answered. VFSMemoryStore recomputes _index.json from the directory on every write instead of from a cached key set. The cached set was wrong in exactly the case the index exists for: keys another process wrote were silently dropped from the file that process reads its key list out of. The recompute is one readdir plus the layout's own decode, guarded by an encode round-trip so a foreign or scratch .mem filename cannot enter the index. Also: vfs.uriScheme is marked deprecated and listed for 5.0 removal (it is the only reason the dispatcher needs a second URI parser), background()'s staleness check is described as content equivalence everywhere rather than message identity, and the archive is documented as storing the compressed span rather than what was evicted.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The library is re-founded on five explicit axes — selection · placement · persistence · retrieval · adaptation — and every existing feature is re-homed on one of them. Ships as core 4.2.0 (ai-sdk-middleware 3.1.0, tanstack-ai 1.1.0): zero forced migration, every renamed option keeps a working
@deprecatedalias; the 5.0 flip list is inMIGRATION-5.md.Phases (one commit each)
_compileInner→ 11 explicit phases;chef.use()slots (6);pipelineChecksStorageBackend, oneStore+ namespaces; legacy stores wrappedVFSMemoryStorecollapses into a namespaceOverflowStrategy:summarize/anchored/server(fallback)/reset/chain/background; Janitor = runnernew_context, window lineagecontexttool (memory_20250818-shaped + search),chef.handleTool,tools: 'legacy' | 'unified',ChefConfig.storeVocabularyresolved once; legacy delegates verbatimGuarantees
vitepress build/changeset statusall clean.Not in this release (5.0)
toolsdefault →unified(tool names are dispatch keys in user code); alias removals;archive/namespace switch;durableCompactionunified vocabulary.Final review (
67f2d36)An adversarial multi-agent review of the completed branch (finders across security / regression-vs-4.1 / silent-failure / docs-truth angles, each finding probe-verified) confirmed 28 defects — 3 critical, 15 major, 10 minor — all fixed in one commit with a regression test per finding (core 1248 → 1329 tests):
context://namespace and path segments were never validated (context://../listed the process cwd);FileSystemBackendhad no physical-path confinement. Both layers now reject.,.., empty segments, backslashes and control characters, and the backend refuses anything resolving outside its namespace directory.janitor.strategy = server()was never server-managed; pinned turns inside a compressed span vanished fromonCompress/ archive / citation counts (OverflowResult.spanrestores the numbers); a finished background job only landed if the history was over budget again (OverflowStrategy.pending());Memory.getAll()reordered integer-like keys;VFSMemoryStorestopped writing_index.json; the construction-time "no compressionModel" warning was dead.requestNewContext()was a no-op undersummarize()/anchored()when the preserved tail already fit, and silently dropped on server-managed or vetoed compiles — now it folds every turn but the last, and a dropped request is reported viapipeline:invariant+ warn-once. The once-per-window handoff flag now survivessnapshot()/restore().onBeforeCompressis not deprecated; onlycompressionMode/compressionScheduling/archiveare; handoff runs before overflow;pipelineChecksruns once per chain; legacy.memmemory directories migrate throughStore.fromMemoryStore(new VFSMemoryStore(dir)), notFileSystemBackend; the integrations pass throughstore/overflowonly;archive/is no longer advertised in 4.x model-facing text.Golden fixtures untouched throughout.