Skip to content

think-tool-recovery: model EOS-es after forced </think> injection — test fails with both official Flash GGUFs #675

Description

@nazerim

./ds4_test --think-tool-recovery fails on current main (b7e9f00). The
recovery mechanism itself still works: detection triggers exactly when the
stanza opening completes (trigger=22/23), and the forced close
("</think>", two tokens) is injected and eval'd into the session. But the
FIRST sampled token after injection is EOS, so the test's final phase —
"verifies the live model then emits a valid, parseable call" (per 91bafb5) —
fails:

tests/ds4_test.c:6396: assertion failed: saw_end
tests/ds4_test.c:6404: assertion failed: calls.len > 0 && ...
ds4-test: think-tool-recovery recovered=1 gen_tokens=2 calls=0 name=-

91bafb5 introduced this on the measurement that "that position predicts a
fresh stanza opening strongly enough that the call restarts cleanly". That
no longer holds for either official Flash GGUF.

(Formatting note: the think open/close and DSML markers in this issue are
written inside quoted/code spans so chat clients that treat them as control
tags render this cleanly.)

Environment

  • M5 Max 128 GB, Metal backend
  • Models: official antirez/deepseek-v4-gguf releases, sha256-verified
    against HF LFS oids:
    • DeepSeek-V4-Flash-Layers37-42Q4KExperts-OtherExpertLayersIQ2XXSGateUp-Q2KDown-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-fixed.gguf
      (undated; oid edabc92a…)
    • same name with -0731 suffix (oid 659e22fb…)

Matrix (all cells run on this hardware)

tree undated GGUF -0731 GGUF
54b36ed PASS FAIL
b7e9f00 (main) FAIL FAIL

Failure signature is identical in every failing cell: recovery triggers,
injection succeeds, then immediate EOS.

Interpretation — two independent effects

  1. Checkpoint effect: the July-31 weights EOS at the injection point even on
    the OLD engine (54b36ed row: undated passes, -0731 fails). Related: DeepSeek-V4-Flash-0731 support #635.
  2. Engine effect: commits in 54b36ed..b7e9f00 shift the older (undated)
    weights into the same EOS behavior (undated column: pass → fail).
    Bisectable using the undated GGUF between those two refs.

Related observations

  • DS4_TEST_RECOVERY_PROBE=1 shows the model's NATURAL turn now closes
    "</think>" before emitting the tool call (well-formed list_files stanza).
    The pathological scenario the recovery targets — stanza opening inside an
    unclosed "<think>" block — no longer arises naturally, so the recovery
    rarely fires; but when it does (or when the test's force-fed prefix is
    replayed), the continuation is EOS instead of a restarted call.
  • 91bafb5 notes re-emitting the stanza opening was rejected because "the
    duplicated opening reads as an already-made call and the model ends the
    turn" — the model now ends the turn even without the duplicated opening.
  • Practical impact is milder than pre-Unclosed </think> before tool call stalls agent session (finish=stop, no tool call delivered) #318: instead of burning to the token
    limit with the call dropped at parse, the turn ends short with no tool
    call (retryable by the agent loop). Agent sessions relying on recovery for
    the pathological case get a dropped call rather than a recovered one.
  • PR cuda: implement real per-(layer,expert) LRU for --ssd-streaming-cache-experts #647's test notes independently describe think-tool-recovery as
    "pre-existing flaky … flip[ping] between runs" on CUDA/GB10, suggesting
    the continuation phase is borderline on other hardware too.

Fork-side mitigations (context from a downstream fork)

Our fork (nazerim/ds4) hardens the recovery's AFTERMATH; all of it passes on
the merged tree and is independent of this regression:

  • KV-cache consistency: recovery's injected close used to diverge from the
    canonical form the next request renders, causing token-mismatch disk-KV
    misses and full-prefill rebuilds. A recovery-fired flag now forces
    checkpoint canonicalization at turn end (in-place rewrite anchored at the
    longest common prefix; disk prefix-anchor fallback for oversized tails).
  • Streaming consistency: THINKING-mode streams hold back DSML markers so
    streamed reasoning matches the canonical KV suffix.
  • Detection hardening: scan-from off-by-one fix, 8-byte hold string, plus
    two regression tests (prose-marker immunity, spurious-then-legitimate
    stanzas) — all green on current main.

So detection/injection/state-consistency are sound; only the live-model
continuation assumption regressed.

Repro

make ds4_test
./ds4_test --think-tool-recovery            # with either official GGUF
DS4_TEST_RECOVERY_PROBE=1 ./ds4_test --think-tool-recovery  # natural turn

Happy to help bisect the engine effect if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions