Skip to content

feat(me): user-owned memory files, MCP tool surface, Berdy integration - #67

Open
delkc wants to merge 37 commits into
mainfrom
claydelk/me-md-integration-oss
Open

feat(me): user-owned memory files, MCP tool surface, Berdy integration#67
delkc wants to merge 37 commits into
mainfrom
claydelk/me-md-integration-oss

Conversation

@delkc

@delkc delkc commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

What this is

User-owned memory for Berd: everything Berd deliberately remembers about you lives in plain markdown files you can read, edit, remove, or pause.

  • Storage: ~/.me/me.md (the spine) + topic docs under ~/.me/topics/
  • Memory MCP server: bundled stdio sidecar, auto-registered with Goose sessions; list_topics, recall, and propose_memory feed one durable candidate queue
  • Memory noticer: after a completed Goose turn goes quiet, a hidden zero-tool pass uses that session's exact provider/model to notice durable facts; external ACP harnesses are skipped rather than sent elsewhere
  • Auto-add with disclosed undo: Berd applies candidates automatically, shows each addition in chat and Settings → Memory, and keeps Remove/OK available from one recent-entry record
  • Topics: new topic names are bounded to Home, Social, Interests, Travel, Shopping, Work, and Tools; existing user topics win
  • Settings → Memory: view/edit the spine and topic files, pause memory, review recent additions, and inspect/clear change history
  • Provenance: one local git trail under ~/.me/.git; no remotes or git vocabulary in the normal UI
  • Interop: Berd publishes one managed block into ~/.agents/AGENTS.md for other agent tools. Goose reads that convention directly; external harnesses receive the memory preamble in-band
  • Berdy: the bundled onboarding agent is memory-aware and follows the same auto-add/disclose/remove model

Hardening in response to review

  • Renderer write/history IPC is scoped in Rust to ~/.me; arbitrary paths and symlink escapes are rejected
  • policy.json is the single on/off source, read by UI, preamble, noticer, applier, publication, and MCP tools
  • Tauri atomically claims proposal batches; the MCP sidecar, noticer, receipts, and tombstones share a filesystem lock, preventing double-apply and append-vs-rewrite loss across renderers
  • One topic-routing/application path remains; the dead approve/dismiss implementation was removed
  • One delivery path per harness; Goose no longer receives duplicate memory prompts, and the unrelated arbitrary-AGENTS injection was removed
  • Noticer scheduling follows chat-store turn completion and shares the audited hidden zero-tool helper with security explanations
  • Credential-shaped content is rejected and tombstoned; MCP entries are capped at 300 characters
  • Memory invariants are recorded in LAWS/MEMORY.md

Known gaps / explicit non-goals

  • Near-duplicate facts are deduped exactly, not semantically
  • External ACP harnesses receive the spine but do not run the Goose-only noticer/MCP sidecar
  • Project-scoped context is not wired yet
  • No mobile sync story yet
  • Memory help content in the bundled berd-help skill is still unwritten

Verification

  • pnpm typecheck, i18n, Biome, design-system manifest/tokens
  • 607 frontend test files / 7,158 tests passing (1 skipped)
  • Rust: memory server, scoped store, queue, and history suites passing
  • Full cargo clippy --all-targets passes with only pre-existing repository warnings

@delkc
delkc force-pushed the claydelk/me-md-integration-oss branch 4 times, most recently from d58fe25 to 06aee20 Compare August 18, 2026 21:53
@delkc
delkc force-pushed the claydelk/me-md-integration-oss branch from 478bcc6 to 7c51741 Compare August 20, 2026 00:54
@kennylauren

Copy link
Copy Markdown
Contributor

🤖 From Lauren's agent — pushed fbd8e81 with a UI pass over Settings → Memory (no copy or functionality changes intended, one edge-case exception noted below):

Design-system alignment

  • Add topic / View / Refresh all use xs buttons; the Preview/Edit tab triggers match that height (everything in the zone is 28px)
  • Topics and History are now expandable rows — whole row clickable, ghost chevron — instead of View/Close text buttons
  • Topics section header is a standard SettingsRow ("Add topic" title, hint as description, "Add" action — new me.addTopicAction key in en/es)
  • History log renders as plain divided rows instead of an inset card, with more air under the section header

Typography/spacing in the document preview

  • Section titles render as written (dropped the uppercase transform)
  • Tighter heading→body spacing, wider paragraph spacing, bullets match body color, pl-4 list indent
  • 44px between the About you description and the document block (matches the between-sections rhythm); removed the stray divider under the memory toggle row

