Joseph Doherty b667a21c99 chore: document regenerate lifecycle-rollback limitation with warning log (T83.4)
When a regenerate replaces an assistant_turn that already produced
lifecycle transitions (``event_started`` / ``event_completed`` /
``event_cancelled``), those transitions are NOT rolled back before
``detect_event_transitions`` re-runs against the new text. A
regenerate-after-completion can therefore double-emit promotion
artifacts.

Phase 3.5 first cut (per the task plan): documentation + WARNING log
naming the affected event_log ids. The actual undo pass is invasive
(re-projection / inverse-handler dispatch) and is deferred to Phase 4.

Implementation:
- TODO docstring block at the top of ``regenerate_assistant_turn``.
- Module-level ``_log = logging.getLogger(__name__)``.
- Scan immediately after the original assistant_turn row is located:
  joins event_log lifecycle rows to the events table on event_id so we
  can scope by chat (lifecycle payloads carry only ``event_id``, not
  ``chat_id``). Filter ``id > original_assistant_event_id`` as the
  positional linkage to "transitions emitted as part of (or after)
  this turn's processing."

Decision (asked in the brief): the scan uses the ``id > original``
heuristic rather than scanning for explicit references. Lifecycle
event payloads do not carry a back-pointer to the assistant_turn that
triggered them — the linkage is positional in the event log. A tighter
linkage would require either adding a payload field on lifecycle
events (cross-cutting schema change) or threading the just-appended
assistant_turn id into ``detect_event_transitions``'s emit calls
(narrow but still cross-cutting). The positional heuristic is loose
but conservative: a turn that emits no lifecycle events produces no
warning, and the warning's purpose is operator-visible breadcrumbs
not an exact rollback set.

Test: test_regenerate_with_prior_lifecycle_logs_warning seeds a turn
that produced ``event_started`` + ``event_completed`` rows and asserts
the WARNING fires with the expected ids.
2026-04-26 22:18:23 -04:00

chat

A local-first roleplay chat engine that treats fiction as a simulation, not a chat log.

The LLM is a renderer for structured world state — it does not hold the state. State lives in an event-sourced SQLite database and is projected on demand. Models can be swapped freely behind a stateless generate(prompt, params) -> text interface.

Status: design phase. No code yet. See rp-engine-design.md for the full design and CLAUDE.md for the working summary and conventions.

Why

Conventional RP chatbots have three persistent failure modes:

  1. Memory loss — old context drops as history grows.
  2. Quality decay — bots get terse and generic over long conversations.
  3. Stale state pollution — bots fixate on past props (the "picnic basket" problem: bring a basket to one scene, the bot reaches for it forever).

The fix is to model the world as structured state — locations, time, who's present, what they're doing, what they remember, how they feel about each other — and use the LLM only to render that state into prose.

Scope

Deliberately small, so the design can be made to actually work:

  • Single user, single machine.
  • Maximum 3 entities per scene: you + up to 2 bots. The 3-entity cap is load-bearing — it makes the relationship graph fully enumerable (6 directed edges + 1 group node).
  • Chat-only. No voice, no real-time.

Multi-session casts and N-entity scenes are explicit non-goals for v1.

How it works (at a glance)

  • Entities (you, botA, botB) have identity, state (mood/goals/status), an activity record (where they are, what they're doing, what they're holding, where their attention is), and per-POV memory.
  • Containers (car, restaurant booth, room) hold entities in defined slots and provide spatial constraints the model can reason over.
  • Relationship graph: 6 directed edges + 1 group node. Asymmetric feelings are first-class — BotA can secretly resent BotB while BotB thinks they're best friends.
  • Witnessed-by flags: every memory carries a 3-bit [you, botA, botB] mask. A speaker can only retrieve memories their bit is set on. This is what stops bots referencing things they couldn't possibly know.
  • Events have lifecycles (planned → active → completed) and own their own props. When the picnic ends, the basket goes back into the closed event record. Only narrative gist, acquired objects, learned facts, and relationship changes promote to permanent memory.
  • Per-POV scene summaries: every witness gets their own version of a closed scene, written from their angle. Different details, different interpretations. This is what gives bots inner lives.
  • Event sourcing: state is a projection of an append-only event log. Free rewind, branching ("what if BotA had said yes"), surgical delete with impact preview, and survivable schema changes — all fall out for free.

Architecture

┌──────────────────────────────────────────────┐    ┌────────────────────────┐
│ Mac (always-on)                              │    │ Inference endpoint     │
│                                              │    │ (stateless)            │
│  Web UI                                      │    │                        │
│  Orchestrator                                │ →  │  Anthropic API         │
│  Event log + projector  ← SQLite (one file)  │    │  OpenAI / OpenRouter   │
│  Persistence + retrieval + prompt builder    │    │  Local MLX / llama.cpp │
│                                              │    │  Rented GPU            │
└──────────────────────────────────────────────┘    └────────────────────────┘

The Mac side holds everything that survives — state, history, retrieval, orchestration. Inference is a swappable, stateless service. State outlives any one model.

Stack

  • SQLite (single file) for everything structured. WAL mode, foreign keys on, each turn in a transaction.
  • sqlite-vss / sqlite-vec for embedding search in the same DB file (Phase 4).
  • JSON for snapshots, character templates, scene exports.
  • No Postgres. No Redis. No Pinecone. No Docker.

Roadmap

  1. Core loop — schema, entities + edges, single container, event log + projector, single-bot conversation, one LLM backend, streaming UI, manual rollback.
  2. Multi-entity — second bot, group node, scene configurations, witness filtering, per-POV memories, activity/containers, scene transitions with compression.
  3. Events & skips — event queue with triggers, time skips (elision and jump), active threads, significance classifier.
  4. Polish — vector retrieval, branching, surgical delete + regenerate, snapshots, backup automation, impact-preview UI for rewinds.

Each phase must work end-to-end before the next begins.

Repository

License

TBD.

S
Description
No description provided
Readme 3.1 MiB
Languages
Python 94.4%
HTML 4.5%
CSS 1%