You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Memories can't be hard-deleted (enVector has no per-vector delete, only delete_index). So we delete logically: vault keeps a deny-list of deleted item_ids and serves it. Vault is the single source of truth; clients do the filtering. Vault itself does not filter scores and does not call enVector.
flowchart LR
C[rune-mcp client] -- "MarkDeleted(item_ids)" --> V
C -- "FilterDeleted(candidates)" --> V
V[(Vault deny-list<br/>SSOT, per index)] -- "deleted subset" --> C
C -. "filters out deleted hits" .-> R[recall / capture results]
V -. "never talks to" .-x E[enVector]
Loading
Why these choices
Decision
Why
Logical delete (deny-list)
enVector exposes no per-vector delete — hard delete is blocked by dependencies
Key = item_id (uint64)
Stable logical id from insert / get_metadata. (shard,row) is physical → moves on compaction (dynamic-IP problem)
SSOT read-through, not client sync
Low-QPS interactive tool → per-query check is cheap; strong consistency for free; same RPC becomes the sync source later if ever needed
Vault stores, client filters
Keeps vault pure (no enVector client, no score-filtering); respects the fixed metadata-decryption flow
Scope (vault only)
2 new RPCs in vault/proto/vault_service.proto:
rpc MarkDeleted (MarkDeletedRequest) returns (MarkDeletedResponse); // write: union item_ids into deny-list
rpc FilterDeleted (FilterDeletedRequest) returns (FilterDeletedResponse); // read: given candidates, return deleted subset
Both carry token + index_name + repeated uint64 item_ids. MarkDeleted returns {version, count}; FilterDeleted returns {deleted_item_ids, version}. Candidate-scoped reads → cost independent of total deny-list size. version (monotonic) lets a future client cache/sync.
Store (vault/internal/...): map[index] -> {set, version}, file-backed with debounce — reuse the tokens/store.go pattern.
Handlers (grpc.go): token-validate → scope-check (mark_deleted / read scope) → audit emit(...), same conventions as existing RPCs.
TL;DR
Memories can't be hard-deleted (enVector has no per-vector delete, only
delete_index). So we delete logically: vault keeps a deny-list of deleteditem_ids and serves it. Vault is the single source of truth; clients do the filtering. Vault itself does not filter scores and does not call enVector.Where vault sits
flowchart LR C[rune-mcp client] -- "MarkDeleted(item_ids)" --> V C -- "FilterDeleted(candidates)" --> V V[(Vault deny-list<br/>SSOT, per index)] -- "deleted subset" --> C C -. "filters out deleted hits" .-> R[recall / capture results] V -. "never talks to" .-x E[enVector]Why these choices
item_id(uint64)get_metadata.(shard,row)is physical → moves on compaction (dynamic-IP problem)Scope (vault only)
2 new RPCs in
vault/proto/vault_service.proto:Both carry
token+index_name+repeated uint64 item_ids.MarkDeletedreturns{version, count};FilterDeletedreturns{deleted_item_ids, version}. Candidate-scoped reads → cost independent of total deny-list size.version(monotonic) lets a future client cache/sync.Store (
vault/internal/...):map[index] -> {set, version}, file-backed with debounce — reuse thetokens/store.gopattern.Handlers (
grpc.go): token-validate → scope-check (mark_deleted/ read scope) → auditemit(...), same conventions as existing RPCs.Checklist
MarkDeleted/FilterDeleted(auth + scope + audit)versionstore_test.goupdategrpc_test.gounit tests (CLAUDE.md)mise run checkgreenOpen questions
item_idstable across compaction/re-index — the whole design rests on thisfilter_deletedown scope vs reusedecrypt_scoresListDeleted(since_version)sync source — defer until needed