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>
Phase 1 tombstoned memory/delete + node/delete, but the generic forget
path was still destructive. This routes every forget through the same
tombstone semantics so nothing in the daemon can hard-delete an engram
node anymore.
Canonical helper: mem_tombstone (memory.el, imported first so every module
can call it) — keep the node + its edges, record a Tombstone marker, never
engram_forget. neuron-api's tombstone_node now delegates to it (single
source of truth).
Per-path before -> after:
- memory.el `mem_forget` hard delete (engram_forget) -> tombstone. This
alone fixes both callers: the /api/neuron/memory/forget route
(handle_api_forget) and the cultivate op=="forget".
- awareness.el autonomous `forget` action engram_forget -> mem_tombstone.
The soul can no longer autonomously hard-delete a memory.
- mcp-wrapper tool_forget was a FAKE no-op that returned {"ok","deleted"}
without deleting OR tombstoning -> now routes to the soul's tombstoning
/api/neuron/memory/delete; tool description fixed to say it
supersedes/tombstones (recoverable), not "Remove a node".
Left intentionally as hard deletes (internal GC / lifecycle, not user
memory, all call engram_forget directly): session-summary replace
(chat.el), mem_consolidate dedup (memory.el), session-start telemetry
pruning (soul.el), session lifecycle (sessions.el).
Regenerated both ship paths under a 3GB physical-RSS watchdog (peak ~32MB):
dist/soul.c (single-TU amalgamation, macOS/Linux) and the per-module
dist/{memory,awareness,neuron-api}.c (Windows/Linux build). Verified: no
forget handler calls engram_forget; a forget leaves the node present +
tombstone marker. Gate (neuron-ui verify-soul-contract.sh, new memory-forget
row): PRESENCE + IMMUTABILITY PASS.
planWork fell through create_typed_node to a generic /api/neuron/memory write — a [BacklogItem]-prefixed
memory blob with title/project/priority DROPPED, never a real BacklogItem. reviewBacklog used a lexical
/recall (top-50, untyped). Now: planWork -> /api/neuron/node/create {node_type:BacklogItem,...} via new
create_node_typed; reviewBacklog -> list_typed('BacklogItem') (GET /api/neuron/list/BacklogItem). elc-clean.
Depends on neuron PR #58 (the list/<type> slice fix) to round-trip; needs the wrapper binary rebuilt +
:7779 restarted to take effect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>