Expose hy-memory 2.0.12's per-client intention dedup in the memory-features
setting: the feature intention_dedup (recall.intention_dedup_enabled, default
on) and the parameter intention_dedup_threshold
(recall.intention_dedup_threshold, default 0.95, accepted 0.8-1.0, else 422).
Both are part of the features key, so a change rebuilds the SDK clients like
the other switches. CLI: memhub settings features set intention_dedup=false,
intention_dedup_threshold=0.9. Refs #241.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The owner's decision for a runtime with a higher build than the App: startup
neither fails nor shows anything. The App adopts the runtime and never stops,
downgrades or duplicates it at startup. Quit gives it no special pass, so it
follows the same rules as any adopted runtime: preserved when the service is
enabled or its state is unknown, stopped when the service is disabled. The
now-unused "后台版本比 App 新" message is removed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The app page assumed one source maps to one host. Harness has none without a
profile, so the switch was greyed out with `unsupported_host` and an installed
profile stayed `install-only` with no way to approve it (#230).
The one switch now covers every `deepseek-harness:<profile>` through the
existing install, grant, revoke and uninstall APIs. On installs and grants each
profile that is not approved, off removes and revokes each linked one. A
profile that fails is named and the others keep their result. The page lists
each profile with its state.
New profiles are connected while the switch is on. The desktop re-reads the
profile list on each host refresh, and the remembered apply gets one more
attempt per new profile. The Runtime re-reads the disk for an unknown profile
id: its list was fixed at start and it outlives the App, so one new profile
made the host list fail for every app.
Rewrite the harness doc to match, removing the sources page it described,
which is not mounted. Add the strings to all 11 locales.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Quit no longer trusts what startup saw. `transientRuntime` and
`preserveManagedRuntimeOnQuit` were derived from the launch path, so an
already-running transient Runtime was treated as a managed service and
outlived the App (#237, #238, #239).
- queryRuntimeServiceState asks launchd/systemd at quit time, every time.
Enabled: a LaunchAgent declaring this data dir is installed, there is no
`launchctl disable` override, and the enable marker ("Launch at login")
is present. A failed or timed-out query is `unknown`.
- Quit: enabled -> never stop. Disabled -> stopAllRuntimeProcesses stops
every Runtime of the data dir (App child, CLI-started, leftover), each
pid verified via the receipt and the authenticated identity endpoint; an
unverifiable lease owner is reported, never signalled. Unknown -> stop
nothing and log it.
- Startup reconciles a running Runtime by build: the Runtime now publishes
build_number/git_commit in runtime-service.json. Same build is adopted;
an older service job is replaced through launchd (installUserService
kickstart/re-bootstrap), an older transient/CLI one is stopped gracefully
and restarted; a newer one is never downgraded or stopped; an unknown
build is adopted and never stopped. A stray beside an enabled service is
stopped so exactly one Runtime remains.
- The tray and the login preference re-read the real state on each refresh.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An unknown session version was skipped record by record. Each skip carried a
cursor and the file ended with a cursor touch, so the refused session sat
behind a checkpoint: the release that learned the format would read nothing,
turning "not imported yet" into "never imported". The skip was also not
reported, since `report_issue` defaults to false.
Refuse the file from its header instead, with the cursorless
`ScanFailure(UNSUPPORTED_SCHEMA)` other sources already use. The parser check
stays for callers that hand it records directly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cover the #175 tool-output half directly: a `tool/result` written as plain
`text` blocks imports next to the older `tool-result` block shape, goes
through the same credential scrubbing and clipping, and names outside
`session[.vN].jsonl[.zstd]` stay out. Correct the contract-suite comments that
still said only `session.jsonl` was accepted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The owner's decision for #243: a zh-Hans UI now sets
MemoryConfig.language="zh-Hans" like every other UI language, so App users
never fall back to the SDK's zh/en auto-detect. Only an unset language
(CLI/API-only installs, or 'settings language system') keeps auto-detect.
status --json pipeline.sdk_language shows zh-Hans for zh-Hans users.
Desktop now syncs the language the GUI shows (snapshot.locale), not the raw
preference: an App that follows the system sends the resolved locale
(e.g. zh-Hans on a Chinese macOS) instead of null.
Refs #243 (MemHub passes the language explicitly; the SDK's System 2
auto-detect is left unchanged for direct SDK users)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
resolvePackagedCommit accepted 7-40 hex characters in any case, while every
downstream check (packaged-runtime install, local install, release download,
release scripts, after-pack) requires 40 lowercase hex characters. It now
accepts exactly 40 hex characters, case-insensitive, returns them lowercased,
and skips a short or malformed value so a bad ONE_MEMORY_COMMIT no longer
shadows the embedded commit. The tsup build fails on a malformed supplied
ONE_MEMORY_COMMIT instead of baking it.
Fixes#236, Fixes#235
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When intention_dedup_enabled is off, no intention dedup should run, but
the writer's batch dedup still dropped near-duplicate intentions from
one extraction. Gate it on the same switch. Its threshold stays on
MEMORY_DEDUP_THRESHOLD.
Also skip the embedding backfill when dedup is off, since only dedup
uses it, and fall back to the env default on an invalid configured
threshold so a bad value cannot fail the read.
Refs #241
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DSH names a session file after its format generation
(`session.v3.jsonl.zstd`, `session.v4.jsonl.zstd`), but discovery matched only
the two unversioned names, so import jobs completed with `discovered 0` while
the sessions sat on disk.
Discover `session[.vN].jsonl[.zstd]` and gate parsing on the header `version`
field: verified generations (0/3/4) import, anything newer is skipped as
`unsupported_schema` rather than interpreted with today's field rules. v4 tool
results use plain `text` blocks, so read that shape through the same clipping
and credential-scrubbing path as the `tool-result` blocks used by v0/v3.
(cherry picked from commit 2a65e5ab2d8548cfb8d6f72a4b68b702f6b4a3cc; conflicts in CHANGELOG.md and the Core test inventory resolved against main)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add MemoryConfig.recall.intention_dedup_enabled (default true, env
MEMORY_INTENTION_DEDUP_ENABLED) and recall.intention_dedup_threshold
(env MEMORY_INTENTION_DEDUP_THRESHOLD, falling back to the shared
MEMORY_DEDUP_THRESHOLD, then 0.95). The reader passes both from the
client config to recall_intentions. When off, read-time intention dedup
is skipped entirely: nothing is hidden and nothing is deleted.
Refs #241
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sets every MemHub package, the Core project and its lock entry to 1.0.6
with set-release-version.mjs, and the build number to 37. The release
facts in memhub/README.md and docs/INSTALLATION.md now name v1.0.6
(build 37).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Core pins and locks the published hy-memory 2.0.11 from PyPI, which adds
MemoryConfig.language and the prompt registry. apply_sdk_language now sets
the language directly; the guard that skipped it on an SDK without the
field (and its debug log) is gone, and the tests use the real field. The
version constant, README, architecture notes, SBOM, notices and their tests
follow. CHANGELOG records 2.0.11 as published.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
UI (packages/i18n, web, desktop):
- Locales: en, zh-Hans, zh-Hant, ja, es, de, pt, fr, ko, ru, id. zh-CN is
now zh-Hans; a stored zh-CN preference (Desktop gui-language.json,
browser localStorage) is read as zh-Hans, so existing users keep it.
- One complete catalog per new locale (packages/i18n/src/locales), every
key with the same placeholders; product names stay untranslated.
- The language selector lists all 11 by their own names.
- The system language resolves to the new locales (zh-TW/HK/MO -> zh-Hant).
- The migration failure dialog and its GitHub issue prefill follow the
display language (new "migration.*" catalog).
- Completeness test: every locale has every key, matching placeholders,
no empty string; a missing key falls back to en.
- The language button keeps long names on one line (CSS ellipsis).
SDK prompt language (Core):
- config.json `ui_language` (written only once set), GET/PUT
/api/v1/settings/ui-language. The Desktop main process sends the
choice at launch and on every change; the browser UI on a change.
- zh-Hans or no choice ("system") passes no language: the SDK keeps its
zh/en detection. Every other code, en included, is set as
MemoryConfig.language in build_sdk_config, only when the pinned SDK has
the field (else logged once at debug level).
- The language joins the read and write handle keys, so a change rebuilds
clients with the #218 use-count retirement; collection names unchanged.
- Status: pipeline.ui_language and pipeline.sdk_language (auto or code).
- CLI: memhub settings language [<code>|system].
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Move every LLM prompt that had a zh variant into one registry,
hy_memory/prompts/, with one module per language (en, zh-Hans, zh-Hant,
ja, es, de, pt, fr, ko, ru, id). Call sites look prompts up with
get_prompt(name, language), so a new language needs only a new module.
The en and zh-Hans strings are moved verbatim, a test pins their sha256,
and the old names (prompts_zh.*_ZH, extractor.EXTRACT_SYSTEM_PROMPT, ...)
still import.
Add an optional language: MemoryConfig.language (env MEMORY_LANGUAGE)
as the client default, plus a per-call language= on add / async_add,
add_extracted, finalize_session, evolve and sweep. It goes through
extract and basic profile, reconcile and search query, the coding
extractor and both coding reconcilers with the chain summary, the
episode and topic summaries, and System 2. Codes are validated, with
aliases (zh, zh-CN, zh-TW, zh-HK, pt-BR); an unknown code raises
ValueError. With an explicit language, memories are written in that
language. Without one, the zh/en auto-detection is unchanged. The
server accepts language on /add and /add_extracted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reviewing the switch against the real SDK, Kuzu and EmbedService found what
the in-memory backend could not:
- Verifying the graph ran beside live clients, and the SDK refuses a graph
one holds open, so an ultra switch failed whenever a read landed after the
copy. Clients are closed for the verify too.
- The graph was only re-embedded in ultra, so a pro home's graph from its
ultra days went with the old space. It moves in every mode now.
- The re-embed built a full client config, which needs the generation key;
an Ollama embedding has none resolved. It uses the retrieval view.
- The scan opened Chroma outside the first-open lock, the race that once lost
a clean install's first write. It takes the client-construction lock.
- A live space whose profile was deleted raised KeyError on every read. It
now answers `embedding_space_profile_missing`.
Also tests the legacy-collection blocker and the real startup migration on a
home mid-switch.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`memhub models activate` asks Core for the switch plan, shows it, and needs
`--yes` (a prompt on a terminal, exit 2 otherwise); with `--yes` it starts
the switch and follows it (`--wait`, JSON lines with `--json`). Ctrl-C only
stops watching. `models add --activate` now probes before activating and
takes the same path. New `models switch status|cancel` and `models spaces
[--cleanup <id> --yes]`; `memhub status` shows the switch, merge or scan
line; `embedding_unavailable` reads as "the embedding service is
unavailable".
docs/contracts/embedding-switch-v1.md is the App's contract: endpoints,
states, progress fields, error codes, and zh/en/es copy for the
loading-screen merge, the confirmation dialog, the progress banner, the
failure states and the `embedding_unavailable` toast.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The embedding space is explicit now. An `embedding_spaces` registry in the
main Core SQLite pins the live space; existing homes register the
collection they already use under its four-attribute name, byte for byte
(an "auto" width keeps its unsuffixed name and only records the measured
width). The identity of a space is the embedding model and width: a new
endpoint for the same model keeps it. `build_sdk_config(profile, space,
mode, features=)` takes the space and refuses a profile whose identity is
not the space's (`embedding_space_mismatch`); mode and features never
change a name. The read handle and the write coordinator resolve the live
space through the registry and key their handles by it.
B2: a probe on the profile that fills the live space no longer records a
width that would move writes to new collections; the same stored width is
a relabel only.
An identity change becomes a switch job (`embedding_switch_jobs`):
preparing, reembedding, verifying, committing, done / failed (rollback) /
rolled_back, with a heartbeat and owner pid. Model writes pause (claim and
admit gates, direct edit/delete/graph-width answer 409
`embedding_switch_in_progress`); intake keeps queueing. Every vector of
every user is re-embedded into a new space through the SDK re-embed
module (`hy_sdk_migration.SdkReembedBackend`), the graph too in ultra,
verified per role and per user, then one SQLite transaction makes the new
space live, retires the old one and activates the profile. Reads serve the
old space until that commit.
Every Core start scans the vector store before model writes resume. A
non-empty stray space (an earlier build wrote one collection and read
another) is merged into the live space with the live embedder; live
records win, merged strays retire. Unfinished jobs resume at startup,
including after an App update: `sdk-migration-status` reports an open job
without requiring or blocking a migration, and `sdk-migrate` leaves the
switch tables alone. Retired spaces are dropped after 14 days or on
request.
Activation answers 409 `embedding_switch_required` with the plan unless
the identity is unchanged; an empty home activates directly. A search,
recall or context call whose remote embedding does not answer fails with
503 `embedding_unavailable` (no lexical fallback).
API: GET/DELETE /api/v1/embedding-spaces, POST
/api/v1/embedding-switches/plan, POST /api/v1/embedding-switches, GET
/api/v1/embedding-switches/current and /{id}, POST
/api/v1/embedding-switches/{id}/cancel; `embedding_switch` on health and
memory-processing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Core pins and locks the published hy-memory 2.0.10 from PyPI, which adds
the re-embed module the embedding switch needs. The version constant,
README, architecture notes, SBOM, notices and their tests follow.
CHANGELOG records 2.0.10 as published.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MemHub hard-codes none of the SDK's optional features any more. A person
can turn them on from the CLI or the API; with nothing set the SDK config
is byte for byte what it was, so the App behaves as before.
- Features (one global `memory_features` document in config.json, beside
`memory_mode`; strict booleans, unknown keys refused, absent = SDK
default):
- graph_rerank: graph_store.graph_rerank_enabled, usage A, ultra only.
- same_series_timeline: recall.same_series_timeline_enabled, needs
graph_rerank.
- episode_recall: recall.episode_recall_enabled, usage B, pro or ultra.
- session_episodes: recall.session_episode_enabled, the episode write
side.
- episode_summary: recall.episode_summary_enabled, one LLM call per
closed episode.
- intention: llm.enable_intention, on by default.
- few_shot: llm.few_shot_enabled.
- How they reach the SDK: build_sdk_config sets the MemoryConfig fields
per client. Every exposed flag is a config field read at call time, never
os.environ; the SDK's own env var still sets the default of an unset
feature. A feature never touches the vector store, embedder or store
paths, so collection identity is unchanged.
- A change rebuilds the handles like a mode change: write handles are keyed
by (profile, revision, mode, features) and retired with the #218 use
count; the read handle (ActiveProfileReadHandle) is keyed by (profile,
mode, features) and builds a new client on the next read, closing the old
one once no request uses it.
- API: GET/PUT /api/v1/settings/memory-features. Each feature reports its
effective value, configured value, default, whether it is active in the
current mode (e.g. graph_rerank in pro: requires_ultra), cost, SDK field,
env var and a description. PUT: a value sets, null resets, an absent key
is kept, anything else is 422.
- Status: pipeline.features (the non-default ones) in
/api/v1/memory-processing, memhub status --json and progress --json.
- CLI: memhub settings features [set <name>=<bool> ... [--yes] |
reset [<name> ...]]; set previews without --yes.
- Contract: docs/contracts/memory-features-v1.md. It has the inventory,
what is left out and why, and zh/en/es copy.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Mode: a saved setting (config.json memory_mode, default pro);
ONE_MEMORY_GRAPH_STORE still forces ultra for development.
- API: GET/PUT /api/v1/settings/memory-mode, which reports the switch's
effects and the graph backfill.
- CLI: memhub settings mode [lite|pro|ultra] [--yes], which prints the plan
first.
- The mode drives every SDK client.
- acquire_client passes it to HyMemoryClient. The read handle rebuilds on a
change.
- Write handles are keyed by (profile, revision, mode) and retired with the
#218 use count, so a batch in flight finishes in the old mode.
- Jobs read the mode when they run: a job completed in lite is never
reprocessed; pending jobs take the new mode.
- Mode never touches the vector collection identity.
- lite needs no generation model: the built-in embedder writes raw, and the
claim gate admits lite without an active profile. lite never calls
add_extracted (a note is one raw user turn through add). Recall serves the
raw layer.
- → ultra: build_memory_graph once per known user, in the background, with a
status (graph_build).
- Raw retention removed in every mode: MemHub deletes no raw (owner
decision); the retention loop, its startup step and its test are gone.
- P1-3 / R7: POST /api/v2/evolve answers 202 with a run receipt;
GET /api/v2/evolve[/{run_id}].
- Ultra only: 409 memory_mode_not_ultra otherwise.
- One run per user. Never beside that user's write: it waits for the write
in flight, and holds that user's jobs (released pre-SDK) until it ends.
- Loops while should_continue, up to 20 rounds.
- Per-user last_run_at, last_result and last_error in evolve-state.json
(counts and codes only).
- CLI: memhub evolve [--user-id] [--agent-id] [--max-batches] [--wait].
- Nightly scheduler: 03:00 local by default, ultra only, with catch-up when
the last run is older than the slot or 24h.
- Users come from Core's records (local, grant holders, write-queue users)
that the SDK holds memories for.
- GET/PUT /api/v1/settings/evolve-schedule.
- R10: pipeline {mode, configured_mode, forced_by_env, evolve, last_evolve_at,
graph_build} in /api/v1/memory-processing, memhub status --json and
memhub progress.
- Contract for the App: docs/contracts/memory-mode-evolve-v1.md, including
what SDK ultra additionally enables and zh/en/es copy.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
- P2-1 / R8: POST /api/v2/context/search and /api/v1/recall pass the public
client.search knobs through, validated: min_score, schema_min_score,
working_fact_min_score, working_experience_min_score, schema_limit,
intention_limit, rerank, reader (legacy|hybrid), scene
(chat|working|coding|all), search_mode (default|agentic),
include_sources, exclude_sources, created_after, session_ids, agent_ids,
workspace_id, branch (needs workspace_id), layers. The v2 ask routes take
the floors and filters. Absent, every route makes the call it made before
(relevance floor 0.35, scene normal, hybrid reader). A caller's floor is
also enforced on what Core returns, per lane (the fused scene=all score is
a rank, so the lane score is used): min_score=0.9 answers only >= 0.9.
user_ids, embed_override, query_embedding and request_id are not exposed.
- P2-2 / R9: memory-library takes layers (or layer), statuses,
exclude_sources, session_ids and agent_ids, repeated or comma separated;
a layer filter lists the chat store and refuses scene=working|coding.
New GET /api/v2/memory-library/layers counts memories per SDK layer.
The memory graph takes layers, session_ids and exclude_sources.
- P2-4: /api/v2/raw lists layers=["l1_raw"] with statuses active+shadow (pro
shadows a raw once extracted), filters by source, session_id, statuses
and order, cuts after/before in Core, and returns each turn's messages
with roles, a sha256 content_hash, status and created.
- P2-3: GET /api/v2/sources, POST /api/v2/sources/{id}/sync,
POST /api/v2/import-jobs, GET /api/v2/import-jobs[/{id}] reuse the v1
handlers on one user's lane (Hermes only for another user; another
user's job answers 404).
- Profile: GET /api/v2/user-profile/versions (get_user_basic_info) returns
the L0 version chains; memhub profile --versions prints them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A remote embedding with no `embedding_base_url` resolved to the generation
`base_url` at runtime (`build_sdk_config`, `vector_collection_bases`, the
probe). The collection name hashes that endpoint, so a proxy or region edit
of the generation endpoint moved writes to a new, empty collection, and
providers without embeddings were sent embedding requests.
- `resolve_embedding_endpoint` no longer inherits; creating or updating a
remote (non-local_onnx) embedding without an endpoint is refused with
422 `embedding_base_url_required`. The runtime and probe read only
`embedding_base_url`. The API key stays shared.
- Opening the profile store pins, once and idempotently, every stored
remote embedding left without an endpoint to the one it resolved to
(the generation base_url, or the provider default), so the collection
name is byte-identical. `read_active_profile` applies the same rule for
`sdk-migrate`, which may run before Core has opened the store.
- Web: settings write the prefilled endpoint instead of relying on the
fallback; onboarding writes an empty custom field as the endpoint it
means. CLI headless configs now get the 422 code.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Raw is kept forever (MemHub dropped its retention; lite stores nothing else),
so trim it once when it is written instead of on every history read.
- hy_memory/utils/raw_trim.py: one idempotent rule per speaker label.
user/system kept; assistant and other speakers cut to 500 chars + "...";
tool head/tail with coding.tool_result_max_bytes (default 2048).
- writer: L1_RAW (lite/pro/ultra) is flattened from trimmed messages and
flagged custom.raw_trimmed; request.messages (extractor input) untouched.
- _get_recent_history concatenates flagged raws as stored; legacy unflagged
raws are trimmed with the same rule on read.
- client: memory_history ADD record uses the same trimmed messages.
- coding preproc: tool/assistant truncation made idempotent so re-trimming
coding_trace history no longer rewrites omitted-count markers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>