Skip to content

feat(context): add ErasureCoordinator for cross-store entity erasure - #1027

Open
pravit-amp wants to merge 4 commits into
semantica-agi:mainfrom
pravit-amp:feature/erasure-coordinator
Open

feat(context): add ErasureCoordinator for cross-store entity erasure#1027
pravit-amp wants to merge 4 commits into
semantica-agi:mainfrom
pravit-amp:feature/erasure-coordinator

Conversation

@pravit-amp

@pravit-amp pravit-amp commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Description

ContextGraph.purge_node() is graph-scope by design (#957), and its changelog entry says so explicitly. That leaves the feature's stated motivation unfinished: the changelog names GDPR Article 17 as the reason purge exists, and an Article 17 erasure that removes the node while the same content survives verbatim as an AgentMemory item and as an embedding is not an erasure. It's worse than not offering one, because purge_node() returns True and writes a tombstone attesting the content is gone.

This adds ErasureCoordinator, which owns references to the stores, drives erasure across all of them, and returns an auditable ErasureReceipt of what was reached and what was not.

ContextGraph keeps its graph-scope contract unchanged — the coordinator composes the existing public APIs rather than modifying them. Nothing in context_graph.py or agent_memory.py changes behaviorally (the only edit to context_graph.py is a docstring pointer), which also keeps this off a file with large diffs pending across #852, #967 and #921.

The property that matters is honest partial reporting. Three vector backends cannot delete at all, so the coordinator must never report a success it didn't achieve. A receipt reading graph: erased, memory: 14 erased, vectors: unsupported on faiss is actionable; a silent True is a compliance liability.

from semantica.context import ErasureCoordinator

coordinator = ErasureCoordinator(graph=graph, memory=memory)
receipt = coordinator.erase_entity("customer-4471", reason="GDPR Art. 17 request #882")

if not receipt.complete:
    print(receipt.incomplete_stores)   # ['vectors'] -- handle out of band

Scope note

Per @Sameer6305's guidance on #1018, this is the standalone follow-up covering the coordinator itself. #1024 by @yzxcj797 separately fixes the find_by_entity() truncation this issue also cited; the two do not overlap and this PR does not touch agent_memory.py. The memory sweep here is written to be correct either way — see "Deviations from the plan" below.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Code refactoring

Related Issues

Closes #1018

Builds on #955 / #957 (retraction and purge for ContextGraph), whose changelog entry documents this exact gap as out of scope. Adjacent to #1024, which fixes the find_by_entity() limit trap independently.

Changes Made

  • New semantica/context/erasure.py, exporting ErasureCoordinator and ErasureReceipt from semantica.context.
    • erase_entity(entity_id, reason=..., at=..., vector_ids=...)ErasureReceipt
    • erase_entities([...], reason=...) → one receipt per entity, in order, so one entity's failure doesn't stop the rest
  • Five per-store statuses, so a shortfall is never indistinguishable from a success: erased, not_found, not_configured (store never bound — normal), unsupported (store cannot delete at all; retrying won't help), failed. receipt.complete is False when any store reports unsupported/failed, and receipt.incomplete_stores names them. Note erased is a weaker claim on the vectors leg than on the memory leg — see Additional Notes.
  • Erasure runs outward-in: vectors → memory → graph. The graph tombstone is the durable attestation that an erasure happened, so writing it first would let a crash mid-cascade leave a record claiming more than occurred. Erasing the graph last means a partial failure leaves the node present and the receipt incomplete — recoverable and honest; the reverse is neither.
  • Partial failure is a result, not an exception. A store that raises is recorded as failed (with the exception type) and the remaining legs still run, rather than aborting into a half-erased state with no record of which half.
  • The memory sweep cannot be silently truncated. It pages until dry — deleting as it goes, so the next page is the remainder — rather than passing one large limit= that's only correct until someone exceeds it, then re-queries once afterwards and reports failed with the residual count if anything survived. It also stops rather than spinning if batch_delete reports no progress on a non-empty page.
  • unsupported vector backends are detected by probing, not by calling and catching — see below. Backends are reached under either supported name, delete_vectors(ids) (pinecone/qdrant) or delete(ids) (pgvector/sqlite-vec), and the receipt records which was used.
  • Every store is optional. vector_store defaults to memory.vector_store, stays overridable for deployments binding a store the memory doesn't own, and accepts False to disable the leg. Vectors owned by memory items are removed by the memory leg's own delete_memory() cascade; the explicit vector leg covers entity-keyed embeddings written by something other than AgentMemory.
  • Docs: purge_node()'s docstring now points at the coordinator, so callers reading the graph-scope caveat find the thing that completes the workflow; new sections in docs/reference/context.md and semantica/context/context_usage.md; CHANGELOG entry.

Deviations from the plan I posted on #1018

Three things the code turned up that are worth a reviewer's attention:

  1. find_by_entity() returns items keyed memory_id, not id. The cascade sketched in the issue body (memory.delete_memory(m["id"])) would KeyError. Handled, and a memory item carrying no identifier reports failed rather than looking erased.
  2. The unsupported probe has to look at the wrapped backend, not the facade. delete_vectors() is declared on the VectorStore facade (vector_store.py:786) for every backend and only raises NotImplementedError once called, so probing the facade alone cannot tell a deletable backend from a delete-less one. Probing rather than calling-and-catching also keeps a missing method distinguishable from an AttributeError raised inside a working one — exactly where guessing wrong produces a false clean bill of health. NotImplementedError at call time is still caught and reported as unsupported; a store returning False is reported as failed.
  3. erased_at is normalized through ContextGraph's own temporal normalizer. Without it the receipt read ...T00:00:00Z while the tombstone for the same erasure read ...T00:00:00 — an audit record contradicting the tombstone it attests to. Normalizing up front also rejects an unparseable at before any store is touched, instead of half way through the cascade.

I also kept the loop-until-dry memory sweep that my #1018 amendment said #1024 would make unnecessary. #1024 hasn't merged, so limit=10 is still the default on main; the loop is correct under both the old and new defaults, so nothing here needs to change when #1024 lands.

Answers to the open questions from #1018

  • Does the receipt persist? Not in this PR — to_dict() is serialization-ready and safe to store, but I'd rather not guess at a storage contract. Easy follow-up if maintainers want it durable.
  • Does erase_entity accept edges? No. Memory and vectors are keyed on entities, so an edge-scoped erasure has only one leg to run; callers should use purge_edge() directly.
  • Module placement: semantica/context/erasure.py, as it reads as context-layer today. Happy to move it to a top-level semantica/erasure/ if the intent is for it to eventually reach graph stores and triplet stores.

Testing

  • Tested locally
  • Added tests for new functionality
  • Package builds successfully (python -m build)

New tests/context/test_erasure_coordinator.py30 tests, run against real ContextGraph and AgentMemory instances rather than mocks. The bug this feature exists to prevent lives in the interaction between them, so mocking that interaction away would test nothing. Most vector stores are fakes, because the point of those tests is backend shape — but a fake is worth exactly as much as the assumption that a real store looks like it, so three tests run against a real VectorStore(backend="inmemory") as well.

  • The truncation regression: 25 memory items on one entity, more than any plausible default limit. A coordinator that calls find_by_entity() once with the default fails this test.
  • All three vector-backend shapesdelete_vectors / delete / neither → erased / erased / unsupported — plus the facade-over-a-delete-less-backend shape, and a store returning Falsefailed.
  • Partial failure: a raising store is recorded as failed, the graph leg still runs, and nothing raises through to the caller.
  • Memory-leg failure modes: residual items after the sweep, batch_delete making no progress, items with no identifier — all failed, never silently erased.
  • Idempotency: a second erase_entity() reports nothing left to do rather than raising, matching how purge_node() already returns False on a repeat.
  • Receipt semantics: complete/incomplete_stores, to_dict() copying rather than aliasing the store results, and erased_at agreeing with the tombstone's purged_at.
  • Against a real VectorStore: the ids given are really removed (count() 2 → 0); the full three-store cascade with a real ContextGraph + AgentMemory + VectorStore, where the embedding is written by AgentMemory.store() and has to be gone afterwards — that exercises the memory leg's own delete_memory() vector cascade rather than the coordinator's model of it; and the erased-means-accepted caveat below.

Test Commands

# New tests
pytest tests/context/test_erasure_coordinator.py -q
# 30 passed

# Full context suite (569 passed before this PR)
pytest tests/context/ -q
# 599 passed

# Build
python -m build
# Successfully built semantica-0.6.5.tar.gz and semantica-0.6.5-py3-none-any.whl

# Format / lint
black --line-length=88 semantica/context/erasure.py tests/context/test_erasure_coordinator.py
isort --profile black --line-length=88 semantica/context/erasure.py tests/context/test_erasure_coordinator.py
flake8 --max-line-length=88 --extend-ignore=E203,W503 semantica/context/erasure.py tests/context/test_erasure_coordinator.py

Documentation

  • Updated relevant documentation
  • Added code examples if applicable
  • Updated API reference if adding new APIs
  • Updated cookbook if adding new examples
  • No documentation changes needed

docs/reference/context.md gains an ErasureCoordinator section (constructor parameters, methods, the status table, and the receipt shape) plus a row in the Exported Classes table; semantica/context/context_usage.md gains a walkthrough; purge_node()'s docstring points at the coordinator. No cookbook notebook — happy to add one if that's wanted.

Breaking Changes

Breaking Changes: No

New module, purely additive. No existing signature, return type, or default changes; the only edit to an existing Python file besides the package __init__ is a docstring addition on purge_node().

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • Package builds successfully

black, isort --profile black, and flake8 --max-line-length=88 --extend-ignore=E203,W503 are all clean on the new files. The new test module emits zero warnings. Annotations use typing generics rather than the collections.abc subscripting that needs 3.9, per the declared 3.8 floor.

Additional Notes

erased is a weaker claim on the vectors leg than on the memory leg, and the receipt should be read with that in mind. The memory leg re-queries after its sweep and only claims erased once nothing referencing the entity comes back. The vectors leg cannot do the same: backends delete by id and report success whether or not the id was there — VectorStore(backend="inmemory").delete_vectors(["never-embedded"]) returns True with the count unchanged — and there is no portable "does this id exist" check to confirm against. So vectors: {"status": "erased", "vector_ids": 1} means the store accepted the delete for one id, not one embedding was really removed. Documented on STATUS_ERASED and _erase_vectors(), and pinned by a test so it can't quietly drift into a stronger-sounding claim. I'd rather state this in the open than have an operator read the receipt as more than it is — but if reviewers would prefer the leg attempt a count()-based confirmation where the backend exposes one, I'm happy to add it.

Known limitation, unchanged by this PR and worth its own issue: erasure still cannot be completed on FAISS, Milvus, or Weaviate — faiss_store.py, milvus_store.py, and weaviate_store.py expose no delete method at all, and FAISS in particular can't remove from a flat index without a rebuild. delete_vectors() is declared on the VectorStore facade but implemented across the backend set under at least three different names. That's the same multi-backend divergence that makes a change tested only against the default pass locally and break everywhere else. The coordinator ships reporting unsupported for those, and starts reporting erased once the backends gain a delete — no API change needed here. I'm happy to open that issue separately.

Reviewers may also want to weigh in on whether complete should treat not_configured as complete. It does today: a store that was never bound leaves no residue, and only unsupported/failed mean data survived. The alternative reading — that an unconfigured vector store means "you don't know whether embeddings exist elsewhere" — is defensible, but it would make every graph-only erasure report incomplete, which I think dulls the signal.

Pravit Ampapathini added 2 commits August 16, 2026 02:08
purge_node() is graph-scope by design (semantica-agi#957), so an entity removed from the
graph can survive verbatim as an AgentMemory item and as an embedding. The
changelog names GDPR Article 17 as purge's motivation, and an Article 17
erasure the vector store can still answer queries from is not an erasure --
it is worse than none, because purge_node() returns True and writes a
tombstone attesting the content is gone.

ErasureCoordinator composes the existing public APIs to drive the cascade and
returns an ErasureReceipt recording what each store reported. Nothing in
context_graph.py or agent_memory.py changes behaviorally; ContextGraph keeps
its documented graph-scope contract instead of acquiring references that would
invert the dependency.

Honest partial reporting is the point. Stores report erased / not_found /
not_configured / unsupported / failed, and complete is False when any store
reports unsupported or failed. FAISS, Milvus and Weaviate expose no delete at
all, so erasure genuinely cannot be completed on them today -- the receipt
says so rather than reporting a success it did not achieve.

Erasure runs outward-in (vectors, memory, graph). The tombstone is the durable
attestation, so writing it first would let a crash mid-cascade leave a record
claiming more than happened; erasing the graph last leaves a partial failure
recoverable and honest.

The memory sweep pages until dry and re-queries afterwards rather than
trusting one find_by_entity() call, whose limit=10 default silently truncates
the very check a caller uses to decide the erasure is done. Unsupported vector
backends are detected by probing the wrapped backend, since the VectorStore
facade declares delete_vectors() for every backend and only raises
NotImplementedError once called.

27 tests against real ContextGraph/AgentMemory instances, including the
25-items-on-one-entity regression that fails against a naive single-call
sweep. Full tests/context/ suite: 596 passed.

Closes semantica-agi#1018
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add ErasureCoordinator for cross-store entity erasure receipts

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add ErasureCoordinator to erase an entity across vectors, memory, and graph.
• Return an auditable ErasureReceipt with honest per-store statuses and completeness.
• Add documentation and tests covering truncation, backend delete shapes, and partial failures.
Diagram

graph TD
  A["Caller"] --> B["ErasureCoordinator"]
  B --> C[("Vector store")]
  B --> D["AgentMemory"]
  B --> E[("ContextGraph")]
  B --> F["ErasureReceipt"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Add cross-store purge directly to ContextGraph
  • ➕ Single entry point for erasure; fewer concepts exposed to callers
  • ➕ Could reuse graph’s audit/tombstone mechanisms more tightly
  • ➖ Inverts dependencies (graph would need memory/vector references)
  • ➖ Higher blast radius on a large, frequently changed file; harder to review/merge safely
  • ➖ Risk of muddying the documented graph-scope contract of purge_node()
2. Define an ErasableStore interface + registry
  • ➕ Cleaner extensibility for future stores (triples stores, external archives, etc.)
  • ➕ Avoids backend-shape probing by standardizing a delete contract
  • ➖ More upfront framework work and migration effort for existing backends
  • ➖ Harder to ship quickly; increases API surface and versioning burden
3. Persist receipts to a durable audit log in this PR
  • ➕ Stronger compliance story; receipts survive process crashes and caller mistakes
  • ➕ Enables centralized reporting/monitoring
  • ➖ Requires choosing a storage contract (file/db/event stream) and retention policy
  • ➖ Couples erasure to deployment architecture; likely better as a follow-up

Recommendation: The PR’s composition-based coordinator is the best tradeoff right now: it preserves ContextGraph’s graph-scope contract, avoids dependency inversion, and makes partial failures explicit via an auditable receipt. If maintainers want stronger compliance guarantees, a follow-up to persist ErasureReceipt to an explicit audit sink would add value without changing the coordinator’s API.

Files changed (6) +1034 / -1

Enhancement (2) +522 / -0
__init__.pyExport ErasureCoordinator and ErasureReceipt from semantica.context +4/-0

Export ErasureCoordinator and ErasureReceipt from semantica.context

• Imports the new erasure module and adds ErasureCoordinator/ErasureReceipt to __all__ so they are part of the public context API surface.

semantica/context/init.py

erasure.pyImplement ErasureCoordinator and ErasureReceipt with honest per-store outcomes +518/-0

Implement ErasureCoordinator and ErasureReceipt with honest per-store outcomes

• Adds a new coordinator that composes existing graph/memory/vector APIs to erase entities outward-in (vectors → memory → graph) and returns an ErasureReceipt. Implements per-store statuses (erased/not_found/not_configured/unsupported/failed), probes vector backend delete capability (delete_vectors vs delete vs none), performs a paged memory sweep with residual re-check, and normalizes erased_at via ContextGraph’s temporal normalizer.

semantica/context/erasure.py

Tests (1) +414 / -0
test_erasure_coordinator.pyAdd integration-style tests for cross-store erasure and failure semantics +414/-0

Add integration-style tests for cross-store erasure and failure semantics

• Adds 27 tests using real ContextGraph/AgentMemory plus fake vector-store shapes to validate the cascade order, truncation-safe memory sweeps, backend delete probing, idempotency, receipt serialization/copying, and partial-failure reporting without aborting remaining legs.

tests/context/test_erasure_coordinator.py

Documentation (3) +98 / -1
CHANGELOG.mdDocument cross-store erasure workflow and known vector backend limitations +17/-0

Document cross-store erasure workflow and known vector backend limitations

• Adds a detailed changelog entry introducing ErasureCoordinator/ErasureReceipt, including status semantics, outward-in ordering rationale, and partial-failure behavior. Calls out unsupported vector backends (FAISS/Milvus/Weaviate) and the backend-method shape divergence.

CHANGELOG.md

context_graph.pyPoint purge_node() docs to ErasureCoordinator for full erasure workflows +5/-1

Point purge_node() docs to ErasureCoordinator for full erasure workflows

• Updates purge_node() docstring to emphasize graph-only scope and direct callers requiring full erasure to use ErasureCoordinator and its receipt semantics.

semantica/context/context_graph.py

context_usage.mdAdd user-facing guide for ErasureCoordinator and receipt semantics +76/-0

Add user-facing guide for ErasureCoordinator and receipt semantics

• Introduces a new section explaining why purge_node() is insufficient for GDPR-style erasure, how to use ErasureCoordinator, and how to interpret receipt statuses and completeness. Documents ordering, partial failures, optional stores, and idempotency.

semantica/context/context_usage.md

The vector-leg tests asserted the three backend shapes the coordinator
expects -- delete_vectors / delete / neither -- against fakes, which is worth
exactly as much as the assumption that a real store looks like one of them.
VectorStore(backend="inmemory") runs without external services, so it can
hold that assumption to account.

Adds the end-to-end case the receipt actually attests to: a real ContextGraph,
AgentMemory and VectorStore, where the embedding is written by
AgentMemory.store() and has to be gone afterwards. That exercises the memory
leg's own delete_memory() vector cascade rather than the coordinator's model
of it.

The real backend also pins a limit worth knowing before trusting the receipt:
it pops the ids and returns True whether or not they were there, and no
backend offers a portable existence check, so `erased` on the vectors leg
means the store accepted the delete for the ids given -- not that embeddings
were really removed. The memory leg re-queries to confirm and so is the
stronger claim. Documented on STATUS_ERASED, _erase_vectors(), and both status
tables.

tests/context/: 599 passed.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Receipt timestamp can drift ✓ Resolved 🐞 Bug ≡ Correctness
Description
erase_entity() computes erased_at up front but passes the original at into the graph purge, so
when at=None the receipt and the graph tombstone can record different timestamps for the same
erasure. This breaks the module’s stated invariant that the receipt cannot disagree with the
tombstone.
Code

semantica/context/erasure.py[R209-212]

+        stores["vectors"] = self._erase_vectors(entity_id, vector_ids)
+        stores["memory"] = self._erase_memory(entity_id)
+        stores["graph"] = self._erase_graph(entity_id, reason, at)
+
Evidence
The coordinator computes erased_at but does not use it when purging the graph, and purge_node()
falls back to its own current timestamp when at is None—so the receipt and tombstone can diverge.

semantica/context/erasure.py[205-212]
semantica/context/context_graph.py[1761-1763]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ErasureCoordinator.erase_entity()` computes `erased_at` once, but `_erase_graph()` is called with the original `at` value. When `at` is `None`, `ContextGraph.purge_node()` will compute its own `now()` later in the cascade, producing a different timestamp than the receipt.
### Issue Context
This module’s docstring and changelog explicitly claim the receipt and tombstone cannot disagree about time; the current control flow violates that guarantee for the default (`at=None`) case.
### Fix Focus Areas
- Ensure a single normalized timestamp is used for both:
- receipt.erased_at
- `graph.purge_node(..., at=...)`
- Avoid passing a timezone-aware ISO string that will be re-normalized into a different representation; prefer using `ContextGraph`’s normalizer output consistently.
### Suggested approach
- Compute `normalized_at` once:
- if `at is None`: create a `datetime.now(timezone.utc)` and normalize it through `_normalize_temporal_input()` (which yields the canonical tombstone string)
- else: normalize `at` through `_normalize_temporal_input(at)`
- Set `receipt.erased_at = normalized_at`
- Pass `at=normalized_at` into `_erase_graph()` / `purge_node()`.
### Fix Focus Areas (code references)
- semantica/context/erasure.py[174-218]
- semantica/context/erasure.py[456-468]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Falsey vector_store rejected ✓ Resolved 🐞 Bug ☼ Reliability
Description
ErasureCoordinator.__init__() uses not vector_store to decide whether a vector store was
provided, so a valid vector store object with falsey truthiness can be rejected as “no stores.” This
also produces a misleading error message claiming vector_store=None even when a vector store
instance was passed.
Code

semantica/context/erasure.py[R157-161]

+        if graph is None and memory is None and not vector_store:
+            raise ValueError(
+                "ErasureCoordinator needs at least one store to erase from; "
+                "got graph=None, memory=None, vector_store=None"
+            )
Evidence
The constructor’s guard uses truthiness rather than explicit sentinel checks, so it can misclassify
a provided (but falsey) store as absent.

semantica/context/erasure.py[151-170]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Construction rejects configurations where `vector_store` is provided but evaluates falsey (e.g., custom store defines `__len__`/`__bool__`). The guard should only reject when *all* stores are truly absent.
### Issue Context
`vector_store=False` is a meaningful sentinel used later to disable the vector leg; the initial guard should distinguish `None`/`False` from a provided object.
### Fix Focus Areas
- Replace `not vector_store` with explicit checks:
- treat `vector_store is None` as not provided
- treat `vector_store is False` as intentionally disabled
- treat any other value (even falsey) as provided
- Update the error message to reflect the actual received values.
### Fix Focus Areas (code references)
- semantica/context/erasure.py[151-170]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Vector delete result ignored ✓ Resolved 🐞 Bug ≡ Correctness
Description
_erase_vectors() treats any return value other than the literal False as success, but some
built-in vector backends return structured dict payloads (not booleans). This prevents the receipt
from recording backend-reported status details and can mask failures if a backend reports
non-exception error states via its payload.
Code

semantica/context/erasure.py[R319-335]

+        if deleted is False:
+            self.logger.warning(
+                "Vector backend %r reported no deletion for %r", backend, entity_id
+            )
+            return {
+                "status": STATUS_FAILED,
+                "backend": backend,
+                "vector_ids": len(ids),
+                "detail": "store reported the ids were not deleted",
+            }
+
+        return {
+            "status": STATUS_ERASED,
+            "backend": backend,
+            "vector_ids": len(ids),
+            "via": method_name,
+        }
Evidence
The coordinator’s success check only handles a boolean False, while real backends in this repo
return dict payloads for delete operations, meaning the coordinator can’t use/record backend status
and may over-report success.

semantica/context/erasure.py[286-335]
semantica/vector_store/qdrant_store.py[607-626]
semantica/vector_store/pinecone_store.py[217-229]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_erase_vectors()` only checks `deleted is False` to decide failure. Several in-repo backends return `Dict[str, Any]` from delete operations, so the coordinator neither validates nor records meaningful status fields.
### Issue Context
- Qdrant delete returns `{"status": response.status}`.
- Pinecone delete returns `{"deleted": True}`.
The coordinator currently discards these details and reports `STATUS_ERASED` for any non-`False` value.
### Fix Focus Areas
- Preserve backend response details in the receipt (e.g., include `result` or `backend_status`).
- Add conservative interpretation rules:
- `bool` return: treat `True` as erased, `False` as failed
- `dict` return: treat explicitly-success markers as erased (e.g., `deleted=True`), and otherwise record status/detail and consider marking `failed` if an explicit failure marker exists.
- `None` return: consider treating as `failed` or record as `failed` unless the backend contract guarantees `None` means success.
### Fix Focus Areas (code references)
- semantica/context/erasure.py[286-335]
- semantica/vector_store/qdrant_store.py[607-626]
- semantica/vector_store/pinecone_store.py[217-229]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. at type hints too narrow ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
ErasureCoordinator’s public at annotations only mention str|datetime, but the shared temporal
normalizer used by ContextGraph also supports epoch seconds (int|float). This mismatch can
confuse callers and static type-checkers and makes the coordinator API appear less capable than the
underlying graph API.
Code

semantica/context/erasure.py[R174-180]

+    def erase_entity(
+        self,
+        entity_id: str,
+        reason: Optional[str] = None,
+        at: Optional[Union[str, datetime]] = None,
+        vector_ids: Optional[Sequence[str]] = None,
+    ) -> ErasureReceipt:
Evidence
The coordinator annotates at as str|datetime, but the graph normalizer it uses supports
additional numeric types; aligning annotations reduces confusion and improves API clarity.

semantica/context/erasure.py[174-180]
semantica/context/context_graph.py[173-188]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The coordinator’s `at` type hints don’t include `int|float` even though the graph’s temporal normalizer supports them. This is an API/documentation mismatch.
### Issue Context
`ContextGraph._normalize_temporal_input()` explicitly accepts `(int, float)` and converts from epoch seconds.
### Fix Focus Areas
- Update type annotations for `at` across coordinator methods/helpers to include `int|float`.
- Ensure docstrings reflect the accepted inputs.
### Fix Focus Areas (code references)
- semantica/context/erasure.py[174-180]
- semantica/context/erasure.py[235-240]
- semantica/context/erasure.py[425-430]
- semantica/context/erasure.py[456-468]
- semantica/context/context_graph.py[173-188]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread semantica/context/erasure.py
Comment thread semantica/context/erasure.py Outdated
Comment thread semantica/context/erasure.py Outdated
Comment thread semantica/context/erasure.py
@pravit-amp

Copy link
Copy Markdown
Contributor Author

Pushed two follow-ups (7a63ae5, 9f99335) and updated the description to match. Both came out of self-review rather than anything a reviewer flagged, so here's what changed and why.

The vector-leg tests were only as good as my guess about the backends

The original tests asserted the three backend shapes the coordinator expects — delete_vectors / delete / neither — against fakes I wrote. That's circular: the fakes returned True because I made them, which said nothing about whether a real store's True means anything. Given this repo's recurring failure mode is a change verified against the default that reaches for internals and breaks on every other backend, "I tested the shapes I imagined" wasn't good enough.

VectorStore(backend="inmemory") runs without external services, so three tests now use it:

  • the ids handed to it are really removed (count() 2 → 0)
  • the full three-store cascade — real ContextGraph + AgentMemory + VectorStore, where the embedding is written by AgentMemory.store() and has to be gone afterwards. This is the one I most wanted: it exercises the memory leg's own delete_memory() vector cascade instead of my model of it, and it passes.
  • the caveat below.

The real backend surfaced a caveat the fakes structurally could not

VectorStore(backend="inmemory").delete_vectors(["never-embedded"]) returns True and leaves the count untouched — it pops the ids and reports success whether or not they were there. No backend offers a portable "does this id exist" check, so the vectors leg cannot confirm the way the memory leg does (which re-queries after its sweep and only claims erased once nothing comes back).

So vectors: {"status": "erased", "vector_ids": 1} means the store accepted the delete for one id, not one embedding was really removed.

For a feature whose entire purpose is not overstating what was erased, that asymmetry shouldn't be buried. It's now documented on STATUS_ERASED and _erase_vectors(), called out in both status tables, and pinned by a test so it can't drift into a stronger-sounding claim later. If you'd prefer the leg attempt a count()-based confirmation where a backend exposes one, say the word — I left it out because it isn't portable and is racy under concurrent writes, but it's a small change.

Docs

docs/reference/context.md had no ErasureCoordinator entry, so the API reference now carries one (constructor parameters, methods, status table, receipt shape) plus a row in the Exported Classes table.

Worth flagging separately: purge_node() / retract_node() appear nowhere in docs/#957 shipped without touching the reference. Out of scope here, but happy to open a docs issue for it.

tests/context/: 599 passed (569 before this PR). black / isort / flake8 clean; new tests emit no warnings.

Timestamp drift (high). erase_entity() resolved erased_at up front but passed
the caller's original `at` down to purge_node(), so on the default at=None
path the coordinator and the graph each took their own now() and the receipt
attested to a different instant than the tombstone it points at -- breaking
the invariant this module states most loudly. The resolved value is now what
the graph receives. The existing test passed only because it supplied an
explicit `at`, which hides the drift; the regression test covers at=None,
which is what callers actually use.

Backend delete results. The vectors leg treated anything other than the
literal False as success, but no in-repo backend returns a bool -- Qdrant
returns {"status": <UpdateStatus>} and Pinecone {"deleted": True}, so every
dict read as success and the backend's own account of the delete was thrown
away. Results are now interpreted by shape and the payload is kept in the
receipt as backend_result, stringified so it stays JSON-serializable as an
audit record. Bool markers match by identity so a 0 count isn't read as
False; string markers match as substrings so an enum rendering as
"UpdateStatus.FAILED" isn't read as success.

Falsey vector store. The "at least one store" guard used `not vector_store`,
rejecting a valid store whose __bool__/__len__ makes an empty instance falsey
and then reporting vector_store=None when an object had been passed. It now
separates None (absent) from False (deliberately disabled) from provided, and
echoes what it received.

`at` annotations. Widened to int/float, matching the ContextGraph normalizer
they delegate to, so the coordinator stops advertising less than the API it
wraps.

tests/context/: 608 passed.
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.

[FEATURE] Erasure coordinator — purge_node() erases the graph but leaves the same content live in AgentMemory and the vector store

1 participant