Skip to content

feat(26.2): World Alive — A3/B2 roam + Animal Crossing gap fix - #19

Merged
moyunzero merged 14 commits into
mainfrom
feat/phase-26.2-world-alive-wander
Jul 15, 2026
Merged

feat(26.2): World Alive — A3/B2 roam + Animal Crossing gap fix#19
moyunzero merged 14 commits into
mainfrom
feat/phase-26.2-world-alive-wander

Conversation

@moyunzero

@moyunzero moyunzero commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

  • Ship Phase 26.2 World Alive: unlock council maxRadius to 40 (A3), replace exclusive 12-bucket with B2 multi-NPC step gating, join_vicinity soft-leash bypass, docs/C-06/Guardrail #109.
  • Close UAT gaps (站桩 / 扎堆): walk–pause cadence, zone commute when outside schedule zone, never-stack destination picks (PERSONAL_SPACE), persona schedules with full-home beginning-fields@v1:home wander.
  • Dev overlay: ?gridDebug=1 draws home/orchard/plaza/pond zone rects; harden verify:phase8 dual-NL adjacency flake.

Test plan

  • pnpm --filter @aetherlife/shared build
  • pnpm --filter @aetherlife/game-server test -- src/ambient/
  • pnpm --filter @aetherlife/shared test (if touching spawn/radius)
  • Restart pnpm dev:stack, new roomId, open http://localhost:5173/?gridDebug=1 — confirm zone overlays
  • Watch 2–3 min: multi-NPC walk/pause on home roam, no permanent freeze, no same-cell stack
  • Speak pauses target NPC; NL「过来」still reaches within join window
  • pnpm verify:phase8 on real stack (no LLM_MOCK) when ready

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added a new Home zone and updated council spawn rules plus NPC daily schedules to use it.
    • Expanded the in-game debug overlay to render Beginning Fields activity-zone boundaries and labels.
  • Bug Fixes

    • NPC ambient movement is more natural: movement/pausing cadence improved, NPCs can move concurrently, and “idle-like” behavior no longer acts like sleep.
    • Reduced lingering pause and improved target selection to avoid crowding/overlap using personal-space and reservation checks.
    • Improved cold-start snapping and prevented unnecessary tweening for NPC animated movement.
    • Refined drawer/chronicle navigation behavior for vote and chronicle access.

moyunzero and others added 10 commits July 15, 2026 12:40
- Unlock zone-scale wander (A3 / D-19–D-21); leash becomes escape-only
- Keep dual SSOT in lockstep (D-02); coords/facing unchanged

Co-authored-by: Cursor <cursoragent@cursor.com>
- Pin disk spawns.json ≡ defaultBeginningFieldsBundle ≡ maxRadius 40
- Drift-sensitive so A3 unlock stays locked in CI (D-02)

Co-authored-by: Cursor <cursoragent@cursor.com>
- D-09: less linger standstill so A3 zone roam feels livelier

Co-authored-by: Cursor <cursoragent@cursor.com>
- Additive pure gates: wander 55%, linger (stationary/poi) 30%
- Hash domain ambient-step:${npcId}:${gameMinute}; bucket loop untouched

Co-authored-by: Cursor <cursoragent@cursor.com>
- Deterministic 1440-minute scans for ~55% wander / ~30% linger
- Co-pass and per-NPC desync assertions (D-22/D-24); stepActiveMinute helper

Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace hashNpcBucket gate with shouldStepThisTick (joinVicinity bypass for D-11)
- Rewrite multi-mover and maxRadius-0 pin tests; drop distinct-bucket test

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add source discriminator on resolveMovementTarget; skip leash when source is join
- Cover D-11 gate+leash bypass and A3 in-region radius-40 clamp neutrality

Co-authored-by: Cursor <cursoragent@cursor.com>
- Uniform maxRadius 40 table; drop dead per-zone 3/4/5 soft anchors
- Document B2 shouldStepThisTick (~55/30), LINGER_PAUSE_PERCENT 15, multi-mover

