diff --git a/ds4_server.c b/ds4_server.c index ee008a163..c345d51f1 100644 --- a/ds4_server.c +++ b/ds4_server.c @@ -9188,6 +9188,8 @@ struct server_slot { live_tool_state anthropic_live; visible_live_state thinking_live; int continued_last_store_tokens; + char disk_reload_path[128]; /* last disk-restored file (repeat-loop guard) */ + int disk_reload_frontier; /* its frontier; same file+depth twice = dead tail */ job *assigned; job *running; @@ -10511,6 +10513,27 @@ static void kv_cache_slot_restore_suppressed(server_slot *slot, } } +/* Reload-loop guard: a disk checkpoint restored twice by the same slot at + * the same frontier proves its tail can never match future prompts' + * re-tokenizations (text-keyed tiers validate text, not tokens). Discard it + * exactly like the prefill-failed path does and let the retry land on the + * next candidate or a cold rebuild, which rewrites fresh under the key. */ +static bool kv_cache_reload_repeat(server *s, server_slot *slot, + const char *path, int frontier) { + if (!slot || !path || !slot->disk_reload_path[0]) return false; + if (slot->disk_reload_frontier != frontier || + strcmp(slot->disk_reload_path, path) != 0) return false; + server_log(DS4_LOG_KVCACHE, + "ds4-server: kv cache discarded reason=frontier-contradicted file=%s", + path); + pthread_mutex_lock(&s->kv_mu); + unlink(path); + pthread_mutex_unlock(&s->kv_mu); + slot->disk_reload_path[0] = '\0'; + slot->disk_reload_frontier = 0; + return true; +} + static void kv_cache_discard_failed_disk_entry(server *s, server_slot *slot, const char *path) { if (!s || !slot || !path) return; @@ -12435,6 +12458,10 @@ static void generate_job_inner(server *s, server_slot *slot, job *j) { j->req.image_count, cached, prompt_for_sync->len); } if (cached == 0) slot->continued_last_store_tokens = 0; + if (cached > 0 && cache_source && cache_source[0] != 'd') { + slot->disk_reload_path[0] = '\0'; + slot->disk_reload_frontier = 0; + } if (!multimodal && s->kv.enabled && cached == 0 && old_pos >= s->kv.opt.min_tokens) { /* Loading a disk snapshot replaces the live Metal session. Persist the @@ -12443,10 +12470,23 @@ static void generate_job_inner(server *s, server_slot *slot, job *j) { kv_cache_store_current(s, slot, "evict"); } if (!multimodal && cached == 0) { - disk_cached = kv_cache_try_load(s, slot, &j->req, &effective_prompt, - &disk_cache_path, - &disk_cache_ext_flags); + for (int kv_reload_try = 0; kv_reload_try < 2; kv_reload_try++) { + disk_cached = kv_cache_try_load(s, slot, &j->req, &effective_prompt, + &disk_cache_path, + &disk_cache_ext_flags); + if (disk_cached > 0 && kv_reload_try == 0 && + kv_cache_reload_repeat(s, slot, disk_cache_path, disk_cached)) { + free(disk_cache_path); + disk_cache_path = NULL; + disk_cached = 0; + continue; + } + break; + } if (disk_cached > 0) { + snprintf(slot->disk_reload_path, sizeof(slot->disk_reload_path), + "%s", disk_cache_path ? disk_cache_path : ""); + slot->disk_reload_frontier = disk_cached; cached = disk_cached; cache_source = "disk-text"; prompt_for_sync = &effective_prompt;