* chore(core): retire memory evidence freshness on read paths
Stop re-validating support_refs against live evidence during library,
context, and call-reference reads. The check hid memories when evidence
changed but caused 503s and segment-key breakage; proofs may still be
written but are ignored until a later teardown removes them.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(docs): align install-docs pin with hy-memory 2.0.4
CI memhub failed because README already documents 2.0.4 / v1.0.4 while
the install-docs contract test still expected 2.0.3 / v1.0.3.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.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>
Series episode memory_at used datetime.utcfromtimestamp() on values produced
by datetime.timestamp(). That yields a naive UTC datetime, which the vector
payload later re-interprets as local time, shifting the stored timestamp by
the machine's UTC offset (8 hours on Asia/Shanghai).
Round-trip through datetime.fromtimestamp() instead, and cover the wall-clock
round trip with a non-UTC TZ regression.
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>
The @dataclass decorator was left at the end of the section-divider comment
line, so SessionSummaryResult was a plain class without __init__. Every
construction raised "takes no arguments" - including the error branch inside
generate_session_summary(), whose TypeError escaped the surrounding except.
Move the decorator onto its own line and add a constructor regression.
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>
BaseServer.shutdown() waits for serve_forever() to return. The SIGINT /
SIGTERM handlers called it directly, and signal handlers run on the same
thread that is executing serve_forever(), so the server could never exit:
the process hung until SIGKILL and the client was never closed.
Ask a helper thread to call shutdown() and move server_close()/client.close()
into a finally block after serve_forever() returns. Add an in-process test
and a subprocess test that fires SIGALRM while serving on the main thread.
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 install blocks still asked for hy-memory 2.0.3 (and 2.0.1 extras),
MemHub 1.0.3 (build 7), and said memhub-cli was unpublished. pyproject.toml
ships 2.0.4, the MemHub workspace ships 1.0.4 (build 13), and README already
announces memhub-cli 1.0.5.
Update the install commands and the release metadata in the root READMEs,
the usage/search guides, the monorepo policy, and the MemHub docs. Historical
CHANGELOG entries are deliberately unchanged.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Four test modules import artifacts that are not in this repository:
one_memory.processing_requests and three scripts/bench_*.py files. A public
checkout therefore fails during collection, so the whole core-fast suite
cannot start even though every collected test is fine.
Skip only when the module or script is missing. Builds that carry the private
artifacts keep running the same tests unchanged.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
The access gate refuses a file over the per-root max_file_bytes with
FILE_TOO_LARGE, but both discovery_failure_code() and _scan_failure_code()
collapsed everything except budget and symlink failures into
UNSUPPORTED_SCHEMA. A user with a 3 GiB Codex rollout therefore saw
"unsupported schema" while the parser was fine; the only problem was size.
Map the access gate's FILE_TOO_LARGE onto the existing ScanIssueCode.
FILE_TOO_LARGE on both paths, and update the enum comment to say it covers
both the import's per-file ceiling and the access gate.
The per-file limit itself is unchanged.
Fixes#102.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
readerEnabled was only honored on the attach path. The normal first launch
spawns its own Core and returned from start() without calling startReader(),
so the read-only Reader never ran: no reader_port in the receipt and every
read fell back to Core.
Start the Reader after every successful owned start (first launch, race
recovery and relaunch). Clear readerChild when it exits so a later restart
can spawn a fresh one, and stop the old Reader before a relaunch. Add a
public-start regression using fake child processes.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
MemHub expects concurrent native reads during precipitation. The
process-local read slot mostly existed for a Chroma/GIL fail-fast and
was never held by writers on main; activating it (as in #98) would only
push recall onto weaker fallbacks. Remove the lock module and the
hy_memory_store_busy path instead.
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(runtime): accept profile-scoped host ids in the host permission layer
DeepSeek Harness configures recall per profile, so recall-hosts.ts reports
`deepseek-harness:<profile>` as the host id. The CLI accepts that shape and
prints it from `host list`, but the runtime rejected it twice:
- HostPermissions.supported() tested `[a-z][a-z0-9-]*`, so preview/install
answered host_permission_transport_unsupported for every Harness profile.
- HostGrantStore.validHostId() applied the same restriction, so even a
supported host could not create its installation or pass the load-time
revalidation.
A host id is an opaque label: the installation directory is a digest of it,
so a colon never becomes a path component. Both checks now allow one optional
`:<profile>` segment, matching the CLI's existing HOST_ID.
Adds an integration test that previews a real `cordis.patch.yml` install for
`deepseek-harness:desktop` and a grant-store test that the colon survives a
reload, plus a regression for malformed ids.
* fix(runtime): share one host-id grammar across CLI, permissions and grants
Review follow-up: the three layers had three different host-id rules, so the
CLI could list an id the permission layer (or the grant store) then refused.
They now import `isHostId` from `@one-memory/contracts`, which is the only
definition:
- host: 1..64 chars, alphanumeric first, lower-case letters/digits/hyphens
- optional `:<profile>`: 1..32 chars, alphanumeric first
- total <= 97, and the pattern asserts true end-of-input so a trailing
newline cannot pass
Digit-leading ids stay accepted (the CLI always took them), and the profile
segment is the only colon allowed. Boundary cases live in
`packages/contracts/src/host-id.test.ts`; the grant-store and permission tests
cover the same grammar from their side.
Packaging: contracts is a source-only workspace package, so every bundle that
now imports it for a value inlines it (`noExternal`): host-bridge entries,
the built Claude Code hook assets, and the CLI. Without that the emitted CJS
asks Node for a `.js` file that only exists as `.ts` and the MCP entry fails
at startup.
---------
Co-authored-by: Vesper <226942762+VE5PER-17@users.noreply.github.com>
Co-authored-by: Vesper <hb.ran@foxmail.com>
The /internal/semantic-ids, /internal/call-event and
/internal/graph-snapshot routes were registered directly on the app without
the local_api_guard dependency used by every other route. Any local process
could read memory graph content, probe semantic IDs, or forge call events
without authorization.
Mount the same guard on all three routes. The Reader already sends
Authorization: Bearer <core_api_token>, so its calls keep working. Add
regression tests for both the 401 and authorized paths.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Session-scoped searches build only isolation_keys and leave user_ids /
agent_ids unset. recall_intentions had no isolation_keys parameter, so the
VDB query ran without any tenancy filter and could return every user's
intentions.
The duplicate bucket also ignored user_id / agent_id. Two users with a
similar intention on the same day were treated as duplicates, and the
loser was hard-deleted through vector_store.delete().
Pass isolation_key / isolation_keys through to the vector search and add
user_id / agent_id to the dedup bucket key. Add regression tests for the
parameter forwarding, cross-user recall, and cross-user dedup.
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>
A single record above MAX_VERBATIM_CHARACTERS failed EvidenceInput
validation. The import retried it three times, failed the whole job at the
read phase, and the byte checkpoint resumed onto the same record, so every
later record in the source stayed unimported.
Clamp verbatim and raw_verbatim in to_evidence(), keeping the head and an
explicit "...[truncated: original N chars]" marker. The identity hash still
uses the clamped raw_verbatim, and records that were previously rejected
never had a stored identity to collide with.
Add regressions for the oversized and normal paths, and update the Core test
inventory.
Refs #68
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
PipelineLogWriter derives its subdirectory from the caller-controlled
user_id prefix before "__". That prefix was joined directly to the log
root, so an absolute prefix or ".." escaped it: user_id="/tmp/evil__u1"
created /tmp/evil and appended the daily log there.
Whitelist a single safe path component and, as defence in depth, verify
the resolved directory stays under the configured root. Sanitize
default_subdir the same way. Add regressions for absolute, parent and
nested prefixes plus a malicious default subdir.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
is_loopback_origin() accepted any hostname starting with "127.". That
includes ordinary DNS names such as 127.0.0.1.attacker.example, which an
attacker can point at 127.0.0.1 and use for DNS rebinding against the
local Core.
Keep the exact loopback names and parse everything else with
ipaddress.ip_address(...).is_loopback. Add regressions for rebinding-style
names and IPv6 loopback.
Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com>
Co-authored-by: Claude Code <noreply@anthropic.com>