Skip to content

Gsd/phase 28 council relationship advanced - #22

Open
moyunzero wants to merge 37 commits into
mainfrom
gsd/phase-28-council-relationship-advanced
Open

Gsd/phase 28 council relationship advanced#22
moyunzero wants to merge 37 commits into
mainfrom
gsd/phase-28-council-relationship-advanced

Conversation

@moyunzero

@moyunzero moyunzero commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added a “关系网” relationship graph showing NPC connections, relationship bands, statuses, and selectable ego/full views.
    • Added NPC-to-NPC mutual conversations, including visible chat bubbles and relationship updates.
    • NPC dialogue can now reference relevant relationship history and social context.
    • Added safeguards for player-led provoke or joint-action interactions, including in-character refusals when conditions are not met.
  • Bug Fixes

    • Relationship updates now refresh the graph without exposing raw affection or trust values.
    • Idle relationships gradually return toward neutral over time.

moyunzero and others added 30 commits July 21, 2026 13:04
- 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>
moyunzero and others added 6 commits July 21, 2026 14:20
- 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>
@coderabbitai

coderabbitai Bot commented Jul 21, 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: 56 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: c93a7cec-5d1b-4ed8-8ad7-df9e13b45864

📥 Commits

Reviewing files that changed from the base of the PR and between 4ad3b3b and a5a321f.

📒 Files selected for processing (15)
  • apps/game-server/src/index.test.ts
  • apps/game-server/src/queue/npc-mutual-chat.ts
  • apps/game-server/src/routes/internal-npc-relationships.ts
  • apps/game-server/src/routes/npc-relationships.test.ts
  • apps/game-server/src/routes/rooms.ts
  • apps/game-server/src/world/npc-mutual-chat.ts
  • apps/game-server/src/world/npc-relationship-decay.test.ts
  • apps/game-server/src/world/npc-relationship-decay.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/game/RoomScene.ts
  • docs/CONTRACTS.md
  • docs/ISSUE-LOG.md
  • workers/agent-worker/src/council/belief_gate.py
  • workers/agent-worker/tests/test_belief_gate.py
  • workers/agent-worker/tests/test_npc_mutual_chat.py
📝 Walkthrough

Walkthrough

This 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.

Changes

NPC mutual chat

