Skip to content

feat(27): Personal Life Timeline — biography, RAG, dual-track calendar - #20

Merged
moyunzero merged 29 commits into
mainfrom
phase/27-personal-life-timeline
Jul 20, 2026
Merged

feat(27): Personal Life Timeline — biography, RAG, dual-track calendar#20
moyunzero merged 29 commits into
mainfrom
phase/27-personal-life-timeline

Conversation

@moyunzero

@moyunzero moyunzero commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Ships Phase 27 personal life timeline end-to-end: npc_personal_timeline persistence (C-11), lifeNodes seeds, weekly/polish/multi/REL worker jobs, roster 传记 UI + hints, speak RAG, and proposalEligible feed for Phase 25.
  • Dual-track calendar (UAT revise): pre-arrival seeds use 生平·{age}; post-arrival / shared world uses 太乙 civil labels; seed repair on live rooms.
  • Non-vote dyad diaries: speak-mention / ambient co-presence → kind=event apply-deltas + forced bilateral REL entries; verify:phase27 waits for all 12 seats + real-LLM multi-perspective gate.

Test plan

  • pnpm --filter @aetherlife/shared test -- personalTimeline
  • pnpm --filter @aetherlife/game-server test -- personal-timeline
  • cd workers/agent-worker && LLM_MOCK=1 uv run pytest tests/test_personal_timeline.py tests/test_personal_timeline_rel07.py tests/test_personal_timeline_rag.py -q
  • pnpm verify:phase27 against pnpm dev:stack (real LLM; no mock)
  • Manual: open council roster → 传记 filters/expand; confirm「近况有更新」hint clears after open

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Added NPC personal timeline biographies (lifetime entries, relationships, council events, weekly diary summaries) with server-backed sync.
    • Council roster biographies now support filtering (all/relationship/council), previews, expansion, per-NPC update indicators, and error states.
  • Improvements
    • Timeline updates now refresh in real time via sync broadcasts.
    • Standardized civil calendar labels and pacing across world history, votes, and personal timelines.
    • Enhanced diary generation with persona-aware prompts and personal timeline RAG context.
  • Bug Fixes
    • Corrected genesis year and calendar labeling, including automatic repair of legacy genesis rows.

moyunzero and others added 25 commits July 19, 2026 10:51
- Encode 360-day year / month labels via aetherCivilFromEpochMinute cases
- Migrate worldHistory chronicle expectations off 1440-min=1-year

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

- Add aetherCivilFromEpochMinute (360-day year, month labels)
- Reimplement chronicleGameYearFromMinute via civil year 0=太乙元年
- Vote epoch uses dayIndex + VOTE_EPOCH_DAYS (default 5)
- Update seed/useWorldHistory labels; document D-CAL-06 in C-07b

Co-authored-by: Cursor <cursoragent@cursor.com>
- RED cases for insert/list, per-npc seq, proposalEligible (D-PROP-01)
- Isolation assertion: no npc_memories / __council__ / appendPlayerMemory

Co-authored-by: Cursor <cursoragent@cursor.com>
- Migration 0011 + Drizzle npcPersonalTimeline; SQL+memory repository
- Public GET and worker-only internal POST; proposalEligible D-PROP-01
- CONTRACTS C-11 + ISSUE-LOG Guardrail #112 bio isolation

Co-authored-by: Cursor <cursoragent@cursor.com>
- Lock D-SEED-02/03/05: 1:1 lifeNodes, idempotency, year-0 month labels + tags

Co-authored-by: Cursor <cursoragent@cursor.com>
- 1:1 lifeNode skeletons across 太乙元年 months 1–12; heuristic tags; inflight idempotency
- Redis polish job list; wire getOrCreate; personalTimelineSync hint constant

Co-authored-by: Cursor <cursoragent@cursor.com>
- Reflect/lore provider ban on zhipu speak slot
- Drain yields when speak busy or npc-turn backlog
- Weekly prompt asserts 第一人称 + 200–400 字

Co-authored-by: Cursor <cursoragent@cursor.com>
- Worker personal_timeline.py (reflect/lore only) + drain after speak
- Weekly stagger enqueue + PATCH body polish + hint-only Colyseus sync
- C-11 documents polish PATCH and personalTimelineSync payload

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

- BIO-06: shared eventAnchorId, locked factualSummary, stagger offsets, divergent bodies
- REL-07: threshold 8 bilateral enqueue; delta 7 no-op; status_tags path

Co-authored-by: Cursor <cursoragent@cursor.com>
- After world_history writeback enqueue 12 staggered multi jobs with locked factualSummary
- After relationship deltas enqueue bilateral REL-07 jobs at |Δ|≥8 (reuse HISTORY_SUMMARY_DELTA_THRESHOLD)
- Process multi/rel kinds via reflect/lore; game-server queue types for multi/rel

Co-authored-by: Cursor <cursoragent@cursor.com>
- Cover 全部/关系/议会 filters, 40–60 char preview, hint clear, no toast
- Slot tests use .ts (vitest include) matching WorldHistoryPanel pattern

Co-authored-by: Cursor <cursoragent@cursor.com>
- usePersonalTimeline GET + read cursors + reconnect reconcile (no toast)
- CouncilBiographySlot filters/expand; roster「近况有更新」clears on open
- personalTimelineSync onMessage unsubscribe only (never removeAllListeners)

Co-authored-by: Cursor <cursoragent@cursor.com>
- RED tests for BIO-09 topic-hit/miss/paraphrase personal timeline RAG
- verify-phase27.mjs with assertE2eRealLlm and TODO stubs for Task 2

Co-authored-by: Cursor <cursoragent@cursor.com>
- BIO-09: personal_timeline_rag + dual-RAG injection for npc-1…12
- BIO-07: proposalEligible read feed into world_vote draft_proposal
- BIO-10 script: verify:phase27 + GF-11 map + C-11 speak RAG notes

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Close Phase 27 UAT revise: pre-arrival 生平· seeds vs post-arrival 太乙 labels, speak/ambient dyad REL jobs, seed repair on live rooms, and verify:phase27 waits for all 12 seats.

Co-authored-by: Cursor <cursoragent@cursor.com>
Expect genesis gameYear 0 (D-CAL SSOT) and clear REDIS_URL/job claims in weekly/dyad tests so sibling dotenv load cannot hang Redis claims.

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

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

Next review available in: 26 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: 70e92afb-3a29-4382-a271-350943ee42bd

📥 Commits

Reviewing files that changed from the base of the PR and between 16b310f and 0096a03.

📒 Files selected for processing (63)
  • apps/game-server/src/ambient/README.md
  • apps/game-server/src/colyseus/GameRoom.ts
  • apps/game-server/src/index.test.ts
  • apps/game-server/src/index.ts
  • apps/game-server/src/queue/personal-timeline.ts
  • apps/game-server/src/queue/world-vote.ts
  • apps/game-server/src/room/store.ts
  • apps/game-server/src/routes/internal-personal-timeline.ts
  • apps/game-server/src/routes/internal-world-vote-trigger.ts
  • apps/game-server/src/routes/personal-timeline.test.ts
  • apps/game-server/src/routes/personal-timeline.ts
  • apps/game-server/src/sse/hub.ts
  • apps/game-server/src/world/personal-timeline-broadcast.ts
  • apps/game-server/src/world/personal-timeline-dyad.test.ts
  • apps/game-server/src/world/personal-timeline-dyad.ts
  • apps/game-server/src/world/personal-timeline-repository.test.ts
  • apps/game-server/src/world/personal-timeline-repository.ts
  • apps/game-server/src/world/personal-timeline-seed.test.ts
  • apps/game-server/src/world/personal-timeline-seed.ts
  • apps/game-server/src/world/personal-timeline-weekly.test.ts
  • apps/game-server/src/world/personal-timeline-weekly.ts
  • apps/game-server/src/world/world-history-seed.test.ts
  • apps/game-server/src/world/world-history-seed.ts
  • apps/game-server/src/world/world-vote-trigger.test.ts
  • apps/game-server/src/world/world-vote-trigger.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/components/CouncilBiographySlot.test.ts
  • apps/web/src/components/CouncilBiographySlot.tsx
  • apps/web/src/components/CouncilRosterPanel.tsx
  • apps/web/src/components/ShellDrawer.tsx
  • apps/web/src/hooks/useColyseusRoom.ts
  • apps/web/src/hooks/usePersonalTimeline.test.ts
  • apps/web/src/hooks/usePersonalTimeline.ts
  • apps/web/src/hooks/useWorldHistory.test.ts
  • apps/web/src/hooks/useWorldHistory.ts
  • apps/web/src/index.css
  • docs/CONTRACTS.md
  • docs/ISSUE-LOG.md
  • package.json
  • packages/npc-memory/migrations/0011_npc_personal_timeline.sql
  • packages/npc-memory/migrations/meta/_journal.json
  • packages/npc-memory/src/index.ts
  • packages/npc-memory/src/schema.ts
  • packages/shared/src/colyseus.ts
  • packages/shared/src/index.ts
  • packages/shared/src/personalTimeline.test.ts
  • packages/shared/src/personalTimeline.ts
  • packages/shared/src/worldHistory.test.ts
  • packages/shared/src/worldHistory.ts
  • scripts/lib/agent-verify-map.mjs
  • scripts/uat-phase27-persona-diary.mjs
  • scripts/verify-phase27.mjs
  • workers/agent-worker/src/council/memory_context.py
  • workers/agent-worker/src/council/personal_timeline_rag.py
  • workers/agent-worker/src/council/relationship_deltas.py
  • workers/agent-worker/src/council/speak_registry.py
  • workers/agent-worker/src/graph/personal_timeline.py
  • workers/agent-worker/src/graph/world_vote.py
  • workers/agent-worker/src/main.py
  • workers/agent-worker/tests/test_personal_timeline.py
  • workers/agent-worker/tests/test_personal_timeline_rag.py
  • workers/agent-worker/tests/test_personal_timeline_rel07.py
  • workers/agent-worker/tests/test_world_vote.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase/27-personal-life-timeline

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.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds Phase 27 personal timelines across shared calendar contracts, database storage, game-server APIs and scheduling, worker-generated entries, Colyseus synchronization, council biography UI, RAG context, relationship events, and end-to-end verification.

