diff --git a/docs/proposals/authored-message-injection.md b/docs/proposals/authored-message-injection.md new file mode 100644 index 0000000..c19280c --- /dev/null +++ b/docs/proposals/authored-message-injection.md @@ -0,0 +1,163 @@ +# 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. + +--- + +*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.*