feat: scope backfill-by-copy and removal reconciliation jobs#904
Open
VVoruganti wants to merge 5 commits into
Open
feat: scope backfill-by-copy and removal reconciliation jobs#904VVoruganti wants to merge 5 commits into
VVoruganti wants to merge 5 commits into
Conversation
Audit for DEV-2000 (Scopes RFC prerequisite): explicit-level documents must stay session-pure so scope memory can be built by copying explicit documents between collections. Two classes of violation were possible: - Exact-content and semantic dedup in crud/document.py matched candidates with no level or session scoping, so an explicit document could be reinforced by — or soft-deleted in favor of — a same-content document from a different session or a different level (silently merging cross-session derivations into one row). - The generic create_observations tool handler accepted level='explicit' from agents with no message context (dreamer/dialectic), which would mint session-less explicit documents. Enforcement (refuse, never rewrite): - create_documents refuses explicit documents with a null session_name - exact dedup keys on (content, level, session-for-explicit); derived levels keep cross-session consolidation - is_rejected_duplicate scopes candidate search to the same level, and the same session for explicit documents - the create_observations tool rejects explicit-level input outside message ingestion (deriver) context Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a lightweight dream variant (DEV-2000, Scopes RFC prerequisite) that
runs ONLY the peer-card update — for event-driven refreshes such as scope
membership changes and cold starts:
- DreamType.CARD_REFRESH alongside OMNI; dispatched by process_dream to a
new run_card_refresh_dream orchestration
- CardRefreshSpecialist: restricted to get_recent_observations,
search_memory, and update_peer_card (no observation-mutating tools), with
a low tool-iteration cap of min(6, DREAM.MAX_TOOL_ITERATIONS)
- rebuild=True mode carried in the dream payload: the existing card is NOT
injected into the prompt and the specialist rebuilds it solely from
observations present in the collection (for use after removals)
- enqueue-able via the manual enqueue_dream path (bypasses volume gates);
the work-unit key already embeds the dream type so a card refresh never
collides with a pending omni dream. POST /v3/workspaces/{id}/schedule_dream
accepts dream_type=card_refresh plus the rebuild flag
- card refreshes never advance the omni dream guard pair
(last_dream_at / last_dream_document_count)
- shared PEER CARD prompt section extracted (verbatim) from
DeductionSpecialist for reuse; CallPurpose gains dream.card_refresh
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retroactive scope membership changes (DEV-1999):
- New queue task types scope_backfill / scope_removal with payloads,
work-unit keys ({task}:{workspace}:{scope_peer}:{session}), deduped
enqueue (mirrors enqueue_dream), and consumer dispatch.
- Backfill copies a session's explicit documents from each sender's
global (P, P) collection into the scope's (scope_peer, P) collection
with internal_metadata.copied_from as the idempotency marker;
soft-deleted copies from an earlier removal are restored, so
add -> remove -> re-add converges on exactly one live copy. Completion
enqueues one manual omni dream per touched collection.
- Removal soft-deletes the session's explicit documents in the scope's
collections and cascades (fail-closed, transitively) to derived
documents whose source_ids intersect anything removed, deletes the
vectors from the external store, then enqueues a card_refresh dream
with rebuild=True plus a manual omni dream per touched collection.
- Zero LLM re-derivation: explicit documents are session-pure (DEV-2000
invariant); the only external call is re-embedding rows whose
embedding column is NULL (external-store deployments).
- Per-session job status lives in the scope peer's internal_metadata
under backfill_status, written via single-statement JSONB merges
(concurrent-writer safe) and surfaced at
GET /v3/workspaces/{w}/scopes/{scope_id}/status.
- Enqueued from the scopes add-sessions route and SessionCreate.scopes
handling, only when the session already has messages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…SONB bug Adds tests/deriver/test_scope_backfill.py covering the DEV-1999 scope backfill-by-copy and removal reconciliation jobs implemented in a prior commit: explicit-doc copying, copied_from idempotency (including add->remove->re-add), multi-peer collection routing, removal cascade to dependent derived docs, dream enqueues (manual omni on backfill; card_refresh rebuild + omni on removal), the status route, and add-sessions route wiring (backfill enqueued only when the session already has messages). Fixes a real production bug surfaced by these tests: update_scope_backfill_status passed json.dumps()'d strings through SQLAlchemy cast(..., JSONB), which double-encodes (psycopg re-serializes the already-JSON string), producing a JSONB string scalar instead of an object. Postgres's `||` between two non-array jsonb scalars doesn't merge — it silently wraps both into a 2-element array, corrupting backfill_status into a list. This crashed clear_scope_backfill_status's `#-` path delete (called on every removal) with "path element is not an integer" once a session had ever completed a backfill. Fixed by passing raw Python dicts to cast() instead, mirroring the working pattern already used in update_collection_internal_metadata. Also adds src.deriver.scope_backfill.tracked_db to conftest's tracked_db patch list — the module was missing from that per-import-site allowlist, so its DB work ran against the real configured database instead of the isolated per-test database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
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.
Summary
Adds the retroactive-membership half of the Scopes RFC (DEV-1970): when a session with pre-existing messages is added to a scope, its history is backfilled into the scope's memory; when a session is removed, the scope's copies are reconciled away.
(P, P)collection into the scope's(scope_peer, P)collection. The only external call is an embedding lookup for rows whose embedding is NULL (external vector store deployments); never an LLM call.copied_from. Each copy stampsinternal_metadata.copied_from = <source doc id>. Re-processing the same backfill is a no-op; a soft-deleted copy left by an earlier removal is restored rather than duplicated, so add → remove → re-add converges on exactly one live copy per source document.source_idsintersect anything removed (a deduction resting on removed evidence leaves with it, and so does an induction resting on that deduction). Enqueues acard_refreshdream withrebuild=True(drops the stale card from the prompt so it's rebuilt from remaining evidence only) plus a manual omni dream to rebuild the higher-order layer.pending/completed/failed, plusdocs_copiedonce complete) lives in the scope peer'sinternal_metadata, written via single-statement JSONB merges (concurrent-writer safe), and served byGET /v3/workspaces/{w}/scopes/{scope_id}/status.POST /scopes/{scope_id}/sessions) or creating a session withscopesset enqueuesscope_backfillonly when the session already has messages; removal (DELETE /scopes/{scope_id}/sessions/{id}) always enqueuesscope_removal.Bug fixed while writing tests
update_scope_backfill_status(src/crud/scope.py) passedjson.dumps(...)'d strings through SQLAlchemy'scast(..., JSONB). That double-encodes: psycopg's JSONB bind adapter re-serializes the already-JSON string, producing a JSONB string scalar instead of an object. Postgres's||between two non-array jsonb scalars doesn't merge keys — it silently wraps both sides into a 2-element array. This corruptedbackfill_statusinto a list, and later crashedclear_scope_backfill_status's#-path-delete (called on every removal) withInvalidTextRepresentation: path element at position 2 is not an integer, since Postgres saw an array where it expected an object. In other words: removal reconciliation broke for any session that had ever completed a backfill. Fixed by passing raw Python dicts tocast()instead of pre-serialized JSON strings, mirroring the already-correct pattern inupdate_collection_internal_metadatain the same file.Also added
src.deriver.scope_backfill.tracked_dbto thetracked_dbpatch allowlist intests/conftest.py— the new module was missing from that per-import-site list (every module that doesfrom src.dependencies import tracked_dbneeds its own entry), so its DB work was silently running against the real configured database instead of the isolated per-test database.Tests
tests/deriver/test_scope_backfill.py(9 tests, all passing):copied_fromset and embeddings carried over.(scope, P)collections.source_idsreferences a removed copy is soft-deleted; an unrelated derived doc survives.card_refreshwithrebuild=Trueplus omni (asserted via QueueItem payloads, no LLM calls).docs_copied.Full non-integration/SDK/bench/live-llm suite: 1339 passed, 3 pre-existing failures unrelated to this change (tests/crud/test_document.py semantic-dedup tests require an OpenAI embedding key).
Fixes DEV-1999 (part of DEV-1970 Scopes RFC)
Test plan
uv run pytest tests/deriver/test_scope_backfill.py— 9/9 passinguv run pytest tests/(excluding integration/sdk/bench/live_llm) — 1339 passed, 3 known pre-existing failures unrelated to this changeuv run basedpyright— 0 errors, 0 warningsuv run ruff check/ruff format— clean on all touched files🤖 Generated with Claude Code