Worldtree accepted the v1 wire validation green; contract rev 1.1 folds all three consumer flags. Flag #1 surfaced a real provider constraint: first-message makes the assistant seq 0 -> vLLM/openai_compat tolerate it (sindra unaffected), but Anthropic-family providers 400 on an assistant-first array. Consumer must gate first-message on provider compat; provider-agnostic normalization deferred.
9.4 KiB
Proposal: Authored History Write (non-generating ledger seed)
From: ratatoskr-dev (reference Tier-3 consumer) To: worldtree-dev (Conversation API / engine owner) Status: Draft for scoping — pre-contract (heid-panel-reviewed 2026-07-05) Date: 2026-07-05
Motivation
Consumer apps need to write a turn into a session's history as the agent (or another author) without triggering a model generation — e.g. an authored opening/greeting, imported history, scripted narration. Ratatoskr's immediate driver is a SillyTavern-style first-message: a fixed authored opening that replaces the model-generated greeting and sets tone/tense/style by example.
This cannot be done client-side. Worldtree assembles context server-side,
and the current API exposes no author-role write path: POST /sessions/{id}/messages's role is a model-role override (role: "assistant" → 404 "Unknown model role"), and assistant as an author-role
exists only as a read-side /search filter. So a model-visible authored turn
needs engine support.
The primitive (recentered)
The fundamental operation is write a turn into the session ledger WITHOUT generation. "Author" (who wrote it) is an attribute of that write, not the defining axis — so we name the operation, not the attribute:
Authored history write — persist a model-visible turn into a session's ledger: no generation, no lived-turn side-effects by default, provenance always set.
The design space is two independent axes; this primitive is one cell:
| side-effects ON | side-effects OFF | |
|---|---|---|
| generation ON | POST /messages (today) |
— |
| generation OFF | (future: affect replay) | authored history write |
First-message = one caller: author=assistant, at session-create, effects=none.
v1 use cases (narrowed)
- First-message / greeting (the driver).
- Append-only narrator / scripted / scene turns.
- Debug / test state injection (ratatoskr instrumentation).
Explicitly OUT of v1 — separate future primitives (share infra, not shape)
- History import (batch) — atomic multi-turn seed with memory/trust policy + idempotency. A batch API, not a single POST.
- Edit / regenerate — history mutation (replace / supersede / tombstone / audit), not injection.
- Few-shot priming — likely context-assembly config (exemplar block), not fake ledger history.
- Arbitrary mid-history insertion — a "rewrite-history" capability with explicit invalidation semantics.
- Prefill / assistant-continuation (
author+ generate) and authored tool-result turns — noted; outside the seed-only contract.
Design decisions
1. Side-effects — DEFAULT OFF; bounded opt-in [operator-locked default; opt-in surface tightened by review]
Authored writes are inert by default: no affect appraisal (no PAD update), no memory write, no Bifrost/tool emission. Opt-in is a bounded enum, not loose booleans:
effects: "none" (default) | "memory_import"
Synthetic affect and Bifrost emission are deliberately not opt-in-able here — replaying affect for authored content is a separate primitive (the generation-OFF / side-effects-ON cell). Rationale: keep this one write-API from becoming a cross-subsystem mutation backdoor. Load-bearing for affect/memory consumers — ratatoskr instruments exactly these signals.
2. Author-role — distinct field, restricted set [rec]
- New field
author, distinct from the model-rolerole(the collision that 404s). - v1 roles:
assistant(agent) +system(OOC / narrator).useris NOT injectable on this endpoint — model-visible spoofed user input is a consent / audit / abuse surface; deferred to the future import API under owner/service scope. - Nuance for the engine owner:
authorrisks doing double duty — provenance ("who wrote it") vs rendering-role ("how it appears in assembled context"; anassistantturn renders as model output, asystemturn as instruction). These likely want to be separable (a rendering/turn-class vs anauthored_byprovenance). Final shape is engine-owned (context assembly is yours) — but the concern is ours to raise, not punt.
3. Generation contract — seed-only, DISTINCT SUB-RESOURCE [position taken]
Authored writes never trigger generation. We take a position (not defer): a
distinct sub-resource, e.g. POST /sessions/{id}/history, not a
generate:false flag on POST /messages. Reasons: explicit-over-implicit
(don't make "did generation happen?" a parameter — the same implicit-mode
coupling that bit us with role); different response contract (no generation
id, no SSE stream, no token usage); different error surface. Exact path is yours.
4. Provenance — structured, always present [rec, expanded]
Not a boolean. Every authored turn carries: the write actor (which consumer/caller injected it), the claimed author, injected-at vs claimed-original timestamps, trust/origin, and visibility flags (model-visible? user-visible? memory-eligible?). Available to admin/audit APIs even when not rendered to the model.
5. Positioning — append-only + create-time (v1) [revised: was arbitrary insertion]
v1 supports create-time seed and append-to-tail only. Arbitrary mid-history insertion is deferred: it breaks turn-numbering, stales existing embeddings, desyncs the affect timeline, and races in-flight generation — a separate future "rewrite-history" capability with explicit invalidation semantics.
Event / lifecycle contract — positions we take (consumer contracts we validate)
- Default-off authored seed emits NO
turn.started/doneand NO Bifrost appraisal wire. Stated explicitly so instrumented consumers (us) don't read silence as failure. - Authored turns get a distinct lifecycle phase — propose
seeded(orauthored), NOTcompleted(which implies generation ran). Consumers filter/display by phase. - Idempotency keys required on authored writes (retries must not duplicate turns).
- In-progress generation — authored writes are rejected or serialized while a session has an active generation (ordering safety).
Inherent property (documented, not a bug)
Indirect affect contamination. Even with effects:none, the next generated
turn is appraised in the context of the authored turn — so an emotionally
charged authored beat perturbs affect regardless of any flag. No flag prevents
it; it is inherent. Consumers (ratatoskr especially, as the affect instrument)
must not misattribute the resulting drift.
Genuinely engine-owned open questions
- Exact endpoint path + field / enum names.
- Model-visible provenance in assembled context — an engine-consistency call
and a security one: an authored
system/userturn indistinguishable from real input is a spoofing vector. Framed as security, not just rendering. memory_importsemantics when the future import API opts in (embedding, origin/trust tagging, retrieval ranking vs lived memory).- Auth/scope: we assume owner-only for v1; per-author-role restrictions
(esp.
system) TBD — confirm or correct.
Ratatoskr as reference consumer
First consumer: first-message (author=assistant, create-time, effects:none)
in the web surface + debug seed in the CLI. We commit to validating the
primitive — including the event-silence contract and the seeded phase —
end-to-end against the reference planes.
Consumer integration constraint (engine-imposed — Worldtree #347)
The primitive is Heimdall-gated with hide-existence (a per-tenant policy decision — some tenants are never granted it, not a rollout stage). Ratatoskr's consumer side MUST tolerate per-tenant absence:
- A granted tenant gets the sub-resource; an ungranted tenant sees
404(not403) — as if the feature never existed. - Treat
404on the authored-history-write sub-resource as "feature absent for this tenant" → fall back gracefully (no authored first-message; the model-generated greeting), never surface it as an error or "denied." - Do NOT capability-probe or advertise-detect — the feature is deliberately
undiscoverable in
/capabilitiesfor ungranted tenants (same hide-existence posture as the R27-V1A cross-owner pattern).
Provider constraint (first-message specifically). A create-time first-message
makes the assistant turn seq 0. Assistant-first-tolerant providers (vLLM /
openai_compat — what our Tier-3 characters, incl. sindra, run) accept it out of
the box. Anthropic-family providers reject an assistant-first array ("first
message must use the user role") → the next generation 400s. So the consumer
must gate first-message on provider compatibility (or treat it as
vLLM/openai_compat-only for v1). Sindra = openai_compat → unaffected;
provider-agnostic normalization is a deferred engine follow-up.
This brief was cold-read-pressure-tested by a cross-frontier panel (Grok /
Codex / GLM) before handoff; the v1 narrowing (append-only, bounded effects
enum, edit/regenerate + import split out) and the positions-taken (sub-resource,
event-silence, seeded phase, structured provenance, user-author restriction)
are the triaged result.