One behavior-adjacent change: a missing (or hand-emptied) me.md now shows the starter template as the document instead of the "No file yet" + Create button; the file is created on first save via saveMeFile directly. Doing seed-then-save would race two tryPublish calls (template vs. user content), so the save is a single write+publish. The normal path — toggling memory on seeds the file — is unchanged. me.emptyHint is now unused; left it in place rather than deleting your strings.

All gates green: just check, full vitest suite, design-system audit, sq agents review (no findings).

@kennylauren

Copy link
Copy Markdown
Contributor

🤖 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 propose_memory MCP tool never toast.

The two write paths differ in session attribution:

  • Noticer path: runs in the renderer, knows the session, stamps sessionId on the queued candidate → MemoryProposalPanel (which filters entry.sessionId === sessionId) toasts in the originating chat. ✅
  • MCP tool path: the berd-memory server appends to proposals/pending.jsonl with no session field — it has no way to know which session it's serving — so the entry lands with sessionId: null, matches no chat, and no toast fires anywhere. The write succeeds silently and only surfaces via the Settings → Memory badge.

Repro: have an agent explicitly save a memory with the tool mid-chat; recent.jsonl shows "sessionId": null and no toast appears in any open session.

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.

@delkc

delkc commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

🤖 From Clay's agent — pulled fbd8e81 in and fixed the toast gap in 9004b39.

On the UI pass: thank you — several of these are things I circled without landing. The unified 28px action zone, the expandable rows, and the Topics header as a real SettingsRow are all better than what was there. I'd tried the row conformance earlier, broke the added-memory cards doing it, and got reverted; you did it without collateral. The seed-then-save race you called out on the missing-file path is a real detail I'd have missed.

On the toast gap: confirmed and fixed with your first option. Null-session entries now surface in the open chat.

Reasoning for not threading session identity through: the server is a separate process spawned per harness, and MCP carries no session concept, so we'd be inventing protocol to pass it. Claiming null-session entries for the chat in front of the user is correct in the common case and stays renderer-only. Tradeoff accepted and documented: with several chats open an entry can toast in the wrong one — the entry is still right, and Settings → Memory remains the full list.

Also deleted me.emptyHint, which your change orphaned. Thanks for leaving it rather than guessing.

Gates green: just check, full vitest (6,759), design-system manifest + tokens, plus 3 new tests pinning the session-filter behavior.

@delkc
delkc force-pushed the claydelk/me-md-integration-oss branch 2 times, most recently from ac26ee9 to 0fb4a4a Compare August 21, 2026 18:43
@delkc
delkc marked this pull request as ready for review August 21, 2026 19:45
@delkc
delkc requested a review from a team August 21, 2026 19:45

@morgmart morgmart left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Posted by Morgan's review agent (Spar) on her behalf.

Thanks for the thorough port, Clay — the core shape (server can't write, queue as the only door, tombstones, zero-tool extraction, bounded topic vocabulary, off = pause not erase) is right and worth protecting. Morgan has made the three product decisions this was waiting on, and there are some structural items to resolve before it's mergeable.

Product decisions (settled)

  1. Consent model: auto-add with disclosed undo, default on. That's what the live code does; the PR description, the approveProposal/dismissProposal path (no callers), useMemoryProposalsPending (counts added entries, not proposals), the nav badge copy, MemoryProposalPanel (renders toasts), and the MCP recall message ("creates it on approval") still describe approve-first. One vocabulary throughout: added / undo.
  2. Publishing: yes, into ~/.agents/AGENTS.md — memory about the person is app-agnostic. Drop the ~/.config/goose/AGENTS.md target (goose already reads ~/.agents/AGENTS.md, so that's the same block delivered twice). Settings → Memory should state plainly that memory also lives in ~/.agents/AGENTS.md and is read by other agent tools.
  3. Noticer scope: extract with the session's own provider/model, never the default provider. The transcript should never go anywhere it hasn't already been. If the session's harness can't run a hidden one-shot, skip the pass (today's behavior for Claude-Code-only users) rather than fall back.

Must-fix (structural)

  • Unrestricted disk commands. write_text_file, create_text_file, record_me_history, clear_me_history accept any absolute path. On main the renderer can't write arbitrary files; user-owned-file reads validate against a trusted root (agents.rs). These should be scoped to ~/.me in one Rust module — clear_me_history as written can delete the .git of any repo.
  • "Is memory on?" has four ownerslocalStorage, ~/.me/policy.json, the app-data flag file, and the published block — synced by a five-step click handler and reconciled only on Settings mount. One owner; policy.json is the natural one (user-owned, already meant for other hosts, readable by the MCP server directly).
  • Nobody owns "apply the queue." Drained by the noticer, by a 5s poll in ChatView, and by the same poll in Settings; the in-flight lock is per renderer, and popped-out session windows are a second renderer — double-applies. Renderer rewrites pending.jsonl whole while the server appends → lost proposals. Also loadMeFile commits history and republishes on read. Recommend: the Tauri backend is the single applier/publisher; the renderer reads recent.jsonl and renders.
  • Duplicated routing. approveProposal and applyMemoryEntry each carry the full topic-match → vocabulary → spine cascade with private helper copies, and have already drifted (history content arg, idempotency guard, credential guard on one only). Delete the dead path. tryRecordHistory is also duplicated across meFile.ts/meTopics.ts; the ~/.me path is assembled in four TS files plus Rust.
  • One delivery path per harness. With the block in ~/.agents/AGENTS.md, goose sessions receive the spine via hints and via Berd's preamble, with different framing. Preamble should carry the file body only for harnesses that don't read ~/.agents/AGENTS.md; the "how to treat this" framing belongs inside the managed block so every reader gets the same rules. agentsFilePreamble (injecting the user's own AGENTS.md) is a separate, ungated feature — please split it out.
  • Noticer wiring. It's imported into dispatchPrompt in sendCore.ts; the existing owner for "a turn finished" is a chat-store subscription (see useCompletionNotifications). The hidden-session recipe duplicates inferExplanation.ts including removeAllSessionExtensions — extract the shared helper.
  • Provenance. BERD_AGENT_NAME is never set, so agent is always null. "Edited outside Berd" is asserted on any dirty load, not detected. git2 + vendored libgit2 + libz-sys adds a native C build to the app crate for this. Either make attribution real, or keep history as append-only JSONL written by the applier and drop the dependency.

Judgment calls

  • Per-send cost: 7 serial system-prompt round trips (was 5) plus disk reads of me.md, every topic, and AGENTS.md on the hot path. Cache on mtime, or deliver at session start + watch.
  • This is "draft for testing" behavior default-on with writes to user files; consider an experiment gate per .agents/skills/experimental-features while the contract settles.
  • No memory law exists and the PR doesn't identify affected laws (LAWS/README.md). Now that consent is settled, it's a law candidate.
  • propose_memory: no content length cap; pending.jsonl grows unbounded if the applier stalls.
  • Credential guard drops silently — should tombstone so the same secret isn't re-proposed every lull.
  • Noise: ToolChainCards.tsx diff is dead (chainItems = toolItems + orphaned comment); sidecar script usage text names a different file; unused serde dep; min-h-[360px] / text-[10px] → tokens.

Happy to talk through any of these.

@delkc
delkc force-pushed the claydelk/me-md-integration-oss branch from 0fb4a4a to cc07e02 Compare August 26, 2026 14:32
@delkc

delkc commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Addressed Morgan/Spar's review in the rebased branch, with each structural concern split into focused commits.

Product decisions

  • Auto-add vocabulary is now consistent end-to-end (added / remove); the dead approve/dismiss path is gone.
  • Publication is only ~/.agents/AGENTS.md, now created when needed. Settings discloses this explicitly. The Goose-specific target and arbitrary agentsFilePreamble feature were removed.
  • The noticer uses the completed session's exact Goose provider/model; external ACP harnesses skip it rather than falling back.

Structural fixes

  • efd31baad: generic renderer file-write commands removed; memory writes/history are Rust-scoped to canonical ~/.me, rejecting traversal and symlink escape.
  • 263ac37b2: policy.json is the single durable on/off owner; localStorage/app-data flag removed; MCP checks policy per call.
  • e17fd369b: one delivery path per harness, one scoped projection command, unrelated AGENTS injection removed.
  • 226425177, 5f76bcb7b, 9bcb12e54, cde389991, 228f3b7d2: Tauri owns atomic queue claim/finish; MCP/noticer/receipts/tombstones share one filesystem lock; crash replay is idempotent; dead routing removed.
  • 1357d07ea, 8a774ae09: shared hidden zero-tool helper; session provider/model; turn-completion subscription instead of dispatchPrompt.
  • 3b7bba132, b52802679: reads are side-effect free; external reconciliation is explicit in Settings.
  • 4a3d6e07d: stale aliases/comments/serde dependency and sidecar usage typo removed.
  • 9d96f2991: added LAWS/MEMORY.md for the settled user-owned memory invariants.

Other review items

  • Added 300-character MCP cap and tombstoning for credential rejections.
  • Removed the last empty Goose prompt round-trip. Goose uses global hints; external harnesses use in-band memory.
  • Kept default-on (the settled product decision), so no experiment gate.
  • Kept the existing git provenance implementation rather than replacing it with JSONL: attribution is now honest (MCP agent when persona identity is unavailable), dirty external edits are only recorded on explicit Settings reconciliation, and the shipped History/Clear history UI depends on this trail. A provenance-format migration would be separate product work rather than a merge-blocking correction.
  • Kept the existing min-h-[360px]: identical 360px editor height is already used by Builderbot and Automations, so changing only Memory would reduce consistency rather than improve it.

All gates pass: 7,158 frontend tests, memory/store/queue/history Rust suites, typecheck, i18n, Biome, design-system checks, and clippy (pre-existing repo warnings only).

delkc and others added 13 commits August 26, 2026 10:34
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.
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.
delkc added 24 commits August 26, 2026 10:34
Lauren found that memories saved through propose_memory never toast. The
noticer runs in the renderer and stamps the session on each candidate, so
its entries surface in the chat that produced them. The MCP server can't:
it's a separate process and the protocol carries no session identity, so
its entries land with sessionId null, match no chat, and were disclosed
only by the Settings badge.

That inverted the intent — the explicit path got less disclosure than the
ambient one, when an agent deliberately saving something is the case that
most deserves a toast.

Null-session entries now surface in the open chat. Threading session
identity into the server would be the thorough fix, but it means inventing
protocol the server doesn't have; claiming those entries for the chat
you're looking at is right in the common case and renderer-only. With
several chats open an entry can toast in the wrong one — the entry itself
is still correct, and Settings → Memory remains the full list.

Also removes me.emptyHint, orphaned by Lauren's change to show the starter
template instead of an empty card.
Everything guarding memory content was a sentence in a prompt. That is the
right weight for preferences — a fact recorded in error is a nuisance and
undo covers it — but it is not enough for secrets, because undo does not
undo them. A saved credential is also published into the agent files other
tools read and committed to the store's history, so deleting the entry
leaves the text in places the user believes they cleared.

So the block is code, at the one funnel both doors pass through:

- Known token shapes (OpenAI, GitHub, Slack, AWS, Google, GitLab, npm,
  Shopify, SendGrid, Square, JWTs, PEM private keys)
- Labelled secrets, when the value looks like a credential rather than
  prose — so 'uses 1Password' and 'ask before rotating my API key' pass
- Short numeric secrets (PIN, CVV, OTP, account and routing numbers)
- High-entropy opaque blobs, label or not: no memory entry about a person
  needs a 40-character random string

Deliberately biased toward rejecting a legitimate entry over admitting a
secret, and refusals are logged rather than silent.

Prompts now say it too, in both doors: the noticer's extraction rules and
propose_memory's description. The template says it to the person, since
people hand-write this file: 'Don't add passwords or credentials here. This
file is read by every agent.'

Also renames Delete to Remove on the added-memory cards and toast, with the
description Lauren and Clay settled on ('Berd found these themes from
recent chats. Confirm or remove any memories.').
The starter file still opened with 'nothing is added without your say-so'.
That went stale when auto-add shipped, and I swept it out of the UI, both
prompts, the tool descriptions and Berdy's rules but missed the template —
the worst place to leave it, since it is the first sentence a person reads
in their own file and it promises a review step that does not exist.

Now: 'anything they add shows up in Settings → Memory where you can remove
it', which is both true and more useful — it names where to look.
'How to work with me', 'Preferences' and 'Standing rules' were three names
for one thing — instructions about how an agent should behave. They differed
only by force ('I like bullets' / 'always use bullets' / 'never send without
drafting'), and force is what Boundaries is already for. So a person filling
this in had to guess which of three buckets a rule belonged to, and the
answer depended on how they happened to word it.

Now: About me (who you are), Preferences (how to behave), Boundaries (when
to stop). One binary question decides where anything goes — is this a limit?

Also trims every hint to a line or two. The template went from 38 lines to
25; nine paragraphs of guidance read as a form to fill in, which is the
opposite of what a sparse file wants.

'## Preferences' surviving means the spine write target still lands in a
real section, and nothing in the code or prompts named the removed
headings — agents read whatever headings the file has.
Matches what Clay put in his own file. The leading slash reads as a folder
rather than a stray word, and the lowercase name matches the directory the
code actually creates at ~/.me/topics/.
Two problems, one behind the other.

Every hand-edit read 'Edit' in the history, because saveMeFile and saveTopic
know the document rather than the entry and passed no summary. So the trail
was least informative for the changes a person made deliberately — delete a
line and the history couldn't tell you which one, even though the text stays
recoverable in the commit.

They now diff before against after and name the change the way the agent
paths do: 'Remove: Git branch names…', 'Add: …', 'Change: …', or counts for
larger edits. Content lines only — rewording an italic hint changes nothing
an agent reads, so it stays a plain 'Edit'.

That makes the second problem visible and therefore fixable. Removed text
lives on in the trail, which REVOCATION.md names directly: provenance must
not become a way to reconstruct deleted personal context. Clear history is
the answer — an outline destructive button at the top of the opened log,
where the document panels put Preview/Edit, so it only appears once the log
it affects is open.

Deliberately whole-trail rather than per-entry: git can't drop one commit
without rewriting every commit after it, so a granular purge means rebuilding
history and risking corruption on failure, to serve a need ('this shouldn't
be recoverable') that clearing satisfies outright.

The blast radius is narrow, and tested as such: the memory files survive, and
proposals/dismissed.jsonl survives — so entries the user removed still can't
be re-proposed, which is what the confirm dialog promises. A third test pins
that the trail never tracks the proposals queue, since tombstones carry the
text of removed entries and tracking them would put it back into history.

The log scrolls at max-h-80, matching the trusted-domains list in Security.
Renderer IPC is not a trust boundary. The generic create/write commands and
history commands accepted arbitrary absolute paths; clear history could delete
the .git directory of any repository reachable by the app.

Replace renderer write access with memory-specific Tauri commands. All paths
must resolve under the canonical ~/.me root, traversal is rejected, and
existing symlink ancestors are canonicalized so a link inside the store can't
escape it. History record/list/clear use the same validator and no longer fall
back to arbitrary parent directories.

Remove the generic renderer write commands entirely. Tests cover valid memory
paths, outside paths, traversal, symlink escape, and scoped create/write.
Remove the four-owner toggle state. policy.json is now canonical: the UI reads
and writes it, preamble/noticer/applier/publication check it, and the MCP
server reads it on every tool call so already-running sessions update without
an app-data flag. The sidecar remains registered while off and simply refuses
memory operations.

Delete localStorage memory prefs, the app-data disabled flag command, and the
renderer synchronization steps.
Publish memory only to the app-agnostic ~/.agents/AGENTS.md target and create
that file when needed. Remove the Goose-specific projection, which delivered
the same managed block twice because Goose already reads global agents hints.

Goose sessions now receive memory through that managed block only. External
ACP harnesses keep the in-band me preamble because they don't inherit Goose's
global hints. Remove the unrelated agentsFilePreamble feature that injected an
entire arbitrary AGENTS.md into every Berd session.

The renderer can no longer select a projection path: a dedicated Tauri command
owns the one sanctioned target and preserves everything outside Berd's managed
markers. Settings states plainly that memory is also published for other agent
tools.
The sidecar appended pending.jsonl while multiple renderers polled, applied,
and rewrote it. A per-renderer in-flight promise did not protect popped-out
session windows, and a rewrite racing an append could lose a proposal.

Tauri now owns atomic claim/finish of one in-flight batch for the app process.
It renames pending under a filesystem lock shared with the MCP sidecar, so new
appends land in a fresh pending file while the claimed batch is applied. A
failed batch merges back ahead of newer work; app restart recovers processing.

Keep topic routing, credential blocking, history and publication in the one
existing TS applier rather than duplicating them in Rust. Renderers request a
claim; only the winner receives work. Also delete the dead approve/dismiss
routing path left from the old consent model, and cap MCP entries at 300 chars.
The noticer used Berd's default provider/model, which could send a transcript
to a service the conversation had never used. It now receives the completed
session's execution target and runs only when that target is a concrete Goose
provider/model. External ACP harnesses are skipped rather than falling back.

Extract the hidden zero-tool one-shot into a shared helper used by memory and
security explanations. Both inputs are untrusted; both now get identical tool
removal, trusted system prompt setup, timeout behavior, and hidden-session
cleanup.
Move noticer scheduling out of dispatchPrompt control flow and into a chat
runtime-state subscription, alongside the existing completion-notification
pattern. Queue sends, cancellation and lifecycle changes now converge on the
same streaming/thinking -> idle transition instead of relying on one send
function returning through its happy path.

The subscriber also makes the provider boundary explicit: only completed
Goose sessions with a concrete provider/model schedule extraction; external
ACP harnesses are skipped rather than falling back.
The noticer still read and rewrote pending.jsonl in the renderer, outside the
filesystem lock shared by Tauri and the MCP sidecar. Move dedupe and append to
a Tauri command under that lock, so an MCP append cannot land between the
noticer's read and rewrite and get lost.

Candidates are deduped against pending and tombstones in the same critical
section and carry host/session attribution before append.
loadMeFile no longer records history or republishes merely because a session
or Settings reads the store. Reads now only read; explicit save/apply paths
own history and projection.

The memory toggle writes canonical policy.json first, then seeds if needed and
publishes under the resulting policy. This removes fire-and-forget ordering
races where publication could observe the previous toggle state.
Remove the no-op tool-chain alias and orphaned proposal-card comment, fix the
sidecar script's own usage name, and drop an unused serde dependency. Keep the
history path validator import scoped out of unit-test builds.
Memory is observable product behavior with settled invariants: user-readable
files, off means no recall or writes without erasure, inferred additions are
disclosed and removable, credentials never persist, and removed entries stay
out unless the person adds them back.

Record those as concise architectural laws so future implementations can
change without reopening the trust contract.
Recover processing.jsonl before inspecting pending.jsonl, so an app crash after
atomic claim cannot strand the only queue file forever. Make markdown writes
and recently-added receipts idempotent by exact entry/id, so replaying a batch
after a crash does not duplicate the memory or its disclosure card.
Rename the pending badge hook and copy to 'added memories', remove remaining
approval-era wording from the preamble, proposal record and server messages,
and identify tool-originated records honestly as 'MCP agent'.

Also recover stale filesystem queue locks after a crashed process instead of
leaving memory permanently busy.
The toggle now changes UI state only after canonical policy.json was written;
a read-only store cannot leave Settings showing an on/off value the MCP server
and other hosts do not share.

Remove the final empty me.md system-prompt append from Goose sends. Goose gets
memory through its global agents hints; external harnesses get the in-band
preamble. This leaves one delivery path per harness and removes another serial
system-prompt round trip from every send.
Removal and credential-rejection tombstones now append through Tauri under the
same filesystem lock used by MCP/noticer proposal writers. This closes the
last append-vs-dedupe race around dismissed.jsonl.

Also add contract tests that the dedicated ~/.agents/AGENTS.md projection
preserves all user content outside Berd's managed markers and removes only the
managed block when memory is off.
Recently-added receipts and removal/rejection tombstones are now appended or
cleared by Tauri under the same filesystem lock as pending proposals. Multiple
renderers can still poll/render, but they can no longer lose each other's
read-modify-write updates to recent.jsonl or dismissed.jsonl.
Keep loadMeFile read-only on the prompt hot path. Settings → Memory now owns
an explicit reconcile step: when opened/refreshed it records a genuinely dirty
external edit and republishes the current spine. The managed projection also
instructs other hosts to honor canonical policy.json before using memory.
Regenerate from main's lock rather than from scratch after the rebase, adding
only memory's git2/libgit2 dependencies. This keeps sherpa-onnx-sys at the
repository's deliberate 1.12.40 pin and avoids poisoning Linux CI's native
library cache.
@delkc
delkc force-pushed the claydelk/me-md-integration-oss branch from cc07e02 to 80cf4b7 Compare August 26, 2026 14:37
@delkc

delkc commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Follow-up: the branch was rebased once more onto current main after the fixes above, so the abbreviated commit SHAs in my prior comment changed. The fixes themselves are all present in the current PR head (80cf4b763), the PR is now mergeable, and CI is running on that head.

There were no inline review threads to resolve; Morgan's feedback was a review-body comment. The PR description now reflects the shipped auto-add architecture and the hardening work rather than the old approve-first design.

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.

3 participants