Gsd/phase 28 council relationship advanced - #22
Conversation
- GS mutual-chat / decay / routes stubs cite D-MUTUAL, D-DECAY, D-API - Worker mutual-chat / belief_gate / relationship_rag pytest skips - Web hook + RelationshipGraphPanel stubs (vitest .test.ts include) Co-authored-by: Cursor <cursoragent@cursor.com>
- Real-LLM gate via assertE2eRealLlm; health + room; plan-12 TODO asserts - package.json verify:phase28 entry (E2E-POLICY + VALIDATION map local via gitignore) Co-authored-by: Cursor <cursoragent@cursor.com>
- Cutoff boundaries and UI-SPEC ZH labels (敌对/冷淡/平常/亲近/亲密) - Assert public DTO omits affection/trust integers Co-authored-by: Cursor <cursoragent@cursor.com>
- RelationshipBand cool/close remaps attitude wary/allied cutoffs - RelationshipEdgeBandPublic omits affection/trust (D-GRAPH-02) - Export helpers and Zod schema from shared index Co-authored-by: Cursor <cursoragent@cursor.com>
- Hint-only ColyseusRelationshipSyncPayload { hasUpdate, latestSeq? }
- Additive C-09b: player GET + invalidate + embedding; C-09 worker apply unchanged
Co-authored-by: Cursor <cursoragent@cursor.com>
- Cover silent apply, |Δ| 1–3 soft bounds, no last_interact bump, council independence Co-authored-by: Cursor <cursoragent@cursor.com>
- Dedicated applyIdleDecayDeltas path; monthly maybeRunRelationshipDecay - Soft floor/ceiling from seed bands; stamp interact abs minutes separately Co-authored-by: Cursor <cursoragent@cursor.com>
- Minimal protected-path hook; no speak defer, no LLM, no relationshipSync Co-authored-by: Cursor <cursoragent@cursor.com>
- Assert npcRelationships.embedding matches npcMemories vector(2048) - Assert migration 0012 SQL adds nullable embedding column Co-authored-by: Cursor <cursoragent@cursor.com>
- Migration 0012 adds embedding vector(2048) IF NOT EXISTS - Drizzle schema mirrors npcMemories.embedding via vector2048 - Register journal tag so drizzle-kit migrate can apply 0012 Co-authored-by: Cursor <cursoragent@cursor.com>
- Unskip Wave 0 stubs: proximity/nemesis, daily cap, speak defer, dyad supersede - Assert claim NX mock path without Redis Co-authored-by: Cursor <cursoragent@cursor.com>
- Mirror personal-timeline claim/LPUSH for aetherlife:npc-mutual-chat:jobs - Chebyshev≤2 + schedule/village filter; score weights only (nemesis eligible) - Daily cap 3 + 12-seat bucket rotation; speak-busy defer helper - Ambient dyad skips pairs already claimed by mutual-chat (A2) Co-authored-by: Cursor <cursoragent@cursor.com>
- Call enqueueNpcMutualChatIfDue from ambient tick before dyad - Early-return when npcSpeakJobs non-empty (D-MUTUAL-07 / world-vote parity) Co-authored-by: Cursor <cursoragent@cursor.com>
- Scoped 403 + band-mapped success leak checks (C-09b) - Assert worker internal list still returns affection/trust Co-authored-by: Cursor <cursoragent@cursor.com>
- Mount createNpcRelationshipsRouter beside personal-timeline - Map edges via toRelationshipEdgeBandPublic; no affection/trust - Align auth with assertScopedPlayerRequest (C-09b / D-API-03) Co-authored-by: Cursor <cursoragent@cursor.com>
- Add broadcastRelationshipSync (hasUpdate only, no edge bodies) - Invoke from worker apply-deltas when linkedEdges non-empty - Cover hint shape + apply-deltas spy in route tests Co-authored-by: Cursor <cursoragent@cursor.com>
- Cover |Δ|≥4 REL-07, |Δ|≥8 linkedEdges hint, memory isolation, speak-yield drain Co-authored-by: Cursor <cursoragent@cursor.com>
- Short LLM dialogue → apply-deltas → REL-07 at |Δ|≥4 → linkedEdges hint at |Δ|≥8 - Dual activity + bubble presentation POST; optional __council__ memory only - Drain yields when speak/npc-turn backlog active (D-MUTUAL-07) Co-authored-by: Cursor <cursoragent@cursor.com>
- Colyseus mutualChatBubble + relationshipLinkedHint payloads (ids/text only) - Internal present + linked-edges-hint routes with worker auth - Client mergeLinkedEdgesHint unions undirected pairs for CouncilRosterPanel Co-authored-by: Cursor <cursoragent@cursor.com>
- Cover hard reject <-30, skeptical bias, reject-no-delta, initiator trust key - Dual caps and explicit phrasing weight for D-PLAYER-01…06 Co-authored-by: Cursor <cursoragent@cursor.com>
- Hybrid hard reject <-30, skeptical LLM <0, normal >=0 - EA-5 initiator attitudes only; reject never applies A↔B - Dual daily caps + player |Δ| 2–6 clamp Co-authored-by: Cursor <cursoragent@cursor.com>
- Cover pre-reply IC refusal, accept clamp, graph wiring, micro-penalty Co-authored-by: Cursor <cursoragent@cursor.com>
- Interactive graph node belief_gate_speak; IC refusal on reject - Accept applies clamped A↔B + REL-07; dual caps pre-apply - Memory tail only applies repeated-reject trust micro-penalty Co-authored-by: Cursor <cursoragent@cursor.com>
- Cover GET on open, band-only edges, and dirty refetch debounce ≤300ms Co-authored-by: Cursor <cursoragent@cursor.com>
- GET band-mapped edges on tab open with playerApiHeaders - relationshipSync dirty refetch debounced ≤300ms when tab focused - Render types strip affection/trust before state Co-authored-by: Cursor <cursoragent@cursor.com>
- Assert 关系网 tab after council, band chips, and no raw affection in DOM Co-authored-by: Cursor <cursoragent@cursor.com>
- RelationshipGraphPanel with ego/full SVG graph and band chips only - ShellDrawer tab after council; useNpcRelationships wired via ChatPage - relationshipSync listener + BEM CSS per 28-UI-SPEC Co-authored-by: Cursor <cursoragent@cursor.com>
- D-EMBED-02 embed text excludes affection/band tokens - D-EMBED-03 async write + searchSimilarEdges mock path Co-authored-by: Cursor <cursoragent@cursor.com>
- New Text bubble with ≤20 char clamp, 3–5s hold + fade tween - prefers-reduced-motion hard-hides at ~4s; frozen nameplates untouched Co-authored-by: Cursor <cursoragent@cursor.com>
- buildRelationshipEmbedText excludes affection/band from vector text - Fire-and-forget embed after applyRelationshipDeltas (not decay) - updateEmbeddingForEdge, searchSimilarEdges, lazy ensure helper Co-authored-by: Cursor <cursoragent@cursor.com>
- Worker relationship_rag with active-NPC-first + third-party analogy - Wire into fetch_dual_rag_context beside personal timeline RAG - Internal ensure-embedding and search-similar routes for lazy embed Co-authored-by: Cursor <cursoragent@cursor.com>
- Subscribe in useColyseusRoom with per-handler off(); bridge via registry - RoomScene shows Text bubble with ≤2 proximity gate; ChatPage pass-through Co-authored-by: Cursor <cursoragent@cursor.com>
- Real-LLM gate with mutual-chat present smoke and 关系网 band chips - Mark 28-VALIDATION nyquist_compliant; document E2E-POLICY phase 28 flow Co-authored-by: Cursor <cursoragent@cursor.com>
- EA-6: belief reject blocks A↔B; decay via applyIdleDecayDeltas; dyad supersede - C-09b verification lists player GET, relationshipSync, embed, verify:phase28 Co-authored-by: Cursor <cursoragent@cursor.com>
DialogueBar drawer actions are hidden until dialogue-overlay is engaged; match verify-phase25 pattern via dialogue-engage helper. Co-authored-by: Cursor <cursoragent@cursor.com>
Planning artifacts live under gitignored .planning/; keep the tracked agent entrypoint aligned with milestone archive and next-step guidance. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Warning Review limit reached
Next review available in: 56 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (15)
📝 WalkthroughWalkthroughThis PR adds Phase 28 NPC social systems: mutual-chat scheduling and worker processing, monthly idle relationship decay, relationship embeddings and graph synchronization, belief-gated player interactions, relationship-aware RAG context, and web UI support for relationship graphs and mutual-chat bubbles. ChangesNPC mutual chat
Relationship graph, embeddings, and RAG
Idle relationship decay
Belief-gated interactions
Phase 28 validation
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ad3b3b22c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| deltas: parsed.data.deltas, | ||
| voteEpoch: parsed.data.voteEpoch, | ||
| }); |
There was a problem hiding this comment.
Stamp relationship deltas with the room clock
When worker-driven relationship changes arrive through this route (world votes, mutual chats, or belief-gate applies), the call never supplies absoluteGameMinute, so applyRelationshipDeltas falls back to 0 for the last-interaction stamp. Edges changed shortly before a month rollover will therefore look idle to maybeRunRelationshipDecay and can be decayed almost immediately instead of one game month after their real interaction; pass the current room vote clock (or validate and forward it from the worker body) with this call.
Useful? React with 👍 / 👎.
| game_minute = int(snap.get("gameMinute") or snap.get("game_minute") or 0) | ||
| return f"day-{game_minute // 1440}" |
There was a problem hiding this comment.
Derive belief caps from a real day counter
In the interactive speak path, room_snapshot comes from /internal/rooms/:id/worker-state as a RoomState, which does not include gameMinute or absoluteGameMinute; this makes game_minute default to 0 and the key always day-0. As a result the per-day pair/player manipulation caps and repeated-reject trust penalty never reset during a long-running worker, so a player can permanently exhaust those limits until process restart.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/game-server/src/routes/internal-npc-relationships.ts (1)
86-95: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winPass
absoluteGameMinutethrough relationship delta writes.
apps/game-server/src/routes/internal-npc-relationships.ts#L86-L95is the only production caller, and it currently omits the room’s game minute.apps/game-server/src/world/npc-relationships-repository.ts#L489-L514then falls back to0, sonoteInteractAbsrecords stale timestamps and idle decay treats recently updated edges as idle after one month. SourcegetRoomVoteState(roomId).absoluteGameMinutehere and forward it.🤖 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/internal-npc-relationships.ts` around lines 86 - 95, Update the production caller around applyRelationshipDeltas in internal-npc-relationships.ts to read getRoomVoteState(roomId).absoluteGameMinute and pass it as absoluteGameMinute. Preserve the repository implementation in npc-relationships-repository.ts; its lines 489-539 require no direct change because the caller-supplied value prevents the fallback to 0.
🧹 Nitpick comments (3)
apps/web/src/hooks/useNpcRelationships.test.ts (1)
114-118: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer top-level import.
Since
useNpcRelationships.jsis already imported at the top of the file, its module-level execution has already occurred using real timers. The dynamic imports simply return the cached module namespace. Consider addingscheduleRelationshipDirtyRefetchto the top-level import list for consistency and readability.
apps/web/src/hooks/useNpcRelationships.test.ts#L114-L118: Remove the dynamic import and use the top-level import.apps/web/src/hooks/useNpcRelationships.test.ts#L124-L128: Remove the dynamic import here as well.🤖 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/useNpcRelationships.test.ts` around lines 114 - 118, Update the top-level import in useNpcRelationships.test.ts to include scheduleRelationshipDirtyRefetch, then remove the dynamic imports at lines 114-118 and 124-128 and use the top-level symbol in both tests.apps/web/src/hooks/useNpcRelationships.ts (1)
73-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the no-op key loop; it implies leak-stripping it doesn't perform.
This loop iterates
rowkeys with an empty body — it drops nothing. The actual guard againstaffection/trustleakage is the explicit whitelist object at Lines 64-72. Leaving misleading "drop affection/trust/etc" scaffolding here risks a future edit relying on it instead of the real construction.♻️ Proposed cleanup
- // Ensure no accidental leak keys survive even if TS widen. - for (const key of Object.keys(row)) { - if (!(RENDER_EDGE_KEYS as readonly string[]).includes(key)) { - /* drop affection/trust/etc */ - } - } return edge;🤖 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/useNpcRelationships.ts` around lines 73 - 78, Remove the no-op Object.keys(row) loop and its misleading comment from the relationship transformation, while preserving the explicit whitelist construction using RENDER_EDGE_KEYS as the sole protection against leaking fields such as affection and trust.scripts/verify-phase28.mjs (1)
1-320: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueScript logic looks correct; only static-analysis noise to flag.
Traced
healthOk→ensureRoom→ band-mapped edge wait/assert → mutual-chat force+smoke → Playwright relationship-graph UI checks → all internally consistent with the D-VERIFY-01/D-GRAPH-01/02/D-MUTUAL-02 goals stated in the header comment, and matches theassertE2eRealLlmreal-LLM gate required by the E2E policy guideline. No functional defects found.The reported
no-undeferrors forprocess/fetch/setTimeout/console/AbortSignalare Node.js globals used correctly here; this is a linter environment/globals configuration gap for this script path rather than a code bug in the diff.🤖 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/verify-phase28.mjs` around lines 1 - 320, Configure static analysis for scripts/verify-phase28.mjs to recognize Node.js globals such as process, fetch, setTimeout, console, and AbortSignal. Do not change the verified flow in healthOk, ensureRoom, assertBandMappedEdges, assertMutualChatActivitySmoke, or assertRelationshipGraphUi.Source: Linters/SAST tools
🤖 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/colyseus/GameRoom.ts`:
- Around line 443-448: Sequence the ambient enqueue operations in onAmbientTick
so enqueuePersonalTimelineDyadAmbientIfDue starts only after
enqueueNpcMutualChatIfDue has completed and registered its pair claims. Replace
the current back-to-back fire-and-forget calls with a promise chain or
equivalent sequencing, while keeping runRelationshipDecayIfDue in the existing
tick flow.
In `@apps/game-server/src/world/npc-relationship-decay.test.ts`:
- Around line 159-169: Bound the idle-function source extraction in the test
around applyIdleDecayDeltas by using the next repository method or function as
the slice endpoint, matching the sibling test’s pattern. Keep the
applyDeltaToRow prohibition scoped only to applyIdleDecayDeltas while preserving
the existing source-presence assertions.
In `@apps/game-server/src/world/npc-relationship-decay.ts`:
- Around line 94-116: Update the positive-affection branch in
computeIdleDecayDelta so the soft floor is applied only when affection is
already at or above that floor, preventing below-floor positive values from
increasing during decay. Preserve the existing 1–3 point negative decay and add
a regression test in npc-relationship-decay.test.ts for a positive below-floor
case such as ally affection 20.
- Around line 21-22: Persist and rehydrate decay tracking for SQL-backed rooms:
initialize lastInteractAbsByEdge and seedAbsByEdge from persisted relationship
rows, and load/save lastDecayMonthByRoom across restarts. Update the
relationship-decay persistence flow around these symbols so active edges retain
their timestamps and each month’s decay is applied only once.
In `@workers/agent-worker/src/council/belief_gate.py`:
- Around line 76-80: Prune stale day_key entries from _pair_claims,
_player_claims, _reject_counts, and _trust_penalty_claims during the normal
claim/note operations, removing entries older than the current game day before
adding or updating counts. Preserve current-day behavior and the existing
clear_manipulation_caps_for_test helper.
- Around line 241-245: The non-mock fallback in default_llm_judge() must fail
closed instead of returning accept when no social-provider judge is wired.
Update the fallback return path to produce a reject judgment with the skeptical
reason and zero proposed delta, while preserving the existing skeptical_bias
handling and mock behavior.
In `@workers/agent-worker/tests/test_npc_mutual_chat.py`:
- Around line 206-210: The assertion in the memory_calls loop is ineffective
because its condition ends with “or True.” Remove this dead assertion from the
test while preserving the meaningful player_id assertions and the existing
memory_calls coverage.
---
Outside diff comments:
In `@apps/game-server/src/routes/internal-npc-relationships.ts`:
- Around line 86-95: Update the production caller around applyRelationshipDeltas
in internal-npc-relationships.ts to read
getRoomVoteState(roomId).absoluteGameMinute and pass it as absoluteGameMinute.
Preserve the repository implementation in npc-relationships-repository.ts; its
lines 489-539 require no direct change because the caller-supplied value
prevents the fallback to 0.
---
Nitpick comments:
In `@apps/web/src/hooks/useNpcRelationships.test.ts`:
- Around line 114-118: Update the top-level import in
useNpcRelationships.test.ts to include scheduleRelationshipDirtyRefetch, then
remove the dynamic imports at lines 114-118 and 124-128 and use the top-level
symbol in both tests.
In `@apps/web/src/hooks/useNpcRelationships.ts`:
- Around line 73-78: Remove the no-op Object.keys(row) loop and its misleading
comment from the relationship transformation, while preserving the explicit
whitelist construction using RENDER_EDGE_KEYS as the sole protection against
leaking fields such as affection and trust.
In `@scripts/verify-phase28.mjs`:
- Around line 1-320: Configure static analysis for scripts/verify-phase28.mjs to
recognize Node.js globals such as process, fetch, setTimeout, console, and
AbortSignal. Do not change the verified flow in healthOk, ensureRoom,
assertBandMappedEdges, assertMutualChatActivitySmoke, or
assertRelationshipGraphUi.
🪄 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: dc120356-51c8-4747-8a63-65e39d833eac
📒 Files selected for processing (60)
AGENTS.mdapps/game-server/src/colyseus/GameRoom.tsapps/game-server/src/index.tsapps/game-server/src/queue/npc-mutual-chat.tsapps/game-server/src/routes/internal-npc-mutual-chat.tsapps/game-server/src/routes/internal-npc-relationships.tsapps/game-server/src/routes/npc-relationships.test.tsapps/game-server/src/routes/npc-relationships.tsapps/game-server/src/world/npc-mutual-chat.test.tsapps/game-server/src/world/npc-mutual-chat.tsapps/game-server/src/world/npc-relationship-decay.test.tsapps/game-server/src/world/npc-relationship-decay.tsapps/game-server/src/world/npc-relationships-embed.test.tsapps/game-server/src/world/npc-relationships-repository.tsapps/game-server/src/world/personal-timeline-dyad.tsapps/game-server/src/world/relationship-broadcast.test.tsapps/game-server/src/world/relationship-broadcast.tsapps/web/src/ChatPage.tsxapps/web/src/components/DialogueBar.tsxapps/web/src/components/PhaserGame.tsxapps/web/src/components/RelationshipGraphPanel.test.tsapps/web/src/components/RelationshipGraphPanel.tsxapps/web/src/components/ShellDrawer.test.tsapps/web/src/components/ShellDrawer.tsxapps/web/src/game/MutualChatBubble.test.tsapps/web/src/game/MutualChatBubble.tsapps/web/src/game/MutualChatBubbleLogic.tsapps/web/src/game/RoomScene.tsapps/web/src/hooks/useColyseusRoom.mutualChatBubble.test.tsapps/web/src/hooks/useColyseusRoom.tsapps/web/src/hooks/useCouncilDeliberation.test.tsapps/web/src/hooks/useCouncilDeliberation.tsapps/web/src/hooks/useNpcRelationships.test.tsapps/web/src/hooks/useNpcRelationships.tsapps/web/src/index.cssdocs/CONTRACTS.mddocs/ISSUE-LOG.mdpackage.jsonpackages/npc-memory/migrations/0012_npc_relationships_embedding.sqlpackages/npc-memory/migrations/meta/_journal.jsonpackages/npc-memory/src/schema.test.tspackages/npc-memory/src/schema.tspackages/shared/src/attitude.tspackages/shared/src/colyseus.tspackages/shared/src/councilRelationships.test.tspackages/shared/src/councilRelationships.tspackages/shared/src/index.tsscripts/verify-phase28.mjsworkers/agent-worker/src/council/belief_gate.pyworkers/agent-worker/src/council/memory_context.pyworkers/agent-worker/src/council/relationship_deltas.pyworkers/agent-worker/src/council/relationship_rag.pyworkers/agent-worker/src/graph/nodes/llm_social_turn.pyworkers/agent-worker/src/graph/npc_loop.pyworkers/agent-worker/src/graph/npc_mutual_chat.pyworkers/agent-worker/src/graph/state.pyworkers/agent-worker/src/main.pyworkers/agent-worker/tests/test_belief_gate.pyworkers/agent-worker/tests/test_npc_mutual_chat.pyworkers/agent-worker/tests/test_relationship_rag.py
| // Mutual-chat before ambient dyad so claimed pairs supersede silent dyad (D-MUTUAL / A2). | ||
| this.enqueueNpcMutualChatIfDue(); | ||
| this.enqueuePersonalTimelineDyadAmbientIfDue(); | ||
| // D-DECAY-04: silent idle decay — no speak-slot defer, no LLM, no relationshipSync. | ||
| this.runRelationshipDecayIfDue(); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Same-tick race lets ambient dyad slip past the mutual-chat "supersede" guarantee.
onAmbientTick calls enqueueNpcMutualChatIfDue() then enqueuePersonalTimelineDyadAmbientIfDue() back-to-back as fire-and-forget promises. maybeEnqueueNpcMutualChat (in world/npc-mutual-chat.ts) only registers pairClaims.add(claim) after await enqueueNpcMutualChatJob(...) resolves — but its entire candidate-selection loop, and maybeEnqueueDyadFromAmbient's isPairClaimedForMutualChat(...) check, run fully synchronously before either function's first await. Since both void ... calls execute synchronously back-to-back in the same tick, the dyad selector reads pre-claim state and can enqueue an ambient dyad for a pair the mutual-chat path is simultaneously claiming — exactly the scenario the inline comment ("Mutual-chat before ambient dyad so claimed pairs supersede silent dyad (D-MUTUAL / A2)") says must not happen. The existing test in apps/game-server/src/world/npc-mutual-chat.test.ts (lines 168-211) only passes because it awaits the mutual-chat call to completion before invoking the dyad function, which doesn't reflect this fire-and-forget call site.
Sequence the calls so the dyad enqueue only starts after mutual-chat claims are settled:
🐛 Proposed fix — sequence via Promise chain
- private enqueueNpcMutualChatIfDue(): void {
- if (this.npcSpeakJobs.size > 0) return;
+ private enqueueNpcMutualChatIfDue(): Promise<void> {
+ if (this.npcSpeakJobs.size > 0) return Promise.resolve();
const abs = getRoomVoteState(this.mapRoomId).absoluteGameMinute;
const { state: mapState } = getOrCreate(this.mapRoomId);
- void maybeEnqueueNpcMutualChat({
+ return maybeEnqueueNpcMutualChat({
roomId: this.mapRoomId,
npcs: mapState.npcs.map((n) => ({ id: n.id, x: n.x, y: n.y })),
absoluteGameMinute: abs,
gameMinuteOfDay: this.gameState.gameMinute,
busyNpcIds: this.npcSpeakJobs,
npcSpeakInFlight: false,
- }).catch((err) => {
- console.error("[GameRoom] npc-mutual-chat enqueue failed", err);
- });
+ })
+ .then(() => undefined)
+ .catch((err) => {
+ console.error("[GameRoom] npc-mutual-chat enqueue failed", err);
+ });
}- this.enqueueNpcMutualChatIfDue();
- this.enqueuePersonalTimelineDyadAmbientIfDue();
+ void this.enqueueNpcMutualChatIfDue().then(() =>
+ this.enqueuePersonalTimelineDyadAmbientIfDue(),
+ );Also applies to: 481-497
🤖 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/colyseus/GameRoom.ts` around lines 443 - 448, Sequence
the ambient enqueue operations in onAmbientTick so
enqueuePersonalTimelineDyadAmbientIfDue starts only after
enqueueNpcMutualChatIfDue has completed and registered its pair claims. Replace
the current back-to-back fire-and-forget calls with a promise chain or
equivalent sequencing, while keeping runRelationshipDecayIfDue in the existing
tick flow.
| it("decay path source does not call applyDeltaToRow", () => { | ||
| const here = dirname(fileURLToPath(import.meta.url)); | ||
| const src = readFileSync(join(here, "npc-relationship-decay.ts"), "utf8"); | ||
| const repo = readFileSync(join(here, "npc-relationships-repository.ts"), "utf8"); | ||
| expect(src).not.toMatch(/applyDeltaToRow/); | ||
| expect(src).toMatch(/applyIdleDecayDeltas/); | ||
| expect(repo).toMatch(/applyIdleDecayDeltas/); | ||
| // Idle path must not reuse the interact bump helper. | ||
| const idleFn = repo.slice(repo.indexOf("export async function applyIdleDecayDeltas")); | ||
| expect(idleFn).not.toMatch(/applyDeltaToRow\(/); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Unbounded .slice() makes this guardrail test check far more than applyIdleDecayDeltas.
repo.slice(repo.indexOf("export async function applyIdleDecayDeltas")) has no end index, so idleFn runs from that point to the end of the file — including every function defined after applyIdleDecayDeltas in npc-relationships-repository.ts. The not.toMatch(/applyDeltaToRow\(/) assertion therefore doesn't isolate the target function; it will incorrectly fail if any later function (e.g. an interact-path helper) calls applyDeltaToRow(, or trivially pass regardless of applyIdleDecayDeltas's own body if the real call site is elsewhere below. The sibling test at Line 177-178 in this same file correctly bounds its slice by finding the next method name — the same pattern should be applied here.
🔧 Proposed fix
- const idleFn = repo.slice(repo.indexOf("export async function applyIdleDecayDeltas"));
+ const idleStart = repo.indexOf("export async function applyIdleDecayDeltas");
+ const nextExportIdx = repo.indexOf("\nexport ", idleStart + 1);
+ const idleFn = nextExportIdx === -1 ? repo.slice(idleStart) : repo.slice(idleStart, nextExportIdx);
expect(idleFn).not.toMatch(/applyDeltaToRow\(/);📝 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.
| it("decay path source does not call applyDeltaToRow", () => { | |
| const here = dirname(fileURLToPath(import.meta.url)); | |
| const src = readFileSync(join(here, "npc-relationship-decay.ts"), "utf8"); | |
| const repo = readFileSync(join(here, "npc-relationships-repository.ts"), "utf8"); | |
| expect(src).not.toMatch(/applyDeltaToRow/); | |
| expect(src).toMatch(/applyIdleDecayDeltas/); | |
| expect(repo).toMatch(/applyIdleDecayDeltas/); | |
| // Idle path must not reuse the interact bump helper. | |
| const idleFn = repo.slice(repo.indexOf("export async function applyIdleDecayDeltas")); | |
| expect(idleFn).not.toMatch(/applyDeltaToRow\(/); | |
| }); | |
| it("decay path source does not call applyDeltaToRow", () => { | |
| const here = dirname(fileURLToPath(import.meta.url)); | |
| const src = readFileSync(join(here, "npc-relationship-decay.ts"), "utf8"); | |
| const repo = readFileSync(join(here, "npc-relationships-repository.ts"), "utf8"); | |
| expect(src).not.toMatch(/applyDeltaToRow/); | |
| expect(src).toMatch(/applyIdleDecayDeltas/); | |
| expect(repo).toMatch(/applyIdleDecayDeltas/); | |
| // Idle path must not reuse the interact bump helper. | |
| const idleStart = repo.indexOf("export async function applyIdleDecayDeltas"); | |
| const nextExportIdx = repo.indexOf("\nexport ", idleStart + 1); | |
| const idleFn = nextExportIdx === -1 ? repo.slice(idleStart) : repo.slice(idleStart, nextExportIdx); | |
| expect(idleFn).not.toMatch(/applyDeltaToRow\(/); | |
| }); |
🤖 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/world/npc-relationship-decay.test.ts` around lines 159 -
169, Bound the idle-function source extraction in the test around
applyIdleDecayDeltas by using the next repository method or function as the
slice endpoint, matching the sibling test’s pattern. Keep the applyDeltaToRow
prohibition scoped only to applyIdleDecayDeltas while preserving the existing
source-presence assertions.
| /** Last monthIndex for which decay ran (per room). */ | ||
| const lastDecayMonthByRoom = new Map<string, number>(); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n "lastInteractAbsByEdge|seedAbsByEdge" apps/game-server/src/world/npc-relationships-repository.ts -B3 -A8Repository: moyunzero/AetherLife
Length of output: 2210
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate guidance and inspect the relevant files with line numbers.
fd -a '^AGENTS\.md$' .
printf '\n--- AGENTS ---\n'
for f in $(fd -a '^AGENTS\.md$' .); do
echo "FILE: $f"
cat -n "$f"
echo
done
printf '\n--- world/npc-relationship-decay.ts (outline) ---\n'
ast-grep outline apps/game-server/src/world/npc-relationship-decay.ts --view expanded || true
printf '\n--- world/npc-relationship-decay.ts (selected lines) ---\n'
sed -n '1,260p' apps/game-server/src/world/npc-relationship-decay.ts | cat -n
printf '\n--- world/npc-relationships-repository.ts (selected lines) ---\n'
sed -n '130,260p' apps/game-server/src/world/npc-relationships-repository.ts | cat -nRepository: moyunzero/AetherLife
Length of output: 35145
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for any persistence or rehydration of the decay timestamps / month markers.
rg -n "lastDecayMonthByRoom|lastInteractAbsByEdge|seedAbsByEdge|getLastInteractAbsMinute|getSeedAbsMinute|noteInteractAbs|noteSeedAbs|last_interact_at|seed.*abs|decay" apps/game-server/src -SRepository: moyunzero/AetherLife
Length of output: 7675
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the decay logic and any code that loads relationship state from SQL.
sed -n '1,260p' apps/game-server/src/world/npc-relationship-decay.ts | cat -n
printf '\n--- repository search ---\n'
rg -n "load.*relationship|hydrate.*relationship|applyIdleDecay|lastDecayMonthByRoom|last_interact_at|memoryByRoom|getLastInteractAbsMinute|getSeedAbsMinute|noteInteractAbs|noteSeedAbs" apps/game-server/src -SRepository: moyunzero/AetherLife
Length of output: 12886
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the call site and any persistent month/clock tracking around it.
sed -n '420,480p' apps/game-server/src/colyseus/GameRoom.ts | cat -n
printf '\n--- persistent month / clock search ---\n'
rg -n "monthIndexFromAbsoluteMinute|lastDecayMonthByRoom|absoluteGameMinute|game month|monthIndex|last_decay|decayMonth|currentMonth|lastMonth" apps/game-server/src -S
printf '\n--- interaction hydration search ---\n'
sed -n '260,620p' apps/game-server/src/world/npc-relationships-repository.ts | cat -nRepository: moyunzero/AetherLife
Length of output: 26569
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Narrow in on the repository functions that mutate or load relationship rows.
rg -n "export async function (load|upsert|seed|apply|list|get|clear)|memoryByRoom.set|noteInteractAbs|noteSeedAbs|last_interact_at" apps/game-server/src/world/npc-relationships-repository.ts -n -A8 -B8Repository: moyunzero/AetherLife
Length of output: 7690
Persist decay state across restarts. lastInteractAbsByEdge / seedAbsByEdge are process-local, and lastDecayMonthByRoom resets on restart. Without rehydrating the stamps from persisted relationship rows and persisting the last-decay month, SQL-backed rooms can treat active edges as idle and apply the same month’s decay again.
🤖 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/world/npc-relationship-decay.ts` around lines 21 - 22,
Persist and rehydrate decay tracking for SQL-backed rooms: initialize
lastInteractAbsByEdge and seedAbsByEdge from persisted relationship rows, and
load/save lastDecayMonthByRoom across restarts. Update the relationship-decay
persistence flow around these symbols so active edges retain their timestamps
and each month’s decay is applied only once.
| # In-process dual-cap claims: (scope, day_key, id) → count | ||
| _pair_claims: dict[tuple[str, str, str], int] = {} | ||
| _player_claims: dict[tuple[str, str, str], int] = {} | ||
| _reject_counts: dict[tuple[str, str, str, str], int] = {} | ||
| _trust_penalty_claims: dict[tuple[str, str, str, str], int] = {} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
In-process cap/reject dicts grow unbounded per game day.
_pair_claims, _player_claims, _reject_counts, and _trust_penalty_claims are keyed by day_key (and room/player/pair) and are only ever cleared by clear_manipulation_caps_for_test. In a long-running worker, each new game day adds entries that are never evicted, so these dicts accumulate indefinitely. Consider pruning entries whose day_key is older than the current day when claiming/noting.
🤖 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/council/belief_gate.py` around lines 76 - 80, Prune
stale day_key entries from _pair_claims, _player_claims, _reject_counts, and
_trust_penalty_claims during the normal claim/note operations, removing entries
older than the current game day before adding or updating counts. Preserve
current-day behavior and the existing clear_manipulation_caps_for_test helper.
Apply-deltas and worker-state now carry absoluteGameMinute so idle decay and belief day-caps stop pinning to abs=0 / day-0. Also harden mutual-chat claim ordering, decay floor (no snap-up), unwired belief reject, and relationship UI/bubble edge cases from CodeRabbit review. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary by CodeRabbit
New Features
Bug Fixes