# 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)

1. **First-message / greeting** (the driver).
2. **Append-only narrator / scripted / scene turns.**
3. **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-role `role` (the collision
  that 404s).
- v1 roles: **`assistant`** (agent) + **`system`** (OOC / narrator). **`user` is
  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: `author` risks doing double duty — *provenance*
  ("who wrote it") vs *rendering-role* ("how it appears in assembled context";
  an `assistant` turn renders as model output, a `system` turn as instruction).
  These likely want to be separable (a rendering/turn-class vs an `authored_by`
  provenance). 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` / `done` and 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`** (or
  `authored`), NOT `completed` (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` / `user` turn indistinguishable
  from real input is a spoofing vector. Framed as security, not just rendering.
- `memory_import` semantics 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` (not
  `403`)** — as if the feature never existed.
- Treat `404` on 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 `/capabilities` for 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 `400`s. 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.*
