Hardening of the multi-user feature from #250, after review.
- Read responses carry `user_id`, the user actually read (`local` when none
was named): memory-library, memory-library/layers, user-profile,
home-summary, memories/graph, raw, context/search, v1/recall and
memories/ask. ask/stream puts it on its first (`recall`) event. The
contracts types gain it as optional, and the Host Bridge context validator
accepts it as an optional key, so an older Core still validates.
- DELETE /api/v2/memories/{id}: 204 only when the caller's own memory is
deleted; another user's memory, a missing id, and an SDK result with
deleted_count 0 (a delete that lost a race) all answer the same 404
`memory_not_found`. The App treats it as already gone; `memhub memories
delete` prints that it was not found, exits 0, and its JSON is committed
with `already_gone: true`.
- Hermes skips a `profiles` directory at any depth (discovery helper:
`excluded_directory_names`), so a root above ~/.hermes never reads another
user's profile. Connect (other-user root, local connect and authorize)
refuses a root that is the `profiles` container (422
`hermes_root_is_profiles_container`) and a root equal to, containing or
inside another user's active Hermes grant, symlinks resolved (409
`hermes_root_overlaps_other_user`); `local` on ~/.hermes with another user
on ~/.hermes/profiles/<name> stays allowed. The CLI explains both codes.
- `sources connect --user-id` with an invalid id says the user id need not
equal the Hermes profile name.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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 #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>
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>
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>
- 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>
- Global --user-id <id> (default local) for memories list/show/add/
add-status/update/delete, recall, profile and write submit/status; sent
only when it is not the default, so default requests are unchanged.
- memhub sources connect hermes --user-id <id> --root <path> [--import]
grants one Hermes profile to its own user; refused for other sources.
- memhub sources list shows the user of every grant (from
GET /api/v1/source-grants, skipped on an older Core).
- Help catalog, contract tests and docs/cli/EXPERIENCE.md updated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`--yes` alone no longer deletes data or restores a backup: the target
directory has to be repeated with `--confirm <path>`. Without it the
command refuses with exit 4 and shows the exact command to run.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
API: adding one memory is POST /api/v2/memories with its status at
GET /api/v2/memories/additions/{request_id}; /api/v1/memory-notes is
removed. Error codes memory_note_* become memory_add_*, contracts types
MemoryAddInput/MemoryAddReceipt, broker /memories, MCP MemHub_write and
the web composer follow. Stored names (memory_note_requests,
one-memory-manual, direct_memory_note) stay. /api/v2/memory-library
takes an optional scene filter.
CLI:
- No plan files. memories add/add-status, models add/test/receipt and
sources connect execute directly; async receipts stay.
- One --yes, only for irreversible operations (memories delete, models
remove, delete-data, backup restore, graph-width rebuild); without it
they preview and exit 0. --confirm is a hidden alias.
- Removed notes, calls, setup plan, sources import, models
plan/configure/probe*, and the unreachable processing, extraction and
import-file commands, with their dead allow-list entries.
- memories list gets --scene/--source/--query; show/update/delete take
--kind/--variant (and --task). All memories JSON is schema_version 2.
- Fixes from the CLI review: global --data-dir, real
--confirm-overrides and help that matches init, server failures exit 1
not 2, catalog exit codes match, JSON errors on stdout, bad filters are
rejected instead of "busy", write submit prints its request_id for
retries, recall labels raw excerpts, stop says it disables login
auto-start, backup help says memories are not included yet.
- Hidden aliases: path -> scan-roots, task -> write status,
graph width -> graph-width, init --source -> sources connect.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>