Docs / Developer / Escrow and round-trip
Escrow and round-trip
Every canonical entity carries a snapshot of its parsed source and any binary carrier. The architecture
calls this concept escrow; the wrapper field that holds it is named original (canonical.ts:80).
That carry supports two guarantees: same-format field fidelity, and a cross-format
conversion loses only what will not cross, on purpose. This page explains the field, the round-trip
mechanism, and why cross-format containment is a safety guarantee rather than a gap. For where escrow sits
in the whole engine, see ../architecture.md.
The escrow envelope
Escrow is a per-source record on the canonical wrapper, Record<FormatId, OriginalEntry>, keyed by the id
of the format it came from (canonical.ts:39-51):
interface OriginalEntry {
raw: unknown; // the parsed source payload
unmapped?: Record<string, unknown>; // fields canonical does not (yet) express
sourceMedia?: { b64: string; mime: string }; // the binary carrier the payload arrived inside
}
type Original = Record<FormatId, OriginalEntry>; // one entry per source format id
rawis the entire parsed source payload, stored untouched. It is the twin the exporter re-projects onto;
this is parsed data, not a promise to retain the source file's JSON whitespace or original bytes.unmappedholds source facts the canonical model does not first-class as its own field, for example the
round-trip variant a Tavern card was read as (v1/flat/v2/v3).sourceMediais the binary carrier the payload arrived inside, not content the canonical body
expresses. A PNG character card's pixels are authored art, so the carrier is kept as a raw-bytes twin
(canonical.ts:44-47).
The original field is filled automatically on import and is never hand-edited (canonical.ts:50). Each
adapter stores its twin under its own format id, so a SillyTavern card's twin lives atoriginal.sillytavern and an adapter only ever reads its own key. primaryOriginalRaw(original) is the one
owner for "which entry is the source", reading the first entry's raw so the labeler and the bundle layer
cannot drift on it (canonical.ts:58-59).
A field earns a first-class canonical slot only if some real wire format serializes it; anything no format
produces does not become a canonical field and at most rides in escrow (architecture.md, "The superset
rule"). Escrow is where that residue lives.
Format-specific extras bags also preserve unmodeled JSON keys as own data properties, including keys
such as __proto__ and constructor; their names do not change their values or the parsed raw twin.
Same-format round-trips preserve fields
The adapter contract is symmetric around escrow: toCanonical stashes the original payload while it reads,
and fromCanonical re-emits from that original for the round-trip (adapter.ts:65-68). SillyTavern's
character codec is the worked example: on import it stores the whole parsed card as the twin, plus the
variant and any PNG carrier (index.ts:105-108); on export it overlays the canonical body onto a clone of
the twin and re-wraps it in the original variant (index.ts:112-148).
The overlay is three-state against the twin's own decode (applyBodyToData, tavern-fields.ts:146-156):
- a canonical value equal to the twin leaves its cloned wire value untouched;
- a changed value is written at its exact wire home;
- a value the twin had but canonical no longer carries is cleared at that same home.
When both the decoded twin and canonical are absent, raw is left alone, which protects values the reader
deliberately cannot represent (for example RoleCall's object-form source) and any unknown keys
(tavern-fields.ts:150-156).
This is field-level fidelity: every raw field the adapter does not edit survives from the twin, while
edited fields are re-encoded (architecture.md, "Escrow: lossless round-trips, contained cross-format
loss"). It does not promise source-file byte identity. Character JSON is pretty-printed on export
(index.ts), and a PNG export rewrites its card text chunks around the retained image; byte identity applies
only where a format specification and fixture explicitly establish that tier. A PNG's pixels are carried
as sourceMedia, served as the entity portrait, and are already available to the PNG export path
(canonical.ts:44-47).
A cross-format conversion is contained loss by design
convertFile narrows on kind and, for a same-kind conversion, passes the target adapter the full entity
(convert.ts:96-115), but only the canonical body plus any re-embedded lorebooks get projected into the
target file. The mechanism is the per-id keying: the entity still carries the source twin underoriginal.<source>, and that twin rides along in memory, but the target adapter reads onlyoriginal.<target>, finds no twin there for a fresh conversion, and full-encodes from the body alone. One
app's private junk (its extensions block, trigger scripts, bespoke layout, internal ids), the raw twin,unmapped, and the carrier all stay in escrow and are deliberately not written into another app's file.
This is a safety posture, not a limitation. bunzai never blind-copies one app's fields or executable payloads
into another; it denies by absence (architecture.md, "Escrow: lossless round-trips, contained cross-format
loss"). The future refinement is opt-in, per-field extension mappers, never a blind copy. Because the
canonical body is a superset of what real formats actually serialize, what does cross a conversion is the
portable core, and the loss is bounded to each app's non-portable extras.
The carrier follows the same rule: on a cross-format write the PNG pixels stay in escrow and are not
smuggled into the target file, but they remain the entity's portrait and are available to a later PNG export
(canonical.ts:44-47).
Per-kind coverage of the same idea
Every entity kind carries escrow the same way, and each format's own page documents where its round-trip is
lossless and where a cross-format write collapses. The SillyTavern page details the character and lorebook
twins, the injection-slot collapse on a cross-format lorebook write, and the persona and regex codecs that
seal a whole multi-record file in escrow so every untouched record re-emits unchanged
(../formats/sillytavern.md, "Escrow and round-trip"). For what each canonical
slot means, see ../entities/character.md and
../entities/lorebook.md.
Source of truth
| Concern | File |
|---|---|
Escrow envelope (original, OriginalEntry, primaryOriginalRaw) |
src/core/canonical.ts |
Adapter round-trip contract (toCanonical stash, fromCanonical re-emit) |
src/core/adapter.ts |
| Cross-format conversion (only the body crosses) | src/convert.ts |
| Three-state overlay onto the twin (worked example) | src/formats/_shared/tavern-fields.ts |
| A codec that stores and re-emits its twin | src/formats/sillytavern/index.ts |
| Escrow in the whole-engine map | docs/reference/architecture.md |