Co-authored-by: Cursor <cursoragent@cursor.com>
- Tick 消费: shouldStepThisTick 55/30 + multi-mover supersedes exclusive bucket
- Join: D-11 bypass of probability gate and soft leash; pin still wins
- Guardrail #109: forbid reintroducing D-MAP-AMB-03 exclusive buckets

Co-authored-by: Cursor <cursoragent@cursor.com>
Close World Alive UAT gaps: walk/pause cadence, zone commute, never-stack
targets, full-home wander schedules by persona, gridDebug zone overlays,
and harden verify:phase8 dual NL adjacency.

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 Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@moyunzero, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 09066e9b-f9cb-4329-a5aa-6f171f0350fa

📥 Commits

Reviewing files that changed from the base of the PR and between 19ea472 and 945667b.

📒 Files selected for processing (6)
  • apps/game-server/src/ambient/README.md
  • apps/game-server/src/ambient/tick.test.ts
  • apps/game-server/src/ambient/tick.ts
  • apps/game-server/src/ambient/zone-wander.test.ts
  • docs/CONTRACTS.md
  • docs/ISSUE-LOG.md
📝 Walkthrough

Walkthrough

The pull request updates ambient NPC schedules and movement, adds Beginning Fields zone and spawn data, centralizes drawer state, improves NPC animation synchronization, and extracts worker-state, memory, enrichment, caching, and prompt-context logic into dedicated modules.

Changes

Ambient movement and Beginning Fields data

Layer / File(s) Summary
World and schedule contracts
apps/game-server/data/schedules/*, apps/game-server/data/world/beginning-fields@v1/*, packages/shared/src/worldRegion.ts, docs/BEGINNING-FIELDS.md
Schedules now use home-zone wandering patterns; the home zone is added; council spawn radii are set to 40 in both data sources.
Occupancy-aware zone targeting
apps/game-server/src/ambient/zone-wander.*
Zone targets account for occupied and reserved cells, personal-space distance, blocked POIs, and commuting into zones.
Persistent ambient walk/pause engine
apps/game-server/src/ambient/tick.ts, apps/game-server/src/colyseus/GameRoom.ts
Per-NPC movement state replaces bucket pacing, with target-source tracking, reservations, walk/pause counters, soft-leash synchronization, and join-vicinity handling.
Ambient validation and contracts
apps/game-server/src/ambient/*.test.ts, docs/CONTRACTS.md, docs/ISSUE-LOG.md, scripts/verify-phase8.mjs
Tests and contracts cover resting-only skips, multi-NPC movement, target spacing, spawn consistency, and adjacency timing.

Web movement and shell UI

Layer / File(s) Summary
Centralized shell drawer state
apps/web/src/hooks/useShellDrawerState.*, apps/web/src/ChatPage.tsx
Drawer state, collective auto-open behavior, chronicle unread clearing, and toast navigation move into a reusable hook.
Beginning Fields zone debug overlay
apps/web/src/game/roomSceneInput.ts
The grid debug picker displays zone labels and draws styled Beginning Fields zone rectangles from the shared registry.
NPC step animation and synchronization
apps/web/src/game/gridMovement.*, apps/web/src/game/roomSceneNpcMotion.ts, apps/web/src/game/roomSceneSync.ts, apps/web/src/game/entitySprites.ts
NPC movement uses gait-aligned timing, queued step targets, catch-up snapping, cold-start snapping, and restarted LPC walk animation.

Worker fetch and prompt context

Layer / File(s) Summary
Worker state and memory orchestration
workers/agent-worker/src/graph/worker_state_fetch.py, workers/agent-worker/src/graph/speak_fetch.py
Worker-state caching, stale fallback, memory loading, nearby lore, collective fields, and enrichment are implemented in dedicated modules.
Shared system prompt construction
workers/agent-worker/src/graph/speak_system_context.py, workers/agent-worker/src/graph/prompt.py, workers/agent-worker/src/graph/nodes/llm_social_turn.py
NPC and social prompt paths use a shared system-context builder for common prompt sections.
Worker wiring and fetch tests
workers/agent-worker/src/graph/npc_loop.py, workers/agent-worker/tests/*
Legacy module re-exports, snapshot persistence, cache tests, memory tests, and enrichment mocks use the extracted module boundaries.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GameRoom
  participant runAmbientTick
  participant zone-wander
  participant NPC
  GameRoom->>runAmbientTick: execute ambient tick
  runAmbientTick->>zone-wander: resolve available movement target
  zone-wander-->>runAmbientTick: return target cell
  runAmbientTick->>NPC: move at most one grid cell
  NPC-->>runAmbientTick: arrive and enter pause state
Loading
sequenceDiagram
  participant GraphWorker
  participant fetch_state_and_memory
  participant WorkerStateCache
  participant MemoryContext
  participant PromptBuilder
  GraphWorker->>fetch_state_and_memory: prepare speak state
  fetch_state_and_memory->>WorkerStateCache: fetch worker snapshot
  fetch_state_and_memory->>MemoryContext: load memory and recall data
  WorkerStateCache-->>fetch_state_and_memory: worker state
  MemoryContext-->>fetch_state_and_memory: memory context
  fetch_state_and_memory->>PromptBuilder: build system context
  PromptBuilder-->>GraphWorker: assembled system prompt
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and matches the main Phase 26.2 World Alive changes, including roaming and the B2/ambient movement fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase-26.2-world-alive-wander

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Split worker speak/state helpers out of npc_loop, move ChatPage drawer
auto-open into useShellDrawerState, and refresh lpc-npc-9 sprite.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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: 10

🧹 Nitpick comments (1)
apps/game-server/src/ambient/zone-wander.ts (1)

100-121: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Optimize cell selection by eliminating the O(N log N) sort and redundant distance calculations.

The current implementation invokes minDistToOccupied multiple times per cell during the sort() operation. Given that candidates can be up to 256 cells and occupied up to 12 cells, this generates thousands of redundant iterations per ambient tick.

Since the goal is only to pick a random cell among those with the maximum distance, you can compute distances in a single O(N) pass and skip sorting entirely. Because collectZoneWalkable generates the pool already sorted by coordinates, removing the sort maintains the exact same deterministic baseline order prior to the random selection.

♻️ Proposed O(N) optimization
 export function pickSpaciousCell(
   pool: readonly GridCell[],
   occupied: readonly GridCell[],
   reserved: readonly GridCell[],
 ): GridCell | null {
   if (pool.length === 0) return null;
   const blocked = [...occupied, ...reserved];
-  const free = pool.filter((c) => !cellTaken(c, blocked));
-  const usable = free.length > 0 ? free : pool;
-  const spacious = usable.filter((c) => minDistToOccupied(c, occupied) >= PERSONAL_SPACE);
-  const ranked = (spacious.length > 0 ? spacious : usable).slice();
-  ranked.sort((a, b) => {
-    const da = minDistToOccupied(a, occupied);
-    const db = minDistToOccupied(b, occupied);
-    if (db !== da) return db - da;
-    return a.x - b.x || a.y - b.y;
-  });
-  // Soft random among top-scored peers so destinations aren't identical every tick.
-  const bestScore = minDistToOccupied(ranked[0]!, occupied);
-  const top = ranked.filter((c) => minDistToOccupied(c, occupied) === bestScore);
-  return top[Math.floor(Math.random() * top.length)] ?? null;
+  
+  // Precompute distances and filter free cells in a single pass
+  const poolStats = pool.map(c => ({
+    cell: c,
+    isFree: !cellTaken(c, blocked),
+    dist: minDistToOccupied(c, occupied),
+  }));
+
+  const free = poolStats.filter(c => c.isFree);
+  const usable = free.length > 0 ? free : poolStats;
+  const spacious = usable.filter(c => c.dist >= PERSONAL_SPACE);
+  const candidates = spacious.length > 0 ? spacious : usable;
+
+  let bestScore = -1;
+  for (const c of candidates) {
+    if (c.dist > bestScore) bestScore = c.dist;
+  }
+
+  const top = candidates.filter(c => c.dist === bestScore);
+  return top[Math.floor(Math.random() * top.length)]?.cell ?? null;
 }
🤖 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/ambient/zone-wander.ts` around lines 100 - 121, Optimize
pickSpaciousCell by replacing the ranked.sort flow and repeated
minDistToOccupied calls with a single pass over the selected usable candidates
that tracks the maximum distance and the cells sharing it. Preserve the existing
pool/free/usable and PERSONAL_SPACE fallback behavior, then randomly return one
of the maximum-distance candidates; rely on collectZoneWalkable’s coordinate
ordering for deterministic baseline behavior.
🤖 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/game-server/src/ambient/tick.test.ts`:
- Around line 471-529: The join-vicinity test currently exercises the legacy
map.player fallback instead of the live player registry. Update the test around
runAmbientTick to register two players through the map’s players registry, use
one as the join initiator, place the other separately, and assert the NPC moves
toward the initiator while preserving the existing bypass and target-selection
setup.

In `@apps/game-server/src/ambient/tick.ts`:
- Around line 39-42: Update runAmbientTick to call shouldStepThisTick before
invoking stepNpcTowardTarget for walking NPCs, skipping movement when the gate
returns false. Preserve join_vicinity as an explicit bypass so that action
continues without applying the deterministic mobility gate.
- Around line 293-295: Update the NPC target-selection flow around
reservedTargets to seed reservations with destinations from active walking
entries in ambientMotion before iterating map.npcs. Ensure persisted walking
destinations are included in the same reservation checks used for newly selected
targets, preserving the never-stack rule while retaining current-loop
reservations.
- Around line 44-52: Update AmbientMotion and the ambient tick handling to
associate each held walk with the current schedule segment using
segmentKey(segment), then invalidate or clear motion when the segment changes so
stale targets are not followed. Preserve existing pause and walk timeout
behavior within the same segment.

In `@apps/web/src/game/roomSceneInput.ts`:
- Around line 62-65: Update zonesAtCell to call getCouncilSpawnSlots inside a
try/catch, returning an empty zone list when it throws while the registry is
still loading. Preserve the existing registry lookup and normal processing when
the call succeeds.

In `@apps/web/src/hooks/useShellDrawerState.ts`:
- Line 1: Remove the redundant openChronicle function from useShellDrawerState
and omit it from the hook return value. In ChatPage, stop destructuring
openChronicle, replace its invocation with openDrawer("chronicle"), and remove
it from the callback dependencies so the existing drawer flow clears chronicle
unread state.

In `@docs/CONTRACTS.md`:
- Line 104: Update the Tick 消费 contract to remove idle from the movement-skip
condition, documenting only resting as gated to zero. Keep the existing
shouldSkipMovement runtime behavior and the per-NPC movement probability rules
unchanged.

In `@workers/agent-worker/src/graph/speak_fetch.py`:
- Around line 312-318: Remove the recalled-memory content from the diagnostic
print in the recent-only miss branch of the memory-context recall flow.
Eliminate the preview extraction and preview field, while retaining only
non-sensitive identifiers and row counts such as room, NPC, player, and recent
length.

In `@workers/agent-worker/src/graph/worker_state_fetch.py`:
- Around line 34-35: Update the worker-state cache flow around
_worker_state_stale_key and its read/write paths to distinguish full snapshots
from skipNearbyLore=1 projections, preventing a partial snapshot from satisfying
a full fetch. Ensure action writes invalidate or update every projection key,
including the additional call sites noted in the diff.
- Line 20: Update the process-global _stale_worker_snapshots cache and its
read/write logic around the affected snapshot helpers to enforce both TTL
expiration and a maximum entry count. Prune expired entries during writes, then
evict the oldest entries when the cache exceeds the configured bound, while
preserving valid snapshot retrieval behavior.

---

Nitpick comments:
In `@apps/game-server/src/ambient/zone-wander.ts`:
- Around line 100-121: Optimize pickSpaciousCell by replacing the ranked.sort
flow and repeated minDistToOccupied calls with a single pass over the selected
usable candidates that tracks the maximum distance and the cells sharing it.
Preserve the existing pool/free/usable and PERSONAL_SPACE fallback behavior,
then randomly return one of the maximum-distance candidates; rely on
collectZoneWalkable’s coordinate ordering for deterministic baseline behavior.
🪄 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: b3736c2c-b5ea-4e53-985f-1af0a161188a

📥 Commits

Reviewing files that changed from the base of the PR and between 33b1287 and d0c4f83.

⛔ Files ignored due to path filters (1)
  • apps/web/public/assets/sprites/lpc-npc-9.png is excluded by !**/*.png
📒 Files selected for processing (40)
  • apps/game-server/data/schedules/README.md
  • apps/game-server/data/schedules/npc-1.json
  • apps/game-server/data/schedules/npc-10.json
  • apps/game-server/data/schedules/npc-11.json
  • apps/game-server/data/schedules/npc-12.json
  • apps/game-server/data/schedules/npc-2.json
  • apps/game-server/data/schedules/npc-3.json
  • apps/game-server/data/schedules/npc-4.json
  • apps/game-server/data/schedules/npc-5.json
  • apps/game-server/data/schedules/npc-6.json
  • apps/game-server/data/schedules/npc-7.json
  • apps/game-server/data/schedules/npc-8.json
  • apps/game-server/data/schedules/npc-9.json
  • apps/game-server/data/world/beginning-fields@v1/spawns.json
  • apps/game-server/data/world/beginning-fields@v1/zones.json
  • apps/game-server/src/ambient/README.md
  • apps/game-server/src/ambient/schedule.test.ts
  • apps/game-server/src/ambient/schedule.ts
  • apps/game-server/src/ambient/tick.test.ts
  • apps/game-server/src/ambient/tick.ts
  • apps/game-server/src/ambient/zone-wander.test.ts
  • apps/game-server/src/ambient/zone-wander.ts
  • apps/game-server/src/colyseus/GameRoom.ts
  • apps/game-server/src/world/council-spawn-radius.test.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/game/roomSceneInput.ts
  • apps/web/src/hooks/useShellDrawerState.ts
  • docs/BEGINNING-FIELDS.md
  • docs/CONTRACTS.md
  • docs/ISSUE-LOG.md
  • packages/shared/src/worldRegion.ts
  • scripts/verify-phase8.mjs
  • workers/agent-worker/src/graph/nodes/llm_social_turn.py
  • workers/agent-worker/src/graph/npc_loop.py
  • workers/agent-worker/src/graph/prompt.py
  • workers/agent-worker/src/graph/speak_fetch.py
  • workers/agent-worker/src/graph/speak_system_context.py
  • workers/agent-worker/src/graph/worker_state_fetch.py
  • workers/agent-worker/tests/test_fetch_state_and_memory.py
  • workers/agent-worker/tests/test_load_memory_recall_fallback.py

Comment thread apps/game-server/src/ambient/tick.test.ts
Comment thread apps/game-server/src/ambient/tick.ts Outdated
Comment thread apps/game-server/src/ambient/tick.ts
Comment thread apps/game-server/src/ambient/tick.ts Outdated
Comment on lines +62 to +65
function zonesAtCell(gx: number, gy: number): string[] {
void getCouncilSpawnSlots();
const registry = getWorldRegistry();
if (!registry) return [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Wrap getCouncilSpawnSlots in a try/catch block.

getCouncilSpawnSlots throws an error if the registry is not yet populated. Since zonesAtCell is called frequently on pointermove to update the HUD, an unhandled exception here will crash the input event handler when the map is first loading.

🐛 Proposed fix
 function zonesAtCell(gx: number, gy: number): string[] {
-  void getCouncilSpawnSlots();
+  try {
+    void getCouncilSpawnSlots();
+  } catch {
+    // registry not ready
+  }
   const registry = getWorldRegistry();
   if (!registry) return [];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function zonesAtCell(gx: number, gy: number): string[] {
void getCouncilSpawnSlots();
const registry = getWorldRegistry();
if (!registry) return [];
function zonesAtCell(gx: number, gy: number): string[] {
try {
void getCouncilSpawnSlots();
} catch {
// registry not ready
}
const registry = getWorldRegistry();
if (!registry) return [];
🤖 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/game/roomSceneInput.ts` around lines 62 - 65, Update zonesAtCell
to call getCouncilSpawnSlots inside a try/catch, returning an empty zone list
when it throws while the registry is still loading. Preserve the existing
registry lookup and normal processing when the call succeeds.

Comment thread apps/web/src/hooks/useShellDrawerState.ts
Comment thread docs/CONTRACTS.md Outdated
Comment thread workers/agent-worker/src/graph/speak_fetch.py
_FETCH_STATE_ATTEMPTS = 2
_FETCH_STATE_HOT_CACHE_TTL_S = 3.0
_STALE_SNAPSHOT_TTL_S = 300.0
_stale_worker_snapshots: dict[str, tuple[dict[str, Any], float]] = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bound and evict the process-global snapshot cache.

Expired entries remain in _stale_worker_snapshots forever. A long-lived worker handling new room/player IDs therefore grows memory without limit. Add size-bounded TTL eviction or periodic pruning on writes.

Also applies to: 38-54

🤖 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/worker_state_fetch.py` at line 20, Update the
process-global _stale_worker_snapshots cache and its read/write logic around the
affected snapshot helpers to enforce both TTL expiration and a maximum entry
count. Prune expired entries during writes, then evict the oldest entries when
the cache exceeds the configured bound, while preserving valid snapshot
retrieval behavior.

Comment thread workers/agent-worker/src/graph/worker_state_fetch.py Outdated
moyunzero and others added 2 commits July 15, 2026 22:33
Use NPC_GRID_STEP_MS=600 so each cell covers a full LPC walk cycle;
enable npcWorldLive from Colyseus roomNpcs; harden ambient spacing,
chronicle drawer, and worker-state cache keys from PR CR follow-ups.

Co-authored-by: Cursor <cursoragent@cursor.com>
Assert LINGER_PAUSE_PERCENT === 15; leave RED B2 wire canaries that
prove shouldStepThisTick is not called from runAmbientTick (45b6455).

Co-authored-by: Cursor <cursoragent@cursor.com>

@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: 3

♻️ Duplicate comments (1)
apps/game-server/src/ambient/tick.test.ts (1)

651-709: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise join-vicinity through players.

The join-vicinity test appears to exercise the legacy map.player fallback instead of the live player registry. As per coding guidelines, human positions live in players, not the lone RoomState.player. Update the test around runAmbientTick to register two players through the map's players registry, use one as the join initiator, place the other separately, and assert the NPC moves toward the initiator while preserving the existing bypass and target-selection setup.

If this has already been addressed, please disregard. Run the script below to verify the current state of the test.

#!/bin/bash
# Description: Verify if the join-vicinity test has been updated to use the player registry.

sed -n '651,710p' apps/game-server/src/ambient/tick.test.ts
🤖 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/ambient/tick.test.ts` around lines 651 - 709, Update the
join-vicinity test around runAmbientTick to use the map.players registry instead
of map.player: register two players, place one as the join initiator at the
existing target location and the other separately, then assert the NPC moves
toward the initiator. Preserve the existing schedule bypass, soft-leash,
obstacle placement, and distance assertions.

Source: Coding guidelines

🤖 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/game-server/src/ambient/tick.test.ts`:
- Line 340: Remove the duplicate recentNpcCells declaration in the affected test
setup, keeping only one Map<string, { x: number; y: number }[]> declaration and
preserving all existing references to it.

In `@apps/web/src/game/entitySprites.ts`:
- Around line 369-370: Update playLpcWalkAnim() so repeated calls for player
avatars do not restart the currently playing LPC walk animation; use the
existing playback API behavior that preserves an active matching animation while
still allowing idle-to-walk transitions. Keep spriteProfileForPlayer() and the
lpc-player-1 animation selection unchanged.

In `@apps/web/src/game/roomSceneNpcMotion.ts`:
- Line 191: Update the path-continuation logic around continuing so a queued
pendingGridX/pendingGridY target stops the current path after the active step
completes. Preserve the current-step completion behavior, then exit the
remaining path and invoke resumeFromPendingOrIdle() so the queued target is
handled immediately.

---

Duplicate comments:
In `@apps/game-server/src/ambient/tick.test.ts`:
- Around line 651-709: Update the join-vicinity test around runAmbientTick to
use the map.players registry instead of map.player: register two players, place
one as the join initiator at the existing target location and the other
separately, then assert the NPC moves toward the initiator. Preserve the
existing schedule bypass, soft-leash, obstacle placement, and distance
assertions.
🪄 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: 1d71c17d-449a-4790-89fa-0c4ef3cedd63

📥 Commits

Reviewing files that changed from the base of the PR and between d0c4f83 and 19ea472.

📒 Files selected for processing (22)
  • apps/game-server/src/ambient/tick.test.ts
  • apps/game-server/src/ambient/tick.ts
  • apps/game-server/src/colyseus/GameRoom.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/game/RoomScene.ts
  • apps/web/src/game/entitySprites.ts
  • apps/web/src/game/gridMovement.npcCatchup.test.ts
  • apps/web/src/game/gridMovement.ts
  • apps/web/src/game/lpcNpc1Sheet.test.ts
  • apps/web/src/game/roomSceneNpcMotion.ts
  • apps/web/src/game/roomSceneSync.ts
  • apps/web/src/game/roomSceneTypes.ts
  • apps/web/src/hooks/useShellDrawerState.test.ts
  • apps/web/src/hooks/useShellDrawerState.ts
  • docs/CONTRACTS.md
  • docs/ISSUE-LOG.md
  • scripts/uat-pr19-cr-screenshots.mjs
  • workers/agent-worker/src/graph/npc_loop.py
  • workers/agent-worker/src/graph/speak_fetch.py
  • workers/agent-worker/src/graph/worker_state_fetch.py
  • workers/agent-worker/tests/test_fetch_state_and_memory.py
  • workers/agent-worker/tests/test_load_memory_recall_fallback.py
💤 Files with no reviewable changes (1)
  • apps/web/src/hooks/useShellDrawerState.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/game-server/src/colyseus/GameRoom.ts
  • workers/agent-worker/src/graph/worker_state_fetch.py
  • apps/web/src/ChatPage.tsx
  • workers/agent-worker/src/graph/speak_fetch.py
  • workers/agent-worker/tests/test_fetch_state_and_memory.py
  • docs/CONTRACTS.md
  • apps/game-server/src/ambient/tick.ts
  • workers/agent-worker/src/graph/npc_loop.py


// Linger pool around (10,10) ≤2 includes reservedDest (12,10). Fill recent so
// the non-reserved alternative peers are less sticky; reservation must still win.
const recentNpcCells = new Map<string, { x: number; y: number }[]>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove duplicate declaration.

recentNpcCells is declared twice, which will cause a SyntaxError (Identifier has already been declared) and break the build.

🐛 Proposed fix
-    const recentNpcCells = new Map<string, { x: number; y: number }[]>();
-    const recentNpcCells = new Map<string, { x: number; y: number }[]>();
+    const recentNpcCells = new Map<string, { x: number; y: number }[]>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const recentNpcCells = new Map<string, { x: number; y: number }[]>();
const recentNpcCells = new Map<string, { x: number; y: number }[]>();
🤖 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/ambient/tick.test.ts` at line 340, Remove the duplicate
recentNpcCells declaration in the affected test setup, keeping only one
Map<string, { x: number; y: number }[]> declaration and preserving all existing
references to it.

Comment on lines +369 to +370
// Always (re)start walk — ignoreIfPlaying=false so idle→walk always swaps frames.
ent.avatar.play(key, false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== file existence =='
git ls-files 'apps/web/src/game/entitySprites.ts' 'apps/web/src/game' | sed -n '1,120p'

echo
echo '== outline entitySprites.ts =='
ast-grep outline apps/web/src/game/entitySprites.ts --view expanded || true

echo
echo '== targeted search: play(key, false), isNpc, lpc-player-1 =='
rg -n "play\\(key, false\\)|isNpc|lpc-player-1|GRID_STEP_MS|ignoreIfPlaying" apps/web/src/game apps/web/src -g '!**/dist/**' -g '!**/build/**' || true

echo
echo '== line numbers around 350-390 =='
sed -n '340,390p' apps/web/src/game/entitySprites.ts

Repository: moyunzero/AetherLife

Length of output: 17485


🏁 Script executed:

#!/bin/bash
set -euo pipefail
pwd
git ls-files 'apps/web/src/game/entitySprites.ts' | cat
sed -n '340,390p' apps/web/src/game/entitySprites.ts

Repository: moyunzero/AetherLife

Length of output: 1947


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== entitySprites.ts line map =='
nl -ba apps/web/src/game/entitySprites.ts | sed -n '330,390p'

echo
echo '== references to entitySprites walk helper =='
rg -n -C 3 "ent\.avatar\.play\(key, false\)|ent\.avatar\.play\(key|isNpc|walk" apps/web/src/game/entitySprites.ts apps/web/src/game -g '!**/dist/**' -g '!**/build/**' || true

Repository: moyunzero/AetherLife

Length of output: 230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== lpc profile mapping =='
rg -n -C 3 "isLpcProfile|lpc-player-1|spriteProfileForPlayer|LpcNpcSpriteProfile|spriteProfileForNpc" apps/web/src/game/lpcNpc1Sheet.ts apps/web/src/game/entitySprites.ts

echo
echo '== playWalkAnim callers =='
rg -n -C 3 "playWalkAnim\\(" apps/web/src/game apps/web/src -g '!**/dist/**' -g '!**/build/**'

echo
echo '== player sprite creation and profile assignment =='
rg -n -C 4 "createPlayerSprite|spriteProfileForPlayer|spriteProfileForNpc|spriteProfile" apps/web/src/game/entitySprites.ts apps/web/src/game/RoomScene.ts apps/web/src/game/roomScene*.ts

Repository: moyunzero/AetherLife

Length of output: 25452


Preserve continuous LPC walk playback for players. apps/web/src/game/entitySprites.ts:362-370playLpcWalkAnim() is used by spriteProfileForPlayer() (lpc-player-1), so ent.avatar.play(key, false) restarts the same 675ms gait on every 200ms step and will stutter. Keep the loop continuous for player avatars.

🤖 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/game/entitySprites.ts` around lines 369 - 370, Update
playLpcWalkAnim() so repeated calls for player avatars do not restart the
currently playing LPC walk animation; use the existing playback API behavior
that preserves an active matching animation while still allowing idle-to-walk
transitions. Keep spriteProfileForPlayer() and the lpc-player-1 animation
selection unchanged.

Source: Coding guidelines

}
},
beginNpcStepTween(ctx, ent, cell.x, cell.y, () => {
const continuing = stepIndex < path.length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Abort the stale path if a new target is queued mid-step.

The comment explicitly states: "Queue at most one follow-up cell; finish current step first." However, the loop only checks stepIndex < path.length and will stubbornly finish the entire remaining multi-cell path before evaluating the new pendingGridX target.

If a new target is queued, we should abort the remaining path cells and yield to resumeFromPendingOrIdle() immediately after the current step finishes.

🐛 Proposed fix to respect the queued interruption
-      const continuing = stepIndex < path.length;
+      const continuing = stepIndex < path.length && ent.pendingGridX == null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const continuing = stepIndex < path.length;
const continuing = stepIndex < path.length && ent.pendingGridX == null;
🤖 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/game/roomSceneNpcMotion.ts` at line 191, Update the
path-continuation logic around continuing so a queued pendingGridX/pendingGridY
target stops the current path after the active step completes. Preserve the
current-step completion behavior, then exit the remaining path and invoke
resumeFromPendingOrIdle() so the queued target is handled immediately.

Re-wire shouldStepThisTick for new strolls; mid-walk and join bypass.
Align C-06, ambient README, Guardrail #111; keep Nyquist canaries green.

Co-authored-by: Cursor <cursoragent@cursor.com>
@moyunzero
moyunzero merged commit 16b310f into main Jul 15, 2026
2 checks passed
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