## Summary
Thread scope (`Scope.thread`) was silently dropped before reaching the Core API. Ingest, search, and list operations never forwarded `session_id` on the wire, so thread-scoped reads returned unfiltered results and thread-scoped writes stored memories without a session association. This fix propagates `thread` as `session_id` on every route that Core honors it, strips it from routes that do not (get, delete, expand), and validates that responses echo back a matching `session_id` for thread-scoped requests.
## Changes
- Add `thread: str | None` field to `UserScope` and `WorkspaceScope`.
- Add `include_thread` flag to `scope_to_fields` and `scope_to_query_pairs`; emit `session_id` when opted in.
- Add `strip_read_filters` to `scope_mapper` — drops both `agent_scope` and `thread` for routes (get, delete, expand) that do not accept those filters.
- Fix `strip_agent_scope` to preserve `thread` when clearing workspace filter.
- Propagate `session_id` on ingest and search request bodies in `provider.py` / `async_provider.py`.
- Extract `_build_list_path` helper that appends `session_id` to the list query string when thread is set; use it in both sync and async providers.
- Use `include_thread=True` in `scope_to_fields` / `scope_to_query_pairs` calls for ingest, search, and list in `handle_impl.py` and `async_handle_impl.py`.
- Add `_build_memory_scope` in `handle_impl.py` and `_build_scope` in `mappers.py`: project `session_id` from the backend response back into the returned scope, and raise `ValueError` when a thread-scoped request receives a response without a matching `session_id`.
- Add comprehensive test coverage across `test_provider.py`, `test_async_provider.py`, `test_handle_base.py`, `test_mappers.py`, and `test_scope_mapper.py` for all affected operations.
## Why
The Core API uses `session_id` to partition memories by conversation thread. Without forwarding `thread` as `session_id`, thread-scoped ingests write memories outside any session, thread-scoped searches return the full unfiltered memory set, and there is no way to verify the backend actually applied the requested scope. The validation step on responses closes a silent correctness gap where a mismatch between requested and returned session could go undetected.
## Validation
All new tests are exercised via `respx` mocks against the full HTTP path. Tests cover:
- `session_id` present in request body/query for ingest, search, list, package, and `search_as_of`.
- `session_id` absent from get and delete query strings (routes that do not filter by thread).
- Expand strips `thread` from returned memory scope.
- Response mapper raises on a missing or mismatched `session_id` when thread scope was requested.
- `strip_read_filters` and `strip_agent_scope` preserve or clear `thread` as expected.
Summary
Thread scope (Scope.thread) was silently dropped before reaching the Core API. Ingest, search, and list operations never forwarded session_id on the wire, so thread-scoped reads returned unfiltered results and thread-scoped writes stored memories without a session association. This fix propagates thread as session_id on every route that Core honors it, strips it from routes that do not (get, delete, expand), and validates that responses echo back a matching session_id for thread-scoped requests.
Changes
Why
The Core API uses session_id to partition memories by conversation thread. Without forwarding thread as session_id, thread-scoped ingests write memories outside any session, thread-scoped searches return the full unfiltered memory set, and there is no way to verify the backend actually applied the requested scope. The validation step on responses closes a silent correctness gap where a mismatch between requested and returned session could go undetected.
Validation
All new tests are exercised via respx mocks against the full HTTP path. Tests cover: