Commit Graph

179 Commits

Author SHA1 Message Date
teknium1
62a56f441c test(byterover): write the fixture .env with an explicit utf-8 encoding 2026-09-13 14:41:26 -07:00
teknium1
444dcca650 test(hindsight): trim salvaged #108866 coverage to two invariants (secondary keeps own shaping; single-profile reads process env) 2026-09-13 14:41:26 -07:00
teknium1
732504c8d6 fix(memory/byterover): brv child carries the served profile's cloud key, never the launch profile's
Under gateway.multiplex_profiles os.environ holds the default profile's .env, so `_run_brv`
building the child env from raw os.environ curated a secondary profile's turns into the DEFAULT
profile's ByteRover cloud account (and prefetched the default's memories into the secondary's
context). The local half was already profile-scoped (`_get_brv_cwd`).

The child env now comes from `build_subprocess_env` and, under multiplex, strips the launch
profile's residue and sets BRV_API_KEY only from the served profile's secret scope — a miss means
no cloud key. Single-profile installs pass the process env through unchanged.

Closes #108993 (report and fix direction by @jonpol01).
2026-09-13 14:41:26 -07:00
John Paul Soliva
2bade5daa7 fix(memory/hindsight): pin the isolation-vs-shaping split for scoped reads
`langfuse._secret` and `azure_identity_adapter._scoped_env` were changed to
raise rather than fall back, because swallowing `UnscopedSecretError` hides the
spawn-site bug the exception exists to surface. `_scoped_setting` looked like it
contradicted that, so make the split explicit and pin it.

Hindsight already follows the contract for everything that decides WHERE data
goes: `mode`, `apiKey` and the `bankId` partition read through bare
`get_secret`, so a scopeless multiplexed read raises. In `_load_config` that
raise happens on `HINDSIGHT_MODE` before any shaping value is reached, so the
swallow below cannot mask an isolation failure.

Presentation shaping is deliberately not in that class. `MemoryManager._each_provider`
logs an `initialize` failure at WARNING and drops the provider for the session,
so raising there would cost the whole memory provider because a speaker prefix
could not be resolved. It degrades to the provider's own default instead —
never to `os.environ`, which under multiplex is the default profile's.

The test names the offending key rather than asserting that something raised:
routing `mode` through the shaping helper shifts the failure to
`HINDSIGHT_API_KEY`, which a bare `pytest.raises` would still accept.
2026-09-13 14:41:26 -07:00
John Paul Soliva
a48dde5316 fix(memory/hindsight): resolve retain shaping through the profile scope, never os.environ
_load_config() reads the Hindsight bank, mode and retain tags through the profile
secret scope, but _apply_retain_settings() then discarded that answer and re-read
os.environ whenever the config value was falsy:

    return cfg.get(key) or os.environ.get(env_var, default)

Under gateway.multiplex_profiles os.environ holds the DEFAULT profile's .env, so a
secondary profile's scoped miss came back as the default profile's retain tags,
observation scopes, source and speaker prefixes — the fallback-after-miss shape
gateway/AGENTS.md forbids. Tags are Hindsight's retrieval partition and
metadata.source is opt-in by design, so the secondary's memories were both
mislabelled and selectable by the default profile's tag filters.

Both halves now go through _scoped_setting(), which resolves the value with
get_secret() and falls back to the provider's OWN default — a miss is a miss, the
same rule embedded.py already applies to the daemon's key and base URL. The three
raw reads left inside _load_config() (retain_source, retain_user_prefix,
retain_assistant_prefix), directly under the comment declaring them per-profile,
go through it too.

Single-profile deployments are unchanged: with no scope installed get_secret()
still reads the process env, where the value IS this profile's own.

