fix(mcp-wrapper): forget/delete tools no longer return fake ok receipts (BUG-18) #101
Reference in New Issue
Block a user
Delete Branch "fix/receipts-wrapper-forget"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Root cause (BUG-18)
The register cites the wrapper's forget/delete handler answering ok without checking whether the soul actually tombstoned the memory (wrapper.el:646-652 — line numbers have drifted; the source is
mcp-wrapper/src/main.el). On this branch the bug survives in two forms:delete_by_id— servingremoveKnowledge,deleteProcess,deleteImprint,dischargeWonder— fabricated{"ok":true,"deleted":"<id>","note":"soft-deleted"}without calling the soul at all. Its "soul does not yet expose a delete HTTP route" comment was stale:/api/neuron/node/deleteexists, tombstones any node type, and errors on unknown ids.tool_forgetforwarded the soul's response but never verified the deletion actually persisted before answering ok.The change
Implements Receipt Contract rule 1 (docs repo,
docs/specs/RECEIPT-CONTRACT-2026-07-22.md): a tool result must reflect what actually happened.delete_by_idnow routes to the soul's real/api/neuron/node/deleteand propagates its answer (honestnode not foundfor bad ids).GET /api/neuron/graph?id=<id>&depth=1must show the tombstone marker (label "tombstone:<id>"); otherwise they answer{"ok":false,"error":"delete_not_persisted","id":"<id>"}— the soul's own not-persisted error shape (api_not_persistedin neuron-api.el).Minimal diff: 31 insertions, 2 deletions, one file.
How to test (exact sandbox repro)
Build soul + wrapper with elb against the vendored release runtime
v1.0.0-20260501(same pin as #97 — the AR "latest"/dev runtime no longer definesengram_prune_telemetry; soul build dir pre-seeded withdist/elp-c-decls.h; link with-DHAVE_CURL -rdynamic). Boot sandboxed (throwaway HOME,SOUL_ENGRAM_PATHto a scratch file, no API keys, dead axon URL, tick timers 3600s):Before (unpatched, branch parent
b784750)After (this branch)
Regression gate
scripts/verify-soul-contract.sh(from main) against the elb-built soul this wrapper talks to (this branch's soul sources): GATE PASS — PRESENCE 27/27 answered, IMMUTABILITY pass (all mutation routes tombstone/supersede, no hard-delete).Note on dist/
No
dist/artifacts are touched — the combined-unit assembly step is Will's, and the committed dist already lags the.elsources by 8 modules.🤖 Generated with Claude Code
Review: honest forget/delete receipts + immutability (BUG-18)
Verdict: looks good / approve. Immutability doctrine confirmed intact -- this PR does not reintroduce any hard-delete or data loss; it removes a fabricated-receipt path and replaces it with real tombstoning.
Immutability (the critical check -- verified end to end)
delete_by_idnow POSTs/api/neuron/node/delete->handle_api_node_delete(neuron-api.el:258) ->tombstone_node->mem_tombstone(memory.el:52).mem_tombstonecreates aTombstonemarker node (labeltombstone:<id>), keeps the original node and all incident edges, and wires atombstonesedge. Neverengram_forget.tool_forgetroutes/api/neuron/memory/delete->handle_api_memory_delete(neuron-api.el:620) -- same tombstone path, node + edges KEPT.delete_by_idwas worse than a false ok: it never called the soul at all and fabricated{"ok":true,...,"note":"soft-deleted"}. Replacing that with a real immutable tombstone is a strict improvement to the doctrine. No hard-delete reintroduced anywhere.Correctness -- read-back verification is sound
GET /graph?id=<id>&depth=1->handle_api_inspect_graph->engram_neighbors_json(id, depth, "both")and it does not applymemory_hide_tombstoned(that hide is bounded-list-only, neuron-api.el:138-144). So theTombstonemarker is visible at depth 1 (reachable via the incomingtombstonesedge, directionboth), and its serializedlabeltombstone:<id>is whatstr_contains(check, "tombstone:" + id)matches. Read-back holds.str_contains(resp, '"ok":true')matches the soul's{"ok":true,"id":...,"tombstoned":true}; soul errors (node not found,memory not found,api_err_protected) contain no"ok":trueand pass through honestly. Good.Two minor nits (non-blocking)
{"ok":false,"error":"delete_not_persisted",...}, but the PR body says it uses "the soul's own not-persisted error shape (api_not_persisted)" -- the soul's actual shape iswrite_not_persisted(neuron-api.el:104). The distinct string is arguably clearer, but the description slightly misstates it. Cosmetic.handle_api_node_deletetombstones again and creates a second marker; read-back still passes. Harmless but produces duplicate markers -- optional idempotency guard (skip if atombstone:<id>marker already exists).Base-branch flag
This PR targets
hotfix/elc-source-typos, notmain.mainis the authoritative branch now, and there is already areconcile/hotfix-to-main-launchline in flight. As-is, this honest-receipt fix lands on a stale hotfix branch and risks being stranded there rather than reachingmain. @tim.lingo / @will: please confirm the intended landing path -- either retarget tomain, or guarantee this flows through the reconcile branch. Flagging, not blocking.Root cause: two false-receipt paths in the wrapper's delete family. - delete_by_id (removeKnowledge, deleteProcess, deleteImprint, dischargeWonder) FABRICATED {"ok":true,...,"note":"soft-deleted"} without calling the soul at all — the 'soul does not yet expose a delete HTTP route' note was stale (/api/neuron/node/delete exists and tombstones any node type). - tool_forget forwarded the soul's response but never verified the deletion actually persisted before answering ok. The change (Receipt Contract rule 1 — a tool result must reflect what actually happened): - delete_by_id now routes to the soul's real /api/neuron/node/delete and propagates its answer (honest 'node not found' for bad ids). - Both handlers read back before answering ok: GET /api/neuron/graph?id=.. &depth=1 must show the tombstone marker (label "tombstone:<id>"); if it does not, answer {"ok":false,"error":"delete_not_persisted",...} in the soul's not-persisted error shape (api_not_persisted). - Soul errors and transport failures pass through unchanged. E2E evidence (sandbox soul :7791 + wrapper :7792, elb builds): - unpatched: removeKnowledge on a NONEXISTENT id -> {"ok":true, "deleted":"kn-DOES-NOT-EXIST-deadbeef","note":"soft-deleted"} (lie) - patched: same call -> {"error":"node not found: ..."} (soul's answer) - happy path: remember -> forget -> {"ok":true,"tombstoned":true}; read-back: hidden from default /list/Memory, present with ?include_deleted=1, node KEPT in full graph view (immutability intact) - scripts/verify-soul-contract.sh on the soul it talks to: GATE PASS (27/27 presence + immutability) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>