Changes

Personal timeline lifecycle

Layer / File(s) Summary
Timeline contract and persistence
packages/shared/*, packages/npc-memory/*, apps/game-server/src/world/personal-timeline-repository.ts
Adds civil/lifetime calendar utilities, typed timeline entries, proposal eligibility, database schema, SQL persistence, in-memory fallback, sequencing, listing, and updates.
Server APIs and scheduling
apps/game-server/src/routes/*personal-timeline*, apps/game-server/src/queue/personal-timeline.ts, apps/game-server/src/world/personal-timeline-{seed,weekly,dyad}.ts, apps/game-server/src/colyseus/GameRoom.ts
Adds authenticated writeback and public read routes, Redis job claims, room seeding and repair, weekly scheduling, speak/ambient dyads, and ambient-tick integration.
Worker generation and vote integration
workers/agent-worker/src/graph/personal_timeline.py, workers/agent-worker/src/graph/world_vote.py, workers/agent-worker/src/council/*, workers/agent-worker/src/main.py
Processes timeline jobs, generates persona-aware BIO/weekly/multi/relationship content, merges RAG context, and connects vote events and relationship deltas to timeline jobs.
Biography synchronization and UI
packages/shared/src/colyseus.ts, apps/web/src/hooks/*PersonalTimeline*, apps/web/src/components/*Biography*, apps/web/src/ChatPage.tsx
Adds hint-only Colyseus synchronization, per-NPC client state, biography filtering and previews, roster integration, update indicators, and styling.
Calendar migration and verification
apps/game-server/src/world/world-*, packages/shared/src/worldHistory.ts, scripts/*phase27*, docs/*
Changes chronicle year zero and day-based vote cadence, documents the calendar contract, and adds Phase 27 verification and persona-diary UAT flows.

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.71% 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 clearly summarizes the Phase 27 personal life timeline work, including biography, RAG, and calendar changes.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase/27-personal-life-timeline

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.

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

🧹 Nitpick comments (2)
packages/npc-memory/src/schema.ts (1)

194-203: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant index duplicates the unique constraint's index.

The unique("npc_personal_timeline_room_npc_seq").on(roomId, npcId, seq) constraint is already backed by a unique index over the exact same columns in the same order, which fully serves the repository's WHERE room_id AND npc_id ORDER BY seq DESC list query and the MAX(seq) lookup in insertSqlRow. The separate npc_personal_timeline_room_npc_seq_idx adds write amplification and storage with no query benefit.

♻️ Proposed fix
     unique("npc_personal_timeline_room_npc_seq").on(
       table.roomId,
       table.npcId,
       table.seq,
     ),
-    index("npc_personal_timeline_room_npc_seq_idx").on(
-      table.roomId,
-      table.npcId,
-      table.seq,
-    ),
     index("npc_personal_timeline_room_anchor_idx").on(
       table.roomId,
       table.eventAnchorId,
     ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/npc-memory/src/schema.ts` around lines 194 - 203, Remove the
redundant `index("npc_personal_timeline_room_npc_seq_idx")` declaration from the
schema while retaining the `unique("npc_personal_timeline_room_npc_seq")`
constraint on `table.roomId`, `table.npcId`, and `table.seq`.
apps/game-server/src/world/personal-timeline-weekly.ts (1)

43-105: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Best-effort clue gathering swallows all errors silently.

Every try/catch in assembleWeeklyRecentBullets discards the error with no logging. If listWorldHistory, listPersonalTimelineForNpc, or listRelationshipsForRoom starts failing consistently (bad query, schema drift), weekly digests will silently degrade to empty bullet lists with no operational signal.

🤖 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/personal-timeline-weekly.ts` around lines 43 -
105, Update assembleWeeklyRecentBullets so each best-effort catch logs the
caught error with enough context to identify whether world history, the NPC
timeline, relationships, or peer timelines failed, while preserving the existing
fallback behavior of continuing and returning available bullets.
🤖 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/queue/personal-timeline.ts`:
- Line 97: Gate every mockJobs.set call in enqueuePersonalTimelinePolishJob,
enqueuePersonalTimelineWeeklyJob, enqueuePersonalTimelineMultiJob,
enqueuePersonalTimelineRelJob, and enqueuePersonalTimelineEventJob so payloads
are stored only when Redis is not configured. Reuse getRedisUrl (or have
lpushJob expose whether Redis was used) consistently across all five enqueue
paths, preserving mock inspection when running without Redis.
- Around line 192-216: Update enqueuePersonalTimelinePolishJob,
enqueuePersonalTimelineMultiJob, and enqueuePersonalTimelineRelJob to call
claimPersonalTimelineJobId with their deterministic job IDs before lpushJob,
matching enqueuePersonalTimelineWeeklyJob and enqueuePersonalTimelineEventJob.
Only push and record the payload after the claim succeeds, and return null when
the claim indicates the job was already enqueued; preserve the documented jobId
return behavior for newly claimed jobs.

In `@apps/game-server/src/world/personal-timeline-dyad.ts`:
- Around line 99-125: Update maybeEnqueueDyadFromSpeak and
maybeEnqueueDyadFromAmbient so dyadDayClaims.add(claim) occurs only after
enqueuePersonalTimelineEventJob completes successfully or returns null for an
already-held durable claim; if the enqueue call throws, remove or avoid
recording the local claim so a later retry remains possible. Preserve the
existing no-spam behavior for the null jobId path.

In `@apps/web/src/hooks/usePersonalTimeline.ts`:
- Around line 192-196: Update the fallback handling in mergePersonalTimelineSync
around shouldShowBiographyHint so an unknown latest sequence explicitly shows
the biography hint instead of passing Number.POSITIVE_INFINITY. Preserve
shouldShowBiographyHint’s finite-value validation for known sequences, and only
invoke it when a finite latest sequence is available.

---

Nitpick comments:
In `@apps/game-server/src/world/personal-timeline-weekly.ts`:
- Around line 43-105: Update assembleWeeklyRecentBullets so each best-effort
catch logs the caught error with enough context to identify whether world
history, the NPC timeline, relationships, or peer timelines failed, while
preserving the existing fallback behavior of continuing and returning available
bullets.

In `@packages/npc-memory/src/schema.ts`:
- Around line 194-203: Remove the redundant
`index("npc_personal_timeline_room_npc_seq_idx")` declaration from the schema
while retaining the `unique("npc_personal_timeline_room_npc_seq")` constraint on
`table.roomId`, `table.npcId`, and `table.seq`.
🪄 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: 70e92afb-3a29-4382-a271-350943ee42bd

📥 Commits

Reviewing files that changed from the base of the PR and between 16b310f and 0096a03.

📒 Files selected for processing (63)
  • apps/game-server/src/ambient/README.md
  • apps/game-server/src/colyseus/GameRoom.ts
  • apps/game-server/src/index.test.ts
  • apps/game-server/src/index.ts
  • apps/game-server/src/queue/personal-timeline.ts
  • apps/game-server/src/queue/world-vote.ts
  • apps/game-server/src/room/store.ts
  • apps/game-server/src/routes/internal-personal-timeline.ts
  • apps/game-server/src/routes/internal-world-vote-trigger.ts
  • apps/game-server/src/routes/personal-timeline.test.ts
  • apps/game-server/src/routes/personal-timeline.ts
  • apps/game-server/src/sse/hub.ts
  • apps/game-server/src/world/personal-timeline-broadcast.ts
  • apps/game-server/src/world/personal-timeline-dyad.test.ts
  • apps/game-server/src/world/personal-timeline-dyad.ts
  • apps/game-server/src/world/personal-timeline-repository.test.ts
  • apps/game-server/src/world/personal-timeline-repository.ts
  • apps/game-server/src/world/personal-timeline-seed.test.ts
  • apps/game-server/src/world/personal-timeline-seed.ts
  • apps/game-server/src/world/personal-timeline-weekly.test.ts
  • apps/game-server/src/world/personal-timeline-weekly.ts
  • apps/game-server/src/world/world-history-seed.test.ts
  • apps/game-server/src/world/world-history-seed.ts
  • apps/game-server/src/world/world-vote-trigger.test.ts
  • apps/game-server/src/world/world-vote-trigger.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/components/CouncilBiographySlot.test.ts
  • apps/web/src/components/CouncilBiographySlot.tsx
  • apps/web/src/components/CouncilRosterPanel.tsx
  • apps/web/src/components/ShellDrawer.tsx
  • apps/web/src/hooks/useColyseusRoom.ts
  • apps/web/src/hooks/usePersonalTimeline.test.ts
  • apps/web/src/hooks/usePersonalTimeline.ts
  • apps/web/src/hooks/useWorldHistory.test.ts
  • apps/web/src/hooks/useWorldHistory.ts
  • apps/web/src/index.css
  • docs/CONTRACTS.md
  • docs/ISSUE-LOG.md
  • package.json
  • packages/npc-memory/migrations/0011_npc_personal_timeline.sql
  • packages/npc-memory/migrations/meta/_journal.json
  • packages/npc-memory/src/index.ts
  • packages/npc-memory/src/schema.ts
  • packages/shared/src/colyseus.ts
  • packages/shared/src/index.ts
  • packages/shared/src/personalTimeline.test.ts
  • packages/shared/src/personalTimeline.ts
  • packages/shared/src/worldHistory.test.ts
  • packages/shared/src/worldHistory.ts
  • scripts/lib/agent-verify-map.mjs
  • scripts/uat-phase27-persona-diary.mjs
  • scripts/verify-phase27.mjs
  • workers/agent-worker/src/council/memory_context.py
  • workers/agent-worker/src/council/personal_timeline_rag.py
  • workers/agent-worker/src/council/relationship_deltas.py
  • workers/agent-worker/src/council/speak_registry.py
  • workers/agent-worker/src/graph/personal_timeline.py
  • workers/agent-worker/src/graph/world_vote.py
  • workers/agent-worker/src/main.py
  • workers/agent-worker/tests/test_personal_timeline.py
  • workers/agent-worker/tests/test_personal_timeline_rag.py
  • workers/agent-worker/tests/test_personal_timeline_rel07.py
  • workers/agent-worker/tests/test_world_vote.py

/** Claim TTL: ~16 Aether days in wall-clock is overkill; 14d wall is enough for restart dedupe. */
const JOB_CLAIM_TTL_SECONDS = 60 * 60 * 24 * 14;

const mockJobs = new Map<string, PersonalTimelineJobPayload>();

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

mockJobs grows unbounded in production, regardless of REDIS_URL.

Every enqueue function (enqueuePersonalTimelinePolishJob, ...WeeklyJob, ...MultiJob, ...RelJob, ...EventJob) unconditionally calls mockJobs.set(jobId, payload) after lpushJob, with no gate on whether Redis is configured. The library context confirms the intent was "mock job inspection to support tests/single-process runs", but the code stores every payload (including full skeletonBody/factualSummary/historyAppend text) in this module-level Map for the lifetime of the process — even when REDIS_URL is set and jobs are actually delivered via Redis. Given this queue backs weekly digests, 12-seat multi-perspective jobs, rel jobs, and dyad events over the entire life of a long-running room, this map will grow without bound and is a genuine OOM risk in production.

Tests in this cohort (personal-timeline-weekly.test.ts, personal-timeline-dyad.test.ts) always delete process.env.REDIS_URL before calling these enqueuers, so gating the mock-store behind the "no Redis" branch would preserve all existing test behavior while fixing production.

🛡️ Proposed fix (apply the same pattern to all 5 enqueue functions)
 export async function enqueuePersonalTimelinePolishJob(input: {
   ...
 }): Promise<string | null> {
   const jobId = personalTimelinePolishJobId(...);
   const payload: PersonalTimelinePolishJobPayload = {
     kind: "polish",
     ...input,
     jobId,
     enqueuedAt: new Date().toISOString(),
   };

-  await lpushJob(payload);
-  mockJobs.set(jobId, payload);
+  const pushedToRedis = await lpushJob(payload);
+  if (!pushedToRedis) {
+    mockJobs.set(jobId, payload);
+  }
   return jobId;
 }