Fixes #108865
2026-09-13 14:41:26 -07:00
kshitijk4poor
d14734df1f fix(honcho): thread registry rides agent.memory_provider.spawn_context_thread
main folded the plugin's contextvars-inheriting thread wrapper into
agent.memory_provider.spawn_context_thread. The registry this stack adds for
join_plugin_threads keeps a thin client.spawn_context_thread that calls the core
spawner and records the thread under its owner; the four spawn sites import it
from client. main's inherit-profile test builds a real provider now that
_spawn_write is an instance method (the owner is what gets joined).
2026-09-13 19:05:39 +05:30
teknium1
019103bfbc docs(mem0): comment and #99121 test state the one fail-loud scope contract
The retained comment still claimed a scope-less multiplex caller could load an
OSS config; identity reads above it raise UnscopedSecretError first. Both the
comment and the regression test now say the same thing: callers are scoped, an
OSS profile whose scope lacks MEM0_API_KEY initializes, and a scope-less caller
is a spawn-site bug that raises.
2026-09-13 05:19:48 -07:00
teknium1
43ec2036ec fix(hindsight): corrupt config.json falls through to the legacy file and env, as before
read_json_or_empty returns {} for malformed JSON; returning that directly from
_load_config made a corrupt profile config.json yield an empty (silently
unconfigured) mapping where the pre-dedup loop kept walking to the legacy
file and then the env branch. Only a non-empty parsed object is now returned.
2026-09-13 05:19:48 -07:00
teknium1
ca2ca5b9e1 fix(secret-scope): mem0, langfuse and azure credential readers stop swallowing UnscopedSecretError
Three shims caught UnscopedSecretError and degraded to "" (mem0._scoped_env)
or to os.environ (langfuse._secret, azure_identity_adapter._scoped_env). Under
multiplex os.environ holds the DEFAULT profile's .env, so the langfuse/azure
fallback could ship another profile's keys, and the mem0 fallback silently
routed a mis-spawned turn's memories into the default profile's account. The
exception exists to surface exactly that spawn-site bug (agent/AGENTS.md:
never add environ fallthrough, never swallow it). All three now call
agent.secret_scope.get_secret directly: with a scope installed a miss returns
the default; single-profile deployments (multiplex off) still read the process
env inside get_secret; a scope-less multiplex caller raises.

Behavior change: a mis-spawned child under gateway.multiplex_profiles now
fails loud with UnscopedSecretError instead of running silently unauthenticated
/ on the default profile's identity. The #99121 contract (OSS mode needs no
MEM0_API_KEY in scope) is unchanged and its test now installs an empty profile
scope, which is the situation the issue described; a genuinely scope-less
caller is asserted to raise in a new test.

Tests: tests/plugins/test_scoped_secret_readers_fail_closed.py (scope wins over
environ; scope-less multiplex raises) for langfuse + azure, sabotage red when
the langfuse fallthrough is restored; tests/plugins/memory/test_mem0_v3.py::
test_load_config_fails_closed_without_scope_even_for_identity_settings,
sabotage red with a swallowing wrapper reinstated.
2026-09-13 05:19:48 -07:00
teknium1
9ba5850e95 refactor(memory-plugins): background threads inherit the profile context; JSON sidecar reads and the holographic config.yaml write use core primitives
Six of eight memory providers spawned plain threading.Thread for prefetch/sync/
writer work. A plain thread starts with an EMPTY contextvars.Context, so under
multiplex profiles the worker resolved the DEFAULT profile's HERMES_HOME (and
fails closed on scoped secrets). honcho and hindsight had each noticed and
written their own copy_context() wrapper; core had a third in memory_manager.
One canonical pair now lives on the ABC module every provider already imports:
agent/memory_provider.py::ctx_bound / spawn_context_thread. memory_manager,
honcho, hindsight, mem0, retaindb, byterover, supermemory and openviking all use
it; the honcho and hindsight wrappers and memory_manager._ctx_bound are deleted.

Five "json.loads(path.read_text()) or {}" readers (mem0._read_mem0_json,
honcho client/oauth/cli _read_config, hindsight save_config/_load_config) fold
into utils.read_json_or_empty, the read half of every read-merge-atomic_json_write
sidecar store.

holographic.save_config was the only config.yaml writer in the tree that
bypassed hermes_cli.config.save_config: raw open("w") + yaml.dump with no config
lock, no managed-mode refusal, no atomic replace, and a swallowed exception. It
now calls save_config(..., merge_existing=True). Behavior change: a managed
install refuses the write (previously silently rewrote config.yaml); other
sections are deep-merged instead of round-tripped through a raw dump.

openviking._hermes_home_path guarded an impossible ImportError of
hermes_constants (the module already imports agent.*) with a ~/.hermes fallback
that is wrong on Windows and under profile overrides; it is replaced by
get_hermes_home() directly.

Tests: tests/plugins/memory/test_provider_threads_inherit_profile.py drives each
provider's real spawn path with a fake backend and asserts the thread sees the
spawner's HERMES_HOME override (sabotage: retaindb back on threading.Thread ->
red). tests/plugins/memory/test_holographic_save_config.py pins merge-with-
existing-sections and managed-mode refusal (sabotage: raw yaml.dump -> red).
2026-09-13 05:19:48 -07:00
Teknium
39ab25a6ac test(mem0): drop the inside-the-cap no-op case (salvage bar: 2 invariant tests) 2026-09-12 05:09:43 -07:00
John Paul Soliva
aca4dc86a3 fix(memory/mem0): trim a synced message at the last sentence boundary, not the first separator kind
_truncate_for_sync documents "the last sentence boundary within max_len", but it
looped over separator KINDS and returned on the first kind that qualified. An early
"。" therefore outranked a "." 240 characters later, and in pure ASCII "." outranked
a later "!" or "?" purely because it comes first in the tuple.

