feat(27): Personal Life Timeline — biography, RAG, dual-track calendar - #20
Conversation
- 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>
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>
|
Warning Review limit reached
Next review available in: 26 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 (63)
✨ 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 |
📝 WalkthroughWalkthroughThis 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. ChangesPersonal timeline lifecycle
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 docstrings
🧪 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.
Actionable comments posted: 4
🧹 Nitpick comments (2)
packages/npc-memory/src/schema.ts (1)
194-203: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRedundant 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'sWHERE room_id AND npc_id ORDER BY seq DESClist query and theMAX(seq)lookup ininsertSqlRow. The separatenpc_personal_timeline_room_npc_seq_idxadds 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 winBest-effort clue gathering swallows all errors silently.
Every
try/catchinassembleWeeklyRecentBulletsdiscards the error with no logging. IflistWorldHistory,listPersonalTimelineForNpc, orlistRelationshipsForRoomstarts 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
📒 Files selected for processing (63)
apps/game-server/src/ambient/README.mdapps/game-server/src/colyseus/GameRoom.tsapps/game-server/src/index.test.tsapps/game-server/src/index.tsapps/game-server/src/queue/personal-timeline.tsapps/game-server/src/queue/world-vote.tsapps/game-server/src/room/store.tsapps/game-server/src/routes/internal-personal-timeline.tsapps/game-server/src/routes/internal-world-vote-trigger.tsapps/game-server/src/routes/personal-timeline.test.tsapps/game-server/src/routes/personal-timeline.tsapps/game-server/src/sse/hub.tsapps/game-server/src/world/personal-timeline-broadcast.tsapps/game-server/src/world/personal-timeline-dyad.test.tsapps/game-server/src/world/personal-timeline-dyad.tsapps/game-server/src/world/personal-timeline-repository.test.tsapps/game-server/src/world/personal-timeline-repository.tsapps/game-server/src/world/personal-timeline-seed.test.tsapps/game-server/src/world/personal-timeline-seed.tsapps/game-server/src/world/personal-timeline-weekly.test.tsapps/game-server/src/world/personal-timeline-weekly.tsapps/game-server/src/world/world-history-seed.test.tsapps/game-server/src/world/world-history-seed.tsapps/game-server/src/world/world-vote-trigger.test.tsapps/game-server/src/world/world-vote-trigger.tsapps/web/src/ChatPage.tsxapps/web/src/components/CouncilBiographySlot.test.tsapps/web/src/components/CouncilBiographySlot.tsxapps/web/src/components/CouncilRosterPanel.tsxapps/web/src/components/ShellDrawer.tsxapps/web/src/hooks/useColyseusRoom.tsapps/web/src/hooks/usePersonalTimeline.test.tsapps/web/src/hooks/usePersonalTimeline.tsapps/web/src/hooks/useWorldHistory.test.tsapps/web/src/hooks/useWorldHistory.tsapps/web/src/index.cssdocs/CONTRACTS.mddocs/ISSUE-LOG.mdpackage.jsonpackages/npc-memory/migrations/0011_npc_personal_timeline.sqlpackages/npc-memory/migrations/meta/_journal.jsonpackages/npc-memory/src/index.tspackages/npc-memory/src/schema.tspackages/shared/src/colyseus.tspackages/shared/src/index.tspackages/shared/src/personalTimeline.test.tspackages/shared/src/personalTimeline.tspackages/shared/src/worldHistory.test.tspackages/shared/src/worldHistory.tsscripts/lib/agent-verify-map.mjsscripts/uat-phase27-persona-diary.mjsscripts/verify-phase27.mjsworkers/agent-worker/src/council/memory_context.pyworkers/agent-worker/src/council/personal_timeline_rag.pyworkers/agent-worker/src/council/relationship_deltas.pyworkers/agent-worker/src/council/speak_registry.pyworkers/agent-worker/src/graph/personal_timeline.pyworkers/agent-worker/src/graph/world_vote.pyworkers/agent-worker/src/main.pyworkers/agent-worker/tests/test_personal_timeline.pyworkers/agent-worker/tests/test_personal_timeline_rag.pyworkers/agent-worker/tests/test_personal_timeline_rel07.pyworkers/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>(); |
There was a problem hiding this comment.
🩺 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.
| 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; | ||
| } |
There was a problem hiding this comment.
🩺 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.
| const knownLatest = latestSeqRef.current[npcId]; | ||
| const show = | ||
| hasUpdate && | ||
| shouldShowBiographyHint(knownLatest ?? latestSeq ?? Number.POSITIVE_INFINITY, readCursorRef.current[npcId]); | ||
| if (!show) return; |
There was a problem hiding this comment.
🎯 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.
| 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.
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>
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>
There was a problem hiding this comment.
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
📒 Files selected for processing (26)
apps/game-server/src/queue/personal-timeline.claim.test.tsapps/game-server/src/queue/personal-timeline.tsapps/game-server/src/routes/personal-timeline.test.tsapps/game-server/src/world/personal-timeline-dyad.test.tsapps/game-server/src/world/personal-timeline-dyad.tsapps/game-server/src/world/personal-timeline-seed.test.tsapps/game-server/src/world/personal-timeline-weekly.test.tsapps/game-server/src/world/world-history-repository.tsapps/game-server/src/world/world-history-seed.test.tsapps/game-server/src/world/world-history-seed.tsapps/web/src/ChatPage.tsxapps/web/src/components/CouncilBiographySlot.tsxapps/web/src/components/CouncilRosterPanel.tsxapps/web/src/components/ShellDrawer.tsxapps/web/src/hooks/usePersonalTimeline.test.tsapps/web/src/hooks/usePersonalTimeline.tsapps/web/src/index.cssdocs/CONTRACTS.mddocs/ISSUE-LOG.mdpackages/npc-memory/src/schema.tsscripts/uat-phase27-persona-diary.mjsworkers/agent-worker/src/graph/personal_timeline.pyworkers/agent-worker/src/graph/world_vote.pyworkers/agent-worker/tests/test_personal_timeline.pyworkers/agent-worker/tests/test_personal_timeline_rag.pyworkers/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
| 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 */ | ||
| }); | ||
| } |
There was a problem hiding this comment.
🎯 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: 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.
Summary
npc_personal_timelinepersistence (C-11), lifeNodes seeds, weekly/polish/multi/REL worker jobs, roster 传记 UI + hints, speak RAG, andproposalEligiblefeed for Phase 25.生平·{age}; post-arrival / shared world uses 太乙 civil labels; seed repair on live rooms.kind=eventapply-deltas + forced bilateral REL entries;verify:phase27waits for all 12 seats + real-LLM multi-perspective gate.Test plan
pnpm --filter @aetherlife/shared test -- personalTimelinepnpm --filter @aetherlife/game-server test -- personal-timelinecd 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 -qpnpm verify:phase27againstpnpm dev:stack(real LLM; no mock)Made with Cursor
Summary by CodeRabbit