Layer / File(s) Summary
Scheduling and queueing
apps/game-server/src/world/npc-mutual-chat.ts, apps/game-server/src/queue/npc-mutual-chat.ts, workers/agent-worker/src/main.py
Selects eligible NPC pairs, applies daily caps and claims, queues jobs, and drains them with speech/backlog fairness.
Worker and server flow
workers/agent-worker/src/graph/npc_mutual_chat.py, apps/game-server/src/routes/internal-npc-mutual-chat.ts, apps/game-server/src/world/relationship-broadcast.ts
Generates dialogue, applies bilateral deltas, emits linked-edge hints, updates intent reasons, and broadcasts bubbles.
Client bubble delivery
packages/shared/src/colyseus.ts, apps/web/src/hooks/useColyseusRoom.ts, apps/web/src/game/*, apps/web/src/components/PhaserGame.tsx
Carries mutual-chat events into Phaser, deduplicates them, and renders proximity-gated bubbles with lifecycle cleanup.

Relationship graph, embeddings, and RAG

Layer / File(s) Summary
Relationship contracts and storage
packages/shared/src/councilRelationships.ts, packages/npc-memory/src/schema.ts, apps/game-server/src/world/npc-relationships-repository.ts
Adds band-mapped public edges, nullable 2048-dimensional embeddings, timestamp tracking, embedding refreshes, and similarity search.
Server APIs and synchronization
apps/game-server/src/routes/npc-relationships.ts, apps/game-server/src/routes/internal-npc-relationships.ts, apps/game-server/src/world/relationship-broadcast.ts
Adds public relationship reads, internal embedding/search endpoints, and hint-only relationship synchronization.
Worker RAG context
workers/agent-worker/src/council/relationship_rag.py, workers/agent-worker/src/council/memory_context.py
Ranks active and referenced third-party edges, requests lazy embeddings, formats paraphrased bullets, and merges them into council context.
Web relationship graph
apps/web/src/hooks/useNpcRelationships.ts, apps/web/src/components/RelationshipGraphPanel.tsx, apps/web/src/components/ShellDrawer.tsx, apps/web/src/ChatPage.tsx
Adds relationship fetching, debounced sync refetching, graph modes, band labels, drawer navigation, and graph styling.

Idle relationship decay

Layer / File(s) Summary
Monthly decay execution
apps/game-server/src/world/npc-relationship-decay.ts, apps/game-server/src/world/npc-relationships-repository.ts, apps/game-server/src/colyseus/GameRoom.ts
Applies bounded toward-zero affection changes to idle edges once per in-game month without interaction metadata or broadcast side effects.

Belief-gated interactions

Layer / File(s) Summary
Belief gate and graph integration
workers/agent-worker/src/council/belief_gate.py, workers/agent-worker/src/graph/npc_loop.py, workers/agent-worker/src/graph/nodes/llm_social_turn.py
Detects manipulation intent, evaluates trust and LLM decisions, enforces caps, routes rejected replies, and applies repeated-reject trust penalties.

Phase 28 validation

Layer / File(s) Summary
Contracts and verification
docs/CONTRACTS.md, docs/ISSUE-LOG.md, scripts/verify-phase28.mjs, package.json
Documents the relationship graph contract and adds a real-stack verification command covering APIs, mutual-chat activity, privacy shaping, and graph UI rendering.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.62% 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 clearly related to the PR’s main theme: Phase 28 council relationship features and related advanced behavior.
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 gsd/phase-28-council-relationship-advanced

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment on lines 89 to 91
deltas: parsed.data.deltas,
voteEpoch: parsed.data.voteEpoch,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +458 to +459
game_minute = int(snap.get("gameMinute") or snap.get("game_minute") or 0)
return f"day-{game_minute // 1440}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

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

Pass absoluteGameMinute through relationship delta writes.

apps/game-server/src/routes/internal-npc-relationships.ts#L86-L95 is the only production caller, and it currently omits the room’s game minute. apps/game-server/src/world/npc-relationships-repository.ts#L489-L514 then falls back to 0, so noteInteractAbs records stale timestamps and idle decay treats recently updated edges as idle after one month. Source getRoomVoteState(roomId).absoluteGameMinute here 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 value

Prefer top-level import.

Since useNpcRelationships.js is 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 adding scheduleRelationshipDirtyRefetch to 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 value

Remove the no-op key loop; it implies leak-stripping it doesn't perform.

This loop iterates row keys with an empty body — it drops nothing. The actual guard against affection/trust leakage 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 value

Script logic looks correct; only static-analysis noise to flag.

Traced healthOkensureRoom → 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 the assertE2eRealLlm real-LLM gate required by the E2E policy guideline. No functional defects found.

The reported no-undef errors for process/fetch/setTimeout/console/AbortSignal are 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

📥 Commits

Reviewing files that changed from the base of the PR and between 857dc96 and 4ad3b3b.

📒 Files selected for processing (60)
  • AGENTS.md
  • apps/game-server/src/colyseus/GameRoom.ts
  • apps/game-server/src/index.ts
  • apps/game-server/src/queue/npc-mutual-chat.ts
  • apps/game-server/src/routes/internal-npc-mutual-chat.ts
  • apps/game-server/src/routes/internal-npc-relationships.ts
  • apps/game-server/src/routes/npc-relationships.test.ts
  • apps/game-server/src/routes/npc-relationships.ts
  • apps/game-server/src/world/npc-mutual-chat.test.ts
  • apps/game-server/src/world/npc-mutual-chat.ts
  • apps/game-server/src/world/npc-relationship-decay.test.ts
  • apps/game-server/src/world/npc-relationship-decay.ts
  • apps/game-server/src/world/npc-relationships-embed.test.ts
  • apps/game-server/src/world/npc-relationships-repository.ts
  • apps/game-server/src/world/personal-timeline-dyad.ts
  • apps/game-server/src/world/relationship-broadcast.test.ts
  • apps/game-server/src/world/relationship-broadcast.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/components/DialogueBar.tsx
  • apps/web/src/components/PhaserGame.tsx
  • apps/web/src/components/RelationshipGraphPanel.test.ts
  • apps/web/src/components/RelationshipGraphPanel.tsx
  • apps/web/src/components/ShellDrawer.test.ts
  • apps/web/src/components/ShellDrawer.tsx
  • apps/web/src/game/MutualChatBubble.test.ts
  • apps/web/src/game/MutualChatBubble.ts
  • apps/web/src/game/MutualChatBubbleLogic.ts
  • apps/web/src/game/RoomScene.ts
  • apps/web/src/hooks/useColyseusRoom.mutualChatBubble.test.ts
  • apps/web/src/hooks/useColyseusRoom.ts
  • apps/web/src/hooks/useCouncilDeliberation.test.ts
  • apps/web/src/hooks/useCouncilDeliberation.ts
  • apps/web/src/hooks/useNpcRelationships.test.ts
  • apps/web/src/hooks/useNpcRelationships.ts
  • apps/web/src/index.css
  • docs/CONTRACTS.md
  • docs/ISSUE-LOG.md
  • package.json
  • packages/npc-memory/migrations/0012_npc_relationships_embedding.sql
  • packages/npc-memory/migrations/meta/_journal.json
  • packages/npc-memory/src/schema.test.ts
  • packages/npc-memory/src/schema.ts
  • packages/shared/src/attitude.ts
  • packages/shared/src/colyseus.ts
  • packages/shared/src/councilRelationships.test.ts
  • packages/shared/src/councilRelationships.ts
  • packages/shared/src/index.ts
  • scripts/verify-phase28.mjs
  • workers/agent-worker/src/council/belief_gate.py
  • workers/agent-worker/src/council/memory_context.py
  • workers/agent-worker/src/council/relationship_deltas.py
  • workers/agent-worker/src/council/relationship_rag.py
  • workers/agent-worker/src/graph/nodes/llm_social_turn.py
  • workers/agent-worker/src/graph/npc_loop.py
  • workers/agent-worker/src/graph/npc_mutual_chat.py
  • workers/agent-worker/src/graph/state.py
  • workers/agent-worker/src/main.py
  • workers/agent-worker/tests/test_belief_gate.py
  • workers/agent-worker/tests/test_npc_mutual_chat.py
  • workers/agent-worker/tests/test_relationship_rag.py

Comment on lines +443 to +448
// 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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

Comment on lines +159 to +169
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\(/);
});

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 | 🟡 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.

Suggested change
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.

Comment on lines +21 to +22
/** Last monthIndex for which decay ran (per room). */
const lastDecayMonthByRoom = new Map<string, 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.

🗄️ 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 -A8

Repository: 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 -n

Repository: 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 -S

Repository: 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 -S

Repository: 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 -n

Repository: 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 -B8

Repository: 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.

Comment thread apps/game-server/src/world/npc-relationship-decay.ts
Comment on lines +76 to +80
# 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] = {}

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 | ⚡ 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.

Comment thread workers/agent-worker/src/council/belief_gate.py Outdated
Comment thread workers/agent-worker/tests/test_npc_mutual_chat.py Outdated
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>
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