// Personal material for the companion, drawn from the growth journal. // // The kitten's cheers are warm but generic — they'd be the same words for // anybody. The journal already knows things that are true of *this* writer and // nobody else ("you're using 'make a decision' on your own now"), and that is a // far better thing to hear after accepting an edit. §5a's own example. // // Three rules keep it from wearing out: // * A given line is served once per session. Personal praise repeated is // wallpaper, and wallpaper is worse than the generic cheer it replaced. // * The journal is fetched lazily, on the first accept, and never awaited — // the first cheer of a session is generic, and that's fine. // * Lines are built at call time from the pack, like every other companion // line, so a bubble composed after /api/me is in the right language. import { api, type GrowthJournal } from '../../api/client' import { pack, type Line } from '../../i18n' let journal: GrowthJournal | null = null let inFlight = false let served = new Set() // warmPersonalCheers starts the one fetch this module ever needs. Safe to call // often; a failure is silent and simply leaves the companion with its generic // pool, which is exactly the pre-journal behaviour. export function warmPersonalCheers(): void { if (journal || inFlight) return inFlight = true api .growth() .then((j) => { journal = j }) .catch(() => { /* no journal, no personal cheer — never a visible failure */ }) .finally(() => { inFlight = false }) } // personalCheer returns an unserved line about her own writing, or null when // there is none — the caller falls back to the generic pool. export function personalCheer(): Line | null { if (!journal) return null const t = pack() const candidates = [ ...journal.stuck.map((s) => ({ key: `stuck:${s.phrase}`, line: () => t.journal.cheerStuck(s.phrase) })), ...journal.faded.map((f) => ({ key: `faded:${f.pattern}`, line: () => t.journal.cheerFaded(f.pattern) })), ].filter((c) => !served.has(c.key)) if (candidates.length === 0) return null const chosen = candidates[Math.floor(Math.random() * candidates.length)] served.add(chosen.key) return chosen.line() } // Test seam, matching resetPackForTests: module state is per-session by design. export function resetPersonalCheersForTests(): void { journal = null inFlight = false served = new Set() }