feat(me): user-owned memory files, MCP tool surface, Berdy integration - #67
feat(me): user-owned memory files, MCP tool surface, Berdy integration#67delkc wants to merge 13 commits into
Conversation
d58fe25 to
06aee20
Compare
Draft for testing, not merge — architecture pending DRI review. User-owned memory for Berd: everything Berd deliberately remembers about you lives in plain markdown files you can read, edit, and delete, with consent gated in code, not prompts. - Storage: ~/.me/me.md (the spine, rides into every session) + topic docs under ~/.me/topics/ (loaded only when relevant) - Memory MCP server: bundled stdio sidecar (berd-memory-mcp), auto-registered with goose sessions. Three tools: list_topics, recall, propose_memory. The server cannot write memory; proposals queue for user approval - Memory noticer: a hidden zero-tool extraction pass after a conversation goes quiet, feeding the same consent queue as the in-conversation propose_memory tool - Consent surfaces: approval cards in the chat that produced the fact, plus a Proposed memories queue in Settings -> Memory with a nav badge - Topics: bounded to seven broad areas (Home, Social, Interests, Travel, Shopping, Work, Tools) so facts route consistently - Settings -> Memory: view/edit the spine and topics in-app, a "Use memory" toggle (pause, not erase, enforced server-side per call), topic creation - Provenance: every change attributed via invisible local git in ~/.me/ (no remotes, no git UI) - Interop: the spine publishes into agents files the user already has (~/.agents/AGENTS.md, goose global hints); never creates them. Berd also reads the user's own global agents file into sessions - Berdy is memory-aware: proposes via the tool, edits directly only when told to, respects the toggle Squashed from 33 commits on the original branch (squareup/berd#1083), ported onto this repo's history after the squareup/berd -> block/berd migration archived the original remote. Co-Authored-By: Claude <noreply@anthropic.com>
Review feedback: a click per memory is how you get an empty file, and an empty file protects nobody. So candidates now apply as soon as Berd sees them, and the user gets a card showing exactly what landed with a delete button. The queue stays the transport for both doors (the MCP server's propose_memory and the noticer), so there's still one write path rather than two that could drift on topic routing or attribution. What changed is that it auto-drains instead of waiting for approval. That trade puts the weight on undo, so: - removeBullet deletes only an exact-match bullet, first occurrence — a delete that quietly took out a line the user reworded would be worse than one that no-ops. - Deleting tombstones the entry in the same file propose_memory already checks, so an auto-add can't resurrect what was just removed. - The removal is attributed to the user in the file history, so the trail shows both the add and the undo. One record, one resolution: entries live in recent.jsonl until acknowledged or deleted *anywhere*, so acting in chat clears the Settings card and vice versa. Unreviewed entries age out after a week so the section can't become a chore. Also sweeps the copy that promised consent-before-write, which would otherwise now be a lie: the toggle description, topics hint, preamble reader rules, the server's tool response, and three of Berdy's rules.
The Use memory and About you descriptions read as two attempts at the same paragraph, so each now answers a different question: what memory is (and where it lives) up top, then what makes the spine file specific (the one every agent reads in every chat). Also: - Drops 'Berdy fills this in over time' — the noticer runs on every conversation regardless of agent and propose_memory is available to any agent with the tools, so naming Berdy implied a gatekeeper that doesn't exist. Berdy stays in the empty-state hint, where it's a real shortcut. - Fixes a stale consent promise the auto-add change missed: the empty state still said 'Nothing is added without your say-so,' which a brand new user reads before their file even exists. - Corrects the template's topics note. It said topic files sit 'right next to this one,' but since namespacing they live in topics/ — someone following that literally would look in the wrong place. Example filenames now match the actual vocabulary (home.md, social.md). - Trades every em dash in the Memory strings for a colon, period, or comma, and retunes the Topics hint for its spot beside Add topic.
Chat: added memories announce as toasts instead of inline cards, per design feedback. Follows CompletionNotificationToast — the app's existing two-action pattern for 'something happened, here's how to respond' — with Delete as the secondary action and OK as primary. Missing the toast costs nothing: unresolved entries stay in Settings → Memory behind the nav badge, and acting in either place clears both since they read one recent.jsonl. A shown-set keeps the 5s poll from re-toasting the same entry. This also lets ToolChainCards go back to treating propose_memory as an ordinary tool call, removing the hoisted- card machinery that existed only so cards could survive chain collapse. Settings, from testing feedback: - The rule inside 'Added to memory' was landing between the description and the cards, because divide-y treats a loose <p> as a row. The description and cards are now one unit with the rule below the section, which is the boundary that matters: the whole block appears and disappears with context. - Topics' description and Add topic move up under the heading with a rule beneath them, so Add topic sits on the other side of a line from the View buttons instead of reading as another topic row. Its pr-4 matches SettingsRow's own right padding so the buttons share a right edge. The 'New topic' subhead is gone; its description labels the naming row. - Delete and OK were identical ghost buttons, which gave no signal about which one changes a file. Delete is now destructive-ghost, OK is outline, both sized to match the View buttons.
Two stale lines the last rebase preserved because main edited elsewhere in the same file: - 'Nothing gets saved without their okay' predates auto-add. Memory is written immediately now, so the honest version is that anything saved is shown right away with a delete button. - Berdy described the disclosure as a card in the chat; it's a toast. Also adds pronouns to the About me hint, since it's exactly the kind of thing you'd rather state once than repeat.
block/me.md now has a normative spec (PROTOCOL.md, HOST.md) with
black-box conformance tests. Two of its requirements were cheap to meet
and make us interoperable, so they're worth doing before review:
Visible proposals/ instead of hidden .proposals/. The contract requires
a proposal to be 'inspectable content in the store's proposal area — not
host-private state — so that any other conforming host, or the person
with a text editor, can see and decide it.' A dotfolder reads as Berd's
business rather than the person's. Proposal records also now carry the
originating host, which the contract asks for alongside agent identity.
policy.json mirrors the memory switch into the store. Ours lived only in
Berd's app preferences, which is invisible to anything else serving the
same ~/.me/; the protocol puts policy in the store so every host honors
one decision. Berd writes it on toggle and reads it on load, so turning
memory off in another tool (or by hand) is respected here. No policy file
means no opinion, and Berd's own preference stands. Unknown keys survive
a round trip, since another host may add its own.
The directory rename needs a migration for one reason: dropping
dismissed.jsonl would let previously deleted memories be re-added, which
is the single regression the delete guarantee can't afford. It appends
rather than overwrites, leaves the old files in place, and latches after
one run.
Still deliberately non-conforming on the pen test: auto-add writes from
agent inference, which HOST.md forbids ('applying a proposal MUST require
an affirmative act'). That's the product decision from design review, not
an oversight, and it's a conversation with David rather than a fix.
Written to protect tombstones that turn out not to need protecting: the only store that ever had the hidden .proposals/ directory is one dev machine, and its contents were test data. Deleting it now, while the population is known to be zero real users, is better than leaving migration code nobody can confidently remove later.
The trail already stored everything; the metadata was written wrong. Every change committed as 'Entry approved in chat' — including deletions — so the log couldn't answer the question a person actually asks of it: did something I deleted come back? Actor and operation were conflated in one 'source' string. Split them: the author says who, the subject says what. Remove: Prefers aisle seats. You (deleted in Berd) Add: Prefers aisle seats. noticer (recorded in chat) Memory turned off You (changed the switch) Also fixes a line that went stale with auto-add: agents 'recorded' entries, they didn't get them 'approved'. And policy changes now land in the trail at all, which they didn't before — turning memory off is the highest-stakes change in the store and it was invisible. Adds a collapsed history at the bottom of Settings → Memory over the existing list_me_history command, which was built and unused. Quiet by design: nobody opens settings to read a changelog, and the answers are wanted occasionally. No new storage. PROVENANCE.md in block/me.md is explicitly non-normative and leaves the mechanism unspecified, so git stays the record and a second event log would only be a way for the two to drift.
Naming a location and then offering a separate 'Go to file' said the same thing twice, and the path is the part a person recognizes. One shared StorePathLink now handles every path in the feature, so they all behave the same way: click the path, the folder opens. Moves the location up to the toggle description, where it's visible whether or not a file exists yet, and drops it from under About you — repeating it per document was the redundancy. The empty state and the off banner link to the folder too; 'your files are untouched' reads better when you can go look. The history is the exception. It's a git object database, so calling it plain text would be false and linking it would open a folder nobody can read. It says how it's stored instead, which tells a git user what to do and everyone else that the answer is in Berd. Retires three strings (me.path, me.reveal, me.offBanner.link) and tightens the toggle description, which described the files as plain text right before the sentence that now says so.
Turning memory on created ~/.me/ but not me.md, so the page still asked for a file while the folder already existed. The folder was a side effect of writing policy.json, which creates parent directories; nothing created the spine. Seed it in both places a person can arrive with memory on: the toggle, and the first visit (memory defaults to on, so most people never touch the switch and would have kept seeing the create button). createMeFile is idempotent, so neither path can overwrite an existing file. This also matches what the rest of the system already does. Agents create the spine silently the moment they save anything — that was the fix for a first preference being unsaveable — so asking permission in the one place a person is looking was the odd behavior, not the seeding. The create button stays as the recovery path if a seed fails. History section polish alongside it: its header now matches the Topics header exactly (same alignment, gap, and no width cap), which is why the two descriptions were breaking at different words. The button reuses the topics' View/Close strings instead of a bespoke pair, holds a stable width so the description keeps one break point through the toggle, and no longer draws a row divider between the description and the opened log.
The off state was an Alert box with a bold title and a trailing ~/.me link. Alert is the wrong component for it — no other settings page uses one for a disabled state, and a warning box implies something is wrong when turning memory off is a legitimate choice. The link also repeated the one in the description directly above it. Both the off state and the missing-file state now sit in the same gray card the documents, added-memory entries and history log already use, so they read as part of the page rather than as exceptions to it. The off copy folds 'your files are untouched' into the sentence that lost the title, and the missing-file hint is down to 'No file yet.' beside its button — the toggle description above already covers what memory is and where it lives. The missing-file state is also nearly unreachable now that the store seeds itself; it stays as the recovery path if a seed fails.
478bcc6 to
7c51741
Compare
Four places still described the pre-auto-add design, and two of them are
text models read every session: the MCP extension description ('Proposals
are reviewed by the user; nothing saves without their okay') and
propose_memory's own description ('Nothing is saved by this call: the user
reviews every proposal in Berd and decides').
An agent told nothing saves without approval will describe memory that way
to the user, which is worse than saying nothing — the user is told to expect
a decision that never comes. All four now say what actually happens: Berd
saves it, shows what was added, and gives them a delete button.
The structural claim is unchanged and still worth stating, so the module doc
keeps it in accurate form: propose_memory never writes a memory file itself,
so no agent can save something the user is never shown.
UI pass over Settings → Memory, keeping behavior intact:
- Unify action sizing: Add topic/View/Refresh all use xs buttons; the
Preview/Edit tab triggers match that height
- Topics and History become expandable rows (whole row clickable,
ghost chevron) instead of View/Close text buttons
- Topics header becomes a standard SettingsRow ('Add topic' + Add)
- History log renders as plain divided rows instead of an inset card
- Document preview typography: sentence-case section titles, tighter
heading-to-body spacing, wider paragraph spacing, body-colored
bullets, pl-4 list indent
- Remove stray section divider under the memory toggle row
- Spacing: 44px between the About you description and document block
One edge case: a missing (or hand-emptied) me.md now shows the starter
template as the document instead of a blank card with a create button;
the file is created on first save. The normal path — toggling memory on
seeds the file — is unchanged.
|
🤖 From Lauren's agent — pushed Design-system alignment
Typography/spacing in the document preview
One behavior-adjacent change: a missing (or hand-emptied) All gates green: |
|
🤖 From Lauren's agent — a disclosure gap found while manually testing this branch (not addressed in our push, since it's a functionality change): Memories saved via the The two write paths differ in session attribution:
Repro: have an agent explicitly save a memory with the tool mid-chat; The MCP server's doc comment promises "Berd applies it, tells the user what was saved" — so this looks like a gap rather than intent, and it inverts expectations a bit: the explicit path gets less disclosure than the ambient one. Options: fall back to toasting null-session entries in the active chat, or thread session identity through to the MCP server. Happy to leave it with you either way. |
What this is
User-owned memory for Berd: everything Berd deliberately remembers about you lives in plain markdown files you can read, edit, and delete — with consent gated in code, not prompts.
~/.me/me.md(the spine — rides into every session) + topic docs under~/.me/topics/(loaded only when relevant)berd-memory-mcp), auto-registered with goose sessions. Three tools:list_topics,recall,propose_memory. The server cannot write memory — proposals queue for user approval~/.me/— no remotes, no git UI~/.agents/AGENTS.md, goose global hints) — never creates them. Berd also reads the user's own global agents file into sessionsdistro/agents/berdy.md) is memory-aware — proposes via the tool, edits directly only when told to, respects the toggleKnown gaps
propose_memory, but that path depends on model judgment and is best-effort~/.me/works and is honest)Migration note
Squashed from 33 commits on the original branch (
squareup/berd#1083), ported onto this repo's history after thesquareup/berd→block/berdmigration archived the original remote. The two repos have unrelated git histories (fresh OSS export, not a continuation), so this was a manual file-by-file port rather than a rebase/cherry-pick:mainsince the original branch was cut (mostly unrelated Windows process-tracking work, release tooling, and subagent-labeling changes) and were hand-ported into their current context, then verified against the semantic diffCargo.lockwas regenerated withcargo generate-lockfilerather than hand-patchedFull local verification before pushing:
cargo check --workspace,cargo clippy --workspace --all-targets(clean, no new warnings),cargo test --workspace(two pre-existing failure categories confirmed unrelated by reproducing them identically on a cleanmaincheckout),pnpm typecheck,pnpm lint,pnpm test(2,705 tests passing),pnpm check:i18n.Generated with Claude Code