Files
nana-story/docs/context-checkpoint-design.md
T

190 lines
8.1 KiB
Markdown

# Current-Branch Context Checkpoints
Status: design frozen for the Wave 7 implementation slice.
## Purpose
Long stories must stay inside the selected LAPP model's real context window without silently
dropping the current input, triggered world-book entries, runtime facts, or recent committed
scenes. A checkpoint is a disposable narrative cache. It is never an authoritative source for
flags, relationships, promises, inventory, knowledge, checks, or branch structure.
## Prerequisites
Checkpoint persistence starts only after these contracts are represented in code:
1. The selected LAPP model's `context_window` and `max_output_tokens` reach the Runtime provider.
2. The stable prompt prefix has a deterministic resource fingerprint.
3. The narrative source path has a canonical ordered hash that covers committed public story
content, not only `RuntimeState`.
4. `Regenerate` excludes the replaced node from narrative history, constrains the replacement with
the target node's committed post-state, creates a sibling on a new branch, and reuses existing
hidden-check outcomes.
## Prompt budget
The provider computes one explicit budget before every model request:
```text
hard input budget =
model context window
- requested output tokens
- system prompt and tool schema
- message framing overhead
- reserved hidden-check continuation overhead
- safety margin
```
- Requested output is `min(model.max_output_tokens ?? 4096, 4096)`.
- A missing context window uses a conservative 16,384-token V1 fallback, but the resulting
`BudgetSource::Assumed` diagnostic must remain visible to the connection/settings layer. The
fallback must not be silent.
- Cross-provider V1 estimation treats every serialized UTF-8 byte as at most one token. This may
compact early but must not optimistically overfill a model window.
- Compression starts at 70% of the hard input budget and compacts back below a lower watermark.
- The complete dynamic tail is budgeted first. If it does not fit by itself, the request fails with
`DynamicTailTooLarge`.
- At least one recent committed node remains verbatim. A single oversized node fails with
`HistoryEntryTooLarge`; beat text is never truncated.
- Every initial and hidden-check continuation `ChatInput` must remain within budget.
## Prompt layout
Prompt schema v3 will replace the current v2 history array with one production checkpoint-aware
encoder:
```text
prompt_schema_version
stable_prefix
branch_context
checkpoint?
raw_tail[]
dynamic_tail
```
`stable_prefix` remains byte-identical while the bound resources do not change. `dynamic_tail`
always contains the current input and the complete safe state projection. A checkpoint replaces
only a continuous oldest prefix of `raw_tail`.
## Checkpoint record
SQLite schema v3 adds a cache table keyed by the immutable host node, not by branch:
```sql
CREATE TABLE context_checkpoints (
story_id TEXT NOT NULL,
at_node_id TEXT NOT NULL,
covered_through_node_id TEXT NOT NULL,
retained_from_node_id TEXT,
checkpoint_schema_version INTEGER NOT NULL,
prompt_schema_version INTEGER NOT NULL,
stable_prefix_hash TEXT NOT NULL,
summary_json TEXT NOT NULL,
source_hash TEXT NOT NULL,
PRIMARY KEY (story_id, at_node_id),
FOREIGN KEY (story_id, at_node_id)
REFERENCES nodes (story_id, node_id) ON DELETE CASCADE,
FOREIGN KEY (story_id, covered_through_node_id)
REFERENCES nodes (story_id, node_id) ON DELETE CASCADE,
FOREIGN KEY (story_id, retained_from_node_id)
REFERENCES nodes (story_id, node_id) ON DELETE CASCADE
);
```
- The nearest checkpoint is found only by walking the current node's `parent_id` chain.
- Shared ancestors naturally share a checkpoint; sibling-only descendants are unreachable.
- The summary is self-contained. A new checkpoint replaces the old summary instead of nesting a
chain of summaries in the prompt.
- Successful checkpoint generation may be persisted independently on the existing host node.
Failure of the later story-generation call may leave this harmless cache in place.
- Deleting every checkpoint must leave nodes, branch heads, materialized state, and `PlayerView`
byte-identical.
## Source and resource hashes
The model never supplies either hash.
`stable_prefix_hash` covers the exact canonical stable-prefix encoding, including character,
Persona, resource provenance, and prompt safety/version fields.
`source_hash` covers:
```text
checkpoint source schema version
prompt schema version
stable_prefix_hash
story id
ordered root-to-covered sequence of:
node id
parent id
player input
public scene
public character visual state
committed beats
```
Unselected suggestions, state delta, exact relationship values, checks, NPC private inventory,
untriggered world-book content, provider responses, credentials, and chain-of-thought never enter
the source manifest or compression request.
## Regenerate boundary
`Regenerate` is a full alternative rendering of one committed player choice:
- the replaced node is not included in the new prompt;
- model history ends at the replaced node's parent, while the target node's authoritative
post-state supplies only the already-committed facts and fixed qualitative check outcomes;
- the replacement creates a sibling on a new runtime-generated branch;
- hidden checks from the original action are supplied as qualitative fixed outcomes and are not
rolled again;
- a model cannot submit forged `RecordCheck` operations;
- old nodes, descendants, and checkpoints remain immutable and become naturally unreachable from
the new branch unless they are shared ancestors.
The sibling keeps the original player action identity, authoritative delta, terminal state, and
exact hidden-check records. Those immutable records intentionally retain their original source-node
provenance; the new branch is an alternative presentation of that same committed action, not a new
roll or a new state transition.
## Summary safety
The summary may retain public causal order, actual player choices, public NPC actions, revealed
facts with their certainty, unresolved conflicts, shared goals, and observable emotional residue.
It may not become a trigger or rules input.
The summary must not contain:
- exact dice mechanics or relationship numbers;
- private NPC knowledge, private inventory, or unrevealed item provenance;
- hidden flags, clocks, event conditions, or untriggered world-book entries;
- unselected suggestions, cancelled output, sibling-branch content, or inferred player thoughts;
- credentials, request headers, raw provider bodies, or chain-of-thought.
The current player inventory projection must not be fed to the summarizer until ownership and
acquisition fields have their own player-knowledge visibility boundary.
## Failure semantics
- No valid checkpoint and history above the high watermark returns `NeedsCompaction`; history is
never silently shortened.
- Invalid schema, source hash, stable-prefix hash, range, or summary size makes a checkpoint
unusable and rebuildable.
- Broken authoritative ancestry or state remains a hard store error and is not downgraded to a
cache miss.
- Cancelled, timed-out, rate-limited, or malformed compression produces no story node and moves no
branch head.
- A first load of a very long legacy branch uses bounded rolling chunks and keeps only the final
self-contained summary.
## Required gates
- Exact window boundary, one-token overflow, unknown-model-limit fallback, and smaller-model switch.
- Every normal and hidden-check request stays within the computed budget.
- 500-node first compaction and incremental compaction from an existing checkpoint.
- v2-to-v3 migration, rollback, restart recovery, cache deletion, and corruption rejection.
- Root, shared-ancestor, pre/post-checkpoint fork, regenerate, and sibling-canary isolation.
- Stable-prefix cache identity and one cache break only when a checkpoint rotates.
- Hidden canaries for checks, exact relationships, NPC facts/items, untriggered resources,
unselected suggestions, credentials, and provider bodies.