feat(intelligence): add Memory Timeline#2950
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds a complete Memory Timeline feature to the Intelligence page, displaying learned facts aggregated by month. The stack includes a pure timeline-computation engine, an RPC facade, a presentational UI panel, a stateful tab container with namespace filtering, comprehensive tests, and i18n translations. ChangesMemory Timeline Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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: 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 `@app/src/components/intelligence/MemoryTimelinePanel.tsx`:
- Around line 124-168: The Polish locale is missing the i18n keys used by
MemoryTimeline (memoryTimeline.heading, memoryTimeline.busiestCaption,
memoryTimeline.undatedNote, memoryTimeline.truncated); add Polish translations
for those keys to the Polish locale chunk and ensure they are exported/merged
into the Polish locale bundle so the app doesn't fall back to English, keeping
interpolation placeholders intact (e.g., {period}, {count}, {shown}, {total})
and matching the key names exactly.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9c06b509-ec5a-4751-b858-8dcdac548293
📒 Files selected for processing (23)
app/src/components/intelligence/MemoryTimelinePanel.test.tsxapp/src/components/intelligence/MemoryTimelinePanel.tsxapp/src/components/intelligence/MemoryTimelineTab.test.tsxapp/src/components/intelligence/MemoryTimelineTab.tsxapp/src/lib/i18n/chunks/ar-1.tsapp/src/lib/i18n/chunks/bn-1.tsapp/src/lib/i18n/chunks/de-1.tsapp/src/lib/i18n/chunks/en-1.tsapp/src/lib/i18n/chunks/es-1.tsapp/src/lib/i18n/chunks/fr-1.tsapp/src/lib/i18n/chunks/hi-1.tsapp/src/lib/i18n/chunks/id-1.tsapp/src/lib/i18n/chunks/it-1.tsapp/src/lib/i18n/chunks/ko-1.tsapp/src/lib/i18n/chunks/pt-1.tsapp/src/lib/i18n/chunks/ru-1.tsapp/src/lib/i18n/chunks/zh-CN-1.tsapp/src/lib/i18n/en.tsapp/src/lib/memory/memoryTimeline.test.tsapp/src/lib/memory/memoryTimeline.tsapp/src/pages/Intelligence.tsxapp/src/services/api/memoryTimelineApi.test.tsapp/src/services/api/memoryTimelineApi.ts
| {t('memoryTimeline.busiestCaption') | ||
| .replace('{period}', report.busiest.period) | ||
| .replace('{count}', String(report.busiest.count))} | ||
| </p> | ||
| )} | ||
|
|
||
| {/* Per-month histogram */} | ||
| {shown.length > 0 && ( | ||
| <section aria-labelledby="memory-timeline-heading" className="space-y-1"> | ||
| <h3 | ||
| id="memory-timeline-heading" | ||
| className="text-xs font-semibold uppercase tracking-wider text-stone-500 dark:text-neutral-400"> | ||
| {t('memoryTimeline.heading')} | ||
| </h3> | ||
| <ul className="space-y-1"> | ||
| {shown.map(bucket => ( | ||
| <li key={bucket.period} className="flex items-center gap-2 text-[11px] tabular-nums"> | ||
| <span className="w-16 shrink-0 text-stone-400 dark:text-neutral-500"> | ||
| {bucket.period} | ||
| </span> | ||
| <div className="flex-1 h-3 rounded bg-stone-100 dark:bg-neutral-800 overflow-hidden"> | ||
| <div | ||
| className="h-full bg-primary-400/70" | ||
| style={{ width: `${(bucket.count / maxCount) * 100}%` }} | ||
| /> | ||
| </div> | ||
| <span className="w-8 shrink-0 text-right text-stone-500 dark:text-neutral-400"> | ||
| {bucket.count} | ||
| </span> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| {truncated && ( | ||
| <p className="text-center text-xs text-stone-400 dark:text-neutral-500"> | ||
| {t('memoryTimeline.truncated') | ||
| .replace('{shown}', String(shown.length)) | ||
| .replace('{total}', String(report.buckets.length))} | ||
| </p> | ||
| )} | ||
| </section> | ||
| )} | ||
|
|
||
| {report.undated > 0 && ( | ||
| <p className="text-[11px] text-stone-400 dark:text-neutral-500"> | ||
| {t('memoryTimeline.undatedNote').replace('{count}', String(report.undated))} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm every memoryTimeline.* key in en.ts exists in each locale chunk.
EN=$(fd -t f 'en.ts' app/src/lib/i18n -x grep -hoE "'memoryTimeline\.[a-zA-Z]+'" {} | sort -u)
echo "Keys in en.ts:"; echo "$EN"
echo "--- Missing per locale ---"
for f in $(fd -e ts . app/src/lib/i18n/chunks); do
for k in $EN; do
grep -q "$k" "$f" || echo "$f missing $k"
done
doneRepository: tinyhumansai/openhuman
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== en.ts: memoryTimeline section ==="
rg -n "memoryTimeline" app/src/lib/i18n/en.ts
# show a small window around the first occurrence
line=$(rg -n "memoryTimeline" app/src/lib/i18n/en.ts | head -n 1 | cut -d: -f1)
start=$((line-20)); end=$((line+80))
sed -n "${start},${end}p" app/src/lib/i18n/en.ts
echo
echo "=== chunks/en-2.ts: lines containing memoryTimeline ==="
rg -n "memoryTimeline" app/src/lib/i18n/chunks/en-2.ts || true
if rg -n "memoryTimeline" app/src/lib/i18n/chunks/en-2.ts >/dev/null; then
line=$(rg -n "memoryTimeline" app/src/lib/i18n/chunks/en-2.ts | head -n 1 | cut -d: -f1)
start=$((line-20)); end=$((line+80))
sed -n "${start},${end}p" app/src/lib/i18n/chunks/en-2.ts
fi
echo
echo "=== chunks/ar-2.ts: lines containing memoryTimeline ==="
rg -n "memoryTimeline" app/src/lib/i18n/chunks/ar-2.ts || true
if rg -n "memoryTimeline" app/src/lib/i18n/chunks/ar-2.ts >/dev/null; then
line=$(rg -n "memoryTimeline" app/src/lib/i18n/chunks/ar-2.ts | head -n 1 | cut -d: -f1)
start=$((line-20)); end=$((line+80))
sed -n "${start},${end}p" app/src/lib/i18n/chunks/ar-2.ts
fiRepository: tinyhumansai/openhuman
Length of output: 5868
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Files in chunks containing 'memoryTimeline' ==="
rg -l "memoryTimeline" app/src/lib/i18n/chunks | sort || true
echo
echo "=== Preview app/src/lib/i18n/chunks/en-2.ts ==="
wc -l app/src/lib/i18n/chunks/en-2.ts
sed -n '1,200p' app/src/lib/i18n/chunks/en-2.ts
echo
echo "=== Preview app/src/lib/i18n/chunks/fr-2.ts ==="
wc -l app/src/lib/i18n/chunks/fr-2.ts
sed -n '1,160p' app/src/lib/i18n/chunks/fr-2.tsRepository: tinyhumansai/openhuman
Length of output: 21102
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Files in chunks containing 'memoryTimeline' ==="
rg -l "memoryTimeline" app/src/lib/i18n/chunks | sort || true
echo
echo "=== Preview app/src/lib/i18n/chunks/en-2.ts ==="
wc -l app/src/lib/i18n/chunks/en-2.ts
sed -n '1,200p' app/src/lib/i18n/chunks/en-2.ts
echo
echo "=== Preview app/src/lib/i18n/chunks/fr-2.ts ==="
wc -l app/src/lib/i18n/chunks/fr-2.ts
sed -n '1,160p' app/src/lib/i18n/chunks/fr-2.tsRepository: tinyhumansai/openhuman
Length of output: 21102
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== en-1.ts memoryTimeline keys ==="
rg -n "memoryTimeline\." app/src/lib/i18n/chunks/en-1.ts | head -n 50
echo
echo "=== ar-1.ts memoryTimeline keys (first 50) ==="
rg -n "memoryTimeline\." app/src/lib/i18n/chunks/ar-1.ts | head -n 50
echo
echo "=== Which chunk files contain ALL of the required keys from the component snippet? ==="
REQUIRED=(
"memoryTimeline.busiestCaption"
"memoryTimeline.undatedNote"
"memoryTimeline.truncated"
"memoryTimeline.heading"
)
for f in $(rg -l "memoryTimeline\." app/src/lib/i18n/chunks); do
ok=1
for k in "${REQUIRED[@]}"; do
grep -q "'${k}'" "$f" || ok=0
done
if [ $ok -eq 1 ]; then
echo "$f"
fi
done | sort
echo
echo "=== For each locale chunk index, list chunk files that contain ANY memoryTimeline key ==="
for idx in 1 2 3 4 5; do
echo "-- *-${idx}.ts --"
rg -l "memoryTimeline\." app/src/lib/i18n/chunks/*-${idx}.ts 2>/dev/null | sed 's#.*/##' | head -n 50 || true
doneRepository: tinyhumansai/openhuman
Length of output: 2708
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== en-1.ts memoryTimeline keys ==="
rg -n "memoryTimeline\." app/src/lib/i18n/chunks/en-1.ts | head -n 50
echo
echo "=== ar-1.ts memoryTimeline keys (first 50) ==="
rg -n "memoryTimeline\." app/src/lib/i18n/chunks/ar-1.ts | head -n 50
echo
echo "=== Which chunk files contain ALL keys used by MemoryTimelinePanel ==="
REQUIRED=(
"memoryTimeline.busiestCaption"
"memoryTimeline.undatedNote"
"memoryTimeline.truncated"
"memoryTimeline.heading"
)
for f in $(rg -l "memoryTimeline\." app/src/lib/i18n/chunks); do
ok=1
for k in "${REQUIRED[@]}"; do
grep -q "'${k}'" "$f" || ok=0
done
if [ $ok -eq 1 ]; then
echo "$f"
fi
done | sort
echo
echo "=== For each chunk index, which chunk files contain ANY memoryTimeline key ==="
for idx in 1 2 3 4 5; do
echo "-- *-${idx}.ts --"
rg -l "memoryTimeline\." app/src/lib/i18n/chunks/*-${idx}.ts 2>/dev/null | sed 's#.*/##' | sort | head -n 100 || true
doneRepository: tinyhumansai/openhuman
Length of output: 2681
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find where locale chunks are loaded/merged
rg -n "chunks/.*-1|load.*chunk|TranslationMap|i18n/chunks" app/src/lib/i18n app/src/components app/src | head -n 80
echo
echo "=== Inspect I18nContext.tsx chunk loading ==="
rg -n "chunk" app/src/lib/i18n/I18nContext.* app/src/lib/i18n -S || true
ls app/src/lib/i18nRepository: tinyhumansai/openhuman
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find i18n chunk loading/merge logic
rg -n "TranslationMap|i18n/chunks|chunks/|chunk" app/src/lib/i18n | head -n 120
echo
echo "=== Files under app/src/lib/i18n ==="
ls -la app/src/lib/i18n
echo
echo "=== Look for where chunks are imported/combined ==="
rg -n "import\\s+.*from\\s+['\"].*chunks/|Object\\.assign|merge|reduce|...chunks" app/src/lib/i18n | head -n 120Repository: tinyhumansai/openhuman
Length of output: 19228
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== hi.ts: how chunks are combined ==="
sed -n '1,80p' app/src/lib/i18n/hi.ts
echo
echo "=== ar.ts: how chunks are combined ==="
sed -n '1,80p' app/src/lib/i18n/ar.ts
echo
echo "=== I18nContext.tsx: how locale map is used ==="
rg -n "const \\w+Locale|getLocale|currentLocale|locale|t\\(|I18nContext|TranslationMap|translations" app/src/lib/i18n/I18nContext.tsx
sed -n '1,140p' app/src/lib/i18n/I18nContext.tsxRepository: tinyhumansai/openhuman
Length of output: 7456
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== hi.ts: how chunks are combined ==="
sed -n '1,80p' app/src/lib/i18n/hi.ts
echo
echo "=== ar.ts: how chunks are combined ==="
sed -n '1,80p' app/src/lib/i18n/ar.ts
echo
echo "=== I18nContext.tsx: how locale map is used ==="
rg -n "useT\\(|t\\(|locale|TranslationMap|translations|I18nContext" app/src/lib/i18n/I18nContext.tsx
sed -n '1,220p' app/src/lib/i18n/I18nContext.tsxRepository: tinyhumansai/openhuman
Length of output: 7456
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check placeholder parity for keys used by MemoryTimelinePanel in each locale chunk1.
LOCALES=$(rg -l "memoryTimeline\\.busiestCaption" app/src/lib/i18n/chunks/*-1.ts | xargs -n1 basename | sed 's/-1\.ts$//')
echo "Locales with memoryTimeline in chunk1: $LOCALES"
for f in app/src/lib/i18n/chunks/*-1.ts; do
# busiestCaption
line=$(rg -n "'memoryTimeline\\.busiestCaption'" "$f" | head -n 1 || true)
if [ -z "$line" ]; then echo "$f missing busiestCaption"; continue; fi
val=$(echo "$line" | sed -E "s/.*'memoryTimeline\\.busiestCaption':\\s*'([^']*)'.*/\\1/")
for ph in '{period}' '{count}'; do
echo "$val" | grep -q "$ph" || echo "$f busiestCaption missing $ph"
done
# undatedNote expects {count}
line=$(rg -n "'memoryTimeline\\.undatedNote'" "$f" | head -n 1 || true)
val=$(echo "$line" | sed -E "s/.*'memoryTimeline\\.undatedNote':\\s*'([^']*)'.*/\\1/")
echo "$val" | grep -q '{count}' || echo "$f undatedNote missing {count}"
# truncated expects {shown} and {total}
line=$(rg -n "'memoryTimeline\\.truncated'" "$f" | head -n 1 || true)
val=$(echo "$line" | sed -E "s/.*'memoryTimeline\\.truncated':\\s*'([^']*)'.*/\\1/")
for ph in '{shown}' '{total}'; do
echo "$val" | grep -q "$ph" || echo "$f truncated missing $ph"
done
doneRepository: tinyhumansai/openhuman
Length of output: 202
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check placeholder parity for keys used by MemoryTimelinePanel in each locale chunk1.
LOCALES=$(rg -l "memoryTimeline\\.busiestCaption" app/src/lib/i18n/chunks/*-1.ts | xargs -n1 basename | sed 's/-1\.ts$//')
echo "Locales with memoryTimeline in chunk1: $LOCALES"
for f in app/src/lib/i18n/chunks/*-1.ts; do
# busiestCaption
line=$(rg -n "'memoryTimeline\\.busiestCaption'" "$f" | head -n 1 || true)
if [ -z "$line" ]; then echo "$f missing busiestCaption"; continue; fi
val=$(echo "$line" | sed -E "s/.*'memoryTimeline\\.busiestCaption':\\s*'([^']*)'.*/\\1/")
for ph in '{period}' '{count}'; do
echo "$val" | grep -q "$ph" || echo "$f busiestCaption missing $ph"
done
# undatedNote expects {count}
line=$(rg -n "'memoryTimeline\\.undatedNote'" "$f" | head -n 1 || true)
val=$(echo "$line" | sed -E "s/.*'memoryTimeline\\.undatedNote':\\s*'([^']*)'.*/\\1/")
echo "$val" | grep -q '{count}' || echo "$f undatedNote missing {count}"
# truncated expects {shown} and {total}
line=$(rg -n "'memoryTimeline\\.truncated'" "$f" | head -n 1 || true)
val=$(echo "$line" | sed -E "s/.*'memoryTimeline\\.truncated':\\s*'([^']*)'.*/\\1/")
for ph in '{shown}' '{total}'; do
echo "$val" | grep -q "$ph" || echo "$f truncated missing $ph"
done
doneRepository: tinyhumansai/openhuman
Length of output: 202
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== memoryTimeline keys in pl chunk files ==="
rg -n "memoryTimeline\." app/src/lib/i18n/chunks/pl-*.ts || echo "No memoryTimeline keys found in pl chunks"
echo
echo "=== memoryTimeline keys in pl.ts (merged locale module) ==="
rg -n "memoryTimeline\." app/src/lib/i18n/pl.ts || echo "No memoryTimeline keys found in pl.ts"
echo
echo "=== Missing required keys per locale chunk1 file ==="
REQUIRED_KEYS=(
"memoryTimeline.busiestCaption"
"memoryTimeline.undatedNote"
"memoryTimeline.truncated"
"memoryTimeline.heading"
)
for f in app/src/lib/i18n/chunks/*-1.ts; do
missing=()
for k in "${REQUIRED_KEYS[@]}"; do
grep -q "'${k}'" "$f" || missing+=("$k")
done
if [ ${`#missing`[@]} -ne 0 ]; then
echo "$(basename "$f") missing: ${missing[*]}"
fi
done | sortRepository: tinyhumansai/openhuman
Length of output: 429
Fix i18n key parity for memoryTimeline.* in the Polish locale.
app/src/lib/i18n/chunks/pl-1.tscontains nomemoryTimeline.*keys (missingmemoryTimeline.heading,memoryTimeline.busiestCaption,memoryTimeline.undatedNote,memoryTimeline.truncated), andapp/src/lib/i18n/pl.tsdoesn’t add them—so Polish users fall back to English for this panel.
🤖 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 `@app/src/components/intelligence/MemoryTimelinePanel.tsx` around lines 124 -
168, The Polish locale is missing the i18n keys used by MemoryTimeline
(memoryTimeline.heading, memoryTimeline.busiestCaption,
memoryTimeline.undatedNote, memoryTimeline.truncated); add Polish translations
for those keys to the Polish locale chunk and ensure they are exported/merged
into the Polish locale bundle so the app doesn't fall back to English, keeping
interpolation placeholders intact (e.g., {period}, {count}, {shown}, {total})
and matching the key names exactly.
6bf44a3 to
0a466da
Compare
1d9a4f7 to
5f073fe
Compare
A new read-only "Timeline" tab showing WHEN the assistant learned about the user — facts bucketed by the calendar month they were last reinforced — so growth, bursts of activity, and quiet stretches are visible, a temporal lens distinct from the structural/scoring views. - Pure deterministic engine (lib/memory/memoryTimeline.ts): buckets by UTC month via new Date(updatedAt*1000) UTC accessors (reads the data timestamp, never the wall clock — so bucketing is timezone-independent and reproducible). Reports per-month counts (chronological), total, undated count, first/last, busiest month (ties -> earliest), and a last-30-days recency count using an injected nowSeconds. No Date.now() in the engine. - Zero new core surface: reuses memoryGraphQuery + memoryListNamespaces. Read-only, recomputed from the live graph. - Container guards the load with a request token; namespace selector; summary tiles + per-month bar histogram (capped to the 24 most-recent months with a truncation note) + undated note. i18n across all 13 locales. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rebase) The post-rebase i18n mirror emitted unformatted keys into the locale chunks; run Prettier so format:check passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5f073fe to
beddfbb
Compare
en.ts carried the 17 memoryTimeline.* / memory.tab.timeline keys but the 13 other locale files did not, breaking i18n parity (pnpm i18n:check) and leaving the Memory Timeline UI falling back to English in every non-English locale. Adds the keys (English placeholders, per the established convention for newly introduced strings) to ar, bn, de, es, fr, hi, id, it, ko, pl, pt, ru, zh-CN. Resolves CodeRabbit's Major comment about the missing Polish (pl) keys (memoryTimeline.heading/busiestCaption/undatedNote/truncated).
# Conflicts: # app/src/pages/Intelligence.tsx
Summary
Adds a new read-only "Timeline" tab showing when the assistant learned about the user — facts bucketed by the calendar month they were last reinforced — so growth, bursts of activity, and quiet stretches are visible. A temporal lens, distinct from the structural/scoring views.
Design
lib/memory/memoryTimeline.ts): buckets by UTC month vianew Date(updatedAt * 1000)with UTC accessors — this reads the data timestamp, never the wall clock, so bucketing is timezone-independent and reproducible. Reports per-month counts (chronological), total, anundatedcount (facts with no valid timestamp), first/last timestamps, the busiest month (ties → earliest), and a last-30-days recency count using an injectednowSeconds(the engine never callsDate.now()).memoryGraphQueryandmemoryListNamespaceswrappers. Read-only — recomputed from the live graph on mount.nowSecondsin handlers (never during render); namespace selector; summary tiles + a per-month bar histogram (capped to the 24 most-recent months with a truncation note) + an undated note. i18n across all 13 locales.Edge cases (tested)
Empty graph, single/multi-month bucketing, cross-year ordering, busiest-month tie-break (earliest), undated facts excluded from buckets but counted, and the last-30-days recency window.
Test plan
vitest— 19 tests (engine: bucketing/ordering/busiest/undated/recency; api facade; panel states; container load + selector + error)tsc --noEmit— cleaneslint— 0 errorsprettier --check— cleanmemoryTimeline.*keys🤖 Generated with Claude Code
Summary by CodeRabbit