(lpushJob would need to return a boolean indicating whether Redis was used, or simply gate on !getRedisUrl() directly at each call site.)

Also applies to: 192-216, 221-249, 254-284, 289-319, 325-355, 357-361

🤖 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/queue/personal-timeline.ts` at line 97, Gate every
mockJobs.set call in enqueuePersonalTimelinePolishJob,
enqueuePersonalTimelineWeeklyJob, enqueuePersonalTimelineMultiJob,
enqueuePersonalTimelineRelJob, and enqueuePersonalTimelineEventJob so payloads
are stored only when Redis is not configured. Reuse getRedisUrl (or have
lpushJob expose whether Redis was used) consistently across all five enqueue
paths, preserving mock inspection when running without Redis.

Comment thread apps/game-server/src/queue/personal-timeline.ts
Comment on lines +99 to +125
const claim = pairClaimKey(input.roomId, dayIndex, input.speakerNpcId, peer);
if (dyadDayClaims.has(claim)) return null;
dyadDayClaims.add(claim);

const { npcAId, npcBId } = normalizeEdgeIds(input.speakerNpcId, peer);
const eventAnchorId = `dyad-speak-${input.roomId}-${dayIndex}-${npcAId}-${npcBId}`;
const factualSummary = truncateFact(
`提及同僚:${truncateFact(combined, 100)}`,
);
const affectionDelta = affectionDeltaFromSpeakText(combined);

const jobId = await enqueuePersonalTimelineEventJob({
roomId: input.roomId,
npcId: input.speakerNpcId,
counterpartNpcId: peer,
eventAnchorId,
factualSummary,
affectionDelta,
aetherEpochMinute: abs,
historyAppend: factualSummary,
});
if (!jobId) {
// Durable claim already held — keep day claim to avoid spam.
return null;
}
return jobId;
}

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 | 🟡 Minor | ⚡ Quick win

Local claim is recorded before the enqueue call is known to succeed.

In both maybeEnqueueDyadFromSpeak (lines 100-101) and maybeEnqueueDyadFromAmbient (line 184), dyadDayClaims.add(claim) runs before the await enqueuePersonalTimelineEventJob(...) call. If that call throws (e.g. a transient Redis error inside claimPersonalTimelineJobId/lpushJob in queue/personal-timeline.ts), the exception propagates up to the caller's .catch (in GameRoom.ts/sse/hub.ts), but the local claim is already permanently held for that room/day/pair — so no retry is possible for the rest of that in-game day even though nothing was actually durably claimed or enqueued. The if (!jobId) { // Durable claim already held — keep day claim to avoid spam. } comment on line 121-122 only covers the "returns null" case, not the "throws" case.

🩹 Proposed fix (speak path; same pattern applies to the ambient loop)
   const claim = pairClaimKey(input.roomId, dayIndex, input.speakerNpcId, peer);
   if (dyadDayClaims.has(claim)) return null;
   dyadDayClaims.add(claim);

   const { npcAId, npcBId } = normalizeEdgeIds(input.speakerNpcId, peer);
   const eventAnchorId = `dyad-speak-${input.roomId}-${dayIndex}-${npcAId}-${npcBId}`;
   const factualSummary = truncateFact(
     `提及同僚:${truncateFact(combined, 100)}`,
   );
   const affectionDelta = affectionDeltaFromSpeakText(combined);

-  const jobId = await enqueuePersonalTimelineEventJob({
-    roomId: input.roomId,
-    npcId: input.speakerNpcId,
-    counterpartNpcId: peer,
-    eventAnchorId,
-    factualSummary,
-    affectionDelta,
-    aetherEpochMinute: abs,
-    historyAppend: factualSummary,
-  });
-  if (!jobId) {
-    // Durable claim already held — keep day claim to avoid spam.
-    return null;
-  }
-  return jobId;
+  try {
+    const jobId = await enqueuePersonalTimelineEventJob({
+      roomId: input.roomId,
+      npcId: input.speakerNpcId,
+      counterpartNpcId: peer,
+      eventAnchorId,
+      factualSummary,
+      affectionDelta,
+      aetherEpochMinute: abs,
+      historyAppend: factualSummary,
+    });
+    // Durable claim already held when jobId is null — keep day claim to avoid spam.
+    return jobId;
+  } catch (err) {
+    dyadDayClaims.delete(claim); // allow a later retry this day on transient failure
+    throw err;
+  }

Also applies to: 179-210

🤖 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/personal-timeline-dyad.ts` around lines 99 - 125,
Update maybeEnqueueDyadFromSpeak and maybeEnqueueDyadFromAmbient so
dyadDayClaims.add(claim) occurs only after enqueuePersonalTimelineEventJob
completes successfully or returns null for an already-held durable claim; if the
enqueue call throws, remove or avoid recording the local claim so a later retry
remains possible. Preserve the existing no-spam behavior for the null jobId
path.

Comment on lines +192 to +196
const knownLatest = latestSeqRef.current[npcId];
const show =
hasUpdate &&
shouldShowBiographyHint(knownLatest ?? latestSeq ?? Number.POSITIVE_INFINITY, readCursorRef.current[npcId]);
if (!show) 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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Number.POSITIVE_INFINITY fallback is silently rejected by shouldShowBiographyHint.

shouldShowBiographyHint returns false whenever !Number.isFinite(latestSeq) (Line 58), but mergePersonalTimelineSync feeds it Number.POSITIVE_INFINITY as the "unknown latest" fallback (Line 195). The intent is clearly "if we don't know the latest seq, show the hint anyway," but the guard makes this fallback always resolve to false — the opposite of the apparent intent. Currently unreachable because the only emitter (internal-personal-timeline.ts) always includes latestSeq, but it's a silent trap if that ever changes.

🐛 Proposed fix
-      const show =
-        hasUpdate &&
-        shouldShowBiographyHint(knownLatest ?? latestSeq ?? Number.POSITIVE_INFINITY, readCursorRef.current[npcId]);
+      const show =
+        hasUpdate &&
+        (knownLatest === undefined && latestSeq === undefined
+          ? true
+          : shouldShowBiographyHint(knownLatest ?? latestSeq, readCursorRef.current[npcId]));
📝 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 knownLatest = latestSeqRef.current[npcId];
const show =
hasUpdate &&
shouldShowBiographyHint(knownLatest ?? latestSeq ?? Number.POSITIVE_INFINITY, readCursorRef.current[npcId]);
if (!show) return;
const knownLatest = latestSeqRef.current[npcId];
const show =
hasUpdate &&
(knownLatest === undefined && latestSeq === undefined
? true
: shouldShowBiographyHint(knownLatest ?? latestSeq, readCursorRef.current[npcId]));
if (!show) 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/hooks/usePersonalTimeline.ts` around lines 192 - 196, Update the
fallback handling in mergePersonalTimelineSync around shouldShowBiographyHint so
an unknown latest sequence explicitly shows the biography hint instead of
passing Number.POSITIVE_INFINITY. Preserve shouldShowBiographyHint’s
finite-value validation for known sequences, and only invoke it when a finite
latest sequence is available.

moyunzero and others added 2 commits July 20, 2026 10:09
Close post-CR gaps: atomic claim+LPUSH with release-on-fail, Redis dyad
day claims, genesis year repair, vote_epoch/REL-07 isolation, and sync
biography refresh with per-npc request sequencing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sibling dotenv injects Postgres URL and assembleWeeklyRecentBullets can
hang the 5s vitest timeout under full-suite pre-push.

Co-authored-by: Cursor <cursoragent@cursor.com>
moyunzero and others added 2 commits July 20, 2026 10:17
Unit tests pass redis_client=None without settings; auto get_settings()
was loading .env Redis and durable claims blocked every enqueue.

Co-authored-by: Cursor <cursoragent@cursor.com>
Full-suite pre-push occasionally saw GET 404/403 when a live room existed
for the shared room id without the test player connected.

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: 1

🤖 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/web/src/hooks/usePersonalTimeline.ts`:
- Around line 242-254: Update the biography sync flow in usePersonalTimeline,
specifically mergePersonalTimelineSync, to accept and use an explicit
currently-open flag rather than treating cache presence as sufficient. Have
CouncilRosterPanel derive that flag from each row’s details toggle and pass it
through, so fetchTimeline and applyFetchedEntries only refresh open biographies
and do not clear hints or read cursors after closure.
🪄 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: fddf4540-f1c6-4120-81fa-746b7ba39f5e

📥 Commits

Reviewing files that changed from the base of the PR and between 0096a03 and 8fbc3f4.

📒 Files selected for processing (26)
  • apps/game-server/src/queue/personal-timeline.claim.test.ts
  • apps/game-server/src/queue/personal-timeline.ts
  • apps/game-server/src/routes/personal-timeline.test.ts
  • apps/game-server/src/world/personal-timeline-dyad.test.ts
  • apps/game-server/src/world/personal-timeline-dyad.ts
  • apps/game-server/src/world/personal-timeline-seed.test.ts
  • apps/game-server/src/world/personal-timeline-weekly.test.ts
  • apps/game-server/src/world/world-history-repository.ts
  • apps/game-server/src/world/world-history-seed.test.ts
  • apps/game-server/src/world/world-history-seed.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/components/CouncilBiographySlot.tsx
  • apps/web/src/components/CouncilRosterPanel.tsx
  • apps/web/src/components/ShellDrawer.tsx
  • apps/web/src/hooks/usePersonalTimeline.test.ts
  • apps/web/src/hooks/usePersonalTimeline.ts
  • apps/web/src/index.css
  • docs/CONTRACTS.md
  • docs/ISSUE-LOG.md
  • packages/npc-memory/src/schema.ts
  • scripts/uat-phase27-persona-diary.mjs
  • workers/agent-worker/src/graph/personal_timeline.py
  • workers/agent-worker/src/graph/world_vote.py
  • workers/agent-worker/tests/test_personal_timeline.py
  • workers/agent-worker/tests/test_personal_timeline_rag.py
  • workers/agent-worker/tests/test_personal_timeline_rel07.py
