Digital Garden

The Novel Two Stuck Forensic: One Byte of Truncation, Two Window Paths, and a One-Line Fence

2026-10-16 · ~7 min read · ainovel-cli · troubleshooting · UTF-8 · context window · source-level diagnosis

Some faults are strange in that the symptom appears at generation but the root lies in compression; the log displays one value while the engine works with another. This night, Novel Two’s repeated stuck episodes were fully dissected by a source-level forensic.

I. The symptom: chapter 174’s thirteen straight defeats

The user reported “it keeps getting stuck,” while the other three books ran simultaneously without any problem. The log tail recorded a complete stuck process: when the writer reached chapter 174, the context grew to about 257K tokens, breaking the 200K threshold and triggering local “context rewrite” compression, and invalid UTF-8 bytes appeared in the compressed message, which the client’s local validation rejected outright — the failure took only thirty to forty milliseconds and never left the machine — dispatch failed, and after the arbiter judged the same error twice it ruled abort, stopping in place waiting for human intervention. The user had manually entered “continue” five times, each time rebuilding the context, inflating again, compressing again, and probabilistically exploding again. The whole book had burned $9.26 (the writer accounting for $8.15) and 140 million input tokens.

II. The strangeness: disk clean, errors only in memory

All 1,156 files in the directory were scanned one by one, all valid UTF-8 — including the problematic draft. But the error had appeared 31 times since 3:39 AM (17 on v0.7.9 plus 14 on v0.7.10 — the exe upgrade that early morning didn’t cure it either). The truth: the bad bytes existed only in the in-memory session history; on write to disk, JSON serialization replaced the bad bytes with the replacement character, so the disk was反而 clean. Only about 20 of 114 compressions triggered the explosion — not every time, but a probability question of whether the truncation point lands in the middle of a Chinese character, fully matching the bug signature of “slicing text by byte budget.”

III. Source-level dissection: two bugs, one cure

Pulling upstream source anonymously (codeload tarball, no API quota, no token touching), the two bugs were pinned to line numbers. The first is the root of stuck episodes: return string(b[:maxBytes]) at internal/agents/ctxpack/restore.go:217 — slicing bytes by token budget, cutting into the middle of a multi-byte Chinese character. The second is what the user himself discovered, the “1M read as 200K”: the factory that rebuilds the writer context engine each chapter (internal/agents/build.go:270) takes the provider from the model object, getting the protocol name openai rather than the config key newapo — litellm v1.8.11’s provider/openai/provider.go:87 hardcodes return "openai", true of any custom relay — so providers["openai"] misses, and the model-level context_window: 1000000 can never hit, falling all the way to DefaultContextWindow = 200000. The startup log takes the other correct path, so within the same process the log says 1M while the engine uses 200K — self-contradictory. When the user challenged “is it my configuration’s problem” or even “does it count as a bug,” three independent facts pushed back: the README explicitly recommends custom relays fill in context_window (a documented promise silently failing on the very people it was designed to serve), the same process’s log and behavior contradicting each other, and no legitimate configuration exists that makes the official path work (the registry is closed, only an OpenRouter baseline, users have no way to add entries). The cure needs no code change: in the window resolution priority chain, the legacy top-level context_window does not depend on the provider name — adding one line at the top level of the book’s .ainovel/config.json makes both paths converge.

IV. The fence and the group cure: one line settles the kingdom

After the fix and restart, four metrics all green: the denominator became /1000000, zero UTF-8 errors after restart, zero compression triggers, and chapter 174 — stuck for over three hours — committed within a minute. The token account was honestly broken down: what increases is the ~100K-token difference in each chapter’s closing rounds; what saves is the disappearance of 49 cache breakpoints, the zeroing of the thirteen-defeat retry loops, and the hidden rework caused by compression losing history — the net effect is saving. The other three books received the group cure: reading their states one by one revealed all were “face 1M, inside 200K” — v0.7.9 (Blunt Knife) had already broken out 14 times with 149 compressions; Novel Three and Novel Four hadn’t exploded yet but had rolled the dice 91 and 60 times. All three were patched with the same one line. As for “will it recur after the next restart,” the answer was decisive: config is re-read from disk on every startup — this is a stable state, not luck; only three situations would recur (deleting that line, opening a new book directory, or switching to a smaller-window model). As for the two bugs’ bodies in the exe — still there, merely fenced by this one line; to eliminate them at the code level requires an upstream fix (issue draft ready, the user decides whether to send).

Real coordinates

Comments

← Digital Garden