# 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.2.0] — 2026-06-21
This release fixes critical bugs that prevented world creation, world editing, and LLM tool-calling with local models (gemma, qwen, etc.).
### Backend — Critical fixes
- **Prompt templates**: fixed `KeyError: 'scene_text, delta_time'` that crashed world_builder during intro scene generation. The prompts contained literal `{scene_text, delta_time}` braces which `str.format()` interpreted as format fields. Rewrote `orchestrator_phase2` and `intro_scene` prompts to describe the tool arguments in prose instead of using brace notation. All 11 prompt templates now format correctly (verified with a test).
- **LLM tool-call parsing for local models**: many local models (gemma4, qwen, etc.) don't use the OpenAI function-calling format — they emit tool calls as text like `call:calc{"expression": "2+2"}` or `calc{...}`. Added `_parse_text_tool_calls()` to `app/core/llm.py` that detects these patterns and converts them to OpenAI-format `tool_calls`. The LLM client now automatically parses text-based tool calls when the model doesn't return them in the standard format. This fixes the issue where `request_tools` was `[]` (empty) in logs even though tools were sent — actually tools WERE sent, but the model returned calls as text and they were ignored.
- **Test endpoints — ascii codec error**: `POST /api/admin/test/llm-tools` failed with `'ascii' codec can't encode character '\u2026'` when the client sent a masked api_key (containing `…`) back as a query parameter. Added `_is_masked()` and `_resolve()` helpers that detect masked values (`…` or `****`) and fall back to the raw DB value. All 4 test endpoints (test/llm, test/llm-tools, test/embeddings, test/embeddings/probe-dimension) now use these helpers.
- **Test LLM tools — better prompt**: the test now sends a system message "You must use the calc tool" and a user message "You MUST call the calc tool with expression '2+2'" to encourage tool use. Also increased timeout from 15s to 30s. The response now includes `raw_response` (the full LLM message) for debugging.
- **World editor — `'str' object has no attribute 'get'`**: the world_editor crashed when tool_calls contained string entries instead of dicts (some models return non-standard formats). Added type normalization: each tool_call is checked with `isinstance(tc, dict)`, strings are parsed as JSON, non-dicts are skipped. Also handles cases where `function` is not a dict.
- **World editor — propose_changes now waits for user**: previously `propose_changes` was auto-accepted (simplified). Now the editor emits `change_proposed` and WAITS for the user to accept/reject via `POST /api/sessions/worlds/{id}/apply` or `/discard`. Implemented using `asyncio.Future` stored in `_pending_changes` dict keyed by world_id. 120s timeout.
- **World editor — ask_user now waits for answer**: similarly, `ask_user` now waits for `POST /api/sessions/worlds/{id}/answer` body `{text: "..."}`. 120s timeout.
- **World editor — apply_diff improved**: now supports paths `world.name`, `world.description`, `schemas` (full replace), and `environment.` (via apply_patch). Previously only `environment.*` paths worked.
- **World editor — better empty-state handling**: when world has no schemas/entities/environment, the prompt now shows "(no schemas yet)", "(no entities yet)", and "(empty — world has no environment yet. Use env_update to add player, current_location, plot_rails.)" instead of empty strings, so the LLM understands the context.
### Backend — New endpoints
- `POST /api/sessions/worlds/{id}/apply` — accept proposed changes (resolves the pending Future)
- `POST /api/sessions/worlds/{id}/discard` — reject proposed changes
- `POST /api/sessions/worlds/{id}/answer` body `{text: "..."}` — answer a clarification
- `POST /api/worlds/{id}/generate-intro` → `{stream_url}` — re-generate intro scene for draft worlds
- `GET /api/sessions/worlds/{id}/intro/stream` (SSE) — runs the intro_scene stage, sets world.status to "ready" on success. Emits `step`, `intro_scene_complete`, `done` events.
### Frontend — Critical fixes (10 files changed, 1 new)
- **World Editor — Accept/Reject buttons**: `change_proposed` events now show a card with the diff (color-coded: green=add, red=remove, yellow=replace) and two buttons. Accept → `POST /apply`, Reject → `POST /discard`. After decision, the card collapses to a status line.
- **World Editor — Answer input**: `clarification` events show a card with either free-text input + Send button, or clickable option buttons (if `options` provided). Submit → `POST /answer`.
- **World Edit Page — Generate Intro Scene**: new `IntroSceneGenerator` component. If `world.status === 'draft'`, shows a "Generate Intro Scene" button. Clicking it opens the SSE stream, shows progress, displays the generated scene, and on `done` refreshes the world (status → "ready") + shows toast "World is ready!".
- **Settings panel — autocomplete="off"**: all inputs now have `autoComplete="off"`. API key fields use `type="text"` (browsers won't save them as passwords). Added a hidden decoy password input to absorb the password manager's attention.
- **Username/email autocomplete**: verified `LoginPage` uses `autoComplete="username"` for the login field, `RegisterPage` uses `autoComplete="email"` for email and `autoComplete="username"` for username.
- **Test LLM Tools — raw response display**: when `has_tool_calls=false`, shows a warning + collapsible `` with the raw LLM response so the user can see what the model returned.
- **World Builder — Retry button**: on error, shows a "Retry" button that re-subscribes to the builder stream. Also fixed a stale-closure bug where `done`/`error` events didn't close the active SSE controller (used `useRef` to track the current controller).
- **World Card — draft state**: draft worlds show a "Continue setup" button (links to edit page) and a "Draft" badge. No "Play" button for drafts.
- **Play page — better not-ready toast**: "This world is not ready yet. Generate the intro scene first."
### Verification
- Backend: 68 unit tests pass, 50 routes.
- Frontend: `tsc --noEmit` → 0 errors. `npm run build` → success (368 KB JS / 23 KB CSS, ~112 KB gzipped).
## [1.1.0] — 2026-06-21
This is a major bugfix release addressing 20+ issues found during user testing.
### Backend — Critical fixes
- **app/api/sessions.py**: fixed `TypeError: 'async_generator' object does not support the asynchronous context manager protocol` that broke ALL SSE streams (world builder, world editor, orchestrator). The `_session_scope()` function was an async generator (used `yield`) but was called with `async with`. Added `@asynccontextmanager` decorator. Without this fix, no LLM calls were ever made — the background task crashed immediately.
- **app/models/__init__.py**: made `WorldPreset.owner_id` nullable (`NOT NULL` → `NULL`, `ondelete=CASCADE` → `SET NULL`). Builtin presets are now system presets with `owner_id=NULL`. Previously, `seed_builtin_presets` crashed on first startup with a foreign key violation because no admin user existed yet.
- **app/main.py**: added `_apply_schema_fixups(engine)` that runs `ALTER TABLE world_presets ALTER COLUMN owner_id DROP NOT NULL` on every startup (idempotent, wrapped in try/except). This fixes existing databases that were created with the old NOT NULL constraint.
- **app/main.py**: admin setup URL is now ALWAYS printed on startup (even if admin already exists), per user request. The URL is blocked by the backend if an admin exists, but the token is visible for reference.
- **app/migrations/seed.py**: `seed_builtin_presets` now uses `owner_id=None` instead of looking for an admin user or a sentinel UUID.
- **requirements.txt**: pinned `bcrypt==4.0.1`. bcrypt 4.1+ removed the `__about__` module which breaks passlib 1.7.4's version detection. This caused the annoying `(trapped) error reading bcrypt version` warning on every password hash operation.
### Backend — API improvements
- **app/api/misc.py**: added `GET /api/settings/public` (no auth) — returns `{page_title, favicon_url, logo_url, og_image_url}`. The frontend uses this on app load to set the document title, favicon, and navbar logo.
- **app/api/worlds.py**: `GET /api/worlds` now excludes archived worlds by default. Pass `?status_filter=archived` to see only archived, or `?status_filter=all` to see everything.
- **app/api/admin.py**: `GET /api/admin/stats` now returns separate counts: `worlds` (active), `worlds_archived`, `worlds_total`. Previously it counted ALL worlds including archived.
- **app/api/admin.py**: added `DELETE /api/admin/worlds/{id}` — hard-delete a world (cascade deletes entities, steps, logs, triggers, story_entries). Also cleans up Qdrant points (best-effort). This gives admins a way to permanently remove archived worlds.
- **app/core/llm.py**: improved error messages for LLM API failures:
- Non-JSON responses now raise `LLMResponseError("LLM provider returned non-JSON response (check that api_url points to an OpenAI-compatible endpoint). First 200 chars: ...")` instead of a generic `parse_error`.
- HTTP 4xx/5xx errors now include the response body (or extracted `error.message` from JSON).
- All error messages now include the HTTP status code for easier debugging.
### Frontend — Critical fixes (19 files changed, 1 new)
- **Black page after settings change**: root cause was `AdminApi.updateSettings` returning `{updated: ...}` but the store expecting a different shape. Fixed the return type and removed the refetch-after-save that caused re-render loops.
- **[object Object] error in chat**: added `toErrorMessage()` helper that properly extracts `.message` from `ApiError` objects. All error toasts now show human-readable strings.
- **Play page access control**: `/worlds/:id/play` now redirects to `/worlds/:id/edit` with a toast if `world.status !== 'ready'`.
- **Retry/Rollback buttons**: disabled when `recentSteps.length === 0` with `opacity-50 cursor-not-allowed`.
- **Archived worlds**: hidden from the worlds list (both server-side and client-side filter). After deleting, removed from local state immediately.
- **World card archived state**: no Play button, "Archived" badge, "Restore" button (`PATCH` status→ready), admin-only "Delete permanently" button (`DELETE /api/admin/worlds/{id}`).
- **LLM logs detail modal**: now shows `request_messages`, `response_message`, `tool_calls`, `error_message`, `prompt_tokens`, `completion_tokens`, `latency_ms`, `temperature`, `model`, `stage`, `status` (color-coded), `created_at` — all in pretty-printed JSON `