Backend: - provider: fall back to parsing a plain JSON body when a server ignores stream=true (was: silent empty turn); error if response has no text (#5) - memorybank: clamp cursors after undo/retry shrinks the action list, and translate summary_cursor (list position) to an Action.index boundary before comparing with Memory.source_end (#7) - memorybank: pinned memories now count toward the top_k budget (#8) - memorybank: cosine() returns 0.0 on dimension mismatch; changing the embedding model clears stored vectors so they re-embed (#9) - scripting: MAX_STORY_CARDS cap now counts cards inserted during the hook, so a script can't add unbounded cards in one turn (#10) - settings: /test tolerates non-dict JSON from /models (#13) - scenarios: import accepts worldInformation as a story-card source (#14) Frontend: - per-key debounce timers in PlotPanel and ScenarioEditor — editing two things within 600ms no longer drops the first save (#15, #16) - Continue button no longer discards typed input (#17) - failed retry resyncs actions from the server instead of leaving the removed action missing (#18) - Settings save/test surface errors instead of hanging on Testing… (#19) - InsightsPanel ignores stale responses from superseded requests (#20) - placeholder scan includes story-card trigger keys (#21) addStoryCard returning the 0-based index (falsy for the first card) matches real AI Dungeon per the scripting guidebook — kept, documented (#11). Statuses updated in CODE_REVIEW_FINDINGS.md; stale entries for previously fixed items (#1-4, #6, #12) corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KFsGHju9szibJJa2YJcdbg
9.8 KiB
Code Review Findings — 2026-07-05
Full-codebase review (no git history, so whole project was the scope).
Status: pending = not yet fixed, fixed = applied, verify-failed = finding was wrong on closer look, skipped = intentionally not fixed.
Resume point: fix pending items top-to-bottom (they are ranked by severity).
2026-07-06 update (branch bugfix-code-review): every finding re-verified against
current code. #1/#2/#3/#4/#6/#12 had already been fixed in earlier sessions (statuses were
stale); #5/#7/#8/#9/#10/#13/#14 (backend) and #15–#21 (frontend) fixed in this pass.
#11 is skipped: real AI Dungeon's addStoryCard also returns the new card's index
(0-falsy included) per the official scripting guidebook, so changing it would break
compatibility — documented in engine.py's prelude instead. The cleanup backlog
(R/S/E/A items) below remains open by design.
Correctness bugs
1. [fixed] seed_demo.py doesn't stamp schema version → server crashes on next start
backend/seed_demo.py:10- Fresh DB created via
Base.metadata.create_allleavesPRAGMA user_versionat 0. Next server start sees tables exist, replays every ALTER TABLE migration →duplicate column namecrash. - Fix: stamp user_version to latest after create_all (reuse migrations.bootstrap logic).
2. [fixed] Turn-lock race: two simultaneous turns can run on the same adventure
backend/app/routers/adventures.py:302ensure_not_generating()runs in the route handler but_active_turns.add()only happens when the StreamingResponse generator is first iterated. Double-click Continue → both requests pass the 409 check → duplicate Action.index rows, interleaved generations.- Fix: atomically test-and-set the lock in the request phase, release in the stream's
finally.
3. [fixed] Migration 10 renumbers indexes with a correlated subquery on the table being updated
backend/app/migrations.py:34- SQLite may evaluate the subquery against partially-updated rows → duplicate indexes survive the "repair".
- Fix: compute new indexes in Python (SELECT ordered, then UPDATE per row).
4. [fixed] SQLite foreign keys never enabled → CASCADE/SET NULL clauses are dead
backend/app/database.py:8- Deleting a Script leaves orphaned
scenario_scriptsrows; SQLite rowid reuse can attach a future script to an old scenario. - Fix:
PRAGMA foreign_keys=ONvia engine connect event.
5. [fixed] Provider generate() silently yields nothing for non-SSE 200 responses
backend/app/providers/openai_compatible.py:84- Server that ignores
stream=trueand returns plain JSON → nodata:lines → empty AI action, no error. - Fix: buffer non-SSE body and fall back to parsing it as a single JSON completion.
6. [fixed] Fire-and-forget asyncio task can be GC'd mid-run and wedge the memory bank
backend/app/memorybank.py:146asyncio.create_taskresult not referenced; task can vanish silently; adventure ID can stay stuck in_running.- Fix: keep strong refs in a set, discard in done-callback.
7. [fixed] Memory cursors are list positions but Memory.source_start/end are Action.index values
backend/app/memorybank.py:213- After any action deletion (indexes keep gaps, positions shift), summarization skips/duplicates blocks and
_update_story_summaryfolds the wrong memories. - Fix: use one space consistently — track cursors by Action.index (position-independent), or renumber on delete.
8. [fixed] Pinned memories don't count toward memory_top_k cap
backend/app/memorybank.py:120- 6 pinned + top_k=5 → 11 memories injected, blowing token budget.
- Fix: fill with unpinned only up to
top_k - len(pinned)(min 0).
9. [fixed] Embedding-model change → cosine() zips different-dimension vectors silently
backend/app/memorybank.py:69- Old 768-dim embeddings scored against new 1536-dim query → garbage similarity, no error, never re-embedded.
- Fix: return 0.0 on length mismatch (and ideally clear stale embeddings so _embed_pending redoes them).
10. [fixed] MAX_STORY_CARDS cap is a no-op for cards created in one hook
backend/app/scripting/pipeline.py:59len(existing) + len(seen_ids) < MAX...never counts newly added cards (seen_ids ⊂ existing). A script can insert unbounded cards in one turn.- Fix: count inserts made during the loop.
11. [skipped] addStoryCard returns 0 (falsy) for the first card, indistinguishable from false rejection
backend/app/scripting/engine.py:39if (!addStoryCard(...))misfires when the card list was empty.- Fix: return
storyCards.length(1-based, always truthy) ortrue; document.
12. [fixed] scenario_id=0 truthiness bug in create_story_card
backend/app/routers/story_cards.py:28scenario_id or ...picks the wrong owner when id is 0. Useis not None.
13. [fixed] test_connection 500s on non-dict JSON from /models
backend/app/routers/settings.py:54- Only ValueError caught;
data.get/m.geton non-dict raises AttributeError → 500 instead of{ok:false}. - Fix: catch (ValueError, AttributeError, TypeError) or validate shapes.
14. [fixed] AI Dungeon exports with worldInformation key lose all story cards silently
backend/app/routers/scenarios.py:133- Import reads only
storyCards/worldInfo;worldInformationis in _IGNORED_KEYS so it's dropped and not reported. - Fix: accept
worldInformationas a card source too.
15. [fixed] Shared debounce timer loses edits (Play PlotPanel)
frontend/src/pages/Play.jsx:28- One
saveTimershared by all plot fields AND story-card saves; editing a second thing within 600ms cancels the first pending PATCH → silent data loss. - Fix: per-key timers (e.g. a Map keyed by field/card id).
16. [fixed] Same shared-debounce data loss in ScenarioEditor
frontend/src/pages/ScenarioEditor.jsx:22- Same fix as #15.
17. [fixed] Continue button silently discards typed input text
frontend/src/pages/Play.jsx:469- Clicking Continue with text in the box sends type 'continue' (backend ignores text) and clears the input.
- Fix: don't clear input on continue (or treat non-empty input as a normal send).
18. [fixed] retry() optimistically deletes last AI action with no rollback on failure
frontend/src/pages/Play.jsx:475- Failed retry (409/network) leaves UI missing an action that still exists server-side.
- Fix: restore the removed action in the catch path (or only remove on first stream event).
19. [fixed] Settings test()/save() have no error handling → stuck on "Testing…"
frontend/src/pages/Settings.jsx:66- Rejection leaves
{pending:true}forever + unhandled rejection. - Fix: try/catch → setTestResult({ok:false, error:msg}).
20. [fixed] InsightsPanel race: slow earlier request overwrites newer report
frontend/src/pages/Play.jsx:302- No staleness guard; slow getAdventureContext can clobber a newer action snapshot.
- Fix: track a request id / cancelled flag in the effect.
21. [fixed] extractPlaceholders ignores ${...} in story-card trigger keys
frontend/src/pages/Scenarios.jsx:50- Backend fills placeholders in card.keys but the modal never prompts for those names → literal
${hero}keys never match. - Fix: also scan card.keys when collecting placeholder names.
Cleanup backlog (reuse / simplification / efficiency / altitude — not bugs, apply later)
- R1
frontend/src/pages/Play.jsx:26+ScenarioEditor.jsx:19+ScriptEditor.jsx:34— three copies of the debounced-autosave + story-card handlers; extract auseDebouncedSavehook / shared StoryCardList component. (Fixing bugs #15/#16 properly may accomplish this.) - R2
backend/seed_demo.py:228— re-implements create_adventure; call the router logic instead. - R3
backend/app/providers/openai_compatible.py:122— complete() duplicates _request() body building; add astreamparam to _request(). - R4
backend/app/routers/adventures.py:476— six copies of child-resource get+owner-check+404; extractget_owned_or_404. - R5
frontend/src/api.js:26— streamSSE duplicates request()'s error extraction; extractthrowIfNotOk(resp). - S1
backend/app/models.py:210—Settings.streamis dead state (never read); delete column + schema fields. - S2
frontend/src/pages/Play.jsx:6— MODES and PLAYER_TYPES are identical constants; lastIsAi/canUndo computed twice. - E1
backend/app/context/builder.py:119— joins+tokenizes the ENTIRE adventure history every turn for the trigger window; walk reversed(actions) until budget instead. - E2
backend/app/scripting/pipeline.py:77— rebuilds full history dicts + JSON + a blocking commit per script per hook; build once per hook, slice to HISTORY_WINDOW first, commit once. - E3
frontend/src/pages/Play.jsx:561— every SSE chunk re-renders all action rows; isolate streaming text in a child component / React.memo rows. - E4
backend/app/context/builder.py:48— Section.tokens uncached, whole context tokenized 2-3×/turn; cache counts, sum sections. - E5
frontend/src/pages/Play.jsx:490— keydown effect has no dep array → listener re-registered every render. - E6
backend/app/memorybank.py:182— catch-up summarization awaits blocks sequentially; gather independent blocks. - A1
backend/app/models.py:145— no UniqueConstraint('adventure_id','index'); index allocation is ad-hoc per writer. (Related to bug #2.) - A2
backend/app/providers/openai_compatible.py:45— CHAT_CONTINUE_HINT appended below the budgeting layer; assemble prompts in context builder. - A3
backend/app/routers/adventures.py:390— import endpoints hand-coerce raw dicts; use a Pydantic bundle schema. - A4
backend/app/routers/adventures.py:207— onModelContext flattens (system, story) and ships everything as user content if modified; pass structure through the hook. - A5
frontend/src/pages/Home.jsx:87— client appends 'Z' to naive datetimes; emit ISO-8601 with offset from the API instead.