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:

  1. Cache STORE-side verifier checkcache-admission-gate now runs Phase 8.3's verifyContract when messages is passed. A failed verifier rejects the entry at store-time with reason contract_verifier_failed. Closes the cache-amplification path that Phase 2's structural gate couldn't catch.
  1. scoreAndRecordValidity messages threading — the audit-chain reward path now receives messages and responseFormat, so the contract verifier inside maybeApplyRewardV2 can extract enum / exact_string references from the original request (previously these verifiers returned not_applicable from this code path).

Skipped (third follow-up from Phase 8.4's ship-log):

  • Streaming X-BR-Contract-Status header. 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 via maybeApplyRewardV2.

What changed

src/router/cache-admission-gate.ts

  • New optional messages?: unknown[] field on CacheAdmissionContext
  • New Rule 2: when messages is 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 passes messages to evaluateCacheAdmission
  • scoreAndRecordValidity signature extended with optional messages and responseFormat
  • Both call sites (non-streaming + streaming) thread the new fields through

Rule order (final)

The admission gate now evaluates in this order:

  1. Null-content (Rule 1) — structurally broken; reject with null_content_no_tool_calls or empty_string_length_truncation
  2. Contract verifier (Rule 2, NEW) — semantically broken vs declared contract; reject with contract_verifier_failed
  3. 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_object requests). 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).
  • messages plumbing through scoreAndRecordValidity means the bandit-V2 audit chain now demotes models that fail enum / exact_string contracts (previously these slipped through as not_applicable).
  • Backward-compatible. The new messages field on CacheAdmissionContext is 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.