SIGN IN SIGN UP

fix(cache): one shared prefix-hit helper, media-history turns reuse their text prefix, DSV4 block size reconciled where every launcher converges

Five defects, four of them the same shape: a rule enforced in one of two
places that both have to obey it.

1. MLLM disk L2 partial hits skipped a token. The helper reported
   cached=len(matched) and dropped matched[-1], on a docstring premise that
   the MLLM disk store "stores the clean prompt-boundary cache under the same
   token key length it reports". Both MLLM store branches disprove it: the
   plain VLM path truncates through _truncate_hybrid_cache(prompt_len - 1)
   and the mixed-SWA / ZAYA clean path re-prefills token_list[:prompt_len-1],
   and both then write under the FULL N-token key. So the last matched token
   was never in the payload, never re-fed, and never got KV -- a warm
   disk-prefix turn answered differently from the same turn cold, and a
   boundary-swap match (thinking sentinel toggling between turns) ate exactly
   the token that changed, silently serving the previous turn's mode. The
   text lane got this in 12ee1c8ee; the MLLM copy did not. Both lanes now
   delegate to vmlx_engine/utils/prefix_hit.py, so there is no second copy to
   forget. Two tests that pinned the wrong contract are corrected.

2. Once an image entered a conversation, EVERY later text-only turn
   re-prefilled the whole history, forever. _media_context is TOKEN-based, so
   it stays True for the rest of the chat; _media_cache_allowed needs
   req._cache_extra_keys, which is PAYLOAD-derived and therefore None as soon
   as the user stops re-attaching the picture. The gate skipped the fetch
   outright -- not even the pure-text prefix stored unsalted on turn 1. Those
   turns now get a POSITIONAL CAP instead of a blanket skip: match only the
   region strictly before the first media placeholder, with the unsalted key
   that stored it. Safe by construction -- recurrent state cannot pair with
   any image because the boundary precedes every placeholder. `remaining` is
   rebuilt from the FULL prompt so the media region is still forwarded.

3. The L2 LRU touch floored to full blocks, so the terminal partial -- always
   a leaf, and the global trim takes leaves oldest-first -- kept its
   store-time timestamp while its siblings were refreshed every warm turn.
   Budget pressure then evicted precisely the block that completes the chain.
   Both existing tests used lengths where this is invisible by construction
   (8 = exact multiple of the block size, 3 = sub-block); the new one uses 11
   with block_size 4, full blocks AND a tail.

4. DSV4's 256-token block force lived only in cli.serve_command, inside a try
   whose except logs at DEBUG -- while generator selection is a separate,
   class-based detection. A registry lookup that threw left a 64-block cutter
   driving native-256 delta records: interval (0, 64) never matches and 100%
   of stores abort, surfaced only as a per-request warning naming no cause.
   Reconciliation moves to the Scheduler, where CLI, module entry, panel and
   direct embedders all converge, and the store now raises a NAMED error
   instead of a generic missing-interval ValueError.

5. _complete_hybrid_base_from_companion fetched with the bare token key while
   every media writer salts, so the splice silently declined and the caller
   fell back to a text-only re-derive of the whole prompt across the image
   placeholder positions. The resulting wrong recurrent state was then stored
   under the CORRECT salted key with is_complete=True and used for the live
   answer.

Also corrects an overclaim in dc5627815: the media arm of
_ssm_capture_boundaries_for is unreachable today -- all three call sites sit
inside `if not has_media_payload:` -- so it is defense in depth, not the
multimodal reuse fix. The comment and test now say so.

Live proof on DSV4 (M5 Max, SSD-only tier, block-disk 10% of volume):
10-turn conversation grown to 31,416 tokens holds 89.6% reuse with `new`
flat at the actual per-turn growth and 10/10 marker fidelity at temperature
0. Kill the engine, reload, replay the final 18,864-token turn: 18,861
tokens restored FROM DISK (3 new), TTFT 5.30s against 14.66s when the same
turn was computed live, marker intact.

Suite: 8961 passed. The one remaining failure is the bundled-python drift
gate, which is expected until the re-bundle.
J
Jinho Jang committed
0c890455859cbc68cd1fea76fe1c74ff7cdefc10
Parent: dc56278