With the 450-char default, "a"*200 + "。" + "b"*240 + "." + "c"*100 kept 201 of the
442 characters available: 241 characters the embedder would have accepted were
discarded, so any fact in the second half of the turn never reached extraction. The
add() call succeeds, so unlike #106235 nothing is logged — the turn is simply
remembered from its first sentence. Raising sync_max_chars widens the gap rather
than closing it.

Take the max over every separator instead, from a named tuple so the set is not
buried in the loop. ".\n" is dropped: its index can never exceed the bare "." it
starts with, so under a max it is unreachable. The first-third guard and the hard-cut
fallback for unsegmented input are unchanged.

Fixes #108868
2026-09-12 05:09:43 -07:00
Teknium
5e04961072 test(hindsight): trim salvaged #103957 tests to the three invariants
Keep: scopeless worker resolves the on-disk key (durability core), a keyed
build still rewrites (rotation), and the API-prefixed vault name resolves.
Drop the mock-patched gate predicate test, the inlined copy of the same
predicate, and the empty-build/empty-disk arm — they re-assert the helper's
shape rather than a behaviour contract.
2026-09-11 15:26:46 -07:00
Teknium
a9838c2100 fix(multiplex): tool and memory-provider env reads stay inside the routed profile
Under gateway.multiplex_profiles, os.environ holds the DEFAULT profile's .env; a
secondary profile's values exist only in the per-turn secret scope. Every reader
below still read os.environ/os.getenv at call time, so a secondary profile's turn
silently used the default profile's value.

Credentials (F6): FIRECRAWL_API_KEY (read_file hosted OCR), OPENVIKING_API_KEY,
mem0-OSS OPENAI_API_KEY, MODAL_TOKEN_ID/SECRET and BROWSER_USE_API_KEY presence
gates, and the xAI video plugin's os.getenv("XAI_API_KEY") fallback AFTER the
scoped resolver had already missed — the exact fallback-after-miss shape
gateway/AGENTS.md forbids. Deleted, not re-scoped: the resolver is the scope.

Identity / tenant (F7): MEM0_USER_ID/AGENT_ID/HOST/MODE, SUPERMEMORY_CONTAINER_TAG,
RETAINDB_PROJECT, OPENVIKING_ACCOUNT/USER/AGENT (and the whole layered() env
read), HINDSIGHT_BANK_ID/MODE/retain shaping, HERMES_HONCHO_HOST. A raw read
put a secondary profile's memories into the default profile's account/bank/
project/tenant and recalled them back into the default's turns. Each now uses
get_secret with the provider's own per-profile default on a miss.

Endpoints (F8): OPENAI_BASE_URL (aux custom runtime + direct-alias expansion),
XAI_BASE_URL/HERMES_XAI_BASE_URL (aux OAuth), NOUS_INFERENCE_BASE_URL (#65941,
both the aux builder and hermes_cli.auth_nous._nous_inference_env_override),
GATEWAY_PROXY_URL (same UnscopedSecretError-only fallback shape as
GATEWAY_PROXY_KEY three lines below), FIRECRAWL_API_URL, BROWSERBASE_BASE_URL,
SUPERMEMORY/RETAINDB/HONCHO/HINDSIGHT URLs. The keys beside them were already
scoped, so a secondary's key was sent to the default profile's proxy or host.

Targets / display (F11): WEIXIN_HOME_CHANNEL (message posted into the default's
chat), HERMES_LANGUAGE, and agent/i18n's process-wide lru_cache of
display.language — now keyed by HERMES_HOME.

Outbound webhooks: hooks.outbound[].secret_env resolved from os.environ while
the gateway registers each profile's targets inside that profile's scope, so a
secondary's deliveries were signed with the default's secret or left unsigned.

Agent-cache eviction: _spawn_release_thread started a bare threading.Thread, so
commit_memory_session -> provider on_session_end ran with an EMPTY context. The
thread now runs copy_context() and, for the unscoped housekeeping sweep, enters
the owning profile's _profile_runtime_scope resolved from the session key
(agent:<profile>:...). The pressure batch does the same per key.

