search:
- rerank=True (default) keeps today's behavior; rerank=False skips the
local cross-encoder, graph rerank and the coding facts' BM25 rerank for
that call (carried on ReadRequest, so concurrent calls are independent).
- MMR now runs only when the caller's limit is below MMR_MAX_TOP_K (20).
It is O(k^2 * pool) Jaccard and made large result pages slow.
- scene="all" searches chat, working and coding concurrently with one
shared embedding and fuses them by Reciprocal Rank Fusion (k=60) over
each lane's own rank, so no lane is buried by its score scale. Items
carry scene, kind, memory_id, content, fused score, lane score/rank,
source and times. A failed or disabled lane is reported in "errors".
list_memories:
- scene="coding"|"working" honour include_sources/exclude_sources with
the filtered total.
- scene="all" lists the three scenes newest first with limit/offset over
the merged order, per-scene totals and the same unified item fields.
update/delete:
- kind/variant disambiguate like get(); an experience update replaces
its solution (task= also replaces the task and its search vectors), a
fact update replaces the statement; delete removes experiences and
facts with their vectors. Chat calls are unchanged.
- The HTTP server maps an ambiguous id to 409 and a bad namespace to 400
instead of 500, passes kind/variant through, and strips the query
string from PUT/DELETE paths, which leaked it into the memory id.
memory_stats now reads productivity times (written as naive UTC) as UTC.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SDK: list_memories(scene="chat"|"coding"|"working") lists one kind of
memory. coding and working list the scene's whole store, newest update
first, with workspace_id still narrowing it, and total is the real count
instead of the page size. memory_stats() returns each scene's count and
latest created/updated time plus their total, reading no memory text.
Without a workspace, coding and working memories could not be listed or
counted, so a 45-memory library listed 22.
MemHub migration: reattaching legacy vectors opened Chroma with its own
settings and never closed it, so the readback that followed in the same
process could not open the store ("An instance of Chroma already exists
... with different settings"). Every 1.0.4 home with legacy vectors
failed its first upgrade launch. Offline code now opens Chroma as the SDK
does and closes it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
get_recent_history() documented "agent_ids 为空时查询该用户所有 agent" but
constructed the isolation key with the literal "default". History records
are written as "{user_id}:{agent_id}", and add() defaults to
"default_agent", so the documented path usually returned nothing.
Add HistoryStore.get_recent_for_user(), which matches the literal user
prefix with an escaped LIKE, and use it from the client. Add regressions
for multiple agents, a sibling user id and LIKE wildcards in the user id.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
A solo client creates a write loop and a separate search loop, but close()
stopped only the write loop and __aexit__ stopped neither. Every client
instance against a long-lived process therefore leaked the search thread,
its selector and its 50ms self-wakeup timer. _LoopThread.stop() also left
the loop object unclosed.
Stop every owned loop from one idempotent helper used by both close() and
__aexit__, leave shared-runtime loops to the runtime, and close the loop
after its thread exits. Add lifecycle regressions for both modes.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
* fix(faiss): reconstruct embeddings for dedup and episode summaries
Normal upsert paths store only the payload and the FAISS index position.
get_embeddings() looked for an "embedding" key that only update_payload
ever wrote, so it returned an empty mapping for every upserted node. Search
dedup and episode summaries therefore saw no vectors on the FAISS backend.
Reconstruct the vector from the live index position, guarded by the same
_idx_to_id check search() uses, and keep the stored fallback for nodes
updated through update_payload. Add a regression test.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix(search): pair dedup markers with their source search result
The public items are filtered by the requested layers, but the private
dedup view was built with zip(result.memories, public_items). Once one
result is filtered out every later _memory_kind shifts by one, so a
canonical_state can enter fuzzy dedup and be shadowed, or an ordinary fact
can silently skip dedup.
Build both views in the same loop, from the same source element, and add a
regression test for the filtered case.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix(episode): use the writer's vector store for background summaries
_schedule_episode_summary accessed self.vector_store, which MemoryWriter
never defines. The AttributeError was swallowed by the task's except block,
so every closed episode silently skipped its L3 summary.
Resolve the store through the existing async _get_vector_store() accessor
and cover the scheduled task with a regression test.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
The public search API accepts agent_ids / session_ids, and the chat reader
applies them, but the coding path dropped both before calling the store.
A caller that relies on agent or session boundaries could therefore recall
facts and experiences written by another agent or session of the same user.
Accept the id lists in the coding store, filter them with IN clauses and
in the post-fetch checks, and forward them from _search_coding. Singular
agent_id / session_id stay supported for existing callers. Add store-level
and client-level regressions for the filtered scopes.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Co-authored-by: Claude Code <noreply@anthropic.com>