`one-memory capture <host>` ran as a separate short-lived process on every
hook and, on every call, constructed `EvidenceStore` (re-creating the retired
evidence/FTS tables on an SDK-authority home), put Evidence rows and assembled
write jobs: three write transactions outside Core's single writer, contending
for SQLite's file lock with Core and with other hosts' hooks.
The hook process now never opens the database. It checks the migration fence,
reads the hook JSON and posts it to the running Core over loopback with the
runtime token (`POST /api/v1/live-capture/{source}`, 1.5 s budget); any
failure drops the turn silently with no local fallback. Core applies the
grant, sync switch, rejection counting, automated-session and Cursor/Claude
rules and stages the lines in-process through the live file-sync path
(`materialize_write_input` -> `stage_sdk_input` -> `finalize_sdk_file`, live
priority). No Evidence table is involved.
The hook and live file sync gave one message different identities (the hook
has no workspace and often no message id) and separate assembly rows, so a
turn both saw was written to the SDK twice. Hook input for a source live sync
reads from its own files is now left to that path; only a source without
file polling is staged from its hook.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Under a long live-sync history, writes stalled behind "database is locked"
refusals, durable leases lapsed, and the App stopped answering for tens of
seconds at a time.
- import_jobs is bounded: finished (completed/cancelled) sync jobs are
pruned to the newest 20 per (user, source), with their issues and skip
counts, in bounded batches at startup and every 10 minutes. History jobs
and every unfinished job are kept.
- Job lookups are SQL (find_jobs / latest_job) instead of reading the whole
table, served by new indexes in schema migration 63. GET
/api/v1/import-jobs keeps its shape and order.
- The durable write queue runs claim, settle and maintenance in its own pool
and lease renewal in a dedicated one, so bulk work cannot starve a lease.
The single writer and its line are unchanged.
- abandon, resume, enqueue, pause, cancel, shutdown and startup resume make
their database calls off the loop; the file watcher resolves changed
paths off the loop; revoke reads and writes its grant off the loop.
- The WAL cadence checkpoint is requested from the write path and run by one
checkpointer thread instead of inline on the writing thread.
- The durable drain is supervised: an unexpected exit is reported with its
stack and the drain restarts with capped backoff under a new lease owner.
memory-processing and memhub status show write_drain and
write_drain_error. Contained per-job failures are reported with stacks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Lite context search, recall and the Host Bridge return every raw vector
hit above the floor, ranked by score, over active and shadow raws. The
turn view shows the turns containing the query, or else the hit's
closest turn, instead of dropping semantic hits. Pro and ultra raw
search stays active-only.
A new test runs the raw routes in a child interpreter against the
installed hy-memory wheel, and the route-test fake SDK now rejects
search kwargs the pinned release does not accept.
The lite raw-turn page projects only raws newly put on top instead of
re-listing the whole raw layer on every poll.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2.0.14 accepts `statuses` on explicit L1_RAW search, which Core has
passed since #316. Covers the same files as the 2.0.13 pin (#285).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
hy-memory 2.0.13 reports a model failure as success: false with
agent_error_code / reconcile_error_code. The job's error_code now carries
that cause (hy_sdk_reconcile_empty_response, hy_sdk_llm_error, ...) instead
of a bare hy_sdk_write_failed; only upper-snake codes are kept, never a
message. Regression tests: a reconcile provider failure (401/503/timeout)
and a coding-path extractor failure wait for the provider and complete after
recovery; an empty or unparseable 200 reply is retried 3 times, then failed
with its cause.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pin the Core dependency, lock, HY_MEMORY_VERSION, SBOM, notices, docs and
version guards to hy-memory 2.0.13. Move the MemHub entries from Unreleased
into the 1.0.6 section (build 37, on hy-memory 2.0.13) and update the v1.0.6
news lines.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Experience reconcile no longer falls back to ADD-all, and fact reconcile no
longer inserts without superseding, when the model call fails, returns empty
content, or returns a reply that is not a JSON object with an ops list. The
write returns success=false, agent_error_code=RECONCILE_FAILED and the
cause in reconcile_error_code (LLM_UNAVAILABLE / EMPTY_RESPONSE /
JSON_PARSE_FAILED), the same as chat.
Each reconciler is split into plan() (retrieve + model decision, no store)
and apply() (store). CodingWriter plans both layers before applying either,
so a decision failure in either layer stores no experience or fact and
supersedes no older fact. reconcile() still runs plan + apply.
An experience reply of {"ops": []} is a valid "no changes" decision and
stores nothing. A non-empty ops list with no recognizable op is a parse
failure. A fact reply of {"ops": []} inserts without superseding.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The SDK keeps one outcome per request id and replays it: a write that failed
under an id fails under it forever, without calling the model. Re-running a
job under its idempotency key (including the old manual retry) therefore never
reached the model again. Every re-run now gets an SDK id of its own:
`idempotency_key` on attempt 0 (unchanged for existing jobs), then
`idempotency_key#rN`, N stored on the job (`sdk_attempt`, migration 61). Before
a re-run the outcome of the previous id is checked; a write that did succeed is
completed from its receipt and not made twice.
- Provider failure (401/403, 404, 408, 429, 5xx, timeout, connection error,
breaker refusal) during the attempt: the job goes back to `pending` as it
was: same place in the queue, no attempt or retry spent, no cap. The breaker
paces it. Attribution: `ModelBackoff.attempt()` charges every provider
failure to every write in flight, because a request cannot be traced to its
job (the SDK batches embeddings on one shared worker).
- Any other failure: retried after 30 s, 60 s, 120 s, then `failed` with its
code; the queue moves on.
- Crash or interruption mid-write (`unknown`): re-run automatically under a
new id after the outcome check, counted as a retry. Existing `unknown` jobs
are handled the same way after upgrade, once. A write still running on its
thread in this process is left until it ends.
- The manual retry (`POST /api/v1/memory-processing/retry-unknown`, the App's
重新整理 button and its strings) is removed.
- App banner while the provider is paused, in 11 locales; `error_code` and
`next_retry_at` on `GET /api/v2/memory-writes/{id}` and
`GET /api/v2/memories/additions/{id}`; `memhub status` shows the pause.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
WAL (#245): the writer sets journal_size_limit (64 MiB), Core runs a
TRUNCATE checkpoint when the main database is idle (every minute, with a
50 ms busy wait, skipped rather than raised when busy) and at shutdown, and
the size alert judges pending frames rather than the file's size.
Disabled sources (#245): jobs of a source that is off or revoked are
suspended. Rows keep their state; status counts their waiting jobs as
`suspended` and queue depths leave them out. The claim gate already skips
them, so turning the source back on resumes them.
Gate agreement: a job the execute gate hands back (release_unstarted) is
held for 5 s through its lease, so a persistent disagreement between the
claim gate (lane user) and the execute gate (Evidence user) cannot spin.
Recall (#231): SDK writes run in their own bounded pool (12), recall's SDK
reads in theirs (8). The recall route has a total budget (timeout + 3 s,
at most 12 s) and answers 504 recall_timeout. The broker maps a Core
timeout to 504 and the client maps 504/ECONNABORTED/ETIMEDOUT to
broker_timeout. retry-unknown requeues off the event loop (#162).
Model backoff: a process-wide breaker per model profile revision records
every provider exchange through the SDK transport. After a provider failure
(auth, not found, rate limit, timeout, 5xx, unreachable) the drain claims no
jobs for 5 s, then one probe; each failed probe doubles the pause up to
3 minutes. The first success, or any model profile create, save, activation
or embedding switch, resets it. memory-processing reports
llm_backoff_until and llm_backoff_error.
Fixes#245, Fixes#231, Refs #162
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reconcile (add / add_extracted): when the reconcile model call fails,
returns an empty 200 reply or an unparseable reply, or reconcile raises
before anything is stored, the write no longer falls back to adding every
extracted memory as-is with success=true. The ADD-all fallback and its
degraded flags are removed. The write returns success=false,
agent_error_code=RECONCILE_FAILED and the cause in reconcile_error_code
(LLM_UNAVAILABLE / EMPTY_RESPONSE / JSON_PARSE_FAILED /
RECONCILE_EXCEPTION). Nothing new is stored, the episode counter does not
advance, and add rolls back its L1 raw (retry_safe=true), so a re-run with
a new request_id is clean. Only a literal [] still means "no changes".
RECONCILE_ENABLED=false keeps writing directly.
Working / coding path: an extraction model failure (LLM_ERROR,
EMPTY_RESPONSE, JSON_PARSE_FAILED) and an experience or fact embedding
failure (EMBEDDING_FAILED) now return success=false instead of success
with 0 ops or a node without a vector. Embeddings are computed before the
SQLite row is written and before an older fact is superseded. A failed
write does not append its trace or record chain rounds. A model that
answers with nothing to remember is still a success.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The overlap check resolved the caller-supplied root a second time in new
code. Resolve it with the same helper the grant uses (`resolved_source_root`
in access.py); the Hermes check now compares an already-resolved root and
only resolves other users' stored grant roots itself. A root that cannot be
resolved is left to the grant that follows, which refuses it with its own
code; the `profiles` container is still refused by name before that.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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 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>
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>
The read handle was always built from `builtin_retrieval_snapshot()`, while
writes follow the active profile, so a custom embedding (2560 dims, say)
wrote `memhub_<sha>_2560` and every read opened the empty built-in 512-dim
collection.
`_sdk_client_handle` now returns an `ActiveProfileReadHandle`. It opens a
`retrieval_view` of the active profile: the profile's embedding fields with
the built-in generation placeholder, so a read never needs the generation
model; a remote OpenAI-compatible embedding gets the profile's shared key.
With no active profile it stays on the built-in space.
A profile change builds a new client and retires the old one, closed once no
task still uses it, as `SdkWriteCoordinator` does (#218). A use is the task
that asked for the client, so every caller (library, search, recall and its
raw fallback, graph, profile, raw, home summary, ask, context, multi-user
routes) keeps its shape; each raw-retention pass runs in its own task.
Refs #242
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#234: with no active model profile the migration aborted in preflight with
`sdk_migration_model_profile_required`. No profile means no generation
model, not no embedding: the vectors are the built-in model's, the space the
Runtime reads with. The runner, and `sdk-migration-status`, now use
`builtin_retrieval_snapshot()` for reattach, the collection bases and the
readback client; `runtime_sdk_client` resolves no secret for a key-less
profile. When legacy memories have vectors only under another embedding
space (a profile since deleted), the preflight stops with
`sdk_migration_vector_profile_missing` before anything changes.
#229/#240: the readback required every confirmed governed chat id to read
back, but a 1.0.4 library can hold confirmed rows whose SDK node was merged
or deleted before the upgrade (658 confirmed, 657 readable, upgrade failed).
The ids in the vector store are now read before reattach; chat is held to
the confirmed ids present there, and the rest is only counted as
`chat_missing_before_migration`. A memory present before the migration but
unreadable after it still fails, and if none of the confirmed chats is in
the store none is excused.
Fixes#234, Fixes#229, Fixes#240
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The two `source_grants` triggers of the visibility cache select from
`governed_memory_evidence`, which the SDK cutover drops. SQLite resolves a
trigger body only when it fires, so every grant insert or update on a
migrated home failed with "no such table": revoke, connect and the
multi-user Hermes connect all answered 503.
`install_triggers` now installs a trigger only when every table its body
names exists, and drops an old copy otherwise. Startup on SDK authority
(which does not install the cache) and the cutover cleanup drop the stale
copies an earlier build left behind.
Fixes#233
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
DELETE of another user's id answered 404 while a missing id answered 204,
so comparing the two told that the id existed. It now answers 204 and
deletes nothing; GET and PUT answer the same 404 as a missing id, and
receipt and addition reads already matched. A test compares every by-id
route's response for another user's id with a nonexistent id's.
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>
One Runtime now serves many user_ids. A user is the root that is read,
never a session or platform field; the App never sends user_id and keeps
reading and writing `local`, and only Hermes may hold another user's grant.
- R1: every memory read/write route takes an optional user_id (query for
GET/DELETE, body for POST/PUT), default `local`, pattern
^[A-Za-z0-9._-]{1,64}$ (no `:`).
- R2: user_id reaches the SDK: Evidence, durable payload and client.add
carry it; reads filter by user. get/update/delete/history and
get_write_outcome take no user in hy-memory 2.0.9, so Core reads the
memory first and answers 404 for another user's id; memory-write keys
are namespaced per user and addition receipts are keyed by the user's
Evidence id.
- R3: grants, cursors, jobs, live sync and the claim gate work per
(user_id, source_id) through lane keys (`hermes` for local,
`hermes@wife` otherwise). POST /api/v1/sources/connect takes user_id and
root (Hermes only, 422 otherwise); revoke takes ?user_id; new
GET /api/v1/source-grants lists every user's grants. Another user's
grant syncs while it stands; the App's per-source switch stays local's.
- R4: a Hermes root scan never enters profiles/** (discovery, history and
live sync share the same walk).
- R5: session keys, assembling rows, FIFO source keys and request ids are
per user, so two users' turns never share a batch.
- R6: memory routes refuse undeclared query parameters and body fields
with 422.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Core pins and locks the published hy-memory 2.0.9 from PyPI; the
version constant, README, architecture notes, SBOM, notices and their
tests follow. CHANGELOG records 2.0.9 as published.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Owner decision on #248: the writer no longer filters basic_info against
the user turns. The placeholder guard (with healing on read) and the
prompt hard rules stay; the guard alone keeps an assistant quoting
"name is 用户" out of the profile.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After an upgrade the diagnostic files still hold an older build's records.
They are valid JSON that the reader leaves out, but were counted as damaged,
so `memhub logs` warned "跳过 N 行损坏的诊断记录" and a support export
failed with diagnostic_export_invalid_lines. Count them apart as
`unrecognized_lines` (reported only in the JSON empty-state record) and
keep the warning for lines that are not JSON.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hermes stores its gateway's own posts in `user` rows: finished background
process notices (often with the gateway's "not written by a human" note),
voice placeholders that only name an audio file, and "Reply with exactly:
PONG" probes. Relayed Telegram messages arrive wrapped in out-of-band
markers and a gateway-origin JSON block. All of it reached extraction as
the person's words.
The Hermes adapter now cleans `user` rows (state.db and session
transcripts) before evidence is built, so the content hash and dedup see
the cleaned text:
- skipped with new reason codes `injected_machine_notice`,
`voice_placeholder` and `probe` (media-only rows as `no_text`), counted
in `import.parse.issue_summary` like other parse skips;
- out-of-band markers, the gateway-origin block and the interrupted
response label are removed, keeping only the person's message.
Assistant and tool rows are unchanged. Tests use real rows with the
Telegram ids and home paths replaced by fake values.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Placeholder values no longer overwrite the user's basic profile (L0). Not
yet published to PyPI.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A weak extraction model filled basic_info with "用户", "The user" or "N/A"
when the user stated nothing, and each placeholder superseded the real L0
value. upsert_basic_profile now drops placeholder values and writes no new
version when nothing is left; profile assembly skips stored placeholders.
The writer keeps a basic_info field only when its value appears in a user
message of the request. The zh and en extraction prompts add a hard rule
and a small-talk counter-example.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The macOS v1.0.5 artifacts were built from release/memhub-build-36-main
(dbba403): main's code at a621262 with build 36. Main moves to the same
build so the Linux Runtimes built from the v1.0.5 tag match them. The
release docs name build 36.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The README news line announces v1.0.5 briefly; details live in the release
notes. The release facts said build 14 and promised a macOS Intel build that
this version does not ship. CHANGELOG's Unreleased section becomes MemHub
1.0.5.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The map hid the previous scope's graph by passing stale into the library
view, which also blanked the list status, pagination and empty states.
A separate graphLoading flag now hides the old graph and shows
"正在读取记忆地图…" while stale keeps its original meaning and text.
The client-side revoked-source graph filter is dropped to match Core.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Memories already distilled from a revoked source stay visible, so the
graph endpoint no longer checks revocation before or after the SDK read,
no longer filters memories by revoked source and reports the SDK total
again. The map and the list now agree. The scene="all" and
statuses=["active"] scope stays.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The lifecycle verification imported a conversation on a runner with no
generation model and then expected recall to find it. Under SDK authority
nothing reaches the SDK, not even its raw layer, until a model is active,
so the check could never pass and blocked the Linux release archive.
It now asserts what the runner can prove: the imported and appended turns
reach Core's durable write queue, and survive an idempotent upgrade.
docs/TODO-raw-recall-without-model.md records the product gap.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- hy_sdk_runtime: construct one HyMemoryClient at a time per home. Two
first-time Chroma opens on a fresh vector dir race Chroma's unlocked
System cache; on a clean install the drain's and the UI's clients both
failed and the first write job was lost as unknown.
- sdk_write_processor: a client that fails to construct raises
hy_memory_client_unavailable before HY is invoked, which parks the job
(waiting_for_model) instead of leaving it unknown.
- sdk_write_coordinator: a retired profile's handle closes only after its
last in-flight use (defensive; a profile revision change used to close
the client under a running write).
- durable_queue: durable_write_job_failed/parked log the exception chain as
file:line:function frames, never the message.
- sdk_migration_runner: the cutover settles pre-cutover unknown (and
invoked running) write jobs as cancelled/abandoned_at_cutover and counts
them as abandoned.unknown_write_jobs; never retried.
- durable_queue/api: retry-unknown and the reconcile helpers no longer
assume the dropped legacy task tables; the endpoint uses the startup
queue so it does not recreate memory_write_job_evidence.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MemHub no longer writes the HY pipeline JSONL, so the Desktop diagnostics
reader stops scanning hy-memory/governed-v2/logs and the renderer drops the
"hy-memory" service and "pipeline" source. A test covers leftover pipeline
files and hy_memory.log staying out of the panel.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- MEMORY_PIPELINE_LOG_ENABLED and MEMORY_PIPELINE_TRACE_ENABLED are false
at every diagnostic level, both at startup and on a live level change.
MEMORY_LOG_LLM_CONTENT keeps its debug-only setting: it still decides
whether HY's DEBUG lines in hy_memory.log carry full prompts/responses.
- After each HyMemoryClient construction (success or failure), Core drops
the SDK's stdout/stderr StreamHandlers, which under launchd duplicated
every SDK line into service.log/service-error.log, and forwards SDK
WARNING+ to Core's core.log handler so the Debug panel still shows SDK
problems.
- A live level change also sets the level of HY's hy_memory.log handler,
which the SDK otherwise fixes at the level of the first client.
- log_file.py no longer claims core.log covers the HY SDK at every level.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2.0.8 adds MEMORY_PIPELINE_LOG_ENABLED and keeps conversation text out of
INFO logs. apps/core/uv.lock is not updated: 2.0.8 is not on PyPI yet, so
the lock is regenerated (uv lock --directory apps/core) once it is
published.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The final RECONCILE JSON parse failure warning embedded the raw response
whenever MEMORY_LOG_LLM_CONTENT was on. It now reports the length; the raw
output is already logged per attempt at DEBUG.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MEMORY_PIPELINE_LOG_ENABLED (default true) turns off the pipeline JSONL
log. With the DB trace also off, store_pipeline_log becomes a no-op that
does no timing, redaction, background work or database write. The init line
states the real on/off state.
Lines that carried conversation or memory text at INFO/WARNING now log ids,
counts and lengths; the text goes to DEBUG: [add] input, search queries and
rewrites, coding tasks and fact statements/subjects, reconcile op detail,
extracted info, embedding inputs and unparsable model output.
Bump hy-memory to 2.0.8 (not yet published).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comparing the lock directory's mtime with Date.now() - os.uptime() breaks
when the wall clock jumps forward after a lease is taken (no RTC, VM
resume, NTP or manual correction): a live lease looks older than the boot
and is cleared, so a second Runtime can open the same data dir.
owner.json now records the kernel boot identity (kern.bootsessionuuid on
macOS, /proc/sys/kernel/random/boot_id on Linux, null elsewhere), read
once per process. removeStaleLease clears a lease whose boot_id differs
from the current one, which still fixes the recycled-PID crash loop of
#184 without consulting the clock; when either id is unknown the PID
probe decides as before. bootTime, BOOT_SLACK_MS and defaultBootTime are
gone; the boot id source is injectable via RuntimeServiceOptions.bootId.
The lease is published in one step: owner.json is written into a
runtime-service.lock.tmp-<uuid> staging dir that is then renamed onto the
lock, so a power cut can no longer leave a lock without an owner. Because
rename() replaces an existing empty directory, an existing lock of any
kind is reported as taken first. releaseLease also removes the lock with
a single rename, so this version never shows an owner-less lock; staging
and cleared-lease debris is swept once the lease is held.
An owner-less lock can therefore only come from an older version. It is
cleared when its mtime predates the kernel-reported boot time
(kern.boottime, /proc/stat btime) and otherwise keeps failing as
runtime_service_lock_invalid, as before.
Refs #184
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After an upgrade, hosts kept the call-rule skill and MCP entry an older
release wrote, so models were still told to call MemHub_ask and the other
removed tools, and WorkBuddy kept listing them from its tool-list cache.
- Stamp every managed MCP entry with ONE_MEMORY_MCP_TOOLSET, a tool
contract version kept beside the tool list (mcp-toolset.ts). WorkBuddy
fingerprints the stdio transport, env included, so a new stamp makes it
restart the server and drop the cached list. An entry without the
current stamp reads as stale in skill drift; verify still passes it.
- HostPermissions.refreshInstalledHosts runs the existing repoint and
stale-sync planners for every host this data dir installed, leaves
user-edited skills alone, emits allowlisted host_refresh.* diagnostics
(host ids and counts only), and records the build it completed in
host-permissions/host-refresh.json so it runs once per build.
- The Runtime calls it as it starts (covering App and `memhub upgrade`),
and Desktop calls it after an embedded Runtime is up.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The repair wrote memory_at only when the coding store already had the
column, which depended on the cutover readback having opened the store
through a 2.0.7 client first. On a migrated 1.0.4 home it now brings each
coding.sqlite3 / coding_cv.sqlite3 to the installed SDK's schema before
repairing, by running CodingMemoryStore's own SQLite schema step (its
initialize() would also open Chroma, which must stay closed offline). The
SDK's DDL is not repeated here.
The test now starts from a 2.0.6-shaped store without the column and
expects the bundle's conversation time in it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2.0.7 stores and returns the conversation time of working/coding
experiences. apps/core/uv.lock is not updated: 2.0.7 is not on PyPI yet,
so the lock is regenerated (uv lock --directory apps/core) once it is
published.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After the SDK upgrade 124 working/coding experiences read 时间未知: the SDK
never stored their conversation time. hy-memory 2.0.7 stores memory_at for
experiences and facts; Core now maps it from list, get and search, so the
memory library, detail, map list and home summary carry it.
An experience stored before 2.0.7 has none. The list labels its write time
写入于 and the detail 写入时间, instead of 时间未知 or calling it an import.
Metadata repair also writes the conversation time recovered from the
migration bundle into coding_memory_meta / working_fact when the store has
the memory_at column. The repair version is unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Coding and working experiences and facts keep their conversation time
(memory_at) and return it from list, get and search. Not yet published to
PyPI.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
client.add passed memory_at only to the chat path; the productivity writer
dropped it and coding_memory_meta / working_fact had no column for it, so
every experience had only its write time and MemHub showed it as 时间未知.
Both tables gain a nullable memory_at column (epoch seconds), added to
existing coding/working databases on open. The writer stamps it on drafts
and new facts, defaulting to the request time as chat does; an UPDATE keeps
the later of the stored and incoming times. list_memories (coding, working,
all), get and search return memory_at in chat's format.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
File import made one job per file-read increment per conversation: a
transcript read mid-turn gave tool-only jobs, which the SDK refused with
400 "content or messages is required" and Core recorded unknown (16
times); a first import gave one giant job per conversation. Hook capture
and catch-up wrote each completed turn alone, splitting long turns into
pieces that repeated the question.
turn_batches (conversation_processing) is now the one rule:
- a turn is a user message and everything up to the next one; records
before the first user message join the first turn, and are dropped if
no user message ever comes;
- a turn is complete when the next user message arrives, or on flush;
- every 5 complete turns are one batch; before 5, the turn that would
pass 8000 characters starts the next batch; one turn over 8000 is a
batch of its own and is never split;
- characters are what HY reads (tool records capped at
_HY_TOOL_CHARACTERS);
- a short batch is written only on flush; no batch lacks a user message.
File import keeps its assembling row per (file, conversation) and now
writes only what the rule completes, carrying the rest to the next
increment. History flushes at end of file. Live polling and hook capture
(which now stage into the same rows) flush after 10 minutes without a
new message, swept on the drain's maintenance timer. A record a job
already holds is not staged again, so a re-read creates no second job;
batch keys stay content hashes. Legacy catch-up groups by the same rule.
Backstop: SdkWriteProcessor completes a batch with no user/assistant
message as nothing to remember, without calling the SDK.
The unused _merge_historical_chunks and the old window code
(_conversation_chunks, segmenting, ONE_MEMORY_SPLIT_TURNS) are removed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After an upgrade 76/76 memories showed source=default: the startup
metadata repair failed with a KeyError. It ran after readiness and
opened its own Chroma client on the SDK's vector store. Chroma keeps one
process-wide System per path with no lock, and the repair's close() (or
a failed client init) released it while the first library request was
opening the SDK client, whose next lookup raised KeyError.
- sdk-migrate now runs the repair offline right after the cutover
commits and writes the receipt, so startup normally finds it and does
nothing. A repair failure there is logged and left to startup.
- Startup keeps the repair as a fallback but holds the home's SDK stores
offline from before readiness until the repair thread ends; every SDK
client is built through acquire_client, which waits meanwhile.
Readiness is not delayed; the first request waits for the repair.
Homes migrated with a failed repair have no receipt, so they are
repaired on the next launch.
- A failed recovery step logs its exception chain as file:line:function
frames (core.log) and safe_stack (diagnostic event), never messages.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On SDK authority the add path submits straight to the intake and writes
no one-memory-manual grant row. The intake admitted the record, but the
drain's claim gate required the grant receipt, so the job was never
eligible: attempts=0 forever while later jobs ran.
_source_writes_on now treats a missing manual grant as allowed and only
an explicit revocation as refused. Intake, claim and execute all ask it,
so jobs already stuck become claimable on the next drain tick. The SDK
add path refuses a revoked manual source with memory_add_source_revoked,
like the legacy path.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Profiles default to disable_thinking, but build_sdk_config never told
the SDK. DeepSeek Flash spent the SDK's 2000 agent_max_tokens thinking
and answered finish_reason=length with no content (EMPTY_RESPONSE).
The provider mapping moves into disable_thinking_params, shared by
attach_disable_thinking and the SDK's llm.extra_body. The SDK sends
extra_body on every chat call, and the coding/working writers build
their LLMProvider from the same llm config.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The home card counted chat memories only (list_memories without a
scene), so it showed 55 while the memory page showed 94. It now reads
memory_stats total (chat + working + coding experiences, facts not
counted) and takes the newest memory from list_memories(scene="all").
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>
A failed SDK migration left a half-migrated home (schema migrations,
moved legacy work, renamed Chroma collections) that the next run
resumed on top of, the maintenance fence re-enabled the LaunchAgent so
launchd retried at once, a deferred service idled forever, and capture
hooks kept writing into the legacy database.
Restore: before the first mutation the run records a restore point
(the pre-cutover bundle, or a fresh bundle when resuming an old ledger
row). Any failure before SDK authority is committed restores the
management database (with WAL), config.json and hy-memory/governed-v2
through a verified staging copy and a journaled rename swap; the failed
state is kept aside in backups/sdk-migration-failed/. A failure after
the commit, or an unreadable authority, never restores. An interrupted
swap is finished on the next start; meanwhile status reports required
and Core refuses to serve. The migration's own code stays the result;
the restore outcome is logged and reported separately.
Freeze: a deferred migration turns the service enable marker off, makes
the runtime service exit instead of idling, and leaves a frozen capture
fence that hooks honour. Desktop launch, memhub install/upgrade and now
start/restart re-run the migration and unfreeze on success, restoring
the previous auto-start.
The failure dialog says when data was restored, and its GitHub issue now
prefills the body: from/to version, code, failing step, whether data was
restored, macOS version and the log path shown with ~. migrate.invoked
records the pre-upgrade version. sdk_data_plane_* codes now reach the
dialog too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pin hy-memory 2.0.6 and use its scene-aware APIs in the memory panel.
- Empty query: list_memories(scene="all") mixes the three scenes newest
first, 20 per page, with per-scene totals and the source filter.
- Search: one SDK search(scene="all", rerank=False, limit=200) per
query; Core keeps the keyword matches in a server-side result window
(window_id, 300s idle TTL, 32 windows, ids only) and every page is cut
from it, so pages never repeat or skip. An expired window re-searches
and returns page 1 with window_reset. Pages are read fresh, so edits
and deletes show.
- Items carry scene, kind, variant, task/solution and times; the panel
shows 对话 / 工作 / 编码 badges. Detail, edit and delete pass
kind/variant (experience edits the solution, optionally the task);
an ambiguous id is 409, a task on a non-experience 422.
- memhub memories list/show print scene/kind/variant.
- SDK errors are re-exported through hy_sdk_runtime to keep the SDK
import boundary.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>