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>
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>