Add hy_memory.reembed for switching the embedding model while writes are
paused. Source and target are named explicitly; a space's identity is the
model name plus the width.
- vector_space_names, plan_reembed, reembed_vector_space (resumable,
cancellable at batch boundaries, widths validated before any upsert, target
wins on existing ids, source never written), verify_reembed,
drop_vector_space.
- scan_vector_spaces and diff_spaces for MemHub's startup reconcile of stray
spaces.
- reembed_graph builds a new Kuzu DB at the target width, copies every node
and edge, recomputes Memory/beh/Topic/HierarchyTopic embeddings and rebuilds
the HNSW index; the source graph is opened read-only. verify_graph_reembed
and drop_graph.
EmbedService.embed_batch now chunks to embedder.batch_size (default 64),
honours Retry-After on 429, takes a per-call max_retries, and rejects vectors
whose width differs from embedding_dims.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
store_metrics_minute() read the row, merged counters in Python, then wrote
with a separate SELECT + UPDATE/INSERT. Two instances flushing the same
minute could both read the old value and overwrite each other, and a
concurrent insert could lose the whole batch to a duplicate-key error that
the outer except swallowed.
Run the read-merge-write in one transaction with SELECT ... FOR UPDATE and
retry once on a duplicate key or deadlock. Add unit regressions for the
insert and merge paths.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Co-authored-by: Claude Code <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>
* 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>