Skip to content

feat(claude-code): mid-session dataset switch (seal bridge + re-register) (SDK-306) - #292

Open
rshkarin wants to merge 5 commits into
mainfrom
feature/sdk-306-mid-session-dataset-switch-seal-bridge-re-register-for-the
Open

feat(claude-code): mid-session dataset switch (seal bridge + re-register) (SDK-306)#292
rshkarin wants to merge 5 commits into
mainfrom
feature/sdk-306-mid-session-dataset-switch-seal-bridge-re-register-for-the

Conversation

@rshkarin

Copy link
Copy Markdown
Contributor

Review + rework of community PR #186 (by @wiz-abhi) for cognee hackathon issue topoteretes/cognee#3689mid-session dataset switch (seal bridge + re-register). Tracked as SDK-306. Base branch main (this repo has no dev).

Switching datasets mid-session repoints where new memory is written without restarting Claude and without losing the current conversation: seal the old (dataset, session_id) bridge, re-register the agent on the new dataset, and keep the same session_id so prior-turn recall still works.

The original commit is preserved as the base (attribution intact); maintainer improvements are layered on top as separate single-concern commits.

Critical finding in #186 — the switch didn't actually switch on main

The original Phase 2 persisted the new dataset to the global config.json (plus os.environ in the switch's own short-lived process, plus .cognee/session-config.json if present). None of those reach a subsequent hook on current main:

  • load_config() deliberately drops the file's dataset key (_file_excluded = {"dataset"}), so get_dataset(load_config()) never sees the written value.
  • load_resolved() carries no dataset, so every write/recall hook falls back to get_dataset(load_config()).
  • an os.environ write cannot cross into a sibling hook process, and no .cognee picker reader exists on main (that's the still-draft feat(claude-code): add project-level dataset picker (#3686) #185 / SDK-304).

So after the switch, store-to-session / session-context-lookup / the final sync kept writing to the old/default dataset. The seal and re-register ran and logged, but the feature was a no-op. (Verified with an adversarial multi-agent read of the code, including an empirical load_config() run.)

What changed (maintainer commits)

  1. fix: redirect subsequent hooks via the launch map. Persist the active dataset into the existing host-keyed launch map record — the store every hook already reads through resolve_cognee_session_id — and have get_dataset() consult it. Precedence mid-session switch > COGNEE_PLUGIN_DATASET env > config default: the switch is the latest explicit user action and wins even over a launch-time env pin (on main that env var is the only working way to select a non-default dataset, so otherwise the switch would silently no-op). The override is keyed by host_key, so it never leaks into other launches (unlike the machine-global config file). A launch that never switched resolves exactly as before.
  2. refactor: drop inert state and scope creep. Removed the switch_state.json ledger and its write-only active/sealed/sealed_at fields, the per-(session,dataset) high-water baseline (a no-op in HTTP mode — per-dataset bridge buckets already partition writes — and in local mode only reachable on the legacy older-cognee cognee.improve fallback), the ignored config.json dataset write, and the .cognee/session-config.json picker write (no reader on main; coupled to unmerged feat(claude-code): add project-level dataset picker (#3686) #185). Seal now just flushes the old dataset's bucket (digest-deduped) and logs old bridge sealed.
  3. test: fixture-free rewrite. The original suite used pytest monkeypatch/tmp_path fixtures and couldn't run under python3 tests/test_*.py (the plugin tree's only runner). Rewrote it to the sibling convention (temp-home save/restore, stubbed network, globals()-iterating __main__ runner) and retargeted the assertions at the reworked behavior.
  4. docs: align the skill with the launch-map redirect (it still described the removed config/picker writes).

Kept from #186 (correct and required by the issue): the seal in both HTTP and local modes (+ old bridge sealed), the in-place re-register with the same conn_uuid + session_id (+ agent re-registered), and context preservation (the switch never re-mints session_id/conn_uuid, and recall is already session_id-scoped). Net production surface ~517 → ~333 lines.

Verification

  • python3 integrations/claude-code/tests/test_dataset_switch.py — 9/9 pass (also under pytest). Full claude-code suite green except the two pre-existing, unrelated urllib-context= failures on main (test_bridge_poll, test_improve_sync).
  • ruff check integrations/ + ruff format --check clean (line-length 100) — the sole automated gate for these plugin-tree dirs.
  • Cross-process behavior (a fresh process, like every hook): after recording a switch to proj-y, a separate python3 process resolves get_dataset → proj-y; a different launch still resolves the default; a switch wins over a COGNEE_PLUGIN_DATASET pin.

Out of scope / follow-ups

  • No-duplicate-writes across a switch is provided in HTTP mode (primary) by the existing per-dataset shadow buckets + dataset-tagged per-turn writes. The local-SDK modern cognee.improve path reads the session cache by session_id, so fully preventing pre-switch turns from being re-emitted into the new dataset depends on cognee core scoping session data by dataset (SDK-283 / cognee PR #4176) — the plugin can't do that client-side without breaking context-preserving recall.
  • Repointing the detached session-end final sync (the exit watcher bakes COGNEE_SYNC_DATASET at spawn): interactive hooks + per-turn writes follow the switch immediately; restarting the watcher on the new dataset is a follow-up aligned with the SDK-212 pattern.
  • Mirroring to the codex twin.

Closes: rework of #186 · gh cognee#3689 · pairs with the picker #185 / SDK-304 · depends on core SDK-283 for full local-mode dedup.

🤖 Generated with Claude Code

wiz-abhi and others added 5 commits July 23, 2026 10:13
…ter)

Add mid-session dataset switching for the Claude Code plugin: seal the
old (dataset, session_id) bridge, re-register the agent on the new
dataset, and preserve conversation context — without orphaning state or
duplicating graph writes.

The switch keeps the same Cognee session_id and conn_uuid, so the session
cache (keyed by session_id, not dataset) is untouched and recall still
returns prior-conversation context. Only where new graph writes land
changes.

- scripts/dataset-switch.py: three-phase orchestrator (seal old -> switch
  active -> re-register), idempotent no-op when the dataset is unchanged,
  best-effort so it never breaks a hook. Works in HTTP and local-SDK modes.
- _plugin_common.py: per-session switch-state ledger + seal_bridge_state()
  for HTTP mode (flush old bucket before switch, mark sealed) and
  high-water baseline helpers.
- config.py: set_active_dataset() persists the new dataset (global config +
  project picker if present); persist_session_cache_to_graph() now honors
  the per-(session,dataset) baseline so the new dataset receives only
  post-switch turns; seal_session_bridge_local() for local-SDK mode.
- hook.log records "old bridge sealed" + "agent re-registered".
- skills/cognee-dataset-switch + README section.
- tests/test_dataset_switch.py (14 tests, fully mocked, no live server).

Closes #3689
…ia the launch map

The mid-session switch persisted the new dataset to the global config.json,
os.environ (own process only), and the .cognee picker "if present". None reach
a subsequent hook on main: load_config() drops the file's dataset key
(_file_excluded), load_resolved() carries no dataset, the env write dies with
the switch process, and no picker reader exists on main. So every later hook
kept resolving the old/default dataset via get_dataset(load_config()) — the
switch never actually switched.

Persist the active dataset into the existing host-keyed launch map record (the
store every hook already reads through resolve_cognee_session_id) and make
get_dataset() consult it. Precedence: mid-session switch > COGNEE_PLUGIN_DATASET
env > config default. The switch is the latest explicit user action, so it wins
even over a launch-time COGNEE_PLUGIN_DATASET — which on main is the only working
way to pick a non-default dataset and would otherwise make the switch a silent
no-op. A launch that never switched has no record entry and resolves exactly as
before; the override is keyed by host_key so it never leaks into other launches
(unlike the machine-global config file).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…and config/picker writes

None of this state was consumed by dataset resolution, so it added surface
without moving any acceptance criterion:

- switch_state.json ledger + the per-session "active" field (read only by tests;
  dataset resolution never consulted it — the redirect now lives in the launch
  map record).
- The per-(session,dataset) high-water baseline: a no-op in HTTP mode (per-dataset
  bridge buckets already partition writes, and it was seeded from bridge-bucket
  counts but consumed by a slice of the SDK session cache — a different store);
  in local mode the primary path is cognee.improve(), so the baseline slice only
  ran on the legacy older-cognee TypeError fallback. Reverted the
  persist_session_cache_to_graph slice to main.
- The "sealed"/"sealed_at" markers (write-only; re-seal safety comes from the
  _state digest, not a flag).
- set_active_dataset's config.json write (ignored by load_config's _file_excluded)
  and its .cognee/session-config.json picker write (no picker reader exists on
  main; couples to the still-draft SDK-304 / #185).

Seal now just flushes the old dataset's bucket (digest-deduped) and logs
"old bridge sealed" in both HTTP and local modes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e sibling convention

The author's suite used pytest monkeypatch/tmp_path fixtures, so it could not run
under plain `python3 tests/test_*.py` the way the sibling suites (and the plugin
tree's only runner) do. Rewrite it fixture-free: redirect every plugin path under
a temp home with save/restore, stub the network, and add a globals()-iterating
__main__ runner so it runs under both `python3` and `pytest`.

Retargets the assertions at the reworked behavior: get_dataset follows the
launch-scoped switch (including over a COGNEE_PLUGIN_DATASET env pin), the old
bridge is sealed (+ "old bridge sealed"), the agent is re-registered in place
(same conn_uuid + session, new dataset, + "agent re-registered"), same/empty
switches are no-ops, and a never-switched launch resolves the configured dataset
unchanged. Drops the tests that exercised the removed ledger/baseline state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… redirect

The skill still described the removed config.json/picker writes and the dropped
"marked sealed" flag. Describe what the reworked switch actually does: record
the new dataset in the launch's session map record (read by every later hook),
taking precedence over a launch-time COGNEE_PLUGIN_DATASET and scoped to this
launch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rshkarin rshkarin added hackathon Hackathon issue core-team Core-team authored/owned PR labels Jul 23, 2026
@Vasilije1990
Vasilije1990 marked this pull request as ready for review July 24, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core-team Core-team authored/owned PR hackathon Hackathon issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants