Skip to content

Architecture v5: five-axis model — pipeline phases, overflow strategies, one store, one context tool (4.2) - #53

Merged
MyPrototypeWhat merged 15 commits into
mainfrom
claude/architecture-v5
Sep 7, 2026
Merged

MyPrototypeWhat merged 15 commits into
mainfrom
claude/architecture-v5

Conversation

@MyPrototypeWhat

@MyPrototypeWhat MyPrototypeWhat commented Sep 6, 2026 •

Copy link
Copy Markdown
Owner

Draft until the final adversarial review completes. Contract: docs/architecture-v5.md.

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 @deprecated alias; the 5.0 flip list is in MIGRATION-5.md.

Phases (one commit each)

Phase What Key numbers
0 Cleanup + golden fixtures (10 configs × 3 targets, snapshotted before any edit) byte-stability guard for everything below
1 _compileInner → 11 explicit phases; chef.use() slots (6); pipelineChecks 1560-line facade → 43-line orchestrator
4a one StorageBackend, one Store + namespaces; legacy stores wrapped VFSMemoryStore collapses into a namespace
2 OverflowStrategy: summarize / anchored / server(fallback) / reset / chain / background; Janitor = runner Janitor 1294 → 870
3 handoff budget (pre-eviction notice), new_context, window lineage Codex-validated pattern, client-side
4b one context tool (memory_20250818-shaped + search), chef.handleTool, tools: 'legacy' | 'unified', ChefConfig.store schema has exactly one enum
4c one Vocabulary resolved once; legacy delegates verbatim unified: no legacy tool name anywhere in the payload (tested)
5 five-axis docs (en/zh, README + docs-site), changesets, MIGRATION-5, TODO, skill docs every snippet typechecked

Guarantees

  • Golden suite untouched from Phase 0 through Phase 5 — default-config payloads are byte-identical to 4.1.
  • Mechanism, not policy: no option combination is rejected; silent-failure classes go through audit/events.
  • Static library-owned tool schemas (prefix cache stability, the 4.1 guarantee) preserved.
  • core 939 → 1248 tests, ai-sdk-middleware 132 → 138, tanstack-ai 114 → 120; typecheck / lint / build / vitepress build / changeset status all clean.

Not in this release (5.0)

tools default → unified (tool names are dispatch keys in user code); alias removals; archive/ namespace switch; durableCompaction unified 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):

  • Security: context:// namespace and path segments were never validated (context://../ listed the process cwd); FileSystemBackend had no physical-path confinement. Both layers now reject ., .., empty segments, backslashes and control characters, and the backend refuses anything resolving outside its namespace directory.
  • 4.1 parity: janitor.strategy = server() was never server-managed; pinned turns inside a compressed span vanished from onCompress / archive / citation counts (OverflowResult.span restores the numbers); a finished background job only landed if the history was over budget again (OverflowStrategy.pending()); Memory.getAll() reordered integer-like keys; VFSMemoryStore stopped writing _index.json; the construction-time "no compressionModel" warning was dead.
  • Silent failures: requestNewContext() was a no-op under summarize()/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 via pipeline:invariant + warn-once. The once-per-window handoff flag now survives snapshot()/restore().
  • Docs: onBeforeCompress is not deprecated; only compressionMode / compressionScheduling / archive are; handoff runs before overflow; pipelineChecks runs once per chain; legacy .mem memory directories migrate through Store.fromMemoryStore(new VFSMemoryStore(dir)), not FileSystemBackend; the integrations pass through store / overflow only; archive/ is no longer advertised in 4.x model-facing text.

Golden fixtures untouched throughout.

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'.
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
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.
@MyPrototypeWhat
MyPrototypeWhat merged commit e773791 into main Sep 7, 2026
2 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 7, 2026
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.

1 participant