Add brain_delete_events MCP tool - #928
Merged
selfcontained merged 7 commits intoAug 10, 2026
Merged
Conversation
Agents could append and query Brain events but never prune them, so noisy collections grew without bound. brain_delete_events deletes either an explicit set of event ids (from brain_query_events) or every event matching a filter, and requires at least one id or filter so the log cannot be wiped by accident. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Reject non-ISO since/until. Postgres treats "now", "epoch", "infinity" and friends as valid timestamptz literals, so a filter that reads as a narrow bound silently matched the whole log. Validated in the zod schema and again in the store so non-MCP callers cannot reintroduce it. - Add dryRun, which counts matches through the same WHERE builder without deleting, so agents can size a prune that exceeds the 200-row query cap. - Move MAX_EVENT_IDS_PER_DELETE into the store and enforce it there, matching MAX_LIST_ITEMS_PER_PUSH. - Cover cross-repo isolation on the ids path, where repo_root is the only predicate keeping one repo out of another's events. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Date.parse rolls Feb 30 over to Mar 2 and accepts year 0, so both slipped past the ISO shape check and failed inside Postgres as a raw untyped error. Check the calendar directly so the caller gets a validation_error instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Job collections share a kind/subject vocabulary — docs-audit, tech-debt, and
test-enforcer all record kind "run" — so brain_delete_events({kind: "run"})
reads as pruning one job's history while actually deleting every job's. Filter
deletes now require a collection; the ids path is unaffected since ids are
unique and already repo-scoped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>An empty ids array reduced an id-delete to a bare filter: deleteEvents(root,
{ collection, ids: [] }) skipped the XOR guard and deleted the whole
collection. The guard now keys on presence, and one exported normalizer,
toEventDeleteSelector, owns the ids-XOR-collection-scoped-filter rule for
both the MCP handler and the store.
Also: isIsoInstant becomes the single instant rule the zod schema refines
against instead of restating it, deleteEvent delegates to deleteEvents so one
DELETE statement remains, and the collection-required error points at
brain_query_events for discovering collection names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>A .refine() is opaque to JSON Schema generation, so consolidating onto isIsoInstant left since/until published as a bare string — dropping the hint a client uses to format a value whose malformed case is destructive. .meta() restores format: "date-time" as advertisement while the refine stays the only thing that decides accept/reject. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The brain_delete_events description was long enough to widen the table, so prettier reflowed all 60 rows for one addition. Shortened to fit the existing column width. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
selfcontained
deleted the
agt_4a1dd3fa8779/build-brain-delete-events-mcp-tool
branch
August 10, 2026 16:02
This was referenced Aug 11, 2026
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 freeto 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.
Agents could append and query Brain events but never prune them, so noisy collections grew without bound.
brain_delete_eventscloses that gap.Shape
Two mutually exclusive modes:
ids— specific events, frombrain_query_events(uuid-validated, max 200)collection, optionally narrowed bykind,subject,tags,since,untildryRunreturns{ deleted: 0, matched: n }by counting through the sameWHERE/params the delete would use, so a preview can't drift from what a subsequent delete removes. Real deletes return{ deleted, matched }. Everything isrepo_root-scoped from server-side agent context, never from tool input.Why a collection is required
This repo's job prompts all record
kind: "run"in their own collection —docs-audit,tech-debt,test-enforcer,componentizer,persona-review. An unscopedbrain_delete_events({ kind: "run" })reads as pruning one job's history while actually deleting all five. Filter deletes now name a collection; the ids path is unaffected since ids are unique and already repo-scoped.The constraint is not a dead end: the tool description and the error message both point at
brain_query_eventswithout a collection, which returns each event'scollectionand so enumerates what exists.Guardrails worth calling out
since/untilmust be real ISO 8601 instants. Postgres acceptsnow,epoch,infinity,yesterdayas timestamptz literals, so a loose string would have silently matched the entire log.Date.parsealone is insufficient too — it rolls Feb 30 to Mar 2 and accepts year 0 — hence an explicit calendar check.isIsoInstantis the single rule; the zod schema refines against it rather than restating it, and.meta({ format: "date-time" })keeps the published schema informative.BrainEventDeleteSelectoris a union, and one exportedtoEventDeleteSelectorowns the ids-XOR-collection-scoped-filter rule — the MCP handler calls it to narrow its args, anddeleteEventscalls it again so untyped callers hit the same gate. Guarding on presence rather than length matters:{ collection, ids: [] }must be rejected, not silently widened to a collection-wide delete.deleteEventdelegates todeleteEvents, so oneDELETEstatement remains and the existing HTTP route's id path is provably identical.Not included
No bulk-delete HTTP route and no UI affordance — the Brains UI still deletes events one at a time. Those can follow.
Testing
pnpm run checkclean ·pnpm run test3344 passed ·pnpm run test:e2e178 passed~20 new tests, plus live verification against a dev MCP endpoint after each change: unscoped filters, Postgres special literals, impossible dates, empty
ids, and ids-plus-filter all rejected as typed errors with nothing deleted; dry run and scoped delete behave. Two guards were confirmed by mutation — dropping therepo_rootpredicate from the ids branch, and reverting the presence-basedidscheck, each make their test fail.Reviewed by the
backend-security-reviewandarchitecture-reviewpersonas; all nine findings resolved.🤖 Generated with Claude Code