# Changelog All notable changes to AI-RPG are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.5.0] — 2026-06-21 Major gameplay release: submit_plan logic, tool retry, production SSE mode, chat history, name bank editor. ### Backend — Critical: submit_plan & tool-call logic - **submit_plan forbidden on first substep**: the terminal tool (submit_plan, submit_step) cannot be called on the first round of Phase 1. The model must do at least some work (call entity_*, env_update, etc.) first. If it tries, a system message is appended: "You called submit_plan without doing any work first." - **submit_plan cancelled if other tools failed**: if the model calls submit_plan in the same response as other tools that FAILED, the submit_plan is cancelled with a tool_result: "submit_plan cancelled because other tools in this response failed. Fix the errors first." The model must retry. - **Configurable tool retry**: new setting `llm.tool_retry_attempts` (default 3). When the model returns no tool calls, the loop retries with a system nudge: "You did not call any tools. You MUST use the available tools. If you tried to call a tool but it didn't work, try again with proper JSON arguments." Up to `tool_retry_attempts` retries before forcing the terminal tool. - **suggest_actions retry**: Phase 3 suggest_actions now retries up to `tool_retry_attempts` times with the same nudge pattern. If all retries fail, defaults to `["Continue exploring", "Talk to someone nearby"]`. - **submit_step retry**: Phase 2 writer now retries up to `tool_retry_attempts + 1` times. ### Backend — Critical: Production SSE mode - **SseEmitter `debug` flag**: when `DEBUG=false` (production), the emitter transforms/filters events: - `tool_call` events → transformed into `status` events with friendly messages (e.g. "Added new entity", "Updated game state", "Updated story progress"). Failed tool calls, RAG queries, calculations, and submit_* calls are filtered out entirely. - `llm_call_start`/`llm_call_end` events → filtered out. - `phase_start` events → friendly names: `planning` (Phase 1), `writing` (Phase 2), `sending` (Phase 3). - All 4 SSE endpoints (builder, editor, iterate, intro streams) now pass `debug=get_settings().debug` to SseEmitter. ### Backend — New: Chat history endpoint - **`GET /api/sessions/worlds/{id}/history?before={seq}&limit=20`** — returns full chat history with pagination. Used by the frontend for scroll-up loading. Returns `{steps: [...], has_more: boolean, oldest_sequence: number | null}`. ### Backend — New: Name bank admin endpoints - `GET /api/admin/names/{language}` → `{language, names: [...], count}` - `PUT /api/admin/names/{language}` body `{names: [...]}` → replaces entire bank - `POST /api/admin/names/{language}/add` body `{name: "..."}` → adds one name - `DELETE /api/admin/names/{language}/{name}` → removes one name ### Backend — New setting - `llm.tool_retry_attempts` (integer, default 3) — number of retries when the LLM returns no tool calls. ### Frontend — 17 files changed, 1 new - **Chat history preservation**: intro_scene ALWAYS shown as first message. New steps APPENDED (not replacing). Scroll-up pagination via `GET /api/sessions/worlds/{id}/history`. "Loading more..." indicator. Scroll position preserved on prepend. - **Removed duplicate suggested actions**: the chips above the input field are gone. Suggested actions only appear under the last GM message. - **Selected action marking**: clicking a suggested action immediately shows it as a player message. The clicked action is highlighted; others are greyed out. Custom input also greys out all suggestions. - **Tool call bubble width fix**: `max-w-full overflow-hidden` on root, `truncate` on tool name, collapsible `
` with `overflow-auto` + `break-all` for args/result. - **Debug-only elements**: "Disconnected" indicator hidden entirely. SseStatus only shows for connecting/error states. Production mode shows friendly phase labels: "Reading...", "Planning...", "Writing...", "Processing...". - **Client-side time formatting**: new `formatGameTime(timeStr, language)` utility. Applied in WorldEditPage, WorldCard, PlayPage (computes `current_time_human` client-side since `GET /api/worlds/{id}` doesn't include it). - **Name bank editor**: new card in SettingsPanel between UI Settings and Text Replacements. Shows en/ru name lists with × remove buttons, add input, and Save button. - **No more large text flash**: WorldBuilder no longer renders intro_scene text before redirect. Redirect delay reduced from 800ms to 150ms. - **Localized builder step messages**: all step labels and messages now use i18n keys. ### Verification - Backend: 68 unit tests pass, 58 routes. - Frontend: `tsc --noEmit` → 0 errors. `npm run build` → success (405 KB JS / 26 KB CSS, ~122 KB gzipped). ## [1.4.0] — 2026-06-21 Major UX release: redirect to edit after world creation, environment panel, admin separate routes, collapsible settings, name bank, page titles. ### Backend — Critical fixes - **KeyError in world_builder_schema prompt**: the prompt contained a literal `{name, type, required, min, max, properties}` brace group that `str.format()` tried to interpret as a format field. Rewrote the prompt to describe field properties in prose. All 11 prompts verified to format correctly. - **propose_changes not applying world.name / world.description**: `apply_diff` only handled `environment.*` paths. Now supports shorthand paths: `name`, `description`, `language`, `player.*`, `current_location`, `plot_rails.*`, `schemas`. Also skips empty `{}` new values (model sometimes returns empty objects). Also syncs `world.plot_rails` column from environment. - **action_source validation error**: `IterateRequest.action_source` was `Literal["custom", "suggested"]` which rejected any other string. Changed to `str` with default "custom" — accepts any value. - **Tool-call retry with nudge**: when the model returns no tool calls, the loop now appends a system message: "You did not call any tools. You MUST use the available tools. If you tried to call a tool but it didn't work, try again with proper JSON arguments." and retries (up to max_substeps). - **Text replacements applied earlier**: now applied to LLM content in the tool loop (not just scene_text), so reasoning/comments are also cleaned before being shown to the user or fed back to the model. - **Language instruction in prompts**: added "Language rule" section to `orchestrator_phase1`, `world_builder_schema`, and other prompts: "All entity names, location names, character names, item names, and descriptions that the PLAYER will see MUST be in `{language}`. Internal reasoning and tool arguments stay in English." - **World description from preset**: when creating a world from a preset, `world.description` is now set from `preset.description` (was using `body.notes` or null). - **Schema `show` field**: `world_builder_schema` prompt now instructs the LLM to include a `show` boolean on each field (default true; if false, the field is hidden from the player UI). ### Backend — New settings & endpoints - **`ui.header_title` setting**: separate title for the navbar header. If empty, falls back to `ui.page_title`. Returned in `GET /api/settings/public` as `header_title`. - **`character_names.en` / `character_names.ru` settings**: name banks for the random name button. Each is a JSON array of ~20 names. Extensible via admin settings. - **`GET /api/names/{language}`** (no auth) — returns `{name: "random name", language, count}`. Used by the world builder form's 🎲 button. ### Frontend — 16 files changed - **Redirect to edit after world creation**: WorldBuilder now redirects to `/worlds/{id}/edit` (not `/play`) after the builder stream completes. The user manually clicks "Generate intro scene" then "Play". - **Play page — intro_scene + suggested actions**: if no recent_steps but `world.intro_scene` exists, it's shown as the first assistant message. `next_actions` shown as clickable buttons. Empty states with links to edit page. - **Environment panel**: shows Player (name, HP progress bar, mana/strength, inventory), Current Location, Plot Rails (hooks, current_goals, completed_goals). "No environment data" empty state. - **Edit world button**: ⚙️ button in PlayPage header → links to `/worlds/{id}/edit`. - **Admin separate routes**: `/admin/stats`, `/admin/logs`, `/admin/users`, `/admin/settings`, `/admin/test`, `/admin/icons`. `/admin` redirects to `/admin/stats`. Page reload preserves the current tab. - **LLM logs auto-refresh**: polls every 5 seconds (first page only, paused when filter inputs are focused). Pause/Resume button. "N new" badge when new logs arrive. - **Collapsible settings cards**: all 7 cards (LLM, Embeddings, Qdrant, Context, Game, UI, Text Replacements) are collapsible, default collapsed. Chevron icon (▶/▼). "Expand all" / "Collapse all" buttons. - **Boolean settings → checkboxes**: replaced dropdowns with `` for all boolean settings. - **Localized setting descriptions**: all ~36 setting descriptions now have translation keys (`admin.setting_desc.{key}`). Also localized the test page hints and the "Each card saves independently..." text. - **Page title**: PlayPage and WorldEditPage set `document.title = "{world.name} | {headerTitle}"`. Navbar uses `header_title` from `/api/settings/public`. - **World builder form simplified**: removed `form_data` JSON. Now has: Setting (textarea), World name (default "New World" / "Новый Мир"), Player name (text + 🎲 random button), Language (select), Notes (textarea). Preset mode hides Setting field, defaults world name to preset name. - **Name bank random button**: 🎲 button next to player name. Calls `GET /api/names/{language}` and fills the field. Disabled while fetching. - **World edit — current_time_human**: shows "Day 1, 08:00" instead of "day_1_hour_8". - **World editor — comment + tool_call display**: `comment` SSE events shown as assistant chat bubbles. `tool_call` events shown as ToolCallBubble components in the chat log. ### Verification - Backend: 68 unit tests pass, 53 routes. - Frontend: `tsc --noEmit` → 0 errors. `npm run build` → success (393 KB JS / 24 KB CSS, ~119 KB gzipped). ## [1.3.0] — 2026-06-21 Major release: world builder rewritten to use tools (instead of JSON), resumable builder flow, admin recovery, LLM model list, text replacements, human-readable time. ### Backend — Critical: World Builder rewritten to use tools - **`world_builder_schema` stage**: previously asked the LLM to output a JSON object with schemas, which frequently failed validation. Now the LLM calls `schema_add_type` tool for each entity type. Added `world_builder_schema` and `world_builder_env` to the `stages` set of all relevant tools (schema_add_type, env_update, entity_create, submit_plan, etc.). - **`world_builder_env` stage**: previously asked for JSON. Now the LLM calls `env_update` tool to set player, current_location, and plot_rails. - **Resumable builder**: each stage checks if the world already has the needed data and skips if so. If schemas exist → skip schema generation. If environment has current_location → skip env generation. If entities exist → skip entity generation. If intro_scene exists → skip intro generation. This allows re-running the builder after a failure at any stage without redoing earlier stages. - **Validation warnings instead of failures**: if `validate_world` finds issues after env generation, the builder emits a `warning` SSE event but continues (instead of failing). The world may still be usable. - **Better tool-loop prompt**: the user message now says "Use the available tools to accomplish the task. When done, call {terminal_tool}." to encourage tool use. - **Updated prompts**: `world_builder_schema` and `world_builder_env` prompts now describe the tools to use and give examples of tool arguments. ### Backend — Critical: World Editor fixes - **No extra LLM call after propose_changes**: after the user accepts or rejects proposed changes, the editor loop now breaks immediately. Previously it made another LLM call (which returned empty text), wasting API requests. - **`'str' object has no attribute 'get'` fix**: already in v1.2.0, but now also handles cases where `function` is not a dict. ### Backend — New: Admin recovery - **`POST /api/admin/recover`** (NO AUTH required) — creates a new admin user using the `admin.setup_token` (printed on every backend startup). Body: `{token, email, username, password}`. For disaster recovery when all existing admins lost access. Returns 403 `invalid_admin_token` if the token doesn't match. ### Backend — New: LLM model list - **`POST /api/admin/llm/models`** (admin) — fetches the list of available models from an OpenAI-compatible API (`GET {api_url}/models`). Returns `{ok: true, models: [...], count: N}` or `{ok: false, error: {...}, models: []}`. Uses the same `_resolve` helper as test endpoints (ignores masked api_key values). ### Backend — New: Text replacements - **New setting `llm.text_replacements`**: a JSON array of `{from: string, to: string}` pairs. Applied to all LLM scene_text output (both orchestrator Phase 2 and intro_scene). Use empty `to` to remove a word/phrase entirely. - **`apply_text_replacements(session, text)`** helper in `settings_service.py`. - Applied in `game_master.py` (Phase 2 writer) and `world_builder.py` (intro scene). ### Backend — New: Human-readable time - **`format_time_human(time_str, language)`** in `time_utils.py` — converts `"day_1_hour_8"` → `"Day 1, 08:00"` (en) or `"День 1, 08:00"` (ru). Supports years, days, hours, minutes. - **`GET /api/worlds`** now returns `current_time_human` alongside `current_time`. - **`GET /api/sessions/worlds/{id}/state`** now returns `current_time_human` and `status` in the world object. ### Backend — New: world_id in LLM logs - **`GET /api/admin/llm-logs`** now includes `world_id` (string UUID or null) on each log item. Useful for the admin UI to show which world a log belongs to, even when not filtering by world_id. ### Backend — Route fix - **404 on generate-intro**: the frontend was calling `/api/worlds/{id}/generate-intro` but the route is at `/api/sessions/worlds/{id}/generate-intro`. Fixed the frontend API helper to use the correct path. ### Frontend — 14 files changed, 1 new - **Admin recovery page**: new `/recover` route (public, no auth). Form with token/email/username/password. Link from LoginPage: "Lost admin access? Recover here". - **LLM model list dropdown**: "Fetch models" button next to the model input in SettingsPanel. Fetches from `POST /api/admin/llm/models`. Shows a `` with `offline_hash`/`openai`; boolean settings → `` with all 16 stage values; LLM log status filter → `` with `min`/`max` attributes. String-to-number conversion on save. - **Test page pre-fills**: diagnostic forms now pre-fill from current settings (`llm.api_url`, `llm.api_key`, `llm.model`, `embeddings.api_url`, `embeddings.model`, `embeddings.provider`). - **Embeddings test provider**: defaults to current `embeddings.provider` setting (was hardcoded to `openai`). Added hint: "If provider=openai and api_url is empty, falls back to llm.api_url". - **Embeddings URL fallback hints**: added muted hint text next to `embeddings.api_url`, `embeddings.api_key`, `embeddings.model` fields. - **UI settings applied**: new `uiSettingsStore` fetches `GET /api/settings/public` on app load. Sets `document.title`, favicon ``, and navbar logo. Re-fetches after admin settings save. - **Light theme fix**: rewrote CSS with CSS-variable-based theming. Light mode now uses `#fafafa`/`#1a1a1a`/`#ffffff` with high-contrast text. Dark mode unchanged. - **Single Create button**: removed redundant "Create World" from navbar. Only one Create button remains (top of worlds list page). - **Username validation**: client-side regex check + 422 error parsing with field-level inline messages. Specific toast: "Username can only contain letters, numbers, and underscores". - **SSE error handling**: `sessionStore` now shows toast on SSE `error` events. One-shot "Connection lost. Retrying…" toast on unexpected stream closure. - **Settings panel UX**: 6 grouped cards (LLM / Embeddings / Qdrant / Context / Game / UI), each with its own Save button. No page reload or full refetch after save — just a success toast. - **Admin stats panel**: now shows 6 cards: Users, Active Worlds, Archived, Total Worlds, Steps, Avg LLM Latency. ### Verification - Backend: 68 unit tests pass. - Frontend: `tsc --noEmit` → 0 errors. `npm run build` → success (354 KB JS / 23 KB CSS, ~109 KB gzipped). ## [1.0.5] — 2026-06-20 ### Fixed - **app/main.py**: added `create_all_tables(engine)` call at the start of `lifespan`. Without this, the backend started but crashed on every DB query with `relation "settings" does not exist` because tables were never created in PostgreSQL. Tables are now created idempotently on every startup via `Base.metadata.create_all` ( SQLAlchemy skips tables that already exist). - **app/main.py**: also added `seed_builtin_presets(session)` call so the 2 builtin presets (Classic Fantasy, Deep Space Outpost) are seeded on first startup, not just settings. - **app/migrations/versions/**: renamed `001_initial_schema.py` → `_001_initial_schema.py`. Python module names cannot start with a digit, so `from app.migrations.versions.001_initial_schema import create_all_tables` was a SyntaxError. The leading underscore is a conventional marker for "internal" modules. - **README.md**: updated references to the renamed migration file. ### Why this happened The lifespan handler was supposed to run DB migrations as its first step, but I forgot to wire it up. The `seed_default_settings()` call immediately tried to `SELECT FROM settings` against a fresh PostgreSQL database with no tables. SQLAlchemy 2.x's `create_all` is idempotent (skips existing tables), so this is safe to call on every startup — equivalent to `alembic upgrade head` for our single-migration MVP. ## [1.0.4] — 2026-06-20 ### Fixed (proper fix for CORS env parsing) - **app/config.py**: replaced the broken `Annotated[list[str], NoDecode]` approach with a simpler, more robust one: - `cors_origins` is now declared as a plain `str` (comma-separated, e.g. `"http://a,http://b"` or `"*"`). - Added a `cors_origins_list` property that splits the string into a `list[str]` on demand. - This sidesteps the entire `EnvSettingsSource.decode_complex_value` / JSON-parsing codepath — pydantic-settings sees a `str` field, takes the env value as-is, no JSON parsing attempted. - **app/main.py**: updated `CORSMiddleware(allow_origins=cfg.cors_origins_list)` to use the new property. - **Why v1.0.3 didn't work**: in pydantic-settings 2.7.0, `NoDecode` is importable but `_annotation_is_complex()` doesn't actually check for it (only checks for `Json`). The marker was added to the package surface but the inner logic was only wired up in a later version. Switching to a plain `str` field is the most reliable fix and works across all pydantic-settings 2.x versions. - All 68 unit tests still pass. ## [1.0.3] — 2026-06-20 ### Fixed - **app/config.py**: fixed `SettingsError: error parsing value for field "cors_origins" from source "EnvSettingsSource"` that crashed the backend on startup when `CORS_ORIGINS` was set as a comma-separated string (e.g. `http://localhost:8080,http://localhost:5173,http://localhost`). - Root cause: pydantic-settings v2 by default tries to JSON-parse complex-typed env vars before applying field validators. The comma-separated string isn't valid JSON, so parsing failed before our `@field_validator(mode="before")` could split it. - Fix: declared `cors_origins: Annotated[list[str], NoDecode]` — `NoDecode` is a pydantic-settings marker that disables JSON pre-parsing, so the raw string reaches our `_split_cors` validator unchanged. - **requirements.txt**: bumped `pydantic` 2.7.1 → 2.9.2 and `pydantic-settings` 2.2.1 → 2.7.0. The `NoDecode` annotation was only introduced in pydantic-settings 2.6+, so the older versions couldn't support the fix above. All 68 unit tests still pass with the new versions. ## [1.0.2] — 2026-06-20 ### Fixed - **requirements.txt**: added missing `email-validator==2.2.0` dependency. Pydantic's `EmailStr` type (used in `RegisterRequest`, `AdminRegisterRequest`, `LoginRequest.user.email`, `UserPublic.email`, `TokenResponse.user.email`) requires this package at runtime, but it's not bundled with pydantic itself. Without it the backend crashed at startup with `ImportError: email-validator is not installed, run pip install pydantic[email]`. Also removed a duplicate `httpx==0.27.0` line. ## [1.0.1] — 2026-06-20 ### Fixed - **docker-compose.yml**: removed obsolete `version: "3.9"` (caused warning in modern Docker Compose). - **docker-compose.yml**: fixed frontend build context — was `./frontend` (broke `COPY deploy/nginx.conf` and `COPY frontend/package*.json` in Dockerfile.frontend). Now correctly `.` (project root) with `dockerfile: deploy/Dockerfile.frontend`. - **docker-compose.yml**: fixed frontend port mapping — was `5173:5173` but the frontend container is nginx on port 80. Now `8080:80` so the app is accessible at `http://localhost:8080`. - **docker-compose.yml**: added `extra_hosts: ["host.docker.internal:host-gateway"]` to the backend service — enables `LLM_API_URL=http://host.docker.internal:11434/v1` to work on Linux hosts (not just Docker Desktop on Mac/Windows). - **docker-compose.yml**: added `VITE_API_BASE_URL: /api` build arg for the frontend service — bakes the relative `/api` URL into the Vite bundle so the browser uses the same origin + nginx proxies `/api` → `backend:8000`. - **deploy/Dockerfile.frontend**: added `ARG VITE_API_BASE_URL=/api` + `ENV` so the build arg actually gets baked into the Vite bundle. - **deploy/nginx.conf**: extended SSE timeouts from 300s → 600s; added `proxy_send_timeout`; added `Upgrade`/`Connection` headers for future WebSocket support; added gzip for static assets. - **.env.example**: clarified `VITE_API_BASE_URL` — only used by local `npm run dev`, ignored by docker build (which uses `/api` relative). Removed the misleading `http://localhost/api` default. - **frontend/src/lib/api.ts**: now respects `VITE_API_BASE_URL` env var with fallback to relative `/api`. Works both for local dev (point at separate backend) and Docker (nginx proxy). - **frontend/src/vite-env.d.ts**: added Vite env type declarations so TypeScript knows about `import.meta.env.VITE_API_BASE_URL`. - **app/api/deps.py**: added `?access_token=` query parameter fallback for SSE endpoints. Native `EventSource` cannot send `Authorization` headers, so the frontend SSE client passes the token via query string. Without this fix, all SSE endpoints (`/iterate/stream`, `/builder/stream`, `/editor/stream`) returned 401. - **.dockerignore**: added at project root — excludes `node_modules/`, `__pycache__/`, `.venv/`, `data/`, `.git/`, etc. from Docker build contexts (faster builds, smaller context transfer). ## [1.0.0] — 2026-06-20 ### Added — Sprint 1: Foundation - docker-compose with 4 services: db (PostgreSQL 15), qdrant (1.9), backend (FastAPI/uvicorn), frontend (Vite dev / nginx prod). - SQLAlchemy 2.x async models for all 10 tables: `users`, `settings`, `world_presets`, `worlds`, `entities`, `steps`, `step_tool_calls`, `deferred_triggers`, `story_entries`, `llm_call_logs`. - Alembic-equivalent idempotent migration `001_initial_schema.py` (creates all tables, no pgvector). - `init_qdrant.py` creates collections `entities` and `story_entries` with payload indexes on `world_id`, `entity_type`, `entry_type`, `deleted`, `created_at`. - Settings seed: 33 default keys (llm.*, embeddings.*, context.*, qdrant.*, game.*, ui.*, admin.setup_token). - 2 builtin presets: Classic Fantasy, Deep Space Outpost. - `GET /api/health` returns `{status, db, qdrant, llm, embeddings, version}`. ### Added — Sprint 2: Auth + Admin - `POST /api/register` (only if at least one admin exists), `POST /api/register/admin?token=`, `POST /api/auth/login` (email or username), `POST /api/auth/refresh`, `POST /api/auth/logout`, `GET /api/auth/me`. - JWT (access + refresh) with python-jose, password hashing with passlib[bcrypt]. - Password strength validation: ≥8 chars, ≥1 letter, ≥1 digit, blacklist of trivial passwords. - Anti-enumeration: same `401 invalid_credentials` for "user not found" and "wrong password". - Admin setup token: auto-generated on first startup, printed in logs. - `GET/PATCH /api/admin/settings` with secret masking. - `GET /api/admin/llm-logs` with filters (world_id, stage, status, pagination) + detail view. - `GET /api/admin/users`, `PATCH /api/admin/users/{id}` (admin/active toggle). - `GET /api/admin/stats` (users, worlds, steps, avg LLM latency). - Diagnostic endpoints: `POST /api/admin/test/llm`, `/test/llm-tools`, `/test/embeddings`, `/test/embeddings/probe-dimension`, `/embeddings/recreate-collections`. - `POST /api/admin/upload-icon` (multipart, favicon/logo/og_image, ≤1MB, PNG/SVG/JPG/WebP/ICO). ### Added — Sprint 3: World Builder - `LlmClient` — OpenAI-compatible chat completions client with retry (3 attempts, exp backoff), streaming, tool-calling, separate-transaction logging. - `MockLlmClient` — replay-based mock for tests/dev when no LLM configured. - `app/prompts/` — 11 stage prompts in English (world_builder_schema/env/entities, world_editor, orchestrator_phase1/2/3_summary/3_suggest, intro_scene, subagent, summary). - `POST /api/worlds` creates draft world + returns SSE URL. - `GET /api/sessions/worlds/{id}/builder/stream` runs the 4-step builder flow: schemas → environment → entities (tool-calling loop) → intro scene (with submit_step + suggest_actions). - If preset_id provided, schemas/environment copied from preset (skip first LLM call). ### Added — Sprint 4: World Editor - `POST /api/worlds/{id}/edit` body `{instruction}` → SSE URL. - `GET /api/sessions/worlds/{id}/editor/stream?instruction=` runs LLM with world_editor tools. - `propose_changes` tool returns diff, `ask_user` emits clarification event, `comment_to_user` for chat. - Optimistic locking via `updated_at` on `PATCH /api/worlds/{id}` (409 state_conflict on mismatch). - `PATCH /api/worlds/{id}` for direct JSON edits. - `DELETE /api/worlds/{id}` soft-delete (status='archived'). ### Added — Sprint 5: Orchestrator - `POST /api/sessions/worlds/{id}/iterate` body `{action, action_source}` creates new step + returns SSE URL. - `GET /api/sessions/worlds/{id}/iterate/stream?step_id=` runs the 3-phase orchestrator: - **Phase 1**: tool-calling loop with `submit_plan` terminator, max 8 substeps (configurable). - **Phase 2**: writer LLM call with `submit_step`, streams `scene_chunk` events. - **Phase 3**: persist + deferred triggers + summary (if history > threshold) + suggest_actions. - `POST /api/sessions/worlds/{id}/retry` soft-deletes last step + creates new one with same action. - `POST /api/sessions/worlds/{id}/rollback` soft-deletes last step. - 17 game tools: entity_create/get/list/update/delete, env_update/get, update_plot_rails, advance_time, schedule_trigger, calc (with dice), random_choice (deterministic), rag_query/add, run_subagent, submit_plan, submit_step, suggest_actions. - 4 schema tools: schema_add_type/add_field/remove_field/modify_field. ### Added — Sprint 6: Frontend polish - React 18 + Vite 5 + TypeScript 5 + Tailwind CSS 3 + zustand 4 + react-i18next 14. - Dark theme by default (Tailwind `dark:` class on ``). - 8 pages: Login, Register, AdminRegister, WorldsList, WorldBuilder, WorldEdit, Play, Admin. - 23 components: 9 UI primitives, 5 session, 3 worlds, 6 admin. - 5 zustand stores: auth, ui, worlds, session, toast. - i18n bundles in English + Russian (complete). - Native EventSource SSE client with reconnect + Last-Event-ID. - Mobile-first responsive layout. - TypeScript strict mode, 0 type errors, `npm run build` succeeds (340 KB JS / 22 KB CSS). ### Added — Sprint 7: RAG + Triggers + Summary + Context - `app/core/rag.py` — two-stage retrieval: Qdrant vector search → PostgreSQL hydration. - `HashEmbedder` (offline, deterministic) and `OpenAIEmbedder` (OpenAI-compatible API) implementations. - Embedder provider selection via `embeddings.provider` setting (`offline_hash` | `openai`). - Auto-fallback: `embeddings.api_url` falls back to `llm.api_url` if empty (and same for api_key). - Embedder cache with `reset_embedder_cache()` (called on settings update). - `rag_query` returns empty list on embedder/Qdrant failure (non-blocking). - `rag_add` saves story entry with `embedding_status='pending'` if embedding fails (background indexer can retry). - `index_entity` helper for entity vectors. - World isolation via Qdrant payload filter `world_id`. - `_cleanup_qdrant(world_id)` deletes all points for a world on world delete (best-effort). - Deferred triggers: Phase 3.1 fires triggers where `fire_at <= current_time`, appends summary to scene_text, marks `is_fired=true`. - Summary: Phase 3.2 generates summary when `len(recent_steps) > compression_threshold_messages`, stores as StoryEntry with `metadata.type=summary`. - Context manager: builds messages with system prompt + optional summary + last N guaranteed messages + current action. ### Added — Sprint 8: Production - `README.md` with quickstart, architecture diagram, API overview, testing instructions. - `pytest.ini` + 65+ unit tests across 8 test files. - `CHANGELOG.md` (this file). - `docker-compose.yml` with healthchecks for db and qdrant. - `deploy/Dockerfile.backend`, `deploy/Dockerfile.frontend`, `deploy/nginx.conf`. - `.env.example` with all 24 env vars documented. - `.gitignore` for Python, Node, env, IDE, data dirs. - Frontend production build verified (`npm run build` produces `dist/`).