🚧 Files skipped from review as they are similar to previous changes (17)
  • apps/web/src/index.css
  • apps/game-server/src/world/personal-timeline-seed.test.ts
  • apps/web/src/hooks/usePersonalTimeline.test.ts
  • apps/web/src/ChatPage.tsx
  • apps/web/src/components/CouncilBiographySlot.tsx
  • apps/game-server/src/world/personal-timeline-weekly.test.ts
  • apps/web/src/components/ShellDrawer.tsx
  • packages/npc-memory/src/schema.ts
  • apps/web/src/components/CouncilRosterPanel.tsx
  • apps/game-server/src/world/personal-timeline-dyad.ts
  • docs/ISSUE-LOG.md
  • workers/agent-worker/tests/test_personal_timeline.py
  • apps/game-server/src/queue/personal-timeline.ts
  • docs/CONTRACTS.md
  • scripts/uat-phase27-persona-diary.mjs
  • apps/game-server/src/routes/personal-timeline.test.ts
  • workers/agent-worker/src/graph/personal_timeline.py

Comment on lines +242 to +254
const hasCache = Object.prototype.hasOwnProperty.call(
entriesByNpcIdRef.current,
npcId,
);
if (shouldRefreshOpenBiography(hasCache, true)) {
void fetchTimeline(npcId)
.then((entries) => {
applyFetchedEntries(npcId, entries);
})
.catch(() => {
/* keep hint; leave error for next explicit open */
});
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 'openPersonalBiography|CouncilBiographySlot|personalTimelineLoadingNpcId|selectedNpc|activeNpcId' apps/web/src/ChatPage.tsx apps/web/src/components/CouncilBiographySlot.tsx apps/web/src/components/CouncilRosterPanel.tsx

Repository: moyunzero/AetherLife

Length of output: 11576


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant hook and UI state usage.
rg -n -C4 'shouldRefreshOpenBiography|clearNpcBiographyHint|readCursorRef|entriesByNpcIdRef|openBiography|personalTimelineLoadingNpcId|onOpenPersonalBiography|dialogueEngaged|CouncilBiographySlot' \
  apps/web/src/hooks/usePersonalTimeline.ts \
  apps/web/src/ChatPage.tsx \
  apps/web/src/components/CouncilBiographySlot.tsx \
  apps/web/src/components/CouncilRosterPanel.tsx

printf '\n--- usePersonalTimeline.ts (around the refresh logic) ---\n'
sed -n '220,275p' apps/web/src/hooks/usePersonalTimeline.ts

printf '\n--- ChatPage.tsx (relevant state wiring) ---\n'
sed -n '120,170p' apps/web/src/ChatPage.tsx
sed -n '560,590p' apps/web/src/ChatPage.tsx

printf '\n--- CouncilBiographySlot.tsx (open/expanded behavior) ---\n'
sed -n '1,120p' apps/web/src/components/CouncilBiographySlot.tsx

printf '\n--- CouncilRosterPanel.tsx (how the slot is shown) ---\n'
sed -n '70,110p' apps/web/src/components/CouncilRosterPanel.tsx

Repository: moyunzero/AetherLife

Length of output: 30830


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- usePersonalTimeline.ts (openBiography and state management) ---'
sed -n '152,220p' apps/web/src/hooks/usePersonalTimeline.ts

printf '\n%s\n' '--- usePersonalTimeline.ts (reset/reconcile effect) ---'
sed -n '299,330p' apps/web/src/hooks/usePersonalTimeline.ts

printf '\n%s\n' '--- CouncilRosterPanel.tsx (details/summary open behavior) ---'
sed -n '1,170p' apps/web/src/components/CouncilRosterPanel.tsx

Repository: moyunzero/AetherLife

Length of output: 10845


Gate biography sync refresh on the open state apps/web/src/hooks/usePersonalTimeline.ts:242-254 and apps/web/src/components/CouncilRosterPanel.tsx:63-95
mergePersonalTimelineSync still refreshes from cached entries even after a row is closed, and applyFetchedEntries clears the hint/read cursor for that NPC. Pass an explicit “currently open” flag from the <details> toggle instead of keying off cache presence alone.

🤖 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/usePersonalTimeline.ts` around lines 242 - 254, Update the
biography sync flow in usePersonalTimeline, specifically
mergePersonalTimelineSync, to accept and use an explicit currently-open flag
rather than treating cache presence as sufficient. Have CouncilRosterPanel
derive that flag from each row’s details toggle and pass it through, so
fetchTimeline and applyFetchedEntries only refresh open biographies and do not
clear hints or read cursors after closure.

@moyunzero
moyunzero merged commit e294740 into main Jul 20, 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