2026-05-21-phase-8-5-cache-store-verifier
2026-05-21 — Cache STORE-side verifier + messages threading (Phase 8.5)
Summary
Closes two of the three follow-ups from Phase 8.4's ship-log:
- Cache STORE-side verifier check —
cache-admission-gatenow runs Phase 8.3'sverifyContractwhenmessagesis passed. A failed verifier rejects the entry at store-time with reasoncontract_verifier_failed. Closes the cache-amplification path that Phase 2's structural gate couldn't catch.
scoreAndRecordValiditymessages threading — the audit-chain reward path now receivesmessagesandresponseFormat, so the contract verifier insidemaybeApplyRewardV2can extract enum / exact_string references from the original request (previously these verifiers returnednot_applicablefrom this code path).
Skipped (third follow-up from Phase 8.4's ship-log):
- Streaming
X-BR-Contract-Statusheader. SSE commits headers before the first byte, so the verifier verdict isn't available at header-write time. Would require trailer headers (implementation-dependent) or a final SSE event with the status. Not worth the complexity in Phase 8.5 — streaming reward path still gets the verifier signal viamaybeApplyRewardV2.
What changed
src/router/cache-admission-gate.ts
- New optional
messages?: unknown[]field onCacheAdmissionContext - New
Rule 2: whenmessagesis present, detect the declared contract and run the verifier. A failed verifier returns{ admit: false, reason: "contract_verifier_failed" } - Existing rules (null-content, json-heuristic) preserved as Rules 1 and 3
- Rule order: null-content > verifier > json-heuristic
- Verifier failure is non-fatal: try/catch falls through to the heuristic on any internal error
src/router/cache-admission-gate.test.ts
- 7 new tests: parseable-but-wrong-shape integer, non-JSON for json_object, valid integer/json admits, messages-absent fallthrough, verifier-not-applicable fallthrough, null-content wins over verifier
src/router/model-router.ts
cacheStore()now passesmessagestoevaluateCacheAdmissionscoreAndRecordValiditysignature extended with optionalmessagesandresponseFormat- Both call sites (non-streaming + streaming) thread the new fields through
Rule order (final)
The admission gate now evaluates in this order:
- Null-content (Rule 1) — structurally broken; reject with
null_content_no_tool_callsorempty_string_length_truncation - Contract verifier (Rule 2, NEW) — semantically broken vs declared contract; reject with
contract_verifier_failed - JSON heuristic (Rule 3) — fallback for json_object when verifier didn't run; reject with
json_object_invalid_payload
The verifier supersedes the JSON heuristic when both apply (verifier knows the exact contract; heuristic is best-effort).
Operational impact
- Cache hit-rate drops further for contract-strict requests where the model is producing parseable-but-wrong outputs (e.g., wrong-shape integers, non-JSON for
json_objectrequests). This is the intended behavior — the OpenClaw 2026-05-20 cache amplification path is fully closed at both lookup-time (Phase 8.2) AND store-time (Phase 8.5). messagesplumbing throughscoreAndRecordValiditymeans the bandit-V2 audit chain now demotes models that fail enum / exact_string contracts (previously these slipped through asnot_applicable).- Backward-compatible. The new
messagesfield onCacheAdmissionContextis optional. Callers that don't pass it get the existing behavior — Rule 2 is skipped, Rule 3 (json heuristic) still catches the most common case.
Verification
$ npx vitest run --config vitest.unit.config.ts src/router/cache-admission-gate.test.ts
✓ 17 passed
$ pnpm tsgo
✓ no errors
Lockstep
- [x] No API surface change.
- [x] SDKs unaffected (internal router change).
- [x] MCP tools unaffected.
- [x] Ship-log (this file).
Plan reference
Phase 8.5 of the contract-aware-routing arc. Closes 2 of the 3 Phase 8.4 follow-ups. Phase 8.6 (MCP gating) is the next milestone — arc/contract-aware-routing is now substantively complete on the BR-internal side.