Let the garden keep what she was given, not only what she sought
Two halves of the same idea, both read out of work Petal already records. Planting: an accepted collocation is a learnable chunk, so it becomes a phrase card. The scheduler didn't need to know — a three-word chunk climbs the ladder exactly like a looked-up word. What needed care was deciding what *isn't* a chunk (single words are word choice; a six-word-plus "collocation" is a rewritten sentence, and sentences make miserable flashcards), and that the example must be the *corrected* sentence — the stored draft still holds the phrasing she just left behind. Re-accepting the same chunk leaves the existing card alone rather than resetting a schedule it has been climbing. The whole thing is best-effort: accepting an edit must never fail because a flashcard couldn't be made. The growth journal: kept this month beside kept the month before, the phrasing that stuck, the patterns that faded. The queries were the easy part; the honesty is the feature. "Stuck" needs the phrase in a *second* document, because one document is just the edit where she left it. "Faded" says nothing at all unless she has been writing lately — otherwise a month away from Petal comes back to her as progress, which is the one way this could lie. And a suggestion had to start recording when she *decided* it, not when the model proposed it, so 0012 adds resolved_at and backfills the old rows to their created_at. It lives as a second tab in the garden, and it feeds the kitten: after an accept she now sometimes hears something true of her alone, once per line, half the time, never waited for. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// 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<string>()
|
||||
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user