Skip to content

refactor(18): modularize RoomScene and code-health batch#6

Merged
moyunzero merged 4 commits into
mainfrom
feat/phase-18-code-health-refactor
Jun 12, 2026
Merged

refactor(18): modularize RoomScene and code-health batch#6
moyunzero merged 4 commits into
mainfrom
feat/phase-18-code-health-refactor

Conversation

@moyunzero

@moyunzero moyunzero commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Phase 18 — RoomScene modularization: Extract syncRoomEntities and related logic into roomSceneSync.ts; split camera, floor, input, and NPC motion into dedicated modules. RoomScene.ts shrinks to lifecycle + entity factory (~1086 lines).
  • useNpcChat refactor: Move speak queue, job registry, and attitude gate into apps/web/src/hooks/npcChat/; preserve frozen speak-busy UX contract.
  • Cross-layer code health: Game-server route/store hygiene, worker job routing and ambient intent fixes, gateway OpenRouter parse hardening, shared/npc-memory schema tweaks.
  • Verify script DRY: Centralize env loading, home spawns, SSE helpers, and shared constants via scripts/lib/*; fix @aetherlife/shared import in phase verify scripts via shared-pack.mjs.
  • Tooling/docs: Add root eslint.config.js, docs/CODE-QUALITY-REVIEW.md, and ISSUE-LOG guardrails.

Test plan

  • pnpm agent:verify (pre-push hook on push)
  • pnpm --filter @aetherlife/web test — 82 passed (incl. RoomScene.activity)
  • pnpm --filter @aetherlife/game-server test — 182 passed
  • cd workers/agent-worker && LLM_MOCK=1 uv run pytest -q — 201 passed
  • pnpm agent:verify --e2e --base on fresh pnpm dev:stack — all mapped golden flows passed (~11 min, real LLM on phase12/8)

Notes

  • Untracked assets/pet-cat/ and apps/web/tsconfig.tsbuildinfo intentionally excluded from this PR.
  • Long E2E runs may leave ambient NPC positions dirty; restart dev:stack before re-running uat:phase7:reset-snap if flaky.

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Intelligent Ambient NPCs (v3) and Speak SLA (v3); improved NPC speech queue & job handling
    • Better accessibility for chat UI (tab panels, live region) and improved NPC memory/connectivity indicators
  • Bug Fixes

    • More reliable speak acknowledgements and concurrency handling; improved moderation/error responses and scoped player authorization
    • Reduced stale state/eviction bugs for rooms, jobs, and movement prediction
  • Documentation

    • Added E2E policy and phase evolution docs; updated testing/verify commands and README
  • Tests

    • New unit and integration tests covering parsing, job registry eviction, moderation outage, and NPC intent behaviors

Split RoomScene sync/camera/floor/input/NPC motion into dedicated modules
and extract useNpcChat into hooks/npcChat/. Cross-layer hygiene across
game-server, worker, gateway, and packages; DRY verify scripts via
scripts/lib; add ESLint gate and CODE-QUALITY-REVIEW documentation.

Verified: pnpm agent:verify; pnpm agent:verify --e2e --base (fresh dev:stack)
Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d323975f-9bca-4ef9-8e65-efe1b980a334

📥 Commits

Reviewing files that changed from the base of the PR and between d7141c9 and a5f2a5d.

📒 Files selected for processing (37)
  • apps/ai-gateway/app/guards/content.py
  • apps/ai-gateway/app/guards/responses.py
  • apps/ai-gateway/app/routers/chat.py
  • apps/ai-gateway/app/routers/parse_route.py
  • apps/ai-gateway/app/services/llm.py
  • apps/ai-gateway/tests/test_moderation_unavailable.py
  • apps/ai-gateway/tests/test_openrouter_parse.py
  • apps/game-server/src/colyseus/job-registry.test.ts
  • apps/game-server/src/colyseus/job-registry.ts
  • apps/game-server/src/queue/npc-turn.test.ts
  • apps/game-server/src/queue/npc-turn.ts
  • apps/game-server/src/routes/chat.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/components/NpcMemoryPanel.tsx
  • apps/web/src/components/PhaserGame.tsx
  • apps/web/src/game/LocalPlayerMovementController.ts
  • apps/web/src/game/RoomScene.ts
  • apps/web/src/game/gridMovement.ts
  • apps/web/src/game/roomSceneInput.ts
  • apps/web/src/hooks/useCollectiveAttitude.ts
  • apps/web/src/lib/clientMovementPredictor.ts
  • packages/npc-memory/migrations/0007_npc_id_default.sql
  • packages/npc-memory/migrations/meta/_journal.json
  • packages/shared/src/speakIntent.ts
  • scripts/agent-verify.mjs
  • scripts/lib/e2e-sse.mjs
  • scripts/lib/env.mjs
  • scripts/lib/env.test.mjs
  • scripts/uat-phase7-reset-snap.mjs
  • workers/agent-worker/src/graph/action_intent.py
  • workers/agent-worker/src/graph/nodes/llm_social_turn.py
  • workers/agent-worker/src/graph/speak_intent.py
  • workers/agent-worker/src/main.py
  • workers/agent-worker/tests/test_action_intent.py
  • workers/agent-worker/tests/test_help_reply_by_npc.py
  • workers/agent-worker/tests/test_llm_social_degrade.py
  • workers/agent-worker/tests/test_physical_reply_turn.py

📝 Walkthrough

Walkthrough

This PR adds scoped player request guards, non-blocking NPC speak acquisition and job rollback handling, worker LLM/queue hardening, extracted RoomScene impl modules, shared script/env utilities, tests, schema defaults, ESLint for scripts, and documentation updates.

Changes

Phase 16-17 Core Implementation

Layer / File(s) Summary
Production Safety & Error Handling
apps/ai-gateway/app/main.py, apps/ai-gateway/app/guards/content.py, apps/game-server/src/routes/internal.ts
API gateway hides exception details in production; moderation guard returns explicit denial code when upstream moderation is unavailable in production; internal worker auth bypass is restricted in production.
Player-Scoped Authorization Bridge & Routes
apps/game-server/src/colyseus/bridge.ts, apps/game-server/src/routes/audit.ts, apps/game-server/src/routes/npc-memory.ts, apps/game-server/src/routes/collective-state.ts
Adds assertScopedPlayerRequest header-based scope enforcement and enforces it in audit, NPC memory, and collective-state routes.
NPC Speak Concurrency Control & Job Acquisition
apps/game-server/src/colyseus/GameRoom.ts, apps/game-server/src/routes/chat.ts, packages/shared/src/colyseus.ts
Replaces blocking acquire with tryAcquireNpcSpeakJob, eager jobId generation in chat route, returns 409 npc_busy when busy, and adds speakAck message constant.
Per-Player Cleanup & State Tracking
apps/game-server/src/collective/action-tracker.ts, apps/game-server/src/collective/move-intent-tracker.ts, apps/game-server/src/routes/rooms.ts
Introduces per-player clearing helpers and updates reset endpoint to perform per-player cleanup after scoped authorization.
Colyseus State & Job Registry Improvements
apps/game-server/src/colyseus/bridge.ts, apps/game-server/src/colyseus/bridge.test.ts, apps/game-server/src/colyseus/job-registry.ts, apps/game-server/src/room/store.ts, apps/game-server/src/ambient/intent-cache.ts
Prefer live Colyseus player positions; add bounded job registry with eviction and tests; cap room store with LRU touch behavior; pass local Colyseus state to intent sync.
LLM Response Validation & Error Handling
apps/ai-gateway/app/services/llm.py, apps/ai-gateway/tests/test_openrouter_parse.py
Adds _parse_openrouter_content to safely extract/validate OpenRouter JSON actions and updates retry/fallback handling with tests for valid, invalid, and malformed responses.
Ambient Intent & Social LLM Attempts
workers/agent-worker/src/graph/ambient_intent.py, workers/agent-worker/src/graph/nodes/llm_social_turn.py, workers/agent-worker/src/graph/npc_loop.py
Ambient intent iterates multiple provider/model attempts; social LLM attempts increased to 3; memory-context HTTP errors are caught and default to empty context.
Worker Speak State Locking & Payload Validation
workers/agent-worker/src/main.py
Introduces locked _speak_in_progress state, centralized _parse_bridge_payload and _job_id_from_payload helpers, and finer error-event emission logic.
Web RoomScene Modules
apps/web/src/game/roomSceneCamera.ts, apps/web/src/game/roomSceneFloor.ts, apps/web/src/game/roomSceneInput.ts, apps/web/src/game/roomSceneNpcMotion.ts, apps/web/src/game/roomSceneSync.ts, apps/web/src/game/roomSceneTypes.ts
Extracts camera, floor, input, NPC motion, sync logic, and shared types into dedicated modules.
RoomScene Delegation & Cleanup
apps/web/src/game/RoomScene.ts
RoomScene delegates to impl modules, adds loaderror handling, improves shutdown cleanup, and calls tickLocalMovement each frame.
NPC Chat Hook Refactor & Abort Handling
apps/web/src/hooks/npcChat/*, apps/web/src/hooks/useNpcChat.ts, apps/web/src/hooks/useNpcChat.test.ts
Introduces npcChat barrel, speak queue and job registry utilities, and makes useNpcChat abort-aware via AbortController for fetches.
Web Colyseus Room Lifecycle Management
apps/web/src/hooks/useColyseusRoom.ts
Tracks and removes onStateChange/onLeave listeners per join generation to avoid stale handlers.
Accessibility & UI Wiring
apps/web/src/ChatPage.tsx, apps/web/src/components/MessageList.tsx, apps/web/src/components/NpcTabBar.tsx
Wraps MessageList in a tabpanel, adds aria-live/log attributes, and links tabs to panels with id/aria-controls.
Local Movement & Prediction Updates
apps/web/src/game/LocalPlayerMovementController.ts, apps/web/src/lib/clientMovementPredictor.ts
Adds tickLocalMovement, bounds local step queue to MAX_PREDICT_AHEAD, and refactors sendWasd enqueue logic.
PhaserGame Lifecycle Cleanup
apps/web/src/components/PhaserGame.tsx
Clears motionBridgeRef on unmount to avoid stale external references.
Shared Script & Env Utilities
scripts/lib/env.mjs, scripts/lib/home-spawn.mjs, scripts/lib/shared-pack.mjs, scripts/lib/e2e-sse.mjs
Adds root .env loader, server URL derivation, home spawn constants, shared-pack re-exports, and SSE job polling.
Verify & UAT Script Migrations
scripts/agent-verify.mjs, scripts/verify-phase*.mjs, scripts/uat-phase7-reset-snap.mjs
Migrate scripts to use shared env and helpers, remove inline .env parsing, and update test coordinates and scope-check behavior.
ESLint & Lint Script
eslint.config.js, package.json
Adds ESLint flat config and lint script plus devDependencies for linting scripts.
Shared Package & NPC Defaults
packages/shared/src/colyseus.ts, packages/shared/src/player.ts, packages/shared/src/index.ts, packages/npc-memory/src/schema.ts, packages/npc-memory/src/repository.ts
Exports DEFAULT_NPC_ID/LEGACY_DEFAULT_NPC_ID, adds speakAck, and migrates npc-memory defaults and schema composite key; adds migration to update existing rows.
Game Actions Transfer
packages/game-actions/src/index.ts, packages/game-actions/README.md
Adds and documents transferActionSchema and example payload.
Internal Memories Validation
apps/game-server/src/routes/internal-memories.ts
Rejects posted internal memories exceeding MAX_PLAYER_MESSAGE_LEN.
Docs and Issue Tracking
CONTRIBUTING.md, README.md, docs/CODE-QUALITY-REVIEW.md, docs/ISSUE-LOG.md
Adds Phase 13–17 verification commands, agent verify flow, E2E policy and phase evolution links, a code-quality review report, and ISSUE-049 entry.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • moyunzero/AetherLife#3: related work touching ambient NPC intent handling and Colyseus sync that may overlap with Phase 16 integration.

"🐰
I hopped through code and cleared the trail,
Scoped players safe, and speak jobs won't fail;
Rooms now tidy, scripts all in a row,
NPCs stroll and the cameras follow.
Cheers to branching carrots in a neat row!"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase-18-code-health-refactor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/game-server/src/routes/chat.ts (1)

56-68: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Register the job before enqueuing the turn.

Line 59 starts the worker turn before the registry entry exists. That recreates the fast-path race GameRoom is already guarding against: if the worker emits early events before Line 63 runs, room routing and /:roomId/events lookup can miss the job entirely.

Suggested fix
     let speakAcquired = Boolean(colyseusRoom);
     try {
+      if (colyseusRoom) {
+        registerJob(jobId, colyseusRoom, roomId, undefined, {
+          npcId,
+          playerId,
+          playerMessage: message,
+        });
+      }
       const casualStub = previewCasualSpeakStub(message);
       await startNpcChatTurn(roomId, message, npcId, playerId, jobId, {
         casualPreviewEmitted: Boolean(casualStub),
       });
-      if (colyseusRoom) {
-        registerJob(jobId, colyseusRoom, roomId, undefined, {
-          npcId,
-          playerId,
-          playerMessage: message,
-        });
-      }
       if (casualStub) {
         emitJobEvent(jobId, "speakPartial", { text: casualStub, npcId });
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/game-server/src/routes/chat.ts` around lines 56 - 68, The worker turn is
started via startNpcChatTurn before the registry entry is created, causing a
race; move the registerJob call to run before startNpcChatTurn so the job is
registered prior to enqueuing work. Specifically, call registerJob(jobId,
colyseusRoom, roomId, undefined, { npcId, playerId, playerMessage: message })
(preserving the colyseusRoom guard) immediately after computing
casualStub/previewCasualSpeakStub and before invoking startNpcChatTurn, then
proceed to await startNpcChatTurn as before to ensure routing and
/:roomId/events lookups can find the job.
scripts/agent-verify.mjs (1)

26-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Load root env before resolving AGENT_SCOPE.

scope is captured before loadRootEnv(root) runs, so an AGENT_SCOPE defined in .env is ignored and --scope-check behaves as if no scope was declared.

Suggested fix
 const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+loadRootEnv(root);
 
 const args = process.argv.slice(2);
 const planOnly = args.includes("--plan");
 const runE2e = args.includes("--e2e");
 const scopeCheck = args.includes("--scope-check");
@@
 function main() {
-  loadRootEnv(root);
   const files = changedFiles();

Also applies to: 87-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/agent-verify.mjs` around lines 26 - 39, The AGENT_SCOPE env is being
read into the local variable scope before calling loadRootEnv(root), so values
from the project's .env are ignored; move the invocation of loadRootEnv(root) to
run before computing scope (or recompute scope after loadRootEnv) so that
process.env.AGENT_SCOPE can be populated and used; update the code paths around
root, args, scopeIdx, and scope (and similarly the later occurrence at lines
~87-89) to ensure scope is resolved only after loadRootEnv(root) completes.
🧹 Nitpick comments (3)
workers/agent-worker/src/graph/nodes/llm_social_turn.py (1)

63-64: ⚡ Quick win

Stale comment contradicts the new value.

The comment says "One attempt per provider" but SOCIAL_LLM_MAX_ATTEMPTS is now 3. Update the comment to reflect the new retry budget.

📝 Suggested comment update
-# One attempt per provider — APITimeout × 3 retries caused ~135s hangs before fallback.
+# Up to 3 attempts per provider/model combo — balances resilience vs. timeout accumulation.
 SOCIAL_LLM_MAX_ATTEMPTS = 3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workers/agent-worker/src/graph/nodes/llm_social_turn.py` around lines 63 -
64, Update the stale comment above SOCIAL_LLM_MAX_ATTEMPTS to reflect the
current retry budget (now 3 attempts) instead of "One attempt per provider";
e.g., mention that there are three attempts per provider or a 3-attempt retry
budget to match the constant name SOCIAL_LLM_MAX_ATTEMPTS and avoid
contradiction.
apps/web/src/hooks/npcChat/attitudeGate.ts (1)

5-7: 💤 Low value

Consider consolidating duplicate case bodies.

The "interact" and "generic" cases return identical text. They can share a single case block for clarity.

♻️ Consolidate duplicate cases
     case "transfer":
       return `${npcName}拒绝配合这个请求。`;
     case "interact":
     case "generic":
       return `${npcName}现在不愿意帮忙。`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/hooks/npcChat/attitudeGate.ts` around lines 5 - 7, The switch
handling NPC attitudes contains duplicate bodies for the "interact" and
"generic" cases; locate the switch in attitudeGate.ts and merge those two cases
into a single case block (e.g., case "interact": case "generic": return
`${npcName}现在不愿意帮忙。`;), removing the duplicate separate return so both labels
share the same body referencing npcName.
package.json (1)

73-80: Expand lint to include verify-phase entrypoints

package.json currently lints only scripts/lib and scripts/agent-verify.mjs, so verify-phase entrypoints like scripts/verify-phase3.mjs and scripts/verify-phase15.mjs are left out of the lint gate.

"lint": "eslint scripts/lib scripts/agent-verify.mjs scripts/verify-phase3.mjs scripts/verify-phase15.mjs"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 73 - 80, The npm "lint" script in package.json
only targets "scripts/lib" and "scripts/agent-verify.mjs", so add the missing
verify-phase entrypoints (e.g., "scripts/verify-phase3.mjs" and
"scripts/verify-phase15.mjs") to the "lint" command so ESLint runs against those
files as well; update the "lint" script value to include those filenames
(referencing the "lint" script key and the verify-phase script filenames) and
run the linter to confirm no remaining issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ai-gateway/app/services/llm.py`:
- Around line 148-155: The except block currently breaks on a parse error for
the final key causing last_error to be raised immediately and preventing the
_heuristic_parse(message) fallback; change the flow so parse errors set
last_error but do not force an immediate raise: stop breaking/raising inside the
loop (remove the final break) and after the loop only raise last_error if there
is no usable message (e.g., if message is falsy), otherwise call and return
_heuristic_parse(message); update the except handling around last_error and the
post-loop check so functions like _heuristic_parse, message, last_error and the
keys/key_idx logic are used as described.

In `@apps/game-server/src/colyseus/job-registry.ts`:
- Around line 11-30: The registry currently evicts the oldest job blindly in
evictOldestJobIfNeeded, which can drop in-flight jobs; change the eviction
policy so active jobs are preserved: either (A) update evictOldestJobIfNeeded to
skip entries that indicate activity (e.g., entries with sessionId set or a
running flag) and only delete the oldest non-active entry, iterating until one
is found or none remain, or (B) move completed jobs into a separate bounded
store and only apply the MAX_JOBS cap to that completedJobs map—update
registerJob and the job-completion path to transfer entries to completedJobs and
leave active entries in jobs so getJobEntry/getJobRoom continue to work for live
jobs. Ensure references to evictOldestJobIfNeeded, registerJob, getJobEntry and
getJobRoom are adjusted accordingly.

In `@apps/game-server/src/queue/npc-turn.ts`:
- Around line 89-90: The enqueue of the BullMQ job (q.add) and the bridge
enqueue (pushBridgeJob) must be made failure-atomic: if q.add succeeds but
pushBridgeJob fails we must rollback the queue job so callers don't treat the
turn as failed while a backend actually accepted it. Wrap the two calls
together, and on pushBridgeJob failure call the queue removal API (remove job by
jobId from q) before rethrowing the error; use the same jobId passed to q.add
and ensure the catch paths in GameRoom/chat still see the original error. This
keeps q.add and pushBridgeJob logically atomic and prevents a concurrent second
turn from being admitted for the same NPC.

In `@apps/web/src/game/roomSceneInput.ts`:
- Around line 101-117: The pointerdown handler in setupRoomSceneInput currently
sends movement immediately (sendMoveTo, setPathTarget, drawPathPreview), which
triggers on the first finger of a multitouch pinch; change the logic to ignore
pointerdown when multiple touch pointers are active (i.e., only proceed if this
is a single-touch/tap intent) or, for full safety, move the tap-to-move
confirmation to pointerup instead of pointerdown: detect single-touch (no other
active pointers) before calling ctx.getMovementSync().sendMoveTo,
ctx.setPathTarget, and drawPathPreview inside the handler (or relocate that
sequence to the pointerup handler to confirm tap), and apply the same change to
the other similar handler block referenced around lines 134-144 so pinch-zoom no
longer dispatches unintended moves.

In `@docs/CODE-QUALITY-REVIEW.md`:
- Around line 96-99: Update the table row that bundles "verify 脚本 env/speak 重复"
into two separate rows: mark the env/bootstrap dedup as completed (✅) and
reference the consolidated implementation in scripts/lib/env.mjs and any
migrated phase scripts (e.g., the centralized env bootstrap changes), and create
a separate deferred row for the speak dedup (⏸ defer) with a note that
speak-specific refactor remains outstanding; ensure the row labels use the
original text "verify 脚本 env 重复" and "verify 脚本 speak 重复" (or similarly distinct
identifiers) so the backlog accurately reflects that env dedup is done while
speak dedup is still pending.

In `@docs/ISSUE-LOG.md`:
- Around line 1882-1885: The ISSUE-LOG entry for reset(roomId) uses non-enum
values; change the status line to "状态: open", remove "deferred" from that enum
field and instead mention the deferred note in the prose of the entry, and
replace "严重性: medium" with one of the documented buckets (blocker | major |
minor) — pick the appropriate mapped value (e.g., major) and ensure the enum
formatting matches other entries for consistency.

In `@eslint.config.js`:
- Around line 7-24: Update the ESLint override that currently targets files:
["scripts/lib/**/*.mjs", "scripts/agent-verify.mjs"] so the same
languageOptions.globals and rules apply to the broader script cohort (e.g.,
include patterns like scripts/verify-*.mjs and scripts/uat-*.mjs); add missing
global names in languageOptions.globals such as window, AbortSignal,
AbortController, KeyboardEvent (and any other browser-like globals referenced by
those scripts) and keep existing readonly entries (process, fetch, console,
TextDecoder, URL, setTimeout, clearTimeout) so no-undef/no-unused-vars warnings
are suppressed for those files.

In `@packages/npc-memory/src/schema.ts`:
- Line 36: The DB default for npc_id is inconsistent: schema.ts sets
DEFAULT_NPC_ID ("npc-1") for npcMemories.npcId and memorySummaries.npcId while
migrations/0001_npc_memory.sql uses DEFAULT '1', leaving legacy rows orphaned;
fix by adding a new migration that ALTERs both columns to DEFAULT 'npc-1' and
runs an UPDATE to convert existing rows WHERE npc_id = '1' → 'npc-1', and/or add
a short-term fallback in MemoryRepository/MemoryService to treat '1' as
LEGACY_DEFAULT_NPC_ID when reading until the migration is applied. Ensure you
reference DEFAULT_NPC_ID, npcMemories.npcId, memorySummaries.npcId,
migrations/0001_npc_memory.sql, and MemoryRepository/MemoryService in your
changes.

In `@scripts/lib/e2e-sse.mjs`:
- Around line 24-57: The SSE reader.read() can block indefinitely so enforce the
timeout per read by wrapping await reader.read() in a Promise.race that races
reader.read() against a timeout Promise based on timeoutMs and started; if the
timeout wins, cancel the reader (reader.cancel()) and throw a timeout error
mentioning jobId/timeoutMs, otherwise proceed with the returned {value, done} as
before; update references to reader.read(), timeoutMs, started, reader.cancel(),
and the outer loop so the helper fails fast when the stream goes idle.

In `@scripts/lib/env.mjs`:
- Around line 41-42: gameServerWsUrl() currently always returns
"ws://127.0.0.1:2567" when GAME_SERVER_WS is unset, causing mismatch with
gameServerHttpBase(); change gameServerWsUrl() to mirror the same derivation
logic as gameServerHttpBase(): if process.env.GAME_SERVER_WS is set use that,
otherwise compute the host/port from process.env.GAME_SERVER_URL and
process.env.GAME_SERVER_PORT (falling back to the same defaults used by
gameServerHttpBase()), and construct the ws(s) URL using ws:// or wss://
consistent with the HTTP scheme and the same host/port defaults so both helpers
point at the same server when env vars are partially customized.
- Around line 20-27: The loader is copying raw tail strings including
surrounding quotes into process.env; update the logic in loadRootEnv (the loop
using variables trimmed, eq, key) to compute value = trimmed.slice(eq +
1).trim(), then if value starts and ends with a matching single or double quote
strip those outer quotes before assigning — finally set process.env[key] =
unquotedValue; preserve existing behavior for empty/unquoted values and
comment/invalid lines.

In `@scripts/uat-phase7-reset-snap.mjs`:
- Around line 103-105: The script reads process.env.WEB_URL and
process.env.GAME_SERVER_URL before the .env is loaded, so move the call to
loadRootEnv(root) to run before those env vars are captured; specifically ensure
loadRootEnv(root) is invoked at the top (or before any reads of
WEB_URL/GAME_SERVER_URL) so main() and any code that sets local constants for
WEB_URL and GAME_SERVER_URL observe the loaded .env values, keeping
assertE2eNoMock("uat:phase7:reset-snap") in place.

---

Outside diff comments:
In `@apps/game-server/src/routes/chat.ts`:
- Around line 56-68: The worker turn is started via startNpcChatTurn before the
registry entry is created, causing a race; move the registerJob call to run
before startNpcChatTurn so the job is registered prior to enqueuing work.
Specifically, call registerJob(jobId, colyseusRoom, roomId, undefined, { npcId,
playerId, playerMessage: message }) (preserving the colyseusRoom guard)
immediately after computing casualStub/previewCasualSpeakStub and before
invoking startNpcChatTurn, then proceed to await startNpcChatTurn as before to
ensure routing and /:roomId/events lookups can find the job.

In `@scripts/agent-verify.mjs`:
- Around line 26-39: The AGENT_SCOPE env is being read into the local variable
scope before calling loadRootEnv(root), so values from the project's .env are
ignored; move the invocation of loadRootEnv(root) to run before computing scope
(or recompute scope after loadRootEnv) so that process.env.AGENT_SCOPE can be
populated and used; update the code paths around root, args, scopeIdx, and scope
(and similarly the later occurrence at lines ~87-89) to ensure scope is resolved
only after loadRootEnv(root) completes.

---

Nitpick comments:
In `@apps/web/src/hooks/npcChat/attitudeGate.ts`:
- Around line 5-7: The switch handling NPC attitudes contains duplicate bodies
for the "interact" and "generic" cases; locate the switch in attitudeGate.ts and
merge those two cases into a single case block (e.g., case "interact": case
"generic": return `${npcName}现在不愿意帮忙。`;), removing the duplicate separate return
so both labels share the same body referencing npcName.

In `@package.json`:
- Around line 73-80: The npm "lint" script in package.json only targets
"scripts/lib" and "scripts/agent-verify.mjs", so add the missing verify-phase
entrypoints (e.g., "scripts/verify-phase3.mjs" and "scripts/verify-phase15.mjs")
to the "lint" command so ESLint runs against those files as well; update the
"lint" script value to include those filenames (referencing the "lint" script
key and the verify-phase script filenames) and run the linter to confirm no
remaining issues.

In `@workers/agent-worker/src/graph/nodes/llm_social_turn.py`:
- Around line 63-64: Update the stale comment above SOCIAL_LLM_MAX_ATTEMPTS to
reflect the current retry budget (now 3 attempts) instead of "One attempt per
provider"; e.g., mention that there are three attempts per provider or a
3-attempt retry budget to match the constant name SOCIAL_LLM_MAX_ATTEMPTS and
avoid contradiction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a94d95c0-9d62-4104-8858-4e32817d11c4

📥 Commits

Reviewing files that changed from the base of the PR and between 4fd060f and d7141c9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (76)
  • CONTRIBUTING.md
  • README.md
  • apps/ai-gateway/app/guards/content.py
  • apps/ai-gateway/app/main.py
  • apps/ai-gateway/app/services/llm.py
  • apps/ai-gateway/tests/test_openrouter_parse.py
  • apps/game-server/src/ambient/intent-cache.ts
  • apps/game-server/src/collective/action-tracker.ts
  • apps/game-server/src/collective/move-intent-tracker.ts
  • apps/game-server/src/colyseus/GameRoom.ts
  • apps/game-server/src/colyseus/bridge.test.ts
  • apps/game-server/src/colyseus/bridge.ts
  • apps/game-server/src/colyseus/job-registry.ts
  • apps/game-server/src/queue/npc-turn.ts
  • apps/game-server/src/room/store.ts
  • apps/game-server/src/routes/audit.ts
  • apps/game-server/src/routes/chat.ts
  • apps/game-server/src/routes/collective-state.ts
  • apps/game-server/src/routes/internal-memories.ts
  • apps/game-server/src/routes/internal.ts
  • apps/game-server/src/routes/npc-memory.ts
  • apps/game-server/src/routes/rooms.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/components/MessageList.tsx
  • apps/web/src/components/NpcTabBar.tsx
  • apps/web/src/components/PhaserGame.tsx
  • apps/web/src/game/RoomScene.ts
  • apps/web/src/game/roomSceneCamera.ts
  • apps/web/src/game/roomSceneFloor.ts
  • apps/web/src/game/roomSceneInput.ts
  • apps/web/src/game/roomSceneNpcMotion.ts
  • apps/web/src/game/roomSceneSync.ts
  • apps/web/src/game/roomSceneTypes.ts
  • apps/web/src/hooks/npcChat/attitudeGate.ts
  • apps/web/src/hooks/npcChat/index.ts
  • apps/web/src/hooks/npcChat/jobRegistry.ts
  • apps/web/src/hooks/npcChat/speakQueue.ts
  • apps/web/src/hooks/npcChat/types.ts
  • apps/web/src/hooks/useColyseusRoom.ts
  • apps/web/src/hooks/useNpcChat.test.ts
  • apps/web/src/hooks/useNpcChat.ts
  • docs/CODE-QUALITY-REVIEW.md
  • docs/ISSUE-LOG.md
  • eslint.config.js
  • package.json
  • packages/game-actions/README.md
  • packages/game-actions/src/index.ts
  • packages/npc-memory/src/repository.ts
  • packages/npc-memory/src/schema.ts
  • packages/shared/src/colyseus.ts
  • packages/shared/src/index.ts
  • packages/shared/src/player.ts
  • scripts/agent-verify.mjs
  • scripts/lib/e2e-sse.mjs
  • scripts/lib/env.mjs
  • scripts/lib/home-spawn.mjs
  • scripts/lib/shared-pack.mjs
  • scripts/uat-phase7-reset-snap.mjs
  • scripts/verify-phase10.mjs
  • scripts/verify-phase11.mjs
  • scripts/verify-phase12.mjs
  • scripts/verify-phase13.mjs
  • scripts/verify-phase14.mjs
  • scripts/verify-phase15.mjs
  • scripts/verify-phase16.mjs
  • scripts/verify-phase2.mjs
  • scripts/verify-phase3.mjs
  • scripts/verify-phase4.mjs
  • scripts/verify-phase5.mjs
  • scripts/verify-phase6.mjs
  • scripts/verify-phase7.mjs
  • scripts/verify-phase8.mjs
  • workers/agent-worker/src/graph/ambient_intent.py
  • workers/agent-worker/src/graph/nodes/llm_social_turn.py
  • workers/agent-worker/src/graph/npc_loop.py
  • workers/agent-worker/src/main.py

Comment thread apps/ai-gateway/app/guards/content.py Outdated
Comment thread apps/ai-gateway/app/services/llm.py
Comment thread apps/game-server/src/colyseus/job-registry.ts Outdated
Comment thread apps/game-server/src/queue/npc-turn.ts Outdated
Comment thread apps/web/src/game/roomSceneInput.ts
roomId: text("room_id").notNull(),
playerId: text("player_id").notNull().default("__legacy__"),
npcId: text("npc_id").notNull().default("1"),
npcId: text("npc_id").notNull().default(DEFAULT_NPC_ID),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a migration script addressing the npc_id value change
fd -t f -e sql -e ts -e js . --exec grep -l -i "npc.id.*=.*1\|UPDATE.*npc_memories\|UPDATE.*memory_summaries" {} \;

# Search for any migration or seed files referencing the old "1" default
rg -n "npc_id.*['\"]1['\"]" --type sql --type ts

Repository: moyunzero/AetherLife

Length of output: 349


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== schema.ts (npcId defaults) =="
sed -n '1,140p' packages/npc-memory/src/schema.ts | nl -ba | sed -n '1,140p' | sed -n '1,120p'
echo "----"
sed -n '30,90p' packages/npc-memory/src/schema.ts | nl -ba

echo "== migrations files =="
ls -la packages/npc-memory/migrations || true
echo "----"
for f in packages/npc-memory/migrations/*; do
  echo "## $f"
  sed -n '1,120p' "$f" | nl -ba
  echo
done

echo "== search for old/new defaults =="
rg -n "npc_id text NOT NULL DEFAULT '1'" packages/npc-memory/migrations || true
rg -n "DEFAULT_NPC_ID|npc-1|npc_id.*DEFAULT" packages/npc-memory/migrations packages/npc-memory/src || true
rg -n "npc_id.*['\"]1['\"]" packages/npc-memory/migrations packages/npc-memory/src || true

Repository: moyunzero/AetherLife

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== schema.ts (npcId defaults) =="
# show the relevant table/columns area
sed -n '1,220p' packages/npc-memory/src/schema.ts | cat -n | sed -n '1,220p'

echo "== migrations files =="
ls -la packages/npc-memory/migrations || true
echo "----"
for f in packages/npc-memory/migrations/*; do
  [ -f "$f" ] || continue
  echo "## $f"
  sed -n '1,140p' "$f" | cat -n | sed -n '1,140p'
  echo
done

echo "== search for old/new defaults =="
rg -n "npc_id text NOT NULL DEFAULT '1'" packages/npc-memory/migrations || true
rg -n "DEFAULT_NPC_ID|npc-1|npc_id.*DEFAULT" packages/npc-memory/migrations packages/npc-memory/src || true
rg -n "npc_id.*['\"]1['\"]" packages/npc-memory/migrations packages/npc-memory/src || true

Repository: moyunzero/AetherLife

Length of output: 14179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DEFAULT_NPC_ID definition =="
rg -n "DEFAULT_NPC_ID" packages/shared/src -S
sed -n '1,120p' packages/shared/src/player.ts | cat -n

echo
echo "== Any migration updating npc_id default? =="
rg -n "ALTER TABLE .*npc_(memories|attitudes|summaries|memory)" packages/npc-memory/migrations -S || true
rg -n "SET DEFAULT|DROP DEFAULT|DEFAULT 'npc-1'|DEFAULT \"npc-1\"|DEFAULT '1'|DEFAULT \"1\"" packages/npc-memory/migrations -S || true

echo
echo "== Any existing schema/migration mismatch for npc_id default =="
rg -n "npc_id.*default\\(" packages/npc-memory/src -S

Repository: moyunzero/AetherLife

Length of output: 2394


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,260p' packages/npc-memory/src/repository.ts | cat -n
echo "----"
rg -n "insert\\(|npcId\\s*\\:" packages/npc-memory/src/repository.ts -n

Repository: moyunzero/AetherLife

Length of output: 9308


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== insert usages in npc-memory =="
rg -n "insert\\(npcMemories|insert\\(memorySummaries|insert\\(collectiveEvents|insert\\(npc_attitudes" packages/npc-memory/src -S

echo
echo "== Any writes relying on DB default for npc_id? =="
rg -n "insert\\(.*\\)\\s*\\.values\\(\\s*\\{[^}]*npcId" packages/npc-memory/src -S || true
rg -n "insert\\(.*\\)\\s*\\.values\\(\\s*\\{(?![^}]*npcId)" packages/npc-memory/src -S || true

echo
echo "== Where LEGACY_DEFAULT_NPC_ID is used =="
rg -n "LEGACY_DEFAULT_NPC_ID" -S packages

echo
echo "== Any raw SQL using npc_id = '1' =="
rg -n "npc_id\\s*=?\\s*'1'|DEFAULT_NPC_ID|npc-1" packages/npc-memory/src -S

Repository: moyunzero/AetherLife

Length of output: 4087


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== LEGACY_DEFAULT_NPC_ID usage =="
rg -n "LEGACY_DEFAULT_NPC_ID" -S packages | head -n 50 || true

echo
echo "== DEFAULT_NPC_ID usage in npc-memory =="
rg -n "DEFAULT_NPC_ID" -S packages/npc-memory/src

echo
echo "== collective/repository.ts (relevant parts) =="
sed -n '1,260p' packages/npc-memory/src/collective/repository.ts | cat -n

echo
echo "== search inserts into npc_attitudes / npc_memories / memory_summaries =="
rg -n "insert\\(npcAttitudes|insert\\(npcMemories|insert\\(memorySummaries" -S packages/npc-memory/src

echo
echo "== raw SQL involving npc_id = '1' or npc_id = \"npc-1\" in npc-memory =="
rg -n "npc_id\\s*=\\s*['\"](1|npc-1)['\"]" -S packages/npc-memory/src || true

Repository: moyunzero/AetherLife

Length of output: 10679


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find appendMemory call sites =="
rg -n "appendMemory\\(" -S packages apps | head -n 50

echo
echo "== Find appendSummary call sites =="
rg -n "appendSummary\\(" -S packages apps | head -n 50

echo
echo "== Find where npcId is passed as '1' in code =="
rg -n "npcId\\s*:\\s*['\"]1['\"]" -S packages apps || true

echo
echo "== Find where npcId param is omitted when calling MemoryRepository methods =="
rg -n "appendMemory\\(\\{[^}]*roomId:[^}]*playerId:[^}]*text:" -S packages apps || true

Repository: moyunzero/AetherLife

Length of output: 1252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' apps/game-server/src/memory/service.ts | cat -n
echo "----"
sed -n '220,520p' apps/game-server/src/memory/service.ts | cat -n

Repository: moyunzero/AetherLife

Length of output: 18447


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DEFAULT_NPC_ID / LEGACY_DEFAULT_NPC_ID usage in apps/game-server =="
rg -n "DEFAULT_NPC_ID|LEGACY_DEFAULT_NPC_ID" apps/game-server -S || true

echo
echo "== Hardcoded npcId values \"npc-1\" or \"1\" in apps/game-server =="
rg -n "npcId\\s*[:=]\\s*['\"](npc-1|1)['\"]|['\"](npc-1|1)['\"]" apps/game-server/src/memory apps/game-server/src/collective apps/game-server/src -S || true

echo
echo "== Where npcId is passed into MemoryService calls (collective context) =="
rg -n "buildMemoryContext\\(|storeReflection\\(|storeBulkSummary\\(|getMemoryCount\\(|appendPlayerMemory\\(|appendNpcMemory\\(" apps/game-server/src -S | head -n 100

Repository: moyunzero/AetherLife

Length of output: 18427


Major: npc_id default mismatch ('1' vs DEFAULT_NPC_ID="npc-1") can strand legacy rows

In packages/npc-memory/src/schema.ts, npcMemories.npcId / memorySummaries.npcId now default to DEFAULT_NPC_ID ("npc-1"). However, packages/npc-memory/migrations/0001_npc_memory.sql still creates the DB columns with npc_id ... DEFAULT '1', and there’s no later migration that updates the column default or migrates existing rows from '1''npc-1'.

MemoryRepository/MemoryService use the provided npcId (typically "npc-1") when reading, with no LEGACY_DEFAULT_NPC_ID fallback, so rows written under the legacy default (npc_id='1') won’t be returned unless callers explicitly query with npcId='1'.

Add a migration to align DB defaults and/or update existing legacy rows, or document that '1' legacy data is intentionally orphaned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/npc-memory/src/schema.ts` at line 36, The DB default for npc_id is
inconsistent: schema.ts sets DEFAULT_NPC_ID ("npc-1") for npcMemories.npcId and
memorySummaries.npcId while migrations/0001_npc_memory.sql uses DEFAULT '1',
leaving legacy rows orphaned; fix by adding a new migration that ALTERs both
columns to DEFAULT 'npc-1' and runs an UPDATE to convert existing rows WHERE
npc_id = '1' → 'npc-1', and/or add a short-term fallback in
MemoryRepository/MemoryService to treat '1' as LEGACY_DEFAULT_NPC_ID when
reading until the migration is applied. Ensure you reference DEFAULT_NPC_ID,
npcMemories.npcId, memorySummaries.npcId, migrations/0001_npc_memory.sql, and
MemoryRepository/MemoryService in your changes.

Comment thread scripts/lib/e2e-sse.mjs
Comment thread scripts/lib/env.mjs Outdated
Comment thread scripts/lib/env.mjs Outdated
Comment thread scripts/uat-phase7-reset-snap.mjs
moyunzero and others added 3 commits June 12, 2026 20:08
Register HTTP speak jobs before enqueue, load .env before scope/URL reads,
harden env helpers (quoted values, WS derivation), add SSE read timeouts,
fall back to heuristic NL parse on malformed OpenRouter payloads, and defer
tap-to-move until pointerup to avoid pinch-zoom conflicts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Return 503 for moderation outages instead of content_blocked, evict only stale jobs from the speak registry, rollback BullMQ when the Redis bridge fails, and migrate legacy npc_id defaults to npc-1.

Co-authored-by: Cursor <cursoragent@cursor.com>
Route physical-reply fast lane by npc_id, extend move-intent patterns, and align client movement prediction with grid sync so speak stubs no longer collapse to identical help text.

Co-authored-by: Cursor <cursoragent@cursor.com>
@moyunzero moyunzero merged commit 0bafccf into main Jun 12, 2026
1 of 2 checks passed
@moyunzero moyunzero deleted the feat/phase-18-code-health-refactor branch June 12, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant