Abstract
Coding agents accumulate context faster than they can use it: build logs, dead ends, superseded diffs. We report on Viscer, a 4B-parameter model fine-tuned to compress a long coding-agent session into a structured JSON state that an agent can resume from, deployed as a 2.7 GB GGUF that runs locally. The work has two parts. First, a 330-run benchmark of the Qwen3.5 2B and 4B candidates established that zero-shot schema-constrained compression is infeasible at both sizes (0% schema-valid), and selected the 4B with a dynamic Q4 quant. Second, a synthetic supervision pipeline — 294 ledger-verified sessions spanning 14 domains and 10 session shapes — produced a model that reaches median 0.712 critical-literal recall on 16 held-out sessions versus 0.337 for the few-shot base model, winning on 15 of 16 cases, and generalizes to 131K-token inputs where the base model degrades to near-zero recall. We also report five negative results that we consider the paper's methodological contribution: a widely recommended sampler that does not fix our failure mode, an evaluation metric that scored degenerate output as clean, a data-format bug that masqueraded as a model defect, a preference signal that was already saturated, and an isolated drafter retraining that damaged the component it was meant to improve. Each is stated with the measurement that settled it.
1. Introduction
A coding agent that has worked for an hour carries an enormous, mostly stale history. Every subsequent turn re-reads it. This is slow, expensive, and eventually impossible: context windows end, and long before they do, the useful state is buried in tool output.
Compression is the obvious response, but the requirements are unusual. The output is not for a human to read; it is for a machine to act on. A summary that reads well but loses a file path, an error signature, or the distinction between resolved and unresolved work is worse than useless — it is actively harmful, because the agent will act on it confidently.
This report describes what happened when we tried to build that compressor with a small local model on a single consumer GPU. We describe the selection benchmark, the training pipeline, the resulting model, and — at length — the ways our own measurement apparatus misled us first.
Contributions
- A selection benchmark for context compression (330 runs) showing the task is beyond zero-shot capability at 2B and 4B, with a measured compression ceiling.
- A synthetic supervision pipeline built on programmatically verified fact ledgers, including eight families of negative/fallback training cases.
- A working artifact: Viscer-4B v0.1, 2.1× the base model's literal recall at parity speed, verified at 131K-token context, released with weights, corpus and tooling.
- Five negative results with mechanisms (§5), offered because each cost us time that better-specified method notes would have saved.
2. Setup
| Hardware | one RTX 5090, 32 GB, single-stream by rule (no concurrent GPU jobs) |
|---|---|
| Base model | Qwen3.5-4B (Apache-2.0), chosen in §3 |
| Deployment target | GGUF, Unsloth-style dynamic Q4_K, MTP speculative decoding |
| Teacher / judge | a hosted 1M-context model for corpus generation; a separate hosted model for judging |
| Evaluation | deployment runtime (llama.cpp) throughout; frozen per-bucket budgets; identical settings for every candidate |
All GPU work in this project ran single-stream deliberately. Contaminated timing from shared-GPU runs silently invalidates the performance axis, and we chose to make that impossible rather than to detect it afterwards.
3. Phase 1: which model, at what precision
3.1 Method
Candidates were scored on semantic fidelity (exact-literal recall against ground-truth fact ledgers), speed (prefill, time-to-first-token, decode), and memory, at four input lengths up to 131K tokens. Hard gates were frozen before results were seen: schema validity ≥99.5%, critical-literal recall ≥99%, zero critical hallucinations, ≥95% downstream retention, ≤0.5% false completion, no OOM at 131K.
3.2 Results
Zero-shot compression is infeasible. Neither size produced schema-valid output
from instructions alone: 0% at 2B and 4B, across every runtime and quantization
tested. The failure taxonomy: degenerate renumbered-key loops (the model emits
path6, path7, path9… until the budget ends), schema invention, and budget
overruns. With two worked examples, validity rose to 45–81% depending on size and
quantization — enough to compare candidates, not enough to ship.
| Phase-1 finding | Measurement |
|---|---|
| 4B over 2B | 2.7× literal recall on valid outputs; half the loop rate |
| Dynamic Q4 over uniform Q8 | equal recall, 20% faster, 35% smaller |
| Deployment config | 4B + dynamic Q4 + MTP depth 3 (on the base model) |
| 131K gate | passed at 167K tokens on both sizes |
| Actual cost | ~90 s end-to-end for a 167K-token session |
3.3 The compression ceiling
Ground-truth targets verified for complete literal retention land at median 5.2× compression (range 2.2–40.7×, n=272). Every attempt at 16× failed: 40–79 mandatory literals cannot fit in 748–1,088 tokens. The base models' apparent “efficiency” (26–130×) was lossy compression, not skill.
Methodological consequence: a single “compression budget” conflates output length with fact retention. Both must be reported together, which is why every result in §4 carries a recall number beside its compression ratio.
4. Phase 2: learning to compress from synthetic supervision
4.1 Data
Sessions are synthesized chapter-by-chapter by a hosted teacher, then compressed into gold targets with the case's fact ledger in the prompt, so the teacher knows which facts must survive. Every target is verified programmatically: schema conformance, ≥90% recall of critical literals, all hard constraints present, zero false completions, and budget treated as soft.
| Sessions | 294 across 14 domains × 10 session shapes (140/140 cells populated) |
|---|---|
| Lengths | 112 S, 98 M, 70 L, 14 XL (real 140K-token sessions) |
| Build cost | 38 minutes at 48-way concurrency (3,340 chapter calls) |
| Verified targets | 262 positive + 88 negative/fallback |
| Rejected | 48, by cause: 38% false completion, 35% truncation, 17% recall, 6% constraints, 4% budget |
The largest rejection category is notable: the teacher marked unresolved work as complete in 38% of its rejected attempts. That is the same failure the trained model must avoid, appearing in the generator. Verified rather than trusted synthetic data is what prevented it from entering the training set.
4.2 Training
QLoRA (r=16, α=32; 21.2M trainable = 0.81% of the model), loss masked to the target JSON only, 239 examples, 2 epochs, 100 minutes on one GPU, peak 15.8 GiB. Three choices we would repeat:
- Transcript clipping, never target clipping — long sessions are trimmed head-and-tail to fit the memory budget; the target JSON is always intact.
- Framing jitter — exemplar presence and instruction wording vary deterministically per case, so the model cannot depend on one prompt.
- Sub-epoch checkpoints selected by held-out schema-valid rate, never by training loss (see §5.4).
4.3 Results
16 held-out sessions across 14 domains, deployment configuration, single seed:
| Metric | Viscer-4B | Base (few-shot) |
|---|---|---|
| parseable | 15/16 (94%) | 14/16 (88%) |
| core schema | 12/16 | 13/16 |
| v2 schema (incl. fallback field) | 12/16 | 1/16 |
| critical-literal recall — median | 0.712 | 0.337 |
| critical-literal recall — mean | 0.656 | 0.311 |
| verbatim anchors — median | 37 | 14 |
| delivered tokens — median | 2,924 | 1,172 |
Three observations:
- The gain is fidelity, not formatting. The base model already emits schema-valid JSON (13/16 core); it is simply empty — 14 anchors against 37, and a third of the recall.
- The fine-tune holds its compression regime. It delivers ~2.9K tokens where the base delivers ~1.2K, i.e. it stops over-compressing and spends its budget on literals — the behaviour the targets taught.
- Long context generalizes. At 144K tokens it compresses validly at 0.514 recall; at 167K, 0.280 — against 0.032 for the base model at that length, with no OOM at 12.5 GB peak.
4.4 Quantization
A dynamic recipe — Q4_K base with an importance matrix and per-tensor overrides, including the MTP/NextN head pinned at Q8_0 — was compared against a uniform Q4_K_M on the same 16 cases:
| dynamic (shipped) | uniform Q4_K_M | |
|---|---|---|
| parseable | 16/16 | 15/16 |
| v2 schema | 14/16 | 12/16 |
| recall median / mean | 0.681 / 0.717 | 0.712 / 0.656 |
At parity fidelity for +2% VRAM, the dynamic recipe removed the parse failures entirely on this sample and is what ships.
5. Findings: five ways our own measurement misled us
We report these in detail because each was expensive, each was settled by a specific measurement, and each generalizes beyond this project.
5.1 The recommended sampler for structured output did not fix our failure mode
Our model's degenerate output is a verbatim repetition of a JSON entry
({"command": "ctest …", "outcome": "100% tests passed"}) repeated until the cap.
Structured-output practice says: never use a flat repetition penalty on JSON
(it penalizes the syntax tokens a schema must repeat) — use DRY instead, which
penalizes suffix repetitions while honoring sequence breakers.
| Configuration | Outcome | distinct-12 | longest repeat |
|---|---|---|---|
| no penalty, no DRY | FAIL — 51K chars, never closes | 0.023 | 480 |
| DRY 0.8 (default window) | FAIL — 63K chars | 0.053 | 240 |
| DRY 1.0 (window 512) | FAIL — 56K chars | 0.043 | 480 |
| repeat-penalty 1.1, last-n 256 | valid JSON, 3.4K tokens | 0.911 | 0 |
DRY cannot see our loop because the loop is made of JSON punctuation, which its sequence breakers deliberately exclude. The general guidance is correct in principle and inapplicable here — and only measurement revealed that.
5.2 An evaluation metric that scored degenerate output as clean
Our repetition detector counted duplicate lines. The failure above lives on one
enormous line, so it scored the degenerate output 0.00 — spotless — while the
model emitted 16K tokens of the same JSON entry. We replaced it with
distinct_n_ratio (unique 12-grams: degenerate 0.006–0.237, healthy 0.887–0.964)
and longest_repeat_span (degenerate 240–480 chars, healthy 0–60). Every earlier
degeneracy rate we had computed was an underestimate.
5.3 A data bug that masqueraded as a model defect
Our first fine-tune emitted a field named reason_for_handling where the schema
requires handling + handling_reason, and we attributed it to the model — and
briefly to penalty-induced syntax drift. The actual cause: the positive training
targets had been generated against schema v1 while the negative examples used v2,
so the model saw the required field in only ~25% of examples. Deterministic
backfill of 272 targets moved v2 conformance from 0/6 to 5/5 on held-out runs.
5.4 A preference signal that was already saturated
We assumed the remaining failure was behavioural and built preference pairs (verified target vs measured runaway) — then measured the margin before training:
loss 0.039 · margin +32.3 log-odds · preference accuracy 1.0
The model already prefers the correct output by 32 log-odds. There was nothing for preference optimization to learn, which explains why three training-side attempts (DPO, ORPO, and a direct implementation) produced nothing. The loop is a locally optimal decoding path, not a preference gap — consistent with §5.1, where a decoding-time constraint fixed what training could not. Measuring the margin before building the preference infrastructure would have saved this sub-project entirely.
5.5 Retraining a runtime-coupled component in isolation damaged it
Fine-tuning dropped speculative-decoding acceptance from 0.865 to 0.658, because the draft head consumes hidden states the fine-tune shifted while its own weights were never trained. Transformers ignores those tensors on load, so we assembled the head from the checkpoint's 15 tensors and trained it (120.6M params, loss 14.25 → 0.78).
| Drafter | acceptance (depth 2) | decode |
|---|---|---|
| original (untouched) | 0.658 | 211.6 tok/s |
| retrained, paper's formulation | 0.141 | 111.7 |
| retrained, assumed runtime-consistent formulation | 0.061 | 96.5 |
Both attempts made it dramatically worse, and the “corrected” one was worse than the buggy one — so the objective alignment was not the error. When a component is consumed by a runtime you do not control, the runtime is the specification: read its source before training, not the tensor shapes.
6. Limitations and threats to validity
- n = 16 held-out sessions, one seed. The 2.1× recall gap is large and consistent (15/16 case wins) but the confidence interval is not yet tight.
- Schema validity is 12/16 against a 99.5% gate. The failures are JSON syntax slips, not semantic errors; retry-on-parse-failure is the current mitigation.
- No independent human audit of the synthetic ledgers. They are programmatically verified only, which is disclosed in the model card and here.
- The base-model comparison partly favours the fine-tune by construction for the v2 schema field, since the base never saw it.
- Speed comparisons across variants must be interleaved. We observed the no-MTP control drift 178.9 → 124.4 tok/s across hours of sustained load with identical weights — thermal and clock drift. Acceptance, normalized within a run, was the reliable signal.
- The teacher's own biases are in the corpus (§4.1), mitigated by rejection sampling but not eliminated.
7. Conclusion
A 4B model with 0.81% of its parameters trained can compress coding-agent sessions to 2.1× the literal recall of its few-shot baseline, at parity speed, in 2.7 GB, locally, at contexts up to 167K tokens. Getting there required a verification-first data pipeline and a benchmark that measured the right thing five separate times after measuring the wrong thing first. The compressor's remaining limitations are specific and enumerated; the negative results are, we suspect, the part most useful to others doing similar work.
8. Artifacts and reproducibility
| Artifact | Location |
|---|---|
| Model (4 GGUF quants + card + license) | HF: TheWickedGustav/viscer-4b ↗ |
| Corpus (294 sessions + 383 verified targets + manifests) | HF: TheWickedGustav/viscer-corpus-v1 ↗ |
| Technical appendix: 18 findings with exact data | docs/phase2-methods-and-findings.md |
| Decision log (48 entries, including every reversal) | DECISIONS.md |
| Benchmarks: 330 Phase-1 runs + Phase-2 campaigns | runs/, reports/ |
| Tooling: corpus builder, target generator, trainers, harness, scorer | scripts/ |
| Base selection report | reports/phase-1-summary.md |
Reproduction: every campaign is resume-safe and its command is documented in
docs/RUNBOOK.md. Every measurement in this report cites the artifact that
produced it.
9. References
- Raschka, S. Repetition loops in generation — debugging order for degenerate fine-tuned output. sebastianraschka.com/faq/docs/repetition-loops-generation.html
- DRY: Don't Repeat Yourself — suffix-repetition penalty with sequence breakers. arXiv 2608.22761
- Why Repetition Penalty Breaks JSON and Code Generation — penalty effects on structured output. dev.to/ji_ai
- Hugging Face transformers PR #40842 — Gated DeltaNet normalization epsilon fix.
- Unsloth PR #6850 — Qwen3.5 gated-delta fast path requiring flash-linear-attention and causal_conv1d.
- llama.cpp b10829 — Gated DeltaNet
l2_normepsilon fix (our pinned build b11100 includes it). - Qwen3.5-4B / -4B-Base model cards — architecture, context length, licensing.