2026-08-04 The ccv3 Chunk SillyTavern Actually Reads
What I set out to do
Answer a small question: does SillyTavern have canned example prompts you can drop into the chatbox, the way the interceptor’s saved-prompts list does on JanitorAI. It does — Quick Replies, with the “Disable send (insert into input field)” toggle and the {{input}} macro for wrapping what you’ve already typed. /impersonate is the other half of the question: it generates as the user persona and writes into #send_textarea rather than committing a message. No ChatGPT-style generated suggestion chips anywhere in core.
What I actually did
Checking that answer against upstream turned into the actual work. The reference clone was four months stale, but the running container is :latest at 1.18.0 built 2026-07-07, which is release tip — the deployment is home-manager’s sillytavern.nix, nothing runs from the checkout. Reading the untracked card-management scripts in that clone is what surfaced the bug: they all write both the chara and ccv3 tEXt chunks, and the interceptor’s scripts write only chara.
That matters because ST’s read returns ccv3 when present and only falls back to chara, while its write emits both (the docstring above write claiming otherwise is stale — the code below it contradicts it). So every edit the repo’s scripts made to an ST-saved card landed in the copy ST never displays. Audit of the live library: 314 of 1,174 cards carry both chunks, 54 had diverged. Fixed the write path to mirror into ccv3 where one exists, made readers follow ST’s precedence, and stamped each chunk’s spec explicitly — an end-to-end probe caught that last one, since readers now hand back the V3-stamped copy and writing it straight back relabelled the V2 chunk as V3.
Underneath sat a second bug with the same fingerprint. buildMergedCard built its result as {...existing.data, ...incoming.data}, so every key the incoming card carried won, including keys it carries only because the schema defaults them. Extension-built cards always write data.tags: [], so each st-merge silently erased the auto-assigned tags — which is exactly why tags survived on 44 cards only in the chunk the merge never touched. It now copies only the fields detectRevision reports as changed.
Then built just st-reconcile to repair the drift. It decides per field, not per card, because 28 of the 54 had fields newer on each side: fill where one chunk has it, revised where one carries the revision history marking it as merged, conflict otherwise. Ran it against a full scratch copy first, then for real: 21 settled by the rules, 27 held for review.
Working through the 27 turned up a hole in the safety net I’d been claiming. The tool recorded a previous_values entry only for fields it took from the other chunk — so whenever the base side won, the losing text was overwritten in both chunks with no copy kept, which is precisely the direction the rules prefer. Diffing the pre-run backup against the live files found exactly one value that had become unrecoverable, and it turned out to differ only by eight carriage returns. Fixed the recorder to log the loser in both directions, restored the 21 from backup, re-ran, and then resolved the remaining 27. Brad and Ryder, the two true conflicts, went the same way as the rule’s default once I looked: chara had the instructions split out into post_history_instructions with real personality and mes_example fields, while ccv3 was an older raw import with jailbreak framing baked into the description and those fields empty. All 314 dual-chunk cards now agree.
What was striking
I told Adam the diverged chunks were “revisions of each other, similarity 0.25 to 1.00” and that was wrong, because I’d measured with difflib.quick_ratio — which compares character multisets and ignores order entirely, so any two English texts score high. Maya.png scored 0.64 on it and 0.02 on the real ratio; the two chunks there are a wholesale “Roleplay Framework” rewrite versus the original persona sheet. The corrected metric (Dice over word bigrams) moved the review pile from an estimated ~10 cards to 27. A convenient-looking number from a function whose semantics I hadn’t checked nearly shipped a rule that would have silently discarded real text on two dozen cards.
The other keeper: neither chunk is uniformly newer. The instinct was “one is stale, pick the fresh one,” and that instinct would have been wrong on more than half of them.
And the recorder hole is the same lesson wearing a different hat. I’d said “nothing is discarded” twice before checking that it was true, and it was only half true — the recovery path existed for the direction that rarely fires and was missing for the direction the rules take by default. Worth checking a safety net in the direction it actually gets used, not the direction that’s easy to test.
Cleaned up after: the reference clone at ~/projects/github.com/SillyTavern is gone (3.7 GB, of which 3.4 GB was an April copy of the data dir). Before deleting it I diffed every subdirectory against the live data — chats, group chats, groups and worlds were fully present, but 36 character cards existed there under no name or description found in the live library, so those were copied across first (1,184 → 1,220 files, all parseable, the five dual-chunk ones internally consistent). What went with it: 1,119 old chat-backup snapshots for chats that all still exist. The card scripts went too, after confirming their output is applied — all 20 Group - *.json lorebooks are in worlds/, and all 19 roster cards carry both post_history_instructions and group_greetings.
Related
- 2026-08-03 Inspector Quick Reply Gets a Menu Mined From Its Own History
- 2026-07-13 SillyTavern Continue Fixed by Turning Prefill Off
docs/sillytavern-integration.md,scripts/lib/chunk-reconcile.ts