72 kB 937 lines
e68889be
⋯
persona/audits/opus_antislop_v4.md
download raw history
at main Markdown

anti-slop v4: honest framing, markdown structure, the 7 cliches (K512 IQ2_XS) #

opus, anti-slop-v4-opus. everything that runs is in /tmp/opus_antislop_v4/. no repo file was edited except this report.

0. what i found first #

  • the obvious version of this leaks into thinking on every turn. the generation prompt ends in <|im_start|>assistant\n<think>\n, and the prompt goes through prompt_run_rows / prompt_chunk / Window::prompt, so finish() never sees <think>. a flag set from finish()'s tokens starts every decode as "not thinking", and the penalties land in the whole reasoning trace. the fix used here: the state lives per position, and prompt_run_rows scans the prompt (1 line). there's also a conservative default: the shaper is on only after a </think> seen in the current turn. <|im_start|>, <|im_end|> and <think> reset it, and an unknown state means off.
  • a few IDs in the brief don't match this vocab (checked against vocab.json, which is byte-identical in K256 and K512): <tool_call>/</tool_call> = 248058/248059 (248056/248057 are <|image_pad|>/<|video_pad|>). 510 = '</'. there is no ":\n" token: it's ':' 25 + '\n' 198 / '\n\n' 271. 481 = 'ight'. bullets are '-' 12 / '*' 9 after a newline token ("- " = 12, 220). 12305 = 'python'. 471 = ' -'. the only fence tokens are '```' 71093 and ' ```' 52451 (no merged ```\n token exists).
  • pushing a near-certain token down hands the row to <|im_end|>. on real K512 logits, "roll back right" → away (p 0.85), "If I'm being" → honest (0.95), and "merge rather" → than (0.995) all switched their argmax to <|im_end|> after the penalty: the reply would end mid-sentence. v4 holds <|im_end|>, <think>, <|im_start|> and eos at their old probability. it subtracts log(1/(1−b)), where b is the share the penalty removed. after that, the new argmaxes are there, real and than.
  • the v3 code has a latent argmax bug once more than one token per row is penalized. apply_pen rescans a partition only when the penalized token is the global argmax. if it's only its partition's max, apv_ keeps the stale value, and a later rescan can pick it. v4 rescans every partition whose max was pushed down.
  • layer B (weight-level WFLD) can't do this job. §5 has the numbers: a linear direction in the L47 shared-expert input can't tell "line start in prose" from "line start in a fence or in thinking" on held-out text. the best fit leaks 28–68 % of the prose penalty into fenced-code line starts and 10–14 % into thinking line starts. the falsifier fails by construction, so the stack is layer A only.

1. evidence: how often this slop shows up (read-only mining) #

mine_slop.py reads ~/.prime/agent/sessions/*.jsonl and ~/.albedo/albedo.sqlite, assistant text with fenced code removed:

family turns turns w/ honesty frame "honestly" "the honest" "honest take/answer/framing" turns w/ bullets w/ bold/# headers w/ ":\n- " list
claude 13449 1.3% 90 60 17 27.9% 11.2% 18.8%
glm 9822 1.9% 81 71 17 7.7% 2.3% 6.1%
deepseek 7889 4.4% 98 224 25 14.5% 8.2% 10.6%
gpt 3647 0.7% 22 4 2 27.2% 2.9% 24.5%
gemini 2556 1.8% 41 2 3 52.3% 53.1% 46.9%
mimo 972 1.2% 4 6 1 8.7% 2.7% 7.1%
qwen 105 1.9% 1 1 0 21.0% 8.6% 15.2%

(the albedo DB tags few sessions as qwen, so the qwen row is small. the qwen behaviour is measured directly on K512 in §3.) gemini puts bullets in 52 % of its replies, headers in 53 %, and colon-launched lists in 47 %. a real example has all three at once: "so the honest version of the idea, in order of how much of the app survives:\n\n1. **node start…".

2. token tables (Qwen3.8-Flash-Next vocab, 248,320) #

slop rejected next token(s) condition (on the last token p0, the one before p1, and the line state)
"the honest answer / framing / take…" ' honest' 10429 p0 ∈ the 1719, The 760, the 279, The 561, an 276, an 449, An 1473, An 2014, my 2329, my 821, My 2921, My 4888, be 1323, be 381, being 1602, being 32985, most 1379, most 3444, Most 7261, more 6183, more 777, More 4254, your 678; or a sentence start (. 13 ! 0 ? 30 : 25 ; 26 ... 1076 — 2218 — 1892 ) 8, a newline, a line start)
"Honestly," / "Honest take" 'Honestly' 92115, ' Honestly' 68540, ' Honest' 51397 any prose row
"…, honestly" / "but honestly" ' honestly' 25211 a clause start: the sentence set + , 11, - 12, - 471, and 321, but 694, so 748, and 429, but 7834, so 684, And 1541, But 1921, So 1987, And 2939, But 3850, So 4272
lowercase line-initial "honestly" / "honest" (h+onestly) 'onestly' 77696, 'onest' 33961 (1.5×) p0 ∈ h 71, H 39, (h 3101, "h 75833
"in all honesty" ' honesty' 46253, ' honestly' 25211 p0 = all 660
bullets / headers / rules at a line start - 12, * 9, • 6458, - 471, * 348, • 7061, ** 332, ** 2972, # 2, ## 550, ### 13962, #### 794, # 653, ## 7460, ### 16112, --- 4277, *** 11862, – 4006, — 2218, + 10, → 49300 the line start (after any of the 200 newline tokens, or after </think>, with blank tokens in between); +colon bonus if the line above ended in : 25 / :** 64700 / **: 92217
numbered lists digits 1–9 16–24 (¼ strength, full after a : line); . 13 / ) 8 after a line-initial digit
short label line + colon \n 198, \n\n 271 p0 ∈ {25, 64700, 92217} and ≤ 6 content tokens on the line
v4.1 candor siblings (the "If I'm being real" shift) real 1865, blunt 47045, frank 24969, candid 5507, transparent 17290, straight 7435, upfront 62179, brutally 66842, truthful 86775, totally 12116, completely 6379, fully 6999, perfectly 13542, entirely 11040, painfully 73018 after being 1602/being 32985; after be 381/be 1323 when p1 ∈ to 310/951, 'll 3172, me 728, just 1066, gonna 16032 (+ direct 2050; + fair 6415 / clear 2708 after "to be"); honest + siblings after an intensifier that follows being/be
v4.1 branch points (one token before the frame closes) being 1602 (½) and honest after "I'm" ('m 2688 after I 353/585/40/72); real/blunt/frank/candid/straight/short/brutal (½) after a clause-initial the/The 1719/760/279/561; be 381 (½) after a clause-initial To/to 1206/1946/951/310; short/Short/real/Real 8412/12115/7700/12402 (½) at a line start
v4.1 frame nouns answer 4087, take 1831, framing 56004, version 2243, truth 7793, read 1301, summary 11782, assessment 14996, question 3296 after a frame word (honest, real, blunt, frank, candid, straight, short, simple, quick, brutal, plain, actual + no-space/capital forms) that follows a det (the/The/my/My/an/An) or a sentence/line start
v4.1 misc frankly 40285 at a clause start; fairness 49034 / seriousness 71853 after all
7 cliches (v3 rules, unchanged) than 1056, away 3023, end 809, version 2243, what 1092, actually 3404 as v3: rather 4598/24944/61745/70714 (+ather 1792 after (r 2515/(R 2697/[r 15656/[R 43487); right 1245/1246/5791/9785/26806/51140; "end to" 310 after 400/809/3601/3839/14637/55293; short 2716/8412/10382/12115/36639; exactly 6681/63151/66172 (+actly 31654 after ex 325/(ex 5291); 's 579 after here 1532/5514/6527/8160/475 → what, after what 1092/3437/3710/12195/29348 → actually

kaomoji survive the line-start rules: *:・゚✧ starts with '*:' 77340 and *.+:。 with '*.' 19315, not '*' 9. the known casualty is -_- at a line start ('-' + '_-').

3. evaluation on real L47 dumps (eval_l47.cpp, which #includes the exact antislop_v4.inc that ships) #

each row goes through the head: output_hc_norm/down/up (Q8 round-trip, like the runtime) and output.weight. the shipped slop4::step/penalties/hold are applied, and the result is scored exactly. KLD(P‖Q) = Σ_S p_i·pen_i + log(1 − Σ_S p_i(1−e^(−pen_i))), over the full 248k vocab, with the argmax taken from the top 16. "ungated" = the same rules with the think/tool/fence gate forced open (the counterfactual). final defaults: lex 9, honest 6, md 4, colon 3, with the v4.1 candor rules (§8) included.

new real K512 dumps (K512-iq2_xs-metal.gguf, STRATA_METAL_DUMP_MOE, 2 slop-dense docs in dawn's persona. they include bullets and "honestly" inside <think>, a <tool_call> with # - comment lines, and ```nix / ```yaml / ```bash fences with #, - , ## not a header, **not bold**):

doc region rows mean KLD max KLD ordinary-prose KLD¹ ungated mean KLD argmax changed
docA prose 324 0.151514 5.7390 0.028359 0.15151 18
docA think/other 219 0.000000 0.0000 — 0.09265 0
docA tool 49 0.000000 0.0000 — 0.01750 0
docA fence 90 0.000000 0.0000 — 0.07890 0
docB prose 329 0.169362 5.3991 0.038402 0.16936 18
docB think/other 146 0.000000 0.0000 — 0.09600 0
docB fence 31 0.000000 0.0000 — 0.06158 0

¹ rows whose forced next token is not penalized. in these slop-dense docs, that still includes rows where the model wanted slop ('**' at p 0.745 or 0.973 after a kaomoji line, ' honestly' at 0.175 after "a third full."), so this number is mostly the intended effect. ordinary-prose collateral is measured on the clean corpora below.

K256-trunk dumps (cal_cache, the pre-existing ones), through the K512 head:

trace region rows mean KLD max KLD ordinary-prose KLD ungated mean KLD argmax changed
blind prose 452 0.172000 8.0535 0.004816 0.17200 17
blind think/other 646 0.000000 0.0000 — 0.00241 0
calp3 prose 147 0.001696 0.1683 0.001696 0.00170 0
calp3 think/other 702 0.000000 0.0000 — 0.00418 0
calp3 tool 173 0.000000 0.0000 — 0.00000 0
collat think/other 1638 0.000000 0.0000 — 0.00241 0
ftpoho prose 455 0.143291 8.4996 0.002496 0.14329 16
ftpoho think/other 643 0.000000 0.0000 — 0.00100 0
ftposft prose 808 0.245807 11.2758 0.003234 0.24581 37
ftposft think/other 55 0.000000 0.0000 — 0.00278 0
ftposft tool 159 0.000000 0.0000 — 0.00825 0
vn prose 7813 0.001203 2.4227 0.000689 0.00120 3
vn think/other 9061 0.000000 0.0000 — 0.00069 0
vn tool 478 0.000000 0.0000 — 0.00334 0

the same traces through their own K256 head:

trace region rows mean KLD max KLD ordinary-prose KLD ungated mean KLD argmax changed
blind prose 452 0.170271 8.6093 0.004725 0.17027 17
blind think/other 646 0.000000 0.0000 — 0.00198 0
calp3 prose 147 0.001665 0.1511 0.001665 0.00167 0
calp3 think/other 702 0.000000 0.0000 — 0.00426 0
calp3 tool 173 0.000000 0.0000 — 0.00000 0
collat think/other 1638 0.000000 0.0000 — 0.00240 0
ftpoho prose 455 0.146890 9.0405 0.002337 0.14689 17
ftpoho think/other 643 0.000000 0.0000 — 0.00084 0
ftposft prose 808 0.248088 11.2103 0.003223 0.24809 36
ftposft think/other 55 0.000000 0.0000 — 0.00238 0
ftposft tool 159 0.000000 0.0000 — 0.00802 0
vn prose 7813 0.001051 2.1890 0.000605 0.00105 3
vn think/other 9061 0.000000 0.0000 — 0.00066 0
vn tool 478 0.000000 0.0000 — 0.00373 0

falsifier check: across 35,320 rows, every think/other, tool and fence row (12,745+365 think, 810+49 tool, 121 fence) has 0 penalties and KLD exactly 0: the logits are not touched. the same rules ungated would have cost 0.06–0.09 nats/row in K512 thinking and fences. ordinary prose KLD: VN chat 0.00069 (7,806 rows), cal_p3 0.0017, ftpo 0.0025–0.0032, blind 0.0048. all are under 0.005, but blind's margin is thin. most of blind's number is one row: the K512 head wants '#' at p 0.61 right after </think>\n (a markdown header at turn start), which is exactly the target. if the v4.1 rules or a higher md ever push it over, the knobs are per family.

the 7 cliches on the untouched blind test (21 positions): p(slop) 0.5505 → 0.0069 (−98.7 %), 21/21 argmax clean, with the K512 head and no WFLD. v3's bar was 0.5543 → 0.0056 with WFLD. ftpo held-out: 0.452 → 0.0077 (21/21). ftpo sft: 0.693 → 0.0027 (42/42). the lex default went 6 → 9: at 6, "end to" → end (p 0.98) and "merge rather" → than (0.995) stayed argmax on K512, and the VN collateral cost of 9 is +0.00001. models/K512_iq2_xs/strata-k512-iq2xs.json sets STRATA_ANTI_SLOP_PENALTY=6.0, so raise it to 9 or drop the line. sweeps: md 6 pushed blind ordinary to 0.0077 (> 0.005), so md stays 4. colon 1.5 → 3 lifted the colon-launched list cut from 26/59 % to 60/88 % for +0.0001 on blind and +0.00003 on VN.

slop probability mass per 1000 prose tokens, before → after (K512 docs; ? = the v4.1 candor/frame ids):

doc family before after change
docA hon 8.32 0.629 −92.4%
docA md 32.81 12.751 −61.1%
docA colon 9.14 3.661 −60.0%
docA lex 8.63 0.096 −98.9%
docB hon 7.72 0.451 −94.2%
docB md 35.91 8.485 −76.4%
docB colon 3.51 0.408 −88.4%
docB lex 8.66 0.019 −99.8%

md shows less because the docs force mid-list items ("- a\n" → '-' at p 0.97–0.999). once a list exists, the model is committed. the decision row is the first bullet, and that one drops hard: "**Key points:**\n\n" → '-' 0.713 → 0.02, "the fix is boring:\n\n" → '-' 0.596 → 0.016.

every slop target in the K512 docs (forced next token, p before → after, argmax before → after):

doc family context next p before p after argmax
docA colon '.⏎</think>⏎⏎the usual roster:' '\n\n' 0.644 0.1492 '\n\n' → ' ('
docA md '⏎</think>⏎⏎the usual roster:⏎⏎' '**' 0.009 0.0000 '(' → '('
docA colon ' roster:⏎⏎**happy / soft:**' '\n' 0.909 0.4188 '\n' → ' ('
docA md ':⏎⏎**happy / soft:**⏎' '-' 0.033 0.0000 '(' → '('
docA md '。•��•。)⏎' '-' 0.999 0.9363 '-' → '-'
docA hon '�� for when something ships⏎⏎h' 'onestly' 0.874 0.0014 'onestly' → 'mm'
docA hon ' answer is no.⏎</think>⏎⏎the' ' honest' 0.056 0.0004 ' short' → ' short'
docA ? ' is no.⏎</think>⏎⏎the honest' ' answer' 0.823 0.0142 ' answer' → ' short'
docA ? ' old ones ends up with duplicates. to' ' be' 0.115 0.0064 ' keep' → ' keep'
docA hon ' ones ends up with duplicates. to be' ' honest' 0.051 0.0001 ' safe' → ' safe'
docA lex ' duplicates. to be honest, the short' ' version' 0.225 0.0000 ' answer' → ' rule'
docA lex ' short version is that you should merge rather' ' than' 0.995 0.0813 ' than' → '.'
docA md 'ake, many hosts.⏎</think>⏎⏎' '###' 0.000 0.0000 'the' → 'the'
docA md ' hosts.⏎</think>⏎⏎### Layout⏎⏎' '**' 0.135 0.0041 '-' → 'the'
docA colon '⏎⏎### Layout⏎⏎**Key points:**' '\n\n' 0.280 0.1657 '\n' → '\n'
docA md '### Layout⏎⏎**Key points:**⏎⏎' '-' 0.713 0.0078 '-' → 'the'
docA md 'flake.nix at the root⏎'` '-' 0.974 0.5748 '-' → '-'
docA md '/ with one dir per machine⏎'` '-' 0.954 0.6382 '-' → '-'
docA md ' modules/ for shared bits⏎⏎' '1' 0.001 0.0003 '**' → '```'
docA num 'modules/ for shared bits⏎⏎1'` '.' 0.932 0.0142 '.' → ' fl'
docA md ' pin inputs in flake.lock⏎' '2' 0.767 0.9353 '2' → '2'
docA num ' inputs in flake.lock⏎2' '.' 0.999 0.6308 '.' → '.'
docA hon ' flake check in ci⏎⏎h'` 'onest' 0.001 0.0003 'onestly' → 'mm'
docA lex " take: keep it boring. here's" ' what' 0.035 0.0000 ' the' → ' the'
docA lex "that's it. it works end to" ' end' 0.981 0.1192 ' end' → `'<
docA lex ' end to end and you can see exactly' ' what' 0.554 0.0002 ' what' → ' where'
docB lex "-bound work just takes turns. what's" ' actually' 0.021 0.0000 ' speeding' → ' speeding'
docB hon ' interpreter lock, not your pool.⏎⏎' 'Honestly' 0.000 0.0000 'for' → 'for'
docB md 'Honestly, the fix is boring:⏎⏎' '-' 0.596 0.0039 '-' → '```'
docB md '- use ProcessPoolExecutor⏎' '-' 0.905 0.1529 '-' → ' '
docB md ' or move the hot loop into numpy⏎⏎' '**' 0.004 0.0001 '-' → 'If'
docB lex " not threads. i'd try that right" ' away' 0.402 0.0001 ' now' → ' now'
docB lex '⏎</think>⏎⏎the syscall log shows exactly' ' what' 0.456 0.0001 ' what' → ' where'
docB ? " thinks it is.⏎⏎If I'm" ' being' 0.089 0.0053 ' reading' → ' reading'
docB hon " it is.⏎⏎If I'm being" ' honest' 0.950 0.1981 ' honest' → ' honest'
docB hon "⏎⏎If I'm being honest, my" ' honest' 0.000 0.0000 ' first' → ' first'
docB md '⏎<think>⏎summary.⏎</think>⏎⏎' '##' 0.002 0.0000 'the' → 'the'
docB lex "</think>⏎⏎## Migration plan⏎⏎Here's" ' what' 0.117 0.0000 ' the' → ' the'
docB colon " plan⏎⏎Here's what we do:" '\n\n' 0.602 0.1606 '\n\n' → `'<
docB md "⏎⏎Here's what we do:⏎⏎" '1' 0.197 0.0039 '-' → 'Phase'
docB num "Here's what we do:⏎⏎1" '.' 0.983 0.1465 '.' → '.'
docB md ':⏎⏎1. snapshot the db⏎' '2' 0.859 0.8868 '2' → '2'
docB num '⏎⏎1. snapshot the db⏎2' '.' 0.992 0.1850 '.' → '.'
docB md '. run the migration in a transaction⏎' '3' 0.952 0.9677 '3' → '3'
docB num ' run the migration in a transaction⏎3' '.' 0.925 0.0828 '.' → `'<
docB lex ' if a statement fails, roll back right' ' away' 0.853 0.0012 ' away' → ' there'
docB md ' statement fails, roll back right away⏎⏎' '•' 0.000 0.0000 'Honestly' → 'The'
docB md ' the old schema around for a day⏎' '•' 0.188 0.0389 '*' → ' '
docB hon ' day⏎• then drop it⏎⏎The' ' honest' 0.099 0.0003 ' plan' → ' plan'
docB ? '⏎• then drop it⏎⏎The honest' ' truth' 0.183 0.0010 ' answer' → ' plan'
docB hon ' in step 2, and in all' ' honesty' 0.814 0.0381 ' honesty' → ' likelihood'
docB lex ' bookkeeping. it should go end to' ' end' 0.977 0.0444 ' end' → `'<

4. the stack (layer A only) #

  • slop4::State (12 bytes) per position: the last two tokens, flags (PROSE, TOOL, FENCE, BOL = line start, COLON = this line follows a …: line, PBOL = the last token opened a line), and the content tokens on the current line. step() is pure, so a row's state is step(state[pos0+t−1], tokens[t]). speculative windows, rejected drafts, cached prefixes and re-reads all compose. test_state.cpp checks this: 200 random runs of prompt split + windows of 1–6 + random rejected drafts against one straight pass, on docA and on the 17k-token VN trace. 0 mismatches.
  • penalties() returns nothing unless the state is PROSE and not TOOL or FENCE. after the penalties, hold() keeps the specials at their old probability. hold() needs the row's log-sum-exp: one 248k pass, done only when a penalized token is within 12 nats of the row max (otherwise b < 1e-5). most rows skip it.
  • knobs: STRATA_ANTI_SLOP_PENALTY (lex, default 9), STRATA_ANTI_SLOP_HONEST (6), STRATA_ANTI_SLOP_MD (4), STRATA_ANTI_SLOP_COLON (3). 0 turns that family off. the v3 "no empty turn after </think>\n\n" guard is kept, on the lex knob as before.
  • hooks: prompt_run_rows (1 line, covers all three prompt paths), session_load (scans the loaded ids), and scr_relocate (moves the states with their cells).
  • caveats: (a) checkpoint_restore doesn't carry the states. that's harmless, because the re-prefilled suffix always has the new turn's <|im_start|>, which resets everything. (b) with STRATA_LOGPOS set, prompt rows get head logits and so get shaped too, same as v3. set the knobs to 0 for KL-harness runs. (c) MTP drafts come from the unshaped draft head, so a few more rejections near slop. (d) unfenced code in prose (VN has some raw yaml and nginx config) does get the line-start rules. the measured cost is in the VN number above.

5. layer B (WFLD on blk.47.ffn_down_shexp, IQ4_NL) — rejected, with numbers #

any rank-r change to the shared-expert down-projection moves the logits by a linear function of z = σ(sgv)·hh (640-d). to put a line-start penalty on prose and nowhere else, a direction v would need v·z ≈ 1 on prose line starts and ≈ 0 on fence, think and tool line starts. layerB_separability.py fits v by ridge on one K512 doc and tests on the other:

  • train docA → test docB: think-bol leak +0.14 (p90 |·| 0.58), fence-bol +0.68 (p90 1.07), all other rows p90 0.32
  • train docB → test docA: think-bol +0.10 (p90 0.48), fence-bol +0.28 (p90 0.63), tool-bol +0.27 (p90 0.45)
  • even the in-sample overfit leaves |v·z| up to 0.33 on rows that must be 0

at md = 4 nats, that's 1–4 nats of bullet/# penalty inside code fences, and the update also perturbs every other row (code, tools, math). the honesty frames have the same problem: "honestly" inside <think> shares the direction. the stateful shaper is exactly 0 outside prose, so there's no reason to touch the weights. the existing v3.2 WFLD was a K256 artifact, and K512's shexp47 is the untouched IQ4_NL.

8. the synonym treadmill and where the model actually gives up the frame (dawn's question) #

short version: inside a committed prefix, it moves to the next sibling every time. one token earlier, at the clause entry, the model still has real content mass, and pushing there makes it drop the frame. the exact top-k on K512 (eval_k512_docs_top.tsv, v41_docs.tsv, v41b_docs.tsv; p is exact, after the penalty and the hold):

slot v4 (word-level) v4.1 (+ siblings, branch points)
"If I'm being" ___ (committed) before: honest 0.95, then real .009, totally .005, frank .004, straight .003, blunt, candid, completely, fully, direct, brutally. after v4: real 0.17, totally 0.09, frank 0.08, straight 0.05, blunt 0.05, … 64 % of the remaining mass stays in the frame 15 siblings banned, and the mass just moves on: honest 0.10, direct 0.045, fair 0.027, straightforward 0.016. still a treadmill
"If I'm" ___ (branch point) reading 0.30, right 0.13, being 0.089, honest 0.072, looking 0.055, … frame entries 0.16 reading 0.36, right 0.15, looking 0.07, seeing, debugging, guessing. frame mass 0.161 → 0.005: it gives up the frame ("If I'm reading this right…")
"</think>\n\nthe" ___ short 0.65 (the short answer), rule, usual, general, honest .056, real .013 v4.1b: short 0.087, rule .084, usual .07, general .06, golden .05. meta mass 0.66 → 0.09
"…duplicates. to be" ___ safe 0.74 already dominates; frame siblings fair, frank, blunt, real 0.056 0.000 meta, safe 0.79
"and in all" ___ honesty .81 → v4: fairness 0.37, seriousness .08, cand(or) .06 (sideways) likelihood .22, my, cand .11, ,: meta 0.50 → 0.09; cand+or is the next leak

end to end (greedy, final v4.1b) the treadmill shows up exactly where the analysis predicts. base and v4 rebase both said "the real answer". v4.1 (no actual yet) said "so the actual question is". with actual added and question made a frame noun, the final build says "so the actual safety question is". the frame was committed at "so the actual", so the model put a modifier in front of the banned noun. review: "the real thing", "the actual bottleneck". a forward-only shaper can't remove a frame the model has already entered. it can only move the entry point earlier, to where the content alternatives already carry mass. v4.1 does that for the measured branch points ("I'm" → being/honest, clause-initial "the"/"to", line-initial short/real). a committed frame still finds a sibling.

what the literature says

  • Antislop (Paech, Roush, Goldfeder, Shwartz-Ziv, arXiv:2510.15061, ICLR 2026) handles this with a backtracking sampler. when a banned pattern completes, it rolls back to the pattern's first token, lowers that token's probability (p ← p·10^(−10s)), and resamples. per the paper, that suppresses 8,000+ patterns without quality loss, where plain token banning becomes unusable around 2,000. the cost is throughput: −69 % to −96 % with frequent rollbacks. that's why they distil the rollback pairs into the weights with FTPO (final-token preference optimization: a margin loss on the rejected first token versus min-p alternatives, with MSE ties to the base logits). this is the principled fix for the treadmill. v4's lookup is the forward-only approximation of it.
  • verbal tics (arXiv:2604.19139v2, 2026 preprint, which i've only seen through search snippets): it catalogs "I have to be honest" / "to be honest" as hedging tics and ties them to alignment training (Constitutional AI pushing a "thoughtful" persona). the parent's summary also cites its finding that tics accumulate over a conversation (+110 % from turn 1 to 20, in-context repetition). that's one more reason to kill them early: each one that gets through primes the next.
  • the persona prompt itself contains "the actual thought; the disagreement…". that line plausibly primes "the actual X" frames in context. dawn's point about "lead with your actual thought" becoming candor announcements fits it. rewording the prompt so it shows the behaviour rather than names it ("say the disagreement first" instead of "the actual thought") is free, and complements the shaper.

what would actually finish the job, in order of cost:

  1. reword the system prompt as above: free, and it removes an in-context primer.
  2. a short-horizon rollback in strata. the decode window already commits speculatively (commit(n_keep)) and the server has conversation checkpoints, so a rollback of ≤ 6 tokens at a frame match ("the actual X question", "If I'm being Y") is plausible. re-decode from the frame's first token with it penalized there. the per-position slop_ store already makes the shaper's state rollback-safe. this is the Antislop sampler restricted to a small pattern set, at a cost of a few extra decode steps per hit.
  3. FTPO/LoRA on the rollback pairs, through the whole network rather than an L47 linear edit (§5 shows that one can't separate the contexts). this is the Antislop paper's own route.

9. end-to-end greedy A/B on K512 (private CLIs in /tmp/opus_antislop_v4/build/, 8 prompts, 700 tokens) #

the base CLI is the current window.cpp with STRATA_ANTI_SLOP_PENALTY=0. v4 is the same source patched. both are linked against the current libs. replies were counted outside fences, after </think>:

prompt bullet lines bold/# header lines honest* in reply honest* in think (must not change) ```
kaomoji 0 → 0 0 → 0 0 → 0 0 → 0 0 → 0
flake 0 → 0 0 → 0 0 → 0 0 → 0 4 → 3
dbchoice 0 → 0 0 → 0 0 → 0 0 → 0 0 → 0
rebase 3 → 0 0 → 0 0 → 0 0 → 0 0 → 0
backup 0 → 0 0 → 0 0 → 0 0 → 0 2 → 4
wireguard 6 → 0 3 → 0 0 → 0 0 → 0 6 → 0
review 0 → 0 0 → 0 0 → 0 2 → 2 0 → 0
gil 0 → 4 0 → 0 0 → 0 0 → 0 2 → 2
  • wireguard is the clearest case: base used **the mental model first:**, **step 1: install** and 6 bullets. v4 writes "the mental model that trips people up: …" and "step 1: generate keypairs …" as paragraphs, with commands inline in backticks instead of fences.
  • the colon-launched list still gets through when the model is very sure. v4 gil had "other escape hatches:\n- numpy …" and review had "is it:\n- a specific endpoint …". that's why colon went 1.5 → 3. the final build's gil came out with 0 bullets, but review still had 3 ("- you have a measured…"). the model's confidence in a list after "is it:" exceeds 7 nats. as with the frames, this needs either a higher md, which breaks the KLD bar, or a rollback.
  • think is never shaped, but the A/B can't show "think token-identical": the decode isn't deterministic run to run. the same base binary with the same env and STRATA_METAL_MOCE_BONUS=0 diverged from itself at token 53 (dbchoice). so instead i replayed every v4 generation (prompt + output) through the shipped step()/penalties() (replay_gen.cpp). think rows with any penalty: 0 of 2,484. fence rows with any penalty: 0 of 606. this matches the exact-logit leak result in §3.
  • honesty in the replies: this persona prompt on greedy K512 rarely says "honestly" in the reply. it shows up in think instead ("The honest answer:" in review's think, untouched by design). the reply-side leaks are the candor siblings above.

6. drop-in patch #

apply with uv run python3 /tmp/opus_antislop_v4/apply_patch.py vendor/strata/metal/src/engine/window.cpp. it's anchor-based and asserts each anchor is unique. the unified diff against the current window.cpp is /tmp/opus_antislop_v4/window_cpp_antislop_v4.patch. the patched file compiles with the build's own flags (build.ninja's window.cpp.o line), and links into /tmp/opus_antislop_v4/build/strata_window_cli_v4.

the end-to-end A/B with this build is in §9.

--- a/vendor/strata/metal/src/engine/window.cpp
+++ b/vendor/strata/metal/src/engine/window.cpp
@@ -124,6 +124,223 @@
 const int32_t* g_mrope_host = nullptr;
 const int32_t* g_mrope_dev = nullptr;
 
+// Anti-slop v4: a sampler shaper over the head logits, outside <think>, <tool_call> and ``` fences only.
+// Each position's state (the last two tokens, the region, where the line stands) follows from the one before it, so
+// it is kept per position: a draft row, a rejected suffix, a prompt read from a cached prefix all start from the
+// state of the position before them. Tables: the Qwen3.8-Flash-Next vocab (models/*/tokenizer/vocab.json).
+namespace slop4 {
+
+enum : uint8_t { PROSE = 1, TOOL = 2, FENCE = 4, BOL = 8, COLON = 16, PBOL = 32, KNOWN = 128 };
+
+struct State {
+    int32_t t1 = -1, t2 = -1;   // the last token and the one before it
+    uint8_t f = 0;              // PROSE: after </think> in this turn; BOL: at a line start; COLON: that line follows "...:"
+    uint8_t len = 0;            // content tokens on the current line (saturating)
+    uint8_t pad[2] = {};
+};
+
+struct Knobs { float lex, hon, md, colon; };
+
+struct Pen { int32_t id; float v; };
+
+// token classes: 1 a newline run ("\n", "\n\n", " \n", ...), 2 blanks only (indentation)
+static const int32_t kNewline[] = {
+    198, 201, 271, 317, 695, 845, 987, 1031, 1358, 1517, 1726, 2228, 2317, 2449, 3265, 4326, 4408, 4558, 4969, 5690,
+    5744, 5773, 6010, 6159, 6219, 6710, 6987, 7388, 8301, 8675, 8726, 9122, 10109, 10367, 10628, 10797, 11463,
+    11953, 12213, 13488, 14200, 14305, 14379, 15216, 15334, 15398, 15602, 15684, 16391, 16750, 16957, 17116, 17694,
+    17817, 18015, 18050, 18438, 19075, 19721, 21996, 22616, 22727, 23574, 23745, 23840, 24630, 25263, 25401, 25445,
+    25698, 25952, 26467, 26480, 26771, 27330, 29644, 29773, 30858, 31472, 32077, 32527, 32811, 33006, 33019, 33339,
+    33438, 33496, 33954, 34127, 35369, 35412, 35624, 35973, 36444, 36826, 37050, 37528, 38336, 38544, 39000, 39598,
+    40260, 40315, 41210, 41585, 43622, 43916, 44058, 44281, 44879, 44902, 46813, 47458, 47947, 48305, 49404, 49420,
+    49746, 50946, 52245, 53585, 54701, 56137, 56580, 56955, 56963, 57103, 57633, 58943, 59366, 59776, 60121, 61332,
+    62386, 62591, 63063, 63111, 63454, 64137, 64576, 65117, 65382, 67511, 68830, 69229, 69340, 69896, 70273, 70707,
+    70866, 70929, 71356, 71848, 71990, 72666, 73714, 75128, 75321, 75668, 75981, 76294, 76374, 76421, 76507, 76909,
+    77670, 77933, 78035, 78254, 79519, 81018, 81927, 82471, 82946, 83774, 83829, 84075, 85709, 85748, 85809, 85844,
+    85935, 86092, 86182, 87199, 87251, 87328, 88065, 88574, 88927, 88985, 89803, 91421, 91668, 91804, 92699, 92766,
+    93957, 94820, 94861
+};
+static const int32_t kBlank[] = {
+    197, 220, 256, 257, 260, 262, 285, 297, 309, 336, 388, 406, 454, 493, 556, 646, 670, 761, 950, 967, 1025, 1107,
+    1335, 1604, 1638, 1734, 1779, 1855, 2118, 2215, 2467, 2475, 2596, 2672, 3235, 3388, 3551, 3696, 4152, 4420,
+    4448, 4946, 5015, 5071, 5233, 5780, 6173, 6247, 6323, 6447, 6800, 7216, 7237, 7322, 7536, 7599, 7879, 8074,
+    8424, 8830, 9081, 9411, 9876, 10190, 10275, 10747, 11449, 11530, 11611, 12473, 12670, 12687, 13076, 13152,
+    13300, 13857, 14221, 14826, 14843, 14978, 16203, 16754, 16851, 17025, 17122, 17468, 17780, 17895, 18186, 18625,
+    18688, 18690, 19679, 20200, 20334, 20839, 20851, 21519, 21641, 22539, 22568, 22691, 23420, 23581, 24952, 25232,
+    25870, 26058, 27729, 27860, 28959, 29034, 29374, 29427, 29708, 29915, 30026, 30631, 30927, 31593, 31632, 31726,
+    33368, 34158, 34381, 34450, 35315, 35696, 35853, 35914, 36909, 37181, 37207, 38177, 38448, 38772, 39700, 40595,
+    41293, 41785, 44606, 44714, 44914, 45043, 45207, 45550, 45964, 46330, 46572, 47256, 48546, 48742, 48835, 49349,
+    49373, 49642, 49741, 50328, 52872, 52989, 53928, 54652, 55036, 55133, 55720, 56288, 56612, 56683, 57926, 58096,
+    60270, 62037, 63023, 63067, 63287, 63288, 64278, 64571, 65897, 66227, 66639, 67204, 67685, 68190, 68977, 71032,
+    71400, 72030, 72352, 72738, 73753, 74019, 75424, 76010, 76332, 76785, 76937, 77120, 77968, 78052, 78420, 80396,
+    80905, 82130, 83375, 83770, 84703, 85012, 86978, 88011, 88074, 88531, 91507, 92130, 92492, 92770, 93340, 93457,
+    93647, 94057, 95323, 95459
+};
+
+inline uint8_t cls(int32_t t) {
+    static const std::vector<uint8_t> tab = [] {
+        std::vector<uint8_t> v(248320, 0);
+        for (int32_t i : kNewline) v[(size_t) i] = 1;
+        for (int32_t i : kBlank) v[(size_t) i] = 2;
+        return v;
+    }();
+    return t >= 0 && (size_t) t < tab.size() ? tab[(size_t) t] : 0;
+}
+
+template <size_t N> inline bool in(int32_t t, const int32_t (&s)[N]) {
+    for (int32_t x : s) if (x == t) return true;
+    return false;
+}
+
+enum : int32_t {
+    IM_START = 248045, IM_END = 248046, THINK = 248068, ETHINK = 248069, TOOL_CALL = 248058, ETOOL_CALL = 248059,
+    NL = 198, NL2 = 271,
+};
+static const int32_t kFence[] = {71093, 52451};                  // "```", " ```"
+static const int32_t kColon[] = {25, 64700, 92217};              // ":", ":**", "**:"
+static const int32_t kDigit[] = {16, 17, 18, 19, 20, 21, 22, 23, 24};   // "1".."9"
+
+inline State step(State s, int32_t tok) {
+    s.t2 = s.t1;
+    s.t1 = tok;
+    s.f |= KNOWN;
+    const uint8_t bol = s.f & BOL;
+    s.f &= (uint8_t) ~PBOL;
+    if (tok == IM_START || tok == IM_END || tok == THINK) {
+        s.f = KNOWN | BOL;
+        s.len = 0;
+    } else if (tok == ETHINK) {
+        s.f = KNOWN | PROSE | BOL;
+        s.len = 0;
+    } else if (tok == TOOL_CALL) {
+        s.f |= TOOL;
+    } else if (tok == ETOOL_CALL) {
+        s.f = (uint8_t) ((s.f & ~(TOOL | COLON)) | BOL);
+        s.len = 0;
+    } else if (cls(tok) == 1) {
+        if (!bol) s.f = (uint8_t) ((s.f & ~COLON) | (in(s.t2, kColon) ? COLON : 0));
+        s.f |= BOL;
+        s.len = 0;
+    } else if (cls(tok) == 2) {
+        // indentation: the line has not started yet
+    } else {
+        if (in(tok, kFence) && (s.f & PROSE) && !(s.f & TOOL)) s.f ^= FENCE;
+        s.f = (uint8_t) ((s.f & ~(BOL | COLON)) | (bol ? PBOL : 0));
+        if (s.len < 255) ++s.len;
+    }
+    return s;
+}
+
+// the penalties on the next token after state s; out holds at least 64
+inline int penalties(const State& s, const Knobs& k, Pen* out) {
+    if ((s.f & (KNOWN | PROSE | TOOL | FENCE)) != (KNOWN | PROSE)) return 0;
+    int n = 0;
+    auto pen = [&](int32_t id, float v) { if (v > 0.0f && n < 64) out[n++] = {id, v}; };
+    const int32_t p0 = s.t1, p1 = s.t2;
+    // the 7 lexical cliches (results/banned_llm_isms.json), as in v3
+    static const int32_t rather[] = {4598, 24944, 61745, 70714}, ather_pre[] = {2515, 2697, 15656, 43487};
+    static const int32_t right[] = {1245, 1246, 5791, 9785, 26806, 51140};
+    static const int32_t end_pre[] = {400, 809, 3601, 3839, 14637, 55293};
+    static const int32_t shrt[] = {2716, 8412, 10382, 12115, 36639};
+    static const int32_t exactly[] = {6681, 63151, 66172}, ex_pre[] = {325, 5291};
+    static const int32_t here[] = {1532, 5514, 6527, 8160, 475}, what[] = {1092, 3437, 3710, 12195, 29348};
+    if (in(p0, rather) || (p0 == 1792 && in(p1, ather_pre))) pen(1056, k.lex);           // " than"
+    if (in(p0, right) || (p0 == 481 && p1 == 2697)) pen(3023, k.lex);                     // " away"
+    if (p0 == 310 && in(p1, end_pre)) pen(809, k.lex);                                    // "end to" -> " end"
+    if (in(p0, shrt)) pen(2243, k.lex);                                                   // " version"
+    if (in(p0, exactly) || (p0 == 31654 && in(p1, ex_pre))) pen(1092, k.lex);             // " what"
+    if (p0 == 579 && in(p1, here)) pen(1092, k.lex);                                      // "here's" -> " what"
+    if (p0 == 579 && in(p1, what)) pen(3404, k.lex);                                      // "what's" -> " actually"
+    // performative honesty
+    static const int32_t det[] = {1719, 760, 279, 561, 276, 449, 1473, 2014, 2329, 821, 2921, 4888, 1323, 381,
+                                  1602, 32985, 1379, 3444, 7261, 6183, 777, 4254, 678};   // the/an/my/be/being/most/more/your
+    static const int32_t clause[] = {13, 11, 0, 30, 25, 26, 1076, 2218, 1892, 12, 471, 8,
+                                     321, 694, 748, 429, 7834, 684, 1541, 1921, 1987, 2939, 3850, 4272};
+    static const int32_t h_[] = {71, 39, 3101, 75833};                                   // "h", "H", "(h", "\"h"
+    static const int32_t sentence[] = {13, 0, 30, 25, 26, 1076, 2218, 1892, 8};          // . ! ? : ; ... — ) before " honest take"
+    const bool line_start = cls(p0) == 1 || (s.f & BOL);
+    const bool sentence_start = in(p0, sentence) || line_start;
+    const bool clause_start = in(p0, clause) || line_start;
+    pen(92115, k.hon);                                                                   // "Honestly"
+    pen(68540, k.hon);                                                                   // " Honestly"
+    pen(51397, k.hon);                                                                   // " Honest"
+    if (in(p0, det) || sentence_start) pen(10429, k.hon);                                  // " honest"
+    if (clause_start) pen(25211, k.hon);                                                 // " honestly"
+    if (in(p0, h_)) { pen(33961, 1.5f * k.hon); pen(77696, 1.5f * k.hon); }              // "h|onest", "h|onestly": committed
+    if (p0 == 660) { pen(46253, k.hon); pen(25211, k.hon); }                             // "in all" -> " honesty"
+    // candor announcements: with " honest" pushed down, "If I'm being" shifts to a sibling (real, frank, totally, ...).
+    // That is still the same frame, so those are pushed down after "being" / "be" too. Earlier in the frame, "I'm" and
+    // "the short/real", most of the mass is on content, so the push is aimed there as well.
+    static const int32_t candor[] = {1865, 47045, 24969, 5507, 17290, 7435, 62179, 66842, 86775,   // real blunt frank candid transparent straight upfront brutally truthful
+                                     12116, 6379, 6999, 13542, 11040, 73018};                      // totally completely fully perfectly entirely painfully
+    static const int32_t be_pre[] = {310, 951, 3172, 728, 1066, 16032};                           // to, 'll, me, just, gonna + " be"
+    static const int32_t intens[] = {12116, 6379, 6999, 13542, 11040, 73018, 66842};              // "totally honest"
+    if (p0 == 1602 || p0 == 32985) for (int32_t id : candor) pen(id, k.hon);                     // being real
+    if ((p0 == 381 || p0 == 1323) && in(p1, be_pre)) {
+        for (int32_t id : candor) pen(id, k.hon);                                                 // to be blunt, i'll be straight
+        pen(2050, k.hon);                                                                         // be direct
+        if (p1 == 310 || p1 == 951) { pen(6415, k.hon); pen(2708, k.hon); }                       // to be fair, to be clear
+    }
+    if (in(p0, intens) && (p1 == 1602 || p1 == 32985 || p1 == 381 || p1 == 1323)) { pen(10429, k.hon); for (int32_t id : candor) pen(id, k.hon); }
+    if (p0 == 2688 && (p1 == 353 || p1 == 585 || p1 == 40 || p1 == 72)) { pen(1602, 0.5f * k.hon); pen(10429, k.hon); }   // I'm being / I'm honest
+    if (clause_start) pen(40285, k.hon);                                                          // " frankly"
+    if (p0 == 660) { pen(49034, k.hon); pen(71853, k.hon); }                                      // "in all" fairness / seriousness
+    // "the honest / real / blunt / short answer", "short answer:", "my straight take": the frame noun after the frame word
+    static const int32_t frame_adj[] = {10429, 1865, 47045, 24969, 5507, 7435, 2716, 4145, 3841, 26860, 13983,
+                                        8412, 12115, 10382, 7700, 12402, 8535, 4880, 32254};          // + short/Short/ Short/real/Real/ Real/ actual/ Actual
+    static const int32_t frame_det[] = {1719, 760, 279, 561, 2329, 821, 2921, 4888, 276, 449};
+    static const int32_t frame[] = {4087, 1831, 56004, 2243, 7793, 1301, 11782, 14996, 3296};    // answer take framing version truth read summary assessment question
+    if (in(p0, frame_adj) && (in(p1, frame_det) || in(p1, sentence) || cls(p1) == 1 || p1 == ETHINK))
+        for (int32_t id : frame) pen(id, k.hon);
+    // the branch points one token before the frame closes: a clause-initial "the" / "to" can still go to content
+    static const int32_t the_[] = {1719, 760, 279, 561}, to_[] = {1206, 1946, 951, 310};
+    const bool p1_root = in(p1, sentence) || cls(p1) == 1 || p1 == ETHINK || p1 < 0;
+    if (in(p0, the_) && (p1_root || cls(p0) == 0 && (s.f & PBOL)))
+        for (int32_t id : {1865, 47045, 24969, 5507, 7435, 2716, 26860}) pen(id, 0.5f * k.hon);   // the real/blunt/frank/candid/straight/short/brutal
+    if (in(p0, to_) && (p1_root || (s.f & PBOL))) pen(381, 0.5f * k.hon);                     // clause-initial "to be"
+    if (s.f & BOL) for (int32_t id : {8412, 12115, 7700, 12402}) pen(id, 0.5f * k.hon);       // "short answer:" / "real talk" at a line start
+    // structure: bullets, bold and # headers, numbered lists, rules at a line start
+    if (s.f & BOL) {
+        const float v = k.md + ((s.f & COLON) ? k.colon : 0.0f);
+        static const int32_t md[] = {12, 9, 6458, 471, 348, 7061, 332, 2972, 2, 550, 13962, 794, 653, 7460, 16112,
+                                     4277, 11862, 4006, 2218, 10, 49300};   // ... "---", "***", "–", "—", "+", "→"
+        for (int32_t id : md) pen(id, v);
+        for (int32_t id : kDigit) pen(id, (s.f & COLON) ? v : 0.25f * v);   // light, unless the line above ends in ":"
+    }
+    if ((s.f & PBOL) && in(p0, kDigit)) { pen(13, k.md + k.colon); pen(8, k.md + k.colon); }   // "1." "1)"
+    // a short label line ending in ":" that would open a list
+    if (in(p0, kColon) && s.len <= 6) { pen(NL, k.colon); pen(NL2, k.colon); }
+    return n;
+}
+
+// The ends of a turn or a thought must not gain from a penalty: with a likely token pushed down, the rest of the row
+// scales up by 1 / (1 - b), b the share taken away, and <|im_end|> is often next in line (a reply cut off mid-sentence).
+// Their logits go down by log(1 / (1 - b)) so their probability stays what it was. lse is the row's log-sum-exp; a
+// row whose penalized tokens all sit 12+ below its max loses under 6e-6 each and is skipped (no lse needed).
+static const int32_t kHold[] = {IM_END, THINK, IM_START, 248044};
+
+inline bool needs_hold(const Pen* p, int n, const float* lg, float row_max) {
+    for (int i = 0; i < n; ++i) if (lg[p[i].id] > row_max - 12.0f) return true;
+    return false;
+}
+
+inline int hold(const Pen* p, int n, const float* lg, double lse, Pen* out) {
+    double b = 0.0;
+    for (int i = 0; i < n; ++i) b += std::exp((double) lg[p[i].id] - lse) * (1.0 - std::exp(-(double) p[i].v));
+    if (b <= 0.0) return 0;
+    const float v = (float) -std::log1p(-std::min(b, 0.999999));
+    int m = 0;
+    for (int32_t id : kHold) out[m++] = {id, v};
+    return m;
+}
+
+inline Knobs knobs() {
+    auto f = [](const char* e, float d) { const char* v = std::getenv(e); return v ? (float) std::atof(v) : d; };
+    return {f("STRATA_ANTI_SLOP_PENALTY", 9.0f), f("STRATA_ANTI_SLOP_HONEST", 6.0f), f("STRATA_ANTI_SLOP_MD", 4.0f),
+            f("STRATA_ANTI_SLOP_COLON", 3.0f)};
+}
+
+}  // namespace slop4
+
 class Window {
 public:
     bool init(const strata::core::ModelGeometry& g, strata::core::SessionState& ss, const std::string& shard1, int max_t,
@@ -132,6 +349,11 @@
     bool commit(int n_keep, std::string& err);
     bool finish(int Tn, const int32_t* tokens, int64_t pos0, bool head, int32_t* out, double t0);
     bool copy_logits(int t, float* host) const;
+    // anti-slop v4: the prompt paths do not pass finish, so the prompt's tokens advance the per-position states here
+    template <class I> void slop_scan(const I* tokens, int64_t n, int64_t pos0) {
+        slop4::State st = slop_at(pos0 - 1);
+        for (int64_t i = 0; i < n; ++i) slop_put(pos0 + i) = st = slop4::step(st, (int32_t) tokens[i]);
+    }
     int64_t vocab() const { return n_vocab_; }
     bool ready() const { return cx_ != nullptr; }
     bool streaming() const { return stream_; }
@@ -371,6 +593,12 @@
     double last_gpu_ms_ = 0;
     int last_n_disp_ = 0;
     int32_t last_tokens_[MAXT] = {};
+    std::vector<slop4::State> slop_;   // anti-slop v4: the state after the token at each position (State{} = unknown)
+    slop4::State slop_at(int64_t p) const { return p >= 0 && (size_t) p < slop_.size() ? slop_[(size_t) p] : slop4::State{}; }
+    slop4::State& slop_put(int64_t p) {
+        if ((size_t) p >= slop_.size()) slop_.resize(((size_t) p + 4096) & ~(size_t) 4095);
+        return slop_[(size_t) p];
+    }
     std::vector<float> last_norm_;   // the window's normalized PLE rows (T x hc_dim), for commit
     // STRATA_METAL_LOGPOS=<path> (test tooling, one-token windows): per decode position the log-probability of the
     // token that came next (the next window's input: with --spec-follow, the forced text) and the top 64, in the
@@ -1617,46 +1845,51 @@
         ++x.n; x.pre += tp_pre_ - t0; x.rg += tp_rg_; x.gpu += last_gpu_ms_; x.fin += t1 - tp_pre_ - tp_rg_;
         x.all += t1 - t0;
     }
-    static const float slop_pen = std::getenv("STRATA_ANTI_SLOP_PENALTY") ? (float) std::atof(std::getenv("STRATA_ANTI_SLOP_PENALTY")) : 6.0f;
-    if (head && slop_pen > 0.0f) {
+    {   // anti-slop v4 (slop4 above): every row's state is stored at its position; the head rows get the penalties
+        static const slop4::Knobs kn = slop4::knobs();
+        const bool on = head && (kn.lex > 0.0f || kn.hon > 0.0f || kn.md > 0.0f || kn.colon > 0.0f);
         const int per = (n_vocab_ + 119) / 120;
+        slop4::State st = slop_at(pos0 - 1);
         for (int t = 0; t < Tn; ++t) {
-            const int32_t p0 = tokens[t];
-            const int32_t p1 = t > 0 ? tokens[t - 1] : (last_T_ > 0 ? last_tokens_[last_T_ - 1] : -1);
-            int rej = -1;
-            if (p0 == 4598 || p0 == 24944 || p0 == 61745 || p0 == 70714 ||
-                (p0 == 1792 && (p1 == 2515 || p1 == 2697 || p1 == 15656 || p1 == 43487))) rej = 1056;
-            else if (p0 == 1245 || p0 == 1246 || p0 == 5791 || p0 == 9785 || p0 == 26806 || p0 == 51140 ||
-                     (p0 == 481 && p1 == 2697)) rej = 3023;
-            else if (p0 == 310 && (p1 == 400 || p1 == 809 || p1 == 3601 || p1 == 3839 || p1 == 14637 || p1 == 55293)) rej = 809;
-            else if (p0 == 2716 || p0 == 8412 || p0 == 10382 || p0 == 12115 || p0 == 36639) rej = 2243;
-            else if (p0 == 6681 || p0 == 63151 || p0 == 66172 || (p0 == 31654 && (p1 == 325 || p1 == 5291))) rej = 1092;
-            else if (p0 == 579) {
-                if (p1 == 1532 || p1 == 5514 || p1 == 6527 || p1 == 8160 || p1 == 475) rej = 1092;
-                else if (p1 == 1092 || p1 == 3437 || p1 == 3710 || p1 == 12195 || p1 == 29348) rej = 3404;
-            }
-            auto apply_pen = [&](int tid_pen) {
-                if (tid_pen < 0 || tid_pen >= n_vocab_) return;
-                float* lg = lm_.as<float>() + (size_t) t * n_vocab_;
-                lg[tid_pen] -= slop_pen;
-                if (tokout_.as<int32_t>()[t] == tid_pen) {
-                    const int part = tid_pen / per, b0 = part * per, b1 = std::min(b0 + per, (int) n_vocab_);
-                    float bv = -INFINITY; int bi = 0x7fffffff;
-                    for (int i = b0; i < b1; ++i) if (!std::isnan(lg[i]) && (lg[i] > bv || (lg[i] == bv && i < bi))) { bv = lg[i]; bi = i; }
-                    float* pv = apv_.as<float>() + (size_t) t * 120;
-                    int32_t* pi = api_.as<int32_t>() + (size_t) t * 120;
-                    pv[part] = bv; pi[part] = bi;
-                    bv = -INFINITY; bi = 0x7fffffff;
-                    for (int k = 0; k < 120; ++k) if (pv[k] > bv || (pv[k] == bv && pi[k] < bi)) { bv = pv[k]; bi = pi[k]; }
-                    tokout_.as<int32_t>()[t] = bi;
-                }
-            };
-            if (rej >= 0) apply_pen(rej);
-            // Step-0 empty-turn guard right after </think>\n\n (248069, 271/198): prevent immediate <|im_end|> (248046) or <think> (248068)
-            if (p1 == 248069 && (p0 == 271 || p0 == 198)) {
-                apply_pen(248046);
-                apply_pen(248068);
+            st = slop4::step(st, tokens[t]);
+            slop_put(pos0 + t) = st;
+            if (!on) continue;
+            slop4::Pen pen[80];
+            int np = slop4::penalties(st, kn, pen);
+            float* lg = lm_.as<float>() + (size_t) t * n_vocab_;
+            float* pv = apv_.as<float>() + (size_t) t * 120;
+            int32_t* pi = api_.as<int32_t>() + (size_t) t * 120;
+            if (np > 0) {
+                float mx = -INFINITY;
+                for (int k = 0; k < 120; ++k) mx = std::max(mx, pv[k]);
+                if (slop4::needs_hold(pen, np, lg, mx)) {
+                    double s = 0.0;
+                    for (int64_t i = 0; i < n_vocab_; ++i) s += std::exp((double) (lg[i] - mx));
+                    np += slop4::hold(pen, np, lg, (double) mx + std::log(s), pen + np);
+                }
             }
+            // no empty turn: <|im_end|> or <think> right after </think>\n\n
+            if (kn.lex > 0.0f && st.t2 == slop4::ETHINK && (st.t1 == slop4::NL2 || st.t1 == slop4::NL)) {
+                pen[np++] = {slop4::IM_END, kn.lex};
+                pen[np++] = {slop4::THINK, kn.lex};
+            }
+            if (np == 0) continue;
+            for (int i = 0; i < np; ++i) lg[pen[i].id] -= pen[i].v;
+            // the argmax (tokout_) from the GPU's 120 partition maxima: a partition whose max was pushed down is rescanned
+            bool moved = false;
+            for (int i = 0; i < np; ++i) {
+                const int part = pen[i].id / per;
+                if (pi[part] != pen[i].id) continue;
+                const int b0 = part * per, b1 = std::min(b0 + per, (int) n_vocab_);
+                float bv = -INFINITY; int bi = 0x7fffffff;
+                for (int j = b0; j < b1; ++j) if (!std::isnan(lg[j]) && (lg[j] > bv || (lg[j] == bv && j < bi))) { bv = lg[j]; bi = j; }
+                pv[part] = bv; pi[part] = bi;
+                moved = true;
+            }
+            if (!moved) continue;
+            float bv = -INFINITY; int bi = 0x7fffffff;
+            for (int k = 0; k < 120; ++k) if (pv[k] > bv || (pv[k] == bv && pi[k] < bi)) { bv = pv[k]; bi = pi[k]; }
+            tokout_.as<int32_t>()[t] = bi;
         }
     }
     logpos(Tn, tokens, pos0, head);
@@ -1909,6 +2142,7 @@
     pend_fold_ = 0;
     last_T_ = ids.empty() ? 0 : 1;
     if (!ids.empty()) last_tokens_[0] = ids.back();
+    slop_scan(ids.data(), (int64_t) ids.size(), 0);
     ss_->ple_prev[0] = h.ple_prev[0];
     ss_->ple_prev[1] = h.ple_prev[1];
     n_cells = h.n_cells;
@@ -1937,6 +2171,10 @@
         });
     for (auto& t : th) t.join();
     if (!ok) err = "metal window: SCR relocation refused";
+    if (ok && n > 0 && (size_t) (c_src + n) <= slop_.size()) {   // the anti-slop states move with their cells
+        slop_put(c_dst + n - 1);
+        std::memmove(slop_.data() + c_dst, slop_.data() + c_src, (size_t) n * sizeof(slop4::State));
+    }
     return ok;
 }
 
@@ -3235,6 +3473,7 @@
 bool prompt_run_rows(const int64_t* tokens, int64_t n, int64_t pos0,
                      const std::function<bool(const float*, int64_t, int64_t, std::string&)>& on_chunk, std::string& err) {
     if (g_window == nullptr) { err = "metal prompt: the decode window is not up yet"; return false; }
+    g_window->slop_scan(tokens, n, pos0);
     static const bool windows = std::getenv("STRATA_METAL_PROMPT") && std::string(std::getenv("STRATA_METAL_PROMPT")) == "window";
     static const int64_t min_rows = std::getenv("STRATA_METAL_PROMPT_MIN") ? std::atoll(std::getenv("STRATA_METAL_PROMPT_MIN")) : 32;
     if (!windows && !g_window->streaming() && n >= min_rows) return g_window->prompt(tokens, n, pos0, on_chunk, err);

antislop_v4.inc, which the diff inlines above class Window:

// Anti-slop v4: a sampler shaper over the head logits, outside <think>, <tool_call> and ``` fences only.
// Each position's state (the last two tokens, the region, where the line stands) follows from the one before it, so
// it is kept per position: a draft row, a rejected suffix, a prompt read from a cached prefix all start from the
// state of the position before them. Tables: the Qwen3.8-Flash-Next vocab (models/*/tokenizer/vocab.json).
namespace slop4 {

enum : uint8_t { PROSE = 1, TOOL = 2, FENCE = 4, BOL = 8, COLON = 16, PBOL = 32, KNOWN = 128 };

struct State {
    int32_t t1 = -1, t2 = -1;   // the last token and the one before it
    uint8_t f = 0;              // PROSE: after </think> in this turn; BOL: at a line start; COLON: that line follows "...:"
    uint8_t len = 0;            // content tokens on the current line (saturating)
    uint8_t pad[2] = {};
};

struct Knobs { float lex, hon, md, colon; };

struct Pen { int32_t id; float v; };

// token classes: 1 a newline run ("\n", "\n\n", " \n", ...), 2 blanks only (indentation)
static const int32_t kNewline[] = {
    198, 201, 271, 317, 695, 845, 987, 1031, 1358, 1517, 1726, 2228, 2317, 2449, 3265, 4326, 4408, 4558, 4969, 5690,
    5744, 5773, 6010, 6159, 6219, 6710, 6987, 7388, 8301, 8675, 8726, 9122, 10109, 10367, 10628, 10797, 11463,
    11953, 12213, 13488, 14200, 14305, 14379, 15216, 15334, 15398, 15602, 15684, 16391, 16750, 16957, 17116, 17694,
    17817, 18015, 18050, 18438, 19075, 19721, 21996, 22616, 22727, 23574, 23745, 23840, 24630, 25263, 25401, 25445,
    25698, 25952, 26467, 26480, 26771, 27330, 29644, 29773, 30858, 31472, 32077, 32527, 32811, 33006, 33019, 33339,
    33438, 33496, 33954, 34127, 35369, 35412, 35624, 35973, 36444, 36826, 37050, 37528, 38336, 38544, 39000, 39598,
    40260, 40315, 41210, 41585, 43622, 43916, 44058, 44281, 44879, 44902, 46813, 47458, 47947, 48305, 49404, 49420,
    49746, 50946, 52245, 53585, 54701, 56137, 56580, 56955, 56963, 57103, 57633, 58943, 59366, 59776, 60121, 61332,
    62386, 62591, 63063, 63111, 63454, 64137, 64576, 65117, 65382, 67511, 68830, 69229, 69340, 69896, 70273, 70707,
    70866, 70929, 71356, 71848, 71990, 72666, 73714, 75128, 75321, 75668, 75981, 76294, 76374, 76421, 76507, 76909,
    77670, 77933, 78035, 78254, 79519, 81018, 81927, 82471, 82946, 83774, 83829, 84075, 85709, 85748, 85809, 85844,
    85935, 86092, 86182, 87199, 87251, 87328, 88065, 88574, 88927, 88985, 89803, 91421, 91668, 91804, 92699, 92766,
    93957, 94820, 94861
};
static const int32_t kBlank[] = {
    197, 220, 256, 257, 260, 262, 285, 297, 309, 336, 388, 406, 454, 493, 556, 646, 670, 761, 950, 967, 1025, 1107,
    1335, 1604, 1638, 1734, 1779, 1855, 2118, 2215, 2467, 2475, 2596, 2672, 3235, 3388, 3551, 3696, 4152, 4420,
    4448, 4946, 5015, 5071, 5233, 5780, 6173, 6247, 6323, 6447, 6800, 7216, 7237, 7322, 7536, 7599, 7879, 8074,
    8424, 8830, 9081, 9411, 9876, 10190, 10275, 10747, 11449, 11530, 11611, 12473, 12670, 12687, 13076, 13152,
    13300, 13857, 14221, 14826, 14843, 14978, 16203, 16754, 16851, 17025, 17122, 17468, 17780, 17895, 18186, 18625,
    18688, 18690, 19679, 20200, 20334, 20839, 20851, 21519, 21641, 22539, 22568, 22691, 23420, 23581, 24952, 25232,
    25870, 26058, 27729, 27860, 28959, 29034, 29374, 29427, 29708, 29915, 30026, 30631, 30927, 31593, 31632, 31726,
    33368, 34158, 34381, 34450, 35315, 35696, 35853, 35914, 36909, 37181, 37207, 38177, 38448, 38772, 39700, 40595,
    41293, 41785, 44606, 44714, 44914, 45043, 45207, 45550, 45964, 46330, 46572, 47256, 48546, 48742, 48835, 49349,
    49373, 49642, 49741, 50328, 52872, 52989, 53928, 54652, 55036, 55133, 55720, 56288, 56612, 56683, 57926, 58096,
    60270, 62037, 63023, 63067, 63287, 63288, 64278, 64571, 65897, 66227, 66639, 67204, 67685, 68190, 68977, 71032,
    71400, 72030, 72352, 72738, 73753, 74019, 75424, 76010, 76332, 76785, 76937, 77120, 77968, 78052, 78420, 80396,
    80905, 82130, 83375, 83770, 84703, 85012, 86978, 88011, 88074, 88531, 91507, 92130, 92492, 92770, 93340, 93457,
    93647, 94057, 95323, 95459
};

inline uint8_t cls(int32_t t) {
    static const std::vector<uint8_t> tab = [] {
        std::vector<uint8_t> v(248320, 0);
        for (int32_t i : kNewline) v[(size_t) i] = 1;
        for (int32_t i : kBlank) v[(size_t) i] = 2;
        return v;
    }();
    return t >= 0 && (size_t) t < tab.size() ? tab[(size_t) t] : 0;
}

template <size_t N> inline bool in(int32_t t, const int32_t (&s)[N]) {
    for (int32_t x : s) if (x == t) return true;
    return false;
}

enum : int32_t {
    IM_START = 248045, IM_END = 248046, THINK = 248068, ETHINK = 248069, TOOL_CALL = 248058, ETOOL_CALL = 248059,
    NL = 198, NL2 = 271,
};
static const int32_t kFence[] = {71093, 52451};                  // "```", " ```"
static const int32_t kColon[] = {25, 64700, 92217};              // ":", ":**", "**:"
static const int32_t kDigit[] = {16, 17, 18, 19, 20, 21, 22, 23, 24};   // "1".."9"

inline State step(State s, int32_t tok) {
    s.t2 = s.t1;
    s.t1 = tok;
    s.f |= KNOWN;
    const uint8_t bol = s.f & BOL;
    s.f &= (uint8_t) ~PBOL;
    if (tok == IM_START || tok == IM_END || tok == THINK) {
        s.f = KNOWN | BOL;
        s.len = 0;
    } else if (tok == ETHINK) {
        s.f = KNOWN | PROSE | BOL;
        s.len = 0;
    } else if (tok == TOOL_CALL) {
        s.f |= TOOL;
    } else if (tok == ETOOL_CALL) {
        s.f = (uint8_t) ((s.f & ~(TOOL | COLON)) | BOL);
        s.len = 0;
    } else if (cls(tok) == 1) {
        if (!bol) s.f = (uint8_t) ((s.f & ~COLON) | (in(s.t2, kColon) ? COLON : 0));
        s.f |= BOL;
        s.len = 0;
    } else if (cls(tok) == 2) {
        // indentation: the line has not started yet
    } else {
        if (in(tok, kFence) && (s.f & PROSE) && !(s.f & TOOL)) s.f ^= FENCE;
        s.f = (uint8_t) ((s.f & ~(BOL | COLON)) | (bol ? PBOL : 0));
        if (s.len < 255) ++s.len;
    }
    return s;
}

// the penalties on the next token after state s; out holds at least 64
inline int penalties(const State& s, const Knobs& k, Pen* out) {
    if ((s.f & (KNOWN | PROSE | TOOL | FENCE)) != (KNOWN | PROSE)) return 0;
    int n = 0;
    auto pen = [&](int32_t id, float v) { if (v > 0.0f && n < 64) out[n++] = {id, v}; };
    const int32_t p0 = s.t1, p1 = s.t2;
    // the 7 lexical cliches (results/banned_llm_isms.json), as in v3
    static const int32_t rather[] = {4598, 24944, 61745, 70714}, ather_pre[] = {2515, 2697, 15656, 43487};
    static const int32_t right[] = {1245, 1246, 5791, 9785, 26806, 51140};
    static const int32_t end_pre[] = {400, 809, 3601, 3839, 14637, 55293};
    static const int32_t shrt[] = {2716, 8412, 10382, 12115, 36639};
    static const int32_t exactly[] = {6681, 63151, 66172}, ex_pre[] = {325, 5291};
    static const int32_t here[] = {1532, 5514, 6527, 8160, 475}, what[] = {1092, 3437, 3710, 12195, 29348};
    if (in(p0, rather) || (p0 == 1792 && in(p1, ather_pre))) pen(1056, k.lex);           // " than"
    if (in(p0, right) || (p0 == 481 && p1 == 2697)) pen(3023, k.lex);                     // " away"
    if (p0 == 310 && in(p1, end_pre)) pen(809, k.lex);                                    // "end to" -> " end"
    if (in(p0, shrt)) pen(2243, k.lex);                                                   // " version"
    if (in(p0, exactly) || (p0 == 31654 && in(p1, ex_pre))) pen(1092, k.lex);             // " what"
    if (p0 == 579 && in(p1, here)) pen(1092, k.lex);                                      // "here's" -> " what"
    if (p0 == 579 && in(p1, what)) pen(3404, k.lex);                                      // "what's" -> " actually"
    // performative honesty
    static const int32_t det[] = {1719, 760, 279, 561, 276, 449, 1473, 2014, 2329, 821, 2921, 4888, 1323, 381,
                                  1602, 32985, 1379, 3444, 7261, 6183, 777, 4254, 678};   // the/an/my/be/being/most/more/your
    static const int32_t clause[] = {13, 11, 0, 30, 25, 26, 1076, 2218, 1892, 12, 471, 8,
                                     321, 694, 748, 429, 7834, 684, 1541, 1921, 1987, 2939, 3850, 4272};
    static const int32_t h_[] = {71, 39, 3101, 75833};                                   // "h", "H", "(h", "\"h"
    static const int32_t sentence[] = {13, 0, 30, 25, 26, 1076, 2218, 1892, 8};          // . ! ? : ; ... — ) before " honest take"
    const bool line_start = cls(p0) == 1 || (s.f & BOL);
    const bool sentence_start = in(p0, sentence) || line_start;
    const bool clause_start = in(p0, clause) || line_start;
    pen(92115, k.hon);                                                                   // "Honestly"
    pen(68540, k.hon);                                                                   // " Honestly"
    pen(51397, k.hon);                                                                   // " Honest"
    if (in(p0, det) || sentence_start) pen(10429, k.hon);                                  // " honest"
    if (clause_start) pen(25211, k.hon);                                                 // " honestly"
    if (in(p0, h_)) { pen(33961, 1.5f * k.hon); pen(77696, 1.5f * k.hon); }              // "h|onest", "h|onestly": committed
    if (p0 == 660) { pen(46253, k.hon); pen(25211, k.hon); }                             // "in all" -> " honesty"
    // candor announcements: with " honest" pushed down, "If I'm being" shifts to a sibling (real, frank, totally, ...).
    // That is still the same frame, so those are pushed down after "being" / "be" too. Earlier in the frame, "I'm" and
    // "the short/real", most of the mass is on content, so the push is aimed there as well.
    static const int32_t candor[] = {1865, 47045, 24969, 5507, 17290, 7435, 62179, 66842, 86775,   // real blunt frank candid transparent straight upfront brutally truthful
                                     12116, 6379, 6999, 13542, 11040, 73018};                      // totally completely fully perfectly entirely painfully
    static const int32_t be_pre[] = {310, 951, 3172, 728, 1066, 16032};                           // to, 'll, me, just, gonna + " be"
    static const int32_t intens[] = {12116, 6379, 6999, 13542, 11040, 73018, 66842};              // "totally honest"
    if (p0 == 1602 || p0 == 32985) for (int32_t id : candor) pen(id, k.hon);                     // being real
    if ((p0 == 381 || p0 == 1323) && in(p1, be_pre)) {
        for (int32_t id : candor) pen(id, k.hon);                                                 // to be blunt, i'll be straight
        pen(2050, k.hon);                                                                         // be direct
        if (p1 == 310 || p1 == 951) { pen(6415, k.hon); pen(2708, k.hon); }                       // to be fair, to be clear
    }
    if (in(p0, intens) && (p1 == 1602 || p1 == 32985 || p1 == 381 || p1 == 1323)) { pen(10429, k.hon); for (int32_t id : candor) pen(id, k.hon); }
    if (p0 == 2688 && (p1 == 353 || p1 == 585 || p1 == 40 || p1 == 72)) { pen(1602, 0.5f * k.hon); pen(10429, k.hon); }   // I'm being / I'm honest
    if (clause_start) pen(40285, k.hon);                                                          // " frankly"
    if (p0 == 660) { pen(49034, k.hon); pen(71853, k.hon); }                                      // "in all" fairness / seriousness
    // "the honest / real / blunt / short answer", "short answer:", "my straight take": the frame noun after the frame word
    static const int32_t frame_adj[] = {10429, 1865, 47045, 24969, 5507, 7435, 2716, 4145, 3841, 26860, 13983,
                                        8412, 12115, 10382, 7700, 12402, 8535, 4880, 32254};          // + short/Short/ Short/real/Real/ Real/ actual/ Actual
    static const int32_t frame_det[] = {1719, 760, 279, 561, 2329, 821, 2921, 4888, 276, 449};
    static const int32_t frame[] = {4087, 1831, 56004, 2243, 7793, 1301, 11782, 14996, 3296};    // answer take framing version truth read summary assessment question
    if (in(p0, frame_adj) && (in(p1, frame_det) || in(p1, sentence) || cls(p1) == 1 || p1 == ETHINK))
        for (int32_t id : frame) pen(id, k.hon);
    // the branch points one token before the frame closes: a clause-initial "the" / "to" can still go to content
    static const int32_t the_[] = {1719, 760, 279, 561}, to_[] = {1206, 1946, 951, 310};
    const bool p1_root = in(p1, sentence) || cls(p1) == 1 || p1 == ETHINK || p1 < 0;
    if (in(p0, the_) && (p1_root || cls(p0) == 0 && (s.f & PBOL)))
        for (int32_t id : {1865, 47045, 24969, 5507, 7435, 2716, 26860}) pen(id, 0.5f * k.hon);   // the real/blunt/frank/candid/straight/short/brutal
    if (in(p0, to_) && (p1_root || (s.f & PBOL))) pen(381, 0.5f * k.hon);                     // clause-initial "to be"
    if (s.f & BOL) for (int32_t id : {8412, 12115, 7700, 12402}) pen(id, 0.5f * k.hon);       // "short answer:" / "real talk" at a line start
    // structure: bullets, bold and # headers, numbered lists, rules at a line start
    if (s.f & BOL) {
        const float v = k.md + ((s.f & COLON) ? k.colon : 0.0f);
        static const int32_t md[] = {12, 9, 6458, 471, 348, 7061, 332, 2972, 2, 550, 13962, 794, 653, 7460, 16112,
                                     4277, 11862, 4006, 2218, 10, 49300};   // ... "---", "***", "–", "—", "+", "→"
        for (int32_t id : md) pen(id, v);
        for (int32_t id : kDigit) pen(id, (s.f & COLON) ? v : 0.25f * v);   // light, unless the line above ends in ":"
    }
    if ((s.f & PBOL) && in(p0, kDigit)) { pen(13, k.md + k.colon); pen(8, k.md + k.colon); }   // "1." "1)"
    // a short label line ending in ":" that would open a list
    if (in(p0, kColon) && s.len <= 6) { pen(NL, k.colon); pen(NL2, k.colon); }
    return n;
}

// The ends of a turn or a thought must not gain from a penalty: with a likely token pushed down, the rest of the row
// scales up by 1 / (1 - b), b the share taken away, and <|im_end|> is often next in line (a reply cut off mid-sentence).
// Their logits go down by log(1 / (1 - b)) so their probability stays what it was. lse is the row's log-sum-exp; a
// row whose penalized tokens all sit 12+ below its max loses under 6e-6 each and is skipped (no lse needed).
static const int32_t kHold[] = {IM_END, THINK, IM_START, 248044};

inline bool needs_hold(const Pen* p, int n, const float* lg, float row_max) {
    for (int i = 0; i < n; ++i) if (lg[p[i].id] > row_max - 12.0f) return true;
    return false;
}

inline int hold(const Pen* p, int n, const float* lg, double lse, Pen* out) {
    double b = 0.0;
    for (int i = 0; i < n; ++i) b += std::exp((double) lg[p[i].id] - lse) * (1.0 - std::exp(-(double) p[i].v));
    if (b <= 0.0) return 0;
    const float v = (float) -std::log1p(-std::min(b, 0.999999));
    int m = 0;
    for (int32_t id : kHold) out[m++] = {id, v};
    return m;
}

inline Knobs knobs() {
    auto f = [](const char* e, float d) { const char* v = std::getenv(e); return v ? (float) std::atof(v) : d; };
    return {f("STRATA_ANTI_SLOP_PENALTY", 9.0f), f("STRATA_ANTI_SLOP_HONEST", 6.0f), f("STRATA_ANTI_SLOP_MD", 4.0f),
            f("STRATA_ANTI_SLOP_COLON", 3.0f)};
}

}  // namespace slop4

7. files (/tmp/opus_antislop_v4/) #

  • antislop_v4.inc: the shipped logic. apply_patch.py, window_cpp_antislop_v4.patch, build/ (patched + base objects and CLIs)
  • eval_l47.cpp: the evaluator. eval_k512_docs.tsv, eval_k256trunk_k512head.tsv, eval_k256trunk_k256head.tsv: per-row results. sw_*: the lex/md sweep
  • k512dump/docA.*, docB.*: the new K512 L47 dumps (R_post47, inj47, hh47, sgv47). docA_tokens.txt, docB_tokens.txt, docs.json
  • eval_k512_docs_top.tsv, v41_docs.tsv, v41b_docs.tsv: per-row top-16 for the treadmill analysis. gen_ids.json, gen_decoded.json, genfinal_text.json: the A/B outputs. replay_gen.cpp: the generation replay check
  • test_state.cpp: the position-store check. layerB_separability.py. mine_slop.py → mined_slop_stats.json. qwen38_tokenizer.json, built from vocab.json + merges.txt with the qwen35 pre-tokenizer (reproduces v3's IDs exactly)
  • to reproduce a dump: STRATA_METAL_DUMP_MOE=<pfx> STRATA_METAL_ARENA_GB=3.4 STRATA_METAL_WIRE_ARENA=0 STRATA_METAL_PROMPT_CHUNK=1024 strata_window_cli --metal-gguf models/K512_iq2_xs/K512-iq2_xs-metal.gguf --ple-gguf <K256 IQ3_S shard 1> --n-expert 512 --ctx 4096 --tokens-file <ids> --n-prompt <n-1> --n-gen 1 (cwd vendor/strata, the rest of the env from strata-k512-iq2xs.json). then keep layer 47 of .hh and .sgv (extract47.py).