7156b25957
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.
188 lines
9.4 KiB
Markdown
188 lines
9.4 KiB
Markdown
# 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.*
|