feat(rwt): real-world topics ride memory — profile taste, ledger footprint, no policy file [spec 13] - #209
Merged
Merged
Conversation
…spec 13] The file existed to give the listener a knob nobody was ever told about: seeded with one debug line, mentioned nowhere a listener reads, and written once (wx) so a later default never reaches an installed copy. The sibling music-policy.md, same shape, measured byte-identical to its template after nine days and 28 sessions. rwt has not shipped in a version yet, so the chain leaves before it ever existed: no seeding, no reading, no path, no config field, and no policy field on the fetch request — the prompt renders the built-in policy itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jej4WJFNKP1JZ8Z77UUG64
With the policy file gone the per-listener half of the taste needs a source, and the fetch had none: a request was language, timezone, date and the held titles, so every install with the same two settings searched for the same things. The profile already grows (compaction), fades ([seen] tags) and forgets (forget_memory); the fetch now reads its (About the listener) section at refresh time — only that section: (Relationship & style) is tone, and tone is not a search term. The block carries the line the code owns: search for what they follow, never for them. An empty profile renders nothing, so a new listener is exactly today's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jej4WJFNKP1JZ8Z77UUG64
…ger [spec 13] Every other content source leaves a footprint in the ledger; a real-world item lived only in cache/rwt.json's used mark, on an entry that expires in 48 h in a directory the listener may delete. A story that runs longer than that — a series, a tournament, an argument — came back as new and got told again. LedgerKind gains 'rwt'; the Director ledgers the title with the take (the pool spends it there too); the fetch is told the pool's titles plus the ledger's last 32, and a covered title returned anyway is dropped before the merge. forget_memory does not touch the ledger, by design: what was heard stays avoided; what the listener stopped following leaves the profile. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jej4WJFNKP1JZ8Z77UUG64
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jej4WJFNKP1JZ8Z77UUG64
…h [spec 13] Filtering after the emptiness check let merge([]) stamp the pool fresh on an all-covered response, leaving it empty and silent for a whole stale interval. The filter now runs first; an all-covered result logs a failure and the next boundary retries, exactly as a thrown fetch does (codex review). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jej4WJFNKP1JZ8Z77UUG64
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jej4WJFNKP1JZ8Z77UUG64
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jej4WJFNKP1JZ8Z77UUG64
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the spec 13 design change in
specs/spec13/13-real-world-topics.md(§3.4 the taste is the profile, §3.7 the ledger footprint). Real-world topics were a content source with no roots in memory: a taste file nobody was told about, a fetch that did not know who was listening, and an anti-repeat that lived only in a 48-hour cache. Three changes, one PR; rwt has not shipped in a version yet (0.2.1 predates it), so the deletion costs no migration.What changed
rwt-policy.mdis gone —POLICY_TEMPLATE,seedRwtPolicy,readRwtPolicy,rwtPolicyPath, the config field, the call sites, the tests.DEFAULT_RWT_POLICYstays insrc/prompts.tsand the fetch prompt renders it directly.music-policy.mdis untouched (shipped; a music taste is likelier to be edited).FetchTopicsRequest.follows;aboutSection(profile)insrc/prompts.tscuts the one section out ofPROFILE_SHAPEwith the[seen]/[stable]tags stripped — never (Relationship & style), which is tone, not a search term.buildFetchTopicsPromptrenders a (What the listener follows) block before the policy with the code-owned line "Search for what they follow, never for them, and never for anything that identifies them."buildRwtreadsmemory.profile()inside therequestclosure, so a compaction or aforget_memorylands on the next refresh. Empty profile → no block → a new listener gets exactly today's fetch.LedgerKindgains'rwt',MemoryStoregainsrecentRwt(n)(besiderecentSongs, the Director's tier). The Director ledgers the title at take time ingenerateTalks— the same moment the pool marks itused. The fetch'savoidispool.titles()then the ledger's lastRWT_AVOID_DEPTH= 32 (the music list's settled depth; a topic is offered far less often than a song plays, so 32 spans weeks), deduplicated; a covered title returned anyway is dropped beforemerge.Grilling — what the self-interrogation changed against the brief
policyleftFetchTopicsRequestentirely. With the file gone the field was alwaysDEFAULT_RWT_POLICY; a field with one possible value is dead weight, so the prompt builder renders the constant itself. The brief only said to keep the constant; this goes one step further.forget_memoryclaim was corrected, not the mechanism. The brief said the ledger "gets forget for free".forget()insrc/memory.tsremoves history rows and profile lines and does not touch the ledger — and it must not: scrubbing the rwt row would let the story come back. "Don't bring that up again" is served by the two changes together: the ledger keeps the heard item avoided, and the forget removes the interest from the profile and so fromfollows. Spec §3.7 says this explicitly.merge, because the prompt is a request and the ledger is the record.music-policy.mdchange.Evidence (the deterministic seams, not the model's word)
Change 1 — nothing seeded.
buildRwtagainst an emptyMURMUR_HOMEleaves norwt-policy.md(test/app.test.ts);grep -rn rwtPolicy src testis empty. The stale~/.murmur/rwt-policy.mdon this machine still carries its 2026-09-03 18:31 mtime after two real runs — nothing reads or writes it.Change 2 — the block is in the real prompt, and the searches are about interests, not the listener.
scratch/smoke_rwt_fetch.ts(throwaway) printed the prompt a realfetchTopicsran with — the (What the listener follows) block carried the machine's actual profile About section (slow jazz/piano/ambient, daily details, time passing) with the guard line — and logged everyWebSearchthe Haiku task issued: 14 queries, all of the shape新加坡 新闻 2026年9月4日,娱乐 电影 音乐 2026年9月,科技 发布 产品 …,体育 运动 亚洲 …; none named the listener or anything from the profile verbatim. Result in 87 s: 6 items, Chinese, dated today/yesterday, two of them leaning the profile's way (a Sonos launch "for people who care about sound", a guide-dog robot told as "care in the small things").Change 3 — the offer is a ledger row and the beat carries the item. A
make devrun (--no-music --max-segments 12,MURMUR_RWT_P=1,MURMUR_RWT_STALE_HOURS=1),.dev/dev.log:~/.murmur/data/memory/ledger.jsonlgained six rows, one per offer, e.g.{"ts":1788512476.171,"session":"dfb166f3","kind":"rwt","key":"新加坡夜间节至今周末,西南亚民间故事化成灯影装置"}— the timestamp is the take, not the air. Every item is named on air (the title, the place, the number), which is #202's first box holding at this seam.An earlier run of the same command without
MURMUR_RWT_STALE_HOURS=1showed no rwt line for eight segments: the pool had been refreshed 5.7 h before by the user's own session, all five entries used, and the stale rule is 6 h — so no refresh and no offer. Not a defect of this PR; it is spec 13 §6's open question ("refresh when empty of unused entries?") observed for real, and it is left there.Gates:
pnpm test1424 passed / 1 skipped (exit 0),pnpm run typecheckclean,npx oxlint src test scripts .github/scriptsclean.Peer review (codex gpt-5.6-sol, xhigh): 1 finding, 1 applied, 0 dismissed. A fetch that returned only ledgered titles passed the emptiness check and
merge([])stamped the pool fresh — empty and silent for six hours. Fixed: the filter runs before the check, an all-covered result logsrwt.refresh failed (nothing new returned)and the next boundary retries; regression test intest/rwt.test.ts.Issues
grep '"kind":"rwt"' ledger.jsonlis the check).~/.murmur/rwt-policy.mdan earlier run seeded is inert; it can be deleted by hand.AI coding brief
Original request: wire real-world topics into the memory system, three changes in one PR — delete the
rwt-policy.mdchain (a knob nobody is told about, frozen at install by its once-only seed; measured on the sibling music policy: byte-identical to its template after 9 days / 28 sessions), feed the profile's About section into the fetch (the fetch did not know who was listening), and give rwt a ledger footprint (the only content source without one; a story longer than the 48 h cache came back as new). Grill the plan first, decide alone, ship to a PR without checking in.Manual interventions: none during the build. The brief pre-decided the non-goals (no reaction tracking, no negative-constraint steer, no
music-policy.mdchange) and they were kept.Retro: the brief's file:line pointers (
src/app.ts:563/844,src/contracts.ts:96/160,src/director.ts:108/1073) made the read fast; what it could not know was thatforget()skips the ledger, which the grilling pass caught and turned from a claimed freebie into a stated design property. For the dev-log check, state the pool's refresh state as part of the brief ("the pool may be freshly refreshed — setMURMUR_RWT_STALE_HOURS") — the first run burned seven minutes proving the stale rule instead of the change.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jej4WJFNKP1JZ8Z77UUG64