Skip to content

feat(rwt): real-world topics ride memory — profile taste, ledger footprint, no policy file [spec 13] - #209

Merged
wine-fall merged 7 commits into
mainfrom
zachg-0904--rwt-memory
Sep 4, 2026
Merged

feat(rwt): real-world topics ride memory — profile taste, ledger footprint, no policy file [spec 13]#209
wine-fall merged 7 commits into
mainfrom
zachg-0904--rwt-memory

Conversation

@wine-fall

Copy link
Copy Markdown
Owner

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

  1. rwt-policy.md is gonePOLICY_TEMPLATE, seedRwtPolicy, readRwtPolicy, rwtPolicyPath, the config field, the call sites, the tests. DEFAULT_RWT_POLICY stays in src/prompts.ts and the fetch prompt renders it directly. music-policy.md is untouched (shipped; a music taste is likelier to be edited).
  2. The profile's (About the listener) section steers the fetch. FetchTopicsRequest.follows; aboutSection(profile) in src/prompts.ts cuts the one section out of PROFILE_SHAPE with the [seen]/[stable] tags stripped — never (Relationship & style), which is tone, not a search term. buildFetchTopicsPrompt renders 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." buildRwt reads memory.profile() inside the request closure, so a compaction or a forget_memory lands on the next refresh. Empty profile → no block → a new listener gets exactly today's fetch.
  3. A told topic is a ledger row. LedgerKind gains 'rwt', MemoryStore gains recentRwt(n) (beside recentSongs, the Director's tier). The Director ledgers the title at take time in generateTalks — the same moment the pool marks it used. The fetch's avoid is pool.titles() then the ledger's last RWT_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 before merge.

Grilling — what the self-interrogation changed against the brief

  • policy left FetchTopicsRequest entirely. With the file gone the field was always DEFAULT_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.
  • The forget_memory claim was corrected, not the mechanism. The brief said the ledger "gets forget for free". forget() in src/memory.ts removes 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 from follows. Spec §3.7 says this explicitly.
  • A belt to the prompt's braces. The brief made the ledger avoid list prompt-only. The pool already hard-filters returned titles it holds; the ledger now gets the same one-line filter before merge, because the prompt is a request and the ledger is the record.
  • Privacy is stated honestly in the spec. The old network-posture paragraph said "nothing about the listener leaves the machine except a language and a timezone". The About section now leaves for a WebSearch task. The spec says so, names the guard line, and the smoke below checked the queries the model actually ran.
  • Kept as briefed, after questioning: only the About section (tone is not a search term); depth 32 with the reasoning in §3.7; no reaction tracking, no negative-constraint steer, no music-policy.md change.

Evidence (the deterministic seams, not the model's word)

Change 1 — nothing seeded. buildRwt against an empty MURMUR_HOME leaves no rwt-policy.md (test/app.test.ts); grep -rn rwtPolicy src test is empty. The stale ~/.murmur/rwt-policy.md on 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 real fetchTopics ran 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 every WebSearch the 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 dev run (--no-music --max-segments 12, MURMUR_RWT_P=1, MURMUR_RWT_STALE_HOURS=1), .dev/dev.log:

16:58:15 rwt.refresh n=7 ms=99648
16:58:15 rwt.pool fresh=7 used=5
16:58:53 rwt.offer 21edba88          ← 菲律宾西南季风引发停课
17:00:22 radio: 我刚才瞥见一条挺远的事儿,说是这两天菲律宾那边,吕宋岛一带,赶上西南季风,雨下得凶……首都那片儿的学校干脆都停了课……
16:59:40 rwt.offer 5814de25          ← 马来西亚泰国贸易目标年底冲破三百亿美元
17:01:16 radio: ……马来西亚跟泰国这俩邻居,做买卖做得挺红火,头八个月就跑出个好数儿来,还琢磨着明年要冲到三百亿美元……
17:00:22 rwt.offer 37ea488f          ← 台湾晶圆展创纪录,一千三百家厂商展出人工智能芯片方案
17:02:15 radio: ……台北南港那个展览馆,这两天办了个国际半导体展,一千三百多家厂商挤在一块儿……
17:01:16 rwt.offer 71beaa90          ← 新加坡夜间节至今周末,西南亚民间故事化成灯影装置
17:03:18 radio: ……新加坡那边办夜间节呢,到这周末,九月五号……

~/.murmur/data/memory/ledger.jsonl gained 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=1 showed 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 test 1424 passed / 1 skipped (exit 0), pnpm run typecheck clean, npx oxlint src test scripts .github/scripts clean.

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 logs rwt.refresh failed (nothing new returned) and the next boundary retries; regression test in test/rwt.test.ts.

Issues

  • By-ear pass: real-world topics, and the clock as bearings [spec 13 §5, spec 04 §3.4] #202 (by-ear) body updated for the new form: no policy file, the profile as taste, the ledger as the record; two boxes added (the pool leans toward the profile over a week and never toward the listener; a story told once is not told again as new — grep '"kind":"rwt"' ledger.jsonl is the check).
  • The stale ~/.murmur/rwt-policy.md an 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.md chain (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.md change) 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 that forget() 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 — set MURMUR_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

wine-fall and others added 7 commits September 4, 2026 16:37
…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
…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
@wine-fall
wine-fall merged commit 7830b82 into main Sep 4, 2026
3 checks passed
@wine-fall
wine-fall deleted the zachg-0904--rwt-memory branch September 4, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant