Delete a session's data from Context Intelligence (server side) - #97
Draft
Diego Colombo (colombod) wants to merge 13 commits into
Draft
Delete a session's data from Context Intelligence (server side)#97Diego Colombo (colombod) wants to merge 13 commits into
Diego Colombo (colombod) wants to merge 13 commits into
Conversation
Add delete_session(session_id) -> int to the BlobStore Protocol and AsyncDiskBlobStore, removing <root>/<session_id>/ and returning the number of blobs removed. Idempotent for a session with no blobs. At the storage Protocol level so the deletion service composes it rather than touching the filesystem directly; shape stays compatible with a later rebase onto the storage-refactor BlobStore.delete(). Part of context-intelligence session data delete (server, A1). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Add resolve_session_family(session_id) at the GraphStore Protocol level (neo4j_store impl + in-memory GraphState equivalent). Given any session id it expands up to the root and returns the whole family: session-id set, distinct $blob_ref URIs across all family nodes, node/edge counts, created_by, started_at, last_change, subsession count, workspace. Shared :SST_CONCEPT nodes bound the traversal so a family's counts never leak through a shared concept. Add BlobStore.size(uri) at the Protocol level; services.total_blob_size composes whole-family blob size via it. Shared component the delete service reuses so preview and apply cannot disagree. Part of context-intelligence session data delete (server, A-SUM). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
#94 now records working_dir on the Session node; the summary resolver reads it from the root and returns it instead of always None. Covers both the Neo4j and in-memory GraphState paths; neo4j test asserts it surfaces from the root. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…session_graph
A2: delete_session_graph(session_id) at the GraphStore Protocol level
(neo4j_store impl + in-memory GraphState parity). Reuses the A-SUM
resolution, partitions owned nodes vs shared :SST_CONCEPT boundary nodes,
and DETACH DELETEs the owned nodes by elementId in batches. Shared
concept nodes survive; only their edges into the deleted graph are
removed. Fails loud if the post-delete gate (owned-gone / concept-
survives) does not hold. Returns node/relationship delete counts.
Also renames the A-SUM-introduced 'session family' vocabulary to
'session graph' to match the approved doc ('the entire session graph,
root + all descendants'): SessionFamily->SessionGraph,
resolve_session_family->resolve_session_graph, and all internal
constants/vars/tests. One concept, one name.
Proven on real isolated Neo4j: tests/neo4j/test_delete_session_graph.py
7 passed (owned removed, shared concept survives, unrelated graph reached
only via shared concept untouched, sub-session id == root id, counts
match, unknown-id no-op); resolve+delete 16 neo4j passed; non-neo4j
suite green; pyright clean.
Part of context-intelligence session data delete (server, A2).
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Add two methods on QueueManager (the sole owner of queue-artifact I/O): - pending_count(session_id): authoritative drained/pending check, reusing the exact _read_committed_offset / _complete_data_end / _count_newlines helpers recover() uses; 0 == fully drained. - delete_session(session_id): removes .log/.offset AND .dead.jsonl (a data delete must drop dead letters too, unlike delete_drained). Computes pending inside the key file-lock and raises rather than delete when anything is still pending -- fail loud, no partial delete, no TOCTOU against a concurrent append. Idempotent. All queue filesystem access stays inside queue_manager.py; the deletion service will call these via the QueueManager surface, never touch files. Proven: tests/test_queue_manager.py 101 passed (pending reflects uncommitted, zero after drain, ignores torn line; delete removes all three artifacts incl dead letters, refuses on pending, idempotent on missing, dead-letter-only session deletable); non-neo4j suite 2006 passed; pyright clean. Part of context-intelligence session data delete (server, A3). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
New context_intelligence_server/deletion.py: DeletionService(graph_store, blob_store, queue_manager) composing ONLY the storage Protocol APIs -- no Neo4j, no filesystem (grep-verified). Two operations: - preview(session_id): dry, mutates nothing; returns whole-graph facts (reusing resolve_session_graph) plus deletable + pending_sessions. - apply(session_id, requested_by): captures session_ids/blob_refs first, refuses (raises, deletes nothing) if ANY session in the graph has pending queue records, then deletes graph -> blobs (every session) -> queue (every session), and logs the applied deletion. preview and apply share one drain check so they cannot disagree. Blob-count reconciliation (blobs_deleted vs whole-graph blob_refs) is a WARNING, not a failure: orphan/already-reclaimed blobs make an exact match non-guaranteed, and raising after an irreversible delete would report failure on a completed op. Proven: tests/test_deletion.py 8 passed (dry no-mutation, pending refusal, whole-graph delete across every session, reconcile-mismatch warns not fails, logging); tests/neo4j/test_deletion_service.py 4 passed on real Neo4j (whole graph gone, shared concept + unrelated graph survive, pending refusal); non-neo4j suite green; pyright clean. Part of context-intelligence session data delete (server, A4). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
New router context_intelligence_server/routers/deletion.py, added to the
app. Two routes, both a thin layer over DeletionService:
- GET /sessions/{session_id}/summary (needs read access): reports what
deleting this session's data would do, without deleting anything. Uses
the read-only Neo4j connection.
- DELETE /sessions/{session_id} (needs write access): with apply=false
(the default) it returns the same preview and deletes nothing, using the
read-only connection; with apply=true it deletes the whole session graph,
its blobs, and its queue data, using the admin connection.
A read always uses the read-only connection and a change always uses the
admin connection: the summary route and the delete route's dry run both
read, so both use the read-only connection; only a real delete uses the
admin connection.
Returns 404 for an unknown session and 409 when a session in the graph is
still receiving data (not yet drained). The caller's id is recorded on the
delete. The routes hold no delete or graph logic of their own.
Proven: tests/routers/test_deletion.py 9 passed (summary fields, 404,
read-only caller allowed on summary, dry run previews and deletes nothing,
apply returns the result, caller id passed through, unknown session 404,
still-receiving-data 409, write access required for delete); non-neo4j
suite 2023 passed; pyright clean.
Part of context-intelligence session data delete (server, A5).
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
The summary and delete worked out a graph's blobs by scanning graph node properties for a $blob_ref marker. That was wrong in two ways: it is the blob store's job, not the graph's, and in real use the marker is stored nested inside a node's data field where the scan never found it, so the blob count came back 0 for real sessions. The blob store is keyed by session id and already lists every blob for a session. So the graph now only supplies the set of session ids, and the DeletionService asks the blob store for the blobs of each session and adds them up. This is correct no matter where the graph keeps its markers. - Remove SessionGraph.blob_refs, the extract_blob_refs helper, and the marker constant; resolve_session_graph no longer reads node properties for blobs (graph and in-memory paths). - preview counts blobs by listing them from the blob store per session; apply deletes them the same way. Removed the old reconcile-and-warn step, since there is no separate graph blob count to compare against anymore. Proven end to end: tests/integration/test_delete_session_endpoint.py now posts real events that the server offloads to real blob files, then checks the summary's blob count is greater than 0 and matches the real blobs on disk. Fast set 1914 passed; neo4j resolve+delete 12 passed; end-to-end 1 passed; pyright clean. Part of context-intelligence session data delete (server). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Add the two new routes to the API reference and a plain-language section explaining how deleting a session's data works (whole graph, shared nodes kept, preview then apply, 409 while still receiving data, permanent). Note in Data Persistence that a delete clears the graph, blobs, and queue files together. Bump the server version 6.7.1 -> 6.8.0 for the new delete feature (single source in pyproject.toml; the version endpoint and package version read it). Part of context-intelligence session data delete (server, release). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…pply flag
Review of the draft PR: a session id is the unique identifier, so the
server now resolves everything from it and neither route takes a workspace
or an apply flag.
- The graph store looks up which workspace a session id belongs to (match
by node_id alone), then scopes the resolve and the delete by that
discovered workspace. Blobs and the queue were already keyed by session
id only. If a session id is found in more than one workspace the server
refuses rather than guessing (raises, turned into HTTP 409).
- GET /sessions/{session_id}/summary is the preview; DELETE
/sessions/{session_id} always deletes. No apply flag: preview by GET,
then DELETE when sure. A session can be deleted once it is no longer
receiving data; a session still receiving data is refused with 409.
- README updated: no workspace or apply parameters; states plainly that a
session that is no longer receiving data can be deleted, and 409 is
returned only while it is still receiving.
Proven: fast set 1917 passed; neo4j resolve+delete 13 passed (incl. a
same-id-in-two-workspaces case that now raises); real-server end-to-end
1 passed; pyright clean.
Part of context-intelligence session data delete (server, API review).
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
The delete bundle's agent needs to resolve "who is the acting user" so it
can compare that against a session's created_by before acting on it. The
server already knows this identity -- it stamps every delete with a
requested_by extracted from the bearer token during auth -- but there was
no way for a client to ask the server "who am I" directly.
Add GET /whoami, gated by the same require_read dependency the summary
route uses. It returns {"contributor_id": "..."}, reading the exact same
contributor_id the auth middleware (auth.py) already writes to
request.scope["state"] for every authenticated request -- the same value
routers/deletion.py's _caller_id reads to stamp a delete's requested_by,
and routers/admin.py's _admin_who reads for audit logging. No new identity
source is introduced.
When auth is disabled (allow_unauthenticated, no credential configured),
contributor_id is null rather than a 500 -- same response shape, anonymous
value.
Bump version 6.8.0 -> 6.9.0 for the new read endpoint (matches this repo's
convention of a version bump per API addition).
Additive only: no delete/summary behavior changed.
Part of context-intelligence session data delete (server, PR #97).
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Collaborator
Author
|
Bundle-side (Phase B) counterpart: microsoft/amplifier-bundle-context-intelligence#108 (draft). That PR adds the |
… hint A delete refused because the session graph still has undrained queue records is a transient, retryable condition -- but the 409 carried only a human message, so a caller could not know WHEN to retry and had to poll by hand. - New typed SessionsPendingError(root_id, pending_sessions) raised by DeletionService.apply() for the pending case (was a bare RuntimeError). - Router maps it to 409 WITH a Retry-After header and a structured body (reason=sessions_pending, pending_sessions, retry_after_seconds). The ambiguous-id 409 stays non-retryable (no Retry-After); a graph-vanished race stays a plain 409. - retry-after value is configurable: Settings.delete_retry_after_seconds (default 2s). - Tests: service raises the typed error with pending_sessions; router emits Retry-After + structured body for pending, and no Retry-After for the vanished-race 409. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ill-draining session Document that the retryable 'still receiving data' 409 now carries Retry-After + retry_after_seconds + pending_sessions, and that the ambiguous-id 409 does not (not retryable). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
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 free
to 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.
What this adds
Until now a user who contributed data to Context Intelligence had no way to delete it. This adds that ability on the server side.
Deleting a session removes the whole session graph — the root session and every session below it (its subsessions and forks) — together with the data those sessions point to: their blobs, their queue files, and their dead-letter records.
Three endpoints
GET /sessions/{session_id}/summary?workspace=<ws>(needs read access): reports what deleting this session's data would remove, without deleting anything.DELETE /sessions/{session_id}?workspace=<ws>&apply=<bool>(needs write access):apply=false(default) returns the same preview and deletes nothing;apply=truedeletes it and reports the counts.GET /whoami(needs read access): a "who am I" read used by the delete bundle's agent to resolve the acting user for ownership warnings -- it returns{"contributor_id": "..."}, the same identity the server already stamps ontocreated_by/requested_by.nullwhen auth is disabled, never a 500.How delete behaves
409if any session in the graph is still receiving data (its queue has records not yet fully written). The summary's started-at / last-change fields show when a session last changed — a change less than a minute ago may mean it is still live.How it is built
What is proven
This reuses the approach proven earlier by the one-off session-purge work (find the graph, delete it in two phases, keep shared concept nodes).
Version
Server version bumped 6.7.1 -> 6.8.0 -> 6.9.0 (6.9.0 for the new
GET /whoamiread endpoint).Status
Opening as a draft to share with the team and start review. The bundle-side user experience (finding sessions, confirming, deleting from the agent) is a separate follow-up and is not part of this PR.