session_search (#82903): agent/inline_tool_executors.py::_session_search
forwarded every schema argument except `profile`, so a gateway agent could
never select a named profile's store. Forwarded; the ownership-scoping design
in #87779/#87847 is a separate design call and is not attempted here.

Live repro (/tmp/mux_audit/fix-tool-memory-reads/repro.py): 28 FAIL on
origin/main -> 0 FAIL with this change; 10 new invariant tests red on base.

Fixes #82903
Fixes #65941
Fixes #99121
Addresses #87779
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: Michael Versluis (Berry) <michael@wve.nl>
2026-09-11 15:26:46 -07:00
이민재
a1b689f12a fix(mem0): skip platform key lookup in OSS mode
Conflict resolution on top of main also routes MEM0_MODE / MEM0_HOST / MEM0_AGENT_ID /
MEM0_USER_ID through get_secret: identity and host are .env values like the key, and a
raw environ read hands a secondary profile the default profile's mem0 account.

(cherry picked from commit 7a5863ebafdb193d8ec2f89a7aca309774d6d69a)
2026-09-11 15:26:46 -07:00
red4711
778ad61017 fix(hindsight): accept HINDSIGHT_API_LLM_API_KEY vault name in key resolution
The vault item is named HINDSIGHT_API_LLM_API_KEY (matching the daemon env var), while the setup wizard uses HINDSIGHT_LLM_API_KEY. Vault-fed scopes carrying only the API-prefixed name resolved empty, so the scopeless path — and the whole fail-closed machinery — never engaged on real deployments. Accept both names.

Proven end-to-end: hydrated scope now resolves the 73-char key and the rewrite gate passes.

(cherry picked from commit 2ce8c2980188ae8e4a3ed398e14fa8ed89a40644)
2026-09-11 15:26:46 -07:00
red4711
d0d5c7745a test(hindsight): run pure gate tests on Windows too
test_gate_refuses_keyless_build_against_keyed_disk and test_rewrite_allowed_when_build_carries_key touch no filesystem modes, so the nt skip guard was needlessly excluding them. Addresses review feedback on PR #103957.

(cherry picked from commit 196cf723571c87829be61e17df27869948db00a0)
2026-09-11 15:26:46 -07:00
red4711
2c24ed4b6e fix(hindsight): keep scopeless daemon worker from clobbering profile env key
The embedded daemon-start worker usually runs with no secret scope, so _embedded_llm_api_key resolved empty and both the profile-env compare and the upstream manager merge overwrote the file's good key with emptiness.

- _embedded_llm_api_key: explicit config, then secret scope, then on-disk profile env fallback; UnscopedSecretError treated as empty without touching os.environ (multiplex safety).
- Add _may_rewrite_profile_env gate: refuse rewrite on keyless-build plus keyed-disk, and skip the daemon stop in that case.
- Document key resolution order in plugins/memory/hindsight/README.md.
- Add 5 tests pinning disk fallback, the gate's refuse branch, rotation, keyless providers, and file survival.

(cherry picked from commit 48a4bcd536fa799b666a8d5eaae73c1816ca9e33)
2026-09-11 15:26:46 -07:00
Erosika
f7d5ac3230 feat(honcho): write bot dms into their own a2a session
A DM relayed from another Hermes profile ran as a turn in the recipient's
Bot Chat session. sync_turn wrote the bot's words and the recipient's reply
into that session, and before per-author writes they landed under the
human's peer. The human's representation absorbed conversations the human
never had.

The turn context now marks such turns with scope a2a:<bot id>.
sync_turn routes a bot-authored turn into a separate Honcho session keyed
<session>:a2a:<sanitized bot id>, created with the sender bot as its user
peer, and never writes it into the human's session. The key is deterministic
so every turn from the same bot reaches the same session, and it stays
inside Honcho's 100 character session id limit. Recall still reads the
human's session only.

a2aSessions (host block, then root, default true) turns the routing on.
With it off, bot-authored turns are skipped. A bot turn that names no
author id is skipped as well, because nothing can key its session. Human
turns are unchanged.

get_or_create takes a user_peer_id override so the a2a session's roster is
the bot and the assistant, not the runtime human.
2026-09-10 10:45:57 -07:00
teknium1
322905e91b feat(memory): make the mem0 sync char cap configurable via mem0.json
A flat 450-char cap fits 512-token embedders (bge-small-zh-v1.5,
all-minilm) but stores only ~5% of the window on 8192-token models
(text-embedding-3-small, jina-embeddings-v3, bge-m3), degrading memory
quality for users those models served fine before truncation existed.

Read `sync_max_chars` from mem0.json once in initialize() (450 default)
and pass it to _truncate_for_sync(). Config over auto-detection: the
Ollama /api/show probe + known-model table proposed in #37427 adds a
network call and a curated list for a number the operator already knows
from their embedder choice; the setup wizard's mem0.json is the plugin's
behavioral-settings surface (no new HERMES_* env var). Documented in the
plugin README and the memory-providers docs page.

Dynamic-cap requirement and measurements (450 OK / 600 -> HTTP 500 on
bge-small-zh-v1.5:f16) by @szicely in #106235.

Refs #37421 #106235
Co-authored-by: szicely <140148567+szicely@users.noreply.github.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-09-09 10:55:02 -07:00
liuhao1024
040742d06b fix(memory): truncate oversized mem0 sync messages at the source
sync_turn() sent the whole turn to backend.add() untruncated. OSS
embedding models with small context windows (Ollama bge-small-zh-v1.5:
512 tokens) reject the request with HTTP 500, and hosted APIs answer
INPUT_TOKEN_LIMIT_EXCEEDED — in both cases _try() only logs, silently
dropping the turn's memory extraction after long conversations.

Cap each synced message at its last sentence boundary within 450 chars
before ingestion: short turns pass through unchanged, long turns keep a
coherent statement for fact extraction. This replaces the previous
retry-on-error approach, which could not match Ollama's HTTP 500 shape.

Salvaged from #37427 with the test suite trimmed to the one invariant
(oversized turn still reaches a small-context backend, short text
untouched, no breaker failure).

Refs #37421 #106235
Co-authored-by: szicely <140148567+szicely@users.noreply.github.com>
2026-09-09 10:55:02 -07:00
Teknium
c8cbc07030 test(mem0): setup module keeps post_setup when discovery imports the package first
The plugin loader execs sibling modules before the package __init__, so a
module-level `from . import _read_mem0_json` in _setup.py failed against the
empty parent shell and the whole module silently dropped out — every later
`hermes memory setup mem0` died with "cannot import name 'post_setup'".
The test loads mem0 through the real discovery path from a cold sys.modules
and asserts the cached _setup module exposes post_setup (red on base).

Also maps the contributor email for #103078 credit.

Campaign tracker: https://github.com/NousResearch/hermes-agent/issues/104154
2026-09-06 05:33:25 -07:00
Teknium
ecf760db96 simplify(compat): plugins/memory — drop 15 re-exports + 1 alias, repoint 13 test callers
hindsight: drop _PORT_HEALTH_GRACE_ENV/_sanitize_bank_segment facade re-exports (tests -> .embedded/.settings).
honcho: drop 5 tool-schema re-exports, _credential_fingerprint (-> client_cache), _is_auth_error (-> session_auth),
and the _redact_tokens alias (callers renamed to redact_tokens).
openviking: drop 5 _setup re-exports; tests call openviking_module._setup.* directly.
2026-09-03 13:04:16 -07:00
Teknium
3cd4d6d69a refactor(plugins/memory): hindsight — split settings/embedded/setup modules, unify config parsing, remove dead helpers 2026-09-02 13:30:09 -07:00
Teknium
365485a9f6 refactor(plugins/memory): openviking — remove dead search/rebuild/migrate paths, extract setup/CLI with dedupe, action dispatch tables 2026-09-02 13:30:09 -07:00
Teknium
cd7811a7a7 fix(memory/hindsight): propagate profile scope into background threads under multiplex
Under multiplex_profiles the Hindsight provider's writer, daemon-start and
prefetch threads were spawned as bare threading.Thread, so they started with
an empty contextvars Context: no profile secret scope and no HERMES_HOME
override. get_secret() fails closed there, so the local_embedded daemon never
booted and every retain raised UnscopedSecretError, even though the spawning
thread (initialize()/sync_turn() inside the gateway's copy_context'd turn) had
the scope all along.

Spawn each thread with contextvars.copy_context().run so the child inherits
the spawner's scope + home override. No environ fallback, no re-parsed .env.
The shared hindsight-loop thread needs no wrap: coroutines submitted via
run_coroutine_threadsafe already run in the submitter's context per call.

Fixes #92608
Fixes #94933

Co-authored-by: KIAgent01 <297567825+KIAgent01@users.noreply.github.com>
Co-authored-by: Parker Fawcett <259203091+Parker-Fawcett@users.noreply.github.com>
2026-09-02 07:00:13 -07:00
ehz0ah
7fef5c7898 fix(openviking): clarify remember submission status 2026-09-01 14:11:55 +05:30
ehz0ah
6446e19cb5 fix(openviking): route remember through session extraction 2026-09-01 14:11:55 +05:30
ehz0ah
64b96bb5d2 fix(openviking): synchronize setup connection state 2026-08-31 17:20:45 +05:30
ehz0ah
823bcc887a feat(openviking): use user memory by default
Remove the implicit hermes peer and the peer question from new connection setup. Preserve explicit peer settings and keep memory paths consistent with the captured client identity.

Add setup, configuration, request, recall, and session regression tests, plus upgrade guidance.
2026-08-31 17:20:45 +05:30
GodsBoy
e38cca50d6 fix(memory): keep Mem0 OSS OpenAI requests direct 2026-08-29 18:02:19 -07:00
kshitijk4poor
fab534b503 fix: omit User-Agent from anonymous OpenViking identity probes
Anonymous probes (_anonymous_json) are designed to probe server identity
before disclosing credentials. Sending the Hermes version on these probes
would fingerprint the exact version to an untrusted/MITM endpoint.

Keep User-Agent on authenticated requests (_headers) and multipart uploads
(_multipart_headers), which already send credentials.
2026-08-26 12:43:17 +05:30
ehz0ah
3db5267008 feat(openviking): identify Hermes requests 2026-08-26 12:43:17 +05:30
ehz0ah
4387e03960 fix(memory): keep OpenViking identity operations consistent 2026-08-25 12:27:21 +05:30
ehz0ah
5ff03cb0c4 fix(memory): scope OpenViking user cache to connection 2026-08-25 12:27:21 +05:30
liuhao1024
7cd43cdf52 fix(memory): emit explicit-uid OpenViking URIs resolved from system status
Review follow-up to the viking://~ migration: the ~ home alias only
expands for USER/ADMIN roles. The DEFAULT dev auth mode (no
server.auth_mode, no root_api_key) resolves every request as ROOT,
which bypasses current-user expansion — the canonical parser rejects
viking://~ with 400 'Home alias URI is not canonical' (verified on a
live 0.4.16 server). A deployment upgrading to 0.4.16 with an
untouched ov.conf is in dev mode, so the ~ spelling would break
exactly the way the old uid-less one will.

Mirror the upstream first-party plugin pattern instead: resolve the
user space client-side from /api/v1/system/status (result.user,
'default' fallback) and emit explicit-uid
viking://user/<user>/memories/... URIs, which are canonical under
every auth mode (dev/ROOT, trusted/USER, api-key) and every server
version. viking://~/... input typed by the user keeps passing through
untouched. (#91995)
2026-08-25 12:27:21 +05:30
liuhao1024
fc4c2f456b fix(memory): migrate OpenViking URIs to the viking://~ home alias
Upstream OpenViking removed the uid-less viking://user/<segment>
shorthand (#4196, merged 2026-08-21): reserved segments like memories
and peers no longer expand to the caller's space and the server
rejects them with HTTP 400 (NamespaceShapeError). First-party clients
were migrated to viking://~ in the same change; the Hermes plugin was
not (#91995).

Migrate every URI the plugin constructs — the profile/preferences/
entities session-start reads, the _build_memory_uri memory-mirroring
write path, and the tool-schema example — to viking://~/... README
uid-less references updated to match; canonical user-scoped forms
(viking://user/default/...) are unchanged. The ~ alias requires
OpenViking server >= 0.4.16 (#4167).
2026-08-25 12:27:21 +05:30
Teknium
32fb12a235 fix(hindsight): let hindsight_retain convey event time via occurred_at
Adds an optional occurred_at (ISO-8601 date/datetime) parameter to the
hindsight_retain tool schema, threaded into the retain item's timestamp
field. When absent, the item timestamp defaults to the configured event
clock (base from PR #82928 by @ragingbulld, authorship preserved) so the
Hindsight server can resolve relative time phrases; previously no item
timestamp was ever sent and temporal memories landed with null
occurred_start/occurred_end.

Fixes #93568. Salvages #82928.
2026-08-24 03:23:52 -07:00
Fabiao De Gongniu
497d6d5a66 fix(hindsight): harden event timestamps 2026-08-24 03:23:52 -07:00
Fabiao De Gongniu
97850afa33 fix(hindsight): send configured event timestamps
Use Hermes timezone-aware timestamps for retained events and turn messages. Pass the public timestamp field supported by hindsight-client 0.6.1 and cover the final serialized request field.
2026-08-24 03:23:52 -07:00
Teknium
aa8ceed4b6 fix(memory): keep a stale holder's late close() from evicting a fresh registry entry
Follow-up to the #88347 salvage: after release_all_under() force-closes a
profile's shared connection, a store re-created on the same path registers
a fresh entry under the same key. A stale holder that later calls close()
would pop that fresh entry (its refs were transferred nowhere), letting a
third store open a second connection to the same database — exactly the
multi-writer contention the shared registry exists to prevent. close()
now evicts the registry entry only when it is still its own.
2026-08-17 16:33:52 -07:00
liuhao1024
4f354c27b7 fix(profiles): release memory-store handles before rmtree on profile delete
The desktop's main serve process opens memory_store.db for every known
profile and nothing closed those connections before delete_profile's
rmtree — on Windows the open SQLite handles make the removal fail with
WinError 32 for both the CLI and the DELETE /api/profiles/<name> route
(#88347). POSIX unlinking of open files hid the same leak.

MemoryStore.close() is refcount-driven, so a live holder keeps the
handle forever; add MemoryStore.release_all_under(directory) to
force-close every shared connection under a directory, and call it in
delete_profile after stopping the profile backends. Inside serve the
handles live in that very process and get released; from the CLI it is
a no-op.

Fixes #88347
2026-08-17 16:33:52 -07:00
ehz0ah
2854fab46c docs(openviking): correct environment handling explanations
Clarify that the Desktop backend can add Hermes venv packages to PYTHONPATH and that current .env loaders use the last duplicate value.
2026-08-17 01:44:09 +08:00
ehz0ah
38175b8c22 fix(openviking): preserve non-UTF-8 env bytes on update 2026-08-17 01:41:49 +08:00
Drexuxux
dde7075d6c fix(openviking): read .env BOM-tolerantly when rewriting credentials
f1ea4a56c ("cover the remaining setup-time .env reads with utf-8-sig",
following 75afc47ba for mem0/hindsight) swept this class; openviking's
_write_env_vars was missed and still reads with strict utf-8.

It copies every existing line through on each update, so the read decides
whether a credential update lands:

  BOM'd .env  -> the first key never matches, so the old line survives and
                 the new value is appended as a duplicate. .env loaders keep
                 the first occurrence, so the update silently does nothing.
  cp1252 .env -> UnicodeDecodeError aborts setup outright.

Read exactly like the canonical hermes_cli/config.py save_env_value
(utf-8-sig + errors="replace"). A plain UTF-8 file rewrites byte-identically.

Scope: hermes_cli/memory_setup.py has the same read but is already the
subject of #30281 / #60587, so it is left alone here.

(cherry picked from commit 175c6852c2c255b3219575b5de0b1b70f1f0efcb)
2026-08-17 01:41:49 +08:00
kyssta-exe
4fdaadd907 fix(openviking): strip PYTHONPATH from autostarted server child env (#78153)
(cherry picked from commit 7afd99155667cde480c0ab4ee31e242dab849d40)
2026-08-17 01:41:49 +08:00
joaomarcos
39e480c051 fix(state): close leaked SessionDB connections on exception paths (#83226)
SessionDB could leave native SQLite handles open when construction failed
partway through schema/pragma/FTS/repair/lock/interrupt handling. Other
short-lived callers (MCP reads/polling, session search, reactions, trace
upload, insights, shutdown recovery) opened temporary SessionDB handles
without a complete ownership boundary. API-server profile caches and
RetainDB shutdown had similar late-close races. Under sustained load this
exhausted file descriptors (EMFILE).

- Close partially initialized SessionDB connections on every constructor
  exception path via a finally block guarded by an initialization-complete
  flag.
- Close temporary/cross-profile SessionDB handles in finally blocks across
  CLI, MCP, search, trace, reactions, insights, and recovery paths.
- Add API-server per-profile cache ownership and disconnect cleanup.
- Make RetainDB writer-queue shutdown exception-safe: track connections per
  thread, close on worker exit, reject new enqueues after shutdown starts,
  and sweep any connections left by short-lived threads.
- Add regression coverage for constructor failures, worker-thread readers,
  API disconnect failures, shutdown recovery, RetainDB late enqueue, and
  foreign-loop async clients.

Salvage notes: the original PR's per-thread WAL-reader ownership changes
were superseded by main's read-connection pool (permits + checkout/return);
its cron timeout-abandon fix is credited separately to #72822's earlier
identical fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 21:41:26 -07:00
Greg Gibeau
c600fd46bd fix(memory): complete discovery and registration parity for out-of-tree providers
Builds on the three salvaged commits: adds the sources and integration points
they leave out, so a pip-installed memory provider is not a second-class
citizen next to a directory install.

Discovery
- Project-local providers (./.hermes/plugins/<name>/), gated on
  HERMES_ENABLE_PROJECT_PLUGINS exactly as PluginManager gates its own project
  scan. Completes the four sources CONTRIBUTING.md and AGENTS.md already
  promised; memory was the only discovery system missing two of them.
- find_provider_dir() now resolves a package entry point to its directory.
  This is load-bearing: config_schema.py (the dashboard panel) and cli.py (the
  `hermes <provider>` subcommands) are read from disk rather than imported, so
  without a directory a pip-installed provider silently lost both.
- list_memory_provider_names() includes entry-point providers, so they appear
  in the dashboard's memory.provider dropdown.

Resolution stays import-free. hermes_cli.plugins.resolve_module_origin() is
extracted from _resolve_module_source() (added by the salvaged #76567) and
shared, so discovery walks a module's file layout instead of importing it.
find_provider_dir() is called from the dashboard and from argparse setup, long
before the operator has chosen a provider — importing every installed candidate
would execute third-party code on the strength of a package being present.
A test asserts the resolution leaves no side effects and no sys.modules entry.

Registration
- PluginContext gains register_memory_provider(). Memory was the only provider
  category without one; context engine, image gen, video gen, web search,
  browser, TTS, transcription, secret source, dashboard auth and platform all
  have one.
- _ProviderCollector delegates unknown register_* calls to a real
  PluginContext instead of carrying three hand-written no-ops. It silently
  dropped register_tool/register_hook, and had no register_auxiliary_task at
  all — despite PluginContext.register_auxiliary_task documenting a memory
  provider (hindsight's pre-retain dedup) as its worked example. It can no
  longer drift behind PluginContext.
- A raise after register_memory_provider() no longer costs the provider. The
  loader caught it into a debug log, discarded the registered instance, and
  fell through to "instantiate any MemoryProvider subclass" — returning a
  different, unconfigured provider. A silent downgrade that looked like
  success, and the exact outcome of calling register_auxiliary_task.

Activation is unchanged: still gated on memory.provider naming the plugin, and
covered by a test so the real PluginContext cannot start requiring
plugins.enabled — that would break every existing user-installed provider.

Verified end to end against a real third-party provider (kainappsinc/elephant)
installed by pip alone, with no directory copy: it appears in the dropdown,
resolves its directory, loads with its tools, and renders its dashboard panel.

Closes #40101.
2026-08-13 11:49:14 -07:00
spfcraze
32238f9942 fix(honcho): resolve peers host keys via profile_host_key (underscore form) (#76414)
_all_profile_host_configs() built per-profile host keys inline as
f"{HOST}.{profile}" ("hermes.work") while profile_host_key() — used by
honcho status/enable/sync and the runtime memory plugin — produces the
underscore form ("hermes_work"). The lookup always missed, so
'hermes honcho peers' showed "(not set)" / leaked the raw malformed key
into the AI-peer column for every non-default profile. Profile names
needing sanitization (dots/spaces) were doubly broken.

Verified live: with hosts["hermes_work"] populated, cmd_peers showed
'work ... hermes.work' before the fix and 'work ... hermes' after.

Tests: host keys match the writer form, sanitized profile names resolve,
peers output shows populated identities with no key leak, and clean
fallback for profiles without a block.
2026-08-13 23:43:15 +05:30
kshitij
8b243dff62 fix: security + efficiency review fixes for salvaged PR #74379
1. Use open_credentialed_url() instead of bare urlopen() in
   templates.py apply_template() and probe_existing_customization().
   Both send Authorization: Bearer headers; bare urlopen forwards
   credentials on cross-origin redirects. The codebase has
   open_credentialed_url() in hermes_cli/urllib_security.py that
   strips credentials on cross-origin redirects — used by 4 other
   modules.

2. Guard unavailable_reason() with the dedup set check before
   calling it. The gateway builds a fresh AIAgent per message, so
   without this guard unavailable_reason() (which calls _load_config()
   → stat + file read + JSON parse, and _check_local_runtime() →
   importlib probes) runs on every gateway turn for an unavailable
   provider, even though the warning is deduped after the first.

3. Move INDICATOR_GLYPH from Hindsight's eye emoji to a generic
   brain (🧠) in core (agent/memory_provider.py). Hindsight overrides
   with its own _HINDSIGHT_GLYPH (👁️) in recall_status() and
   _emit_saving_indicator(). Other memory providers no longer inherit
   Hindsight's brand mark as the default glyph.
2026-08-13 23:15:25 +05:30