Commit Graph

335 Commits

Author SHA1 Message Date
Teknium 74b28c8910 fix(secrets): scope memory plugin credential reads (hindsight/honcho/supermemory/mem0/retaindb)
Route HINDSIGHT_API_KEY/HINDSIGHT_LLM_API_KEY, HONCHO_API_KEY, SUPERMEMORY_API_KEY, MEM0_API_KEY and RETAINDB_API_KEY reads through agent.secret_scope.get_secret so multiplexed turns resolve the active profile's key instead of the process environment. Also guard supermemory post_setup's os.environ write on not is_multiplex_active() — writing one profile's key into the process-global env pollutes sibling profiles; the single-profile convenience path is unchanged.
2026-08-02 10:04:48 -07:00
kshitij 9bbd956b73 fix(memory/hindsight): evict timed-out retain ops + coarser status polls
Review follow-up on the #62871 salvage (simplify pass, HIGH):

1. Ops unresolved at the wait deadline were RETAINED in the pending set.
   A permanently failing status endpoint (auth error, endless 500s, or a
   server that loses ops without 404) would grow the set forever and make
   EVERY later prefetch burn the full 10s budget re-polling it — and
   prefetch()'s bounded 3s join sits on the reply path, so that money-quote
   'adds no response latency' claim breaks. Timed-out ops are now dropped
   (identical degradation to prefetch_waits_for_retain=False: possibly
   stale recall) with a WARNING so persistent server trouble is visible.
   Guard test mutation-checked (fails with eviction disabled).

2. Status polls now spaced 0.5s (was 0.05s shared with the local drain
   poll): a wedged op cost up to ~200 get_operation_status round trips
   per prefetch; now ~20 max over the default 10s budget.
2026-08-02 22:33:43 +05:30
And 1be353bf9c fix(memory/hindsight): gate prefetch on server-side retain completion, not just queue drain
Address PR #62871 review: with the default retain_async=True, aretain_batch
returns when the write is accepted, not when it's durable/recall-visible, so
draining the local writer queue (task_done) is not a read-after-write signal.
The next-turn prefetch could still recall before the just-completed turn was
observable on the server.

- Track the async operation_id/operation_ids returned by aretain_batch
- _wait_for_retains_drained now applies two ordered, budget-bounded barriers:
  (1) local writer queue drains, then (2) tracked server-side async ops report
  completion via operations.get_operation_status (an explicit read-after-write
  condition). NotFound (completed+evicted) counts as done; transient errors
  keep waiting until the deadline
- Completed ops are removed from the pending set so later prefetches don't
  re-poll them; the whole wait stays off the reply path
- Add TestPrefetchServerRetainVisibility: op-id tracking (single/multiple),
  no-op tracking when retain_async=False, prefetch waiting for server
  completion before recall, timeout fallback on a wedged op, and NotFound /
  transient-error status handling
2026-08-02 22:33:43 +05:30
And 94b10eccf5 feat(memory/hindsight): order background prefetch after pending retains
Async retain already keeps the memory WRITE off the reply path (writes drain
on the single writer thread while the user gets their response immediately).
This closes the remaining retain/prefetch race: the next turn's warm prefetch
runs on its own thread and could recall BEFORE the just-enqueued retain write
lands, silently dropping the latest turn from recall.

- The background prefetch now waits (bounded) for pending retains to drain
  before recalling, so warmed context includes the just-completed turn.
- The wait runs only on the background prefetch thread, never the reply path,
  so it adds zero latency to the user's response and loses no writes.
- Bounded by prefetch_retain_drain_timeout (default 10s) and polls
  unfinished_tasks so a wedged write can't hang the prefetch.
- New config keys: prefetch_waits_for_retain (default true),
  prefetch_retain_drain_timeout (default 10.0).
2026-08-02 22:33:43 +05:30
JabberELF 7a450ca5ce fix(memory): resolve dim=1 float32/float64 blob ambiguity
When hrr_dim=1 the prefixed float32 blob (4+4=8 bytes) collides in
size with a raw float64 blob (1×8=8 bytes), making the format
discriminator in bytes_to_phases ambiguous — a legacy blob starting
with HRR1 would be misread as a prefixed float32 vector.

- phases_to_bytes now accepts an optional dim and falls back to
  writing raw float64 when the two blob sizes are equal.
- bytes_to_phases prefers the legacy float64 interpretation when
  sizes collide and dim is provided, since phases_to_bytes never
  writes a prefixed float32 blob at dim=1.
- Three regression tests cover dim=1 write, round-trip, and the
  legacy-prefix collision case.

Addresses hermes-sweeper review on PR #30499.
2026-08-02 22:33:13 +05:30
JabberELF 958ffd1085 perf(memory): store holographic vectors as float32 2026-08-02 22:33:13 +05:30
spfcraze 89f74d58f6 perf(memory): hoist loop-invariant HRR encodes out of retrieval loops
FactRetriever.search() re-encoded the query vector once per candidate,
related() re-encoded both role atoms once per fact row, and probe()
re-encoded the role-content atom once per row. All three encoders are
deterministic (SHA-256 counter blocks), so the hoisted vectors are
bit-identical to the per-iteration values they replace.

Measured (300-fact store, dim=1024, median of 30 calls): search()
11.62 -> 1.46 ms/call (8.0x; encode_text 30 -> 1 per call), related()
63.08 -> 16.17 ms/call (3.9x; encode_atom 601 -> 3 per call), probe()
431.93 -> 389.36 ms/call (1.1x; dominated by per-fact content encoding,
which is inherent to the algorithm and unchanged).

Tests: call-count regression tests for each hoist plus a bit-exact
parity test of search() against the pre-fix per-candidate loop.
2026-08-02 21:16:34 +05:30
Teknium fae0c4f5f4 fix(hindsight): create embedded profile env file owner-only (0600)
The embedded Hindsight daemon's profile env file carries the plaintext
HINDSIGHT_API_LLM_API_KEY but was written via bare write_text(), leaving
it with umask-derived (typically world-readable) permissions.

- Create/truncate the file via os.open(..., 0o600); chmod a pre-existing
  file to 0600 BEFORE writing new secret bytes.
- Post-write validation on POSIX: verify 0600, retry chmod, and raise if
  the file still isn't owner-only.
- If validation fails, unlink the secret file so a plaintext key is never
  left behind with unverified permissions.
- Regression tests under tests/plugins/ for fresh-write mode, tightening a
  pre-existing 0644 file, and cleanup on validation failure.

Narrowed reimplementation of #74236 confined to plugins/memory/hindsight/;
the core utils.py atomic-replace opt-out from the PR was dropped.

Co-authored-by: carrion256 <carrion256@proton.me>
2026-07-31 22:39:34 -07:00
eapwrk bd1a850fa2 fix(honcho): network-hermetic unit tests + lazy async writer start
Re-port of PR #67576:
- plugins/memory/honcho/session.py: start the async writer thread lazily on
  first enqueue via _ensure_async_writer (idempotent, lock-guarded) instead
  of eagerly in __init__; shutdown tolerates a never-started thread
- tests/honcho_plugin/conftest.py: package-wide socket guard so no honcho
  unit test can reach a live server
- tests/honcho_plugin/test_network_isolation.py: regression tests
- test_async_memory.py / test_oauth_flow.py: rebased hunks onto the pruned
  suite (pruned tests not resurrected)

Salvaged-from: #67576
Co-authored-by: eapwrk <eapwrk@gmail.com>
2026-07-29 18:55:10 -07:00
teknium1 ed33ebca1d refactor: canonical config loaders for behavioral reads + guarded raw-read primitive (kills the managed-scope/env-expansion drift class)
The disease: ~15 scattered raw yaml.safe_load(config.yaml) reads that
silently miss managed-scope overlay, ${ENV_VAR} expansion, profile-aware
pathing, and root-model normalization. Every new config feature needed an
N-site sweep (incident chain 9cbcc0c9c8732293cf87b0e47a98f91928aa0443). This commit assigns every raw read to an owner and adds a
lint-guard test so the class cannot regrow.

New primitive (additive-only change to hermes_cli/config.py):
  read_user_config_raw(path=None) — reads the user file EXACTLY as
  written; docstring states it is ONLY legal for write-back round-trips
  and raw-file diagnostics. Behavioral reads must use
  load_config()/load_config_readonly().

BEHAVIOR FIXES (class-a sites migrated to a canonical loader — these
previously read values that could DIFFER from the effective config):

  gateway/run.py _try_resolve_fallback_provider → _load_gateway_runtime_config
    keys: fallback_providers/fallback_model (provider, model, base_url,
    api_key). Drift fixed: a managed-pinned fallback chain was ignored;
    an api_key of "${OPENROUTER_API_KEY}" reached the resolver unexpanded.
  gateway/run.py GatewayRunner._load_provider_routing → same loader
    key: provider_routing. Drift fixed: managed-pinned routing prefs and
    ${VAR} templates were ignored.
  gateway/run.py GatewayRunner._load_fallback_model → same loader
    keys: fallback chain. Same drift as above.
  gateway/run.py GatewayRunner._refresh_fallback_model
    keeps the raw primitive (its last-known-good-on-parse-failure contract
    forbids the fail-open loader, which returns {} on a torn write) but now
    applies managed overlay + env expansion inline. Drift fixed: chain
    edits under managed scope / env templates were previously frozen out.
  tui_gateway/server.py _load_cfg (72 behavioral call sites)
    now = raw read + managed overlay (pre-existing) + NEW ${VAR} expansion,
    split from a new _load_cfg_raw() write-back primitive. Drift fixed:
    e.g. custom_prompt: "hello ${VAR}", agent.system_prompt, model,
    api_key/base_url templates reached sessions unexpanded. DEFAULT_CONFIG
    is deliberately NOT merged (callers treat missing keys as unset;
    `_load_cfg() == {}` sentinels and _save_cfg round-trips depend on it).
  tui_gateway/server.py _profile_configured_cwd
    keys: terminal.cwd of a NON-launch profile. Drift fixed: managed
    overlay + ${VAR} expansion now apply (load_config() would resolve the
    wrong profile's home, so the raw primitive + inline pipeline is used).
  plugins/platforms/telegram/adapter.py _reload_dm_topics_from_config
    → load_config_readonly(). keys: platforms.telegram.extra.dm_topics.
    Drift fixed: managed overlay + profile-aware pathing + expansion.
  plugins/memory/holographic _load_plugin_config → load_config_readonly().
    keys: plugins.hermes-memory-store.*. Same drift class.

WRITE-BACK ROUND-TRIPS (class-b: stay raw BY DESIGN via read_user_config_raw;
merging defaults/overlay would pollute the saved user file):
  gateway/slash_commands.py: model persist x2, _save_gateway_config_key,
    memory/skills write_approval toggles
  gateway/platforms/yuanbao.py auto-sethome
  tui_gateway/server.py _write_config_key + all cfg→_save_cfg blocks
    (reasoning show/hide/full/clamp, details_mode[.section], prompt)
    → new _load_cfg_raw()
  plugins/memory/holographic save_config

RAW-FILE DIAGNOSTICS + presence-sensitive bridges (class-c: stay raw,
now via the shared primitive with an explanatory comment):
  hermes_cli/doctor.py x5 (model validation, stale-root-keys, .env drift,
    deprecation sweep, memory-provider probe — the latter two keep their
    inline managed overlay where they had one)
  gateway/run.py _bridge_max_turns_from_config and the module-level
    TERMINAL_*/HERMES_* env bridge (bridging merged defaults would export
    all of DEFAULT_CONFIG into the environment; both keep their inline
    overlay + expansion)
  hermes_cli/send_cmd.py env bridge (same presence-sensitivity)
  hermes_cli/gateway.py multiplex-conflict probe (reads the DEFAULT root's
    config, not the active profile's — load_config is the wrong owner)
  hermes_cli/profiles.py / hermes_cli/web_server.py / tools/wake_word.py
    multi-profile reads (load_config targets only the ACTIVE profile home)
  cron/jobs.py _resolve_default_model_snapshot and cron/scheduler.py
    run_job config read keep their existing inline overlay+expansion but
    now share the primitive (their fail-open + last-value semantics and
    the deliberate no-defaults merge are preserved exactly).

Failure-semantics audit: every migrated site preserves its exact previous
behavior on missing file ({} / early return) and parse failure (raise into
the caller's existing except, warn, last-known-good, or fail-open) —
read_user_config_raw intentionally mirrors bare open()+safe_load semantics
(raises on parse errors, {} only on FileNotFoundError/non-dict root).

Guard: tests/hermes_cli/test_config_read_guard.py scans the tree for
yaml.safe_load within 6 lines of a 'config.yaml' reference outside an
explicit ALLOWLIST (hermes_cli/config.py, gateway/config.py, gateway/run.py
fallback path, hermes_cli/managed_scope.py which reads the MANAGED file,
gateway/readiness.py parse-health probe) and fails on new offenders.

E2E: tests/hermes_cli/test_config_loader_e2e.py runs a subprocess with a
temp HERMES_HOME (config.yaml containing ${E2E_PROMPT_SUFFIX}) plus a
HERMES_MANAGED_DIR overlay pinning agent.reasoning_effort, asserting
tui _load_cfg resolves "hello world"/"high" while _load_cfg_raw +
_save_cfg round-trip the template and user value verbatim with no
managed/default leakage.
2026-07-29 10:53:29 -07:00
Teknium 0227872bf5 fix(hindsight): route setup + auto-upgrade installs through lazy_deps
Widen NS-605 to the two remaining direct-install sites in the hindsight
plugin, which still shelled out to 'uv pip install --python
sys.executable' and therefore failed (EROFS/EACCES) on immutable hosted
images with sealed venvs, and lost packages on redeploy:

- post_setup dependency install (~L835): install_specs() with ok /
  blocked-reason / stderr handling matching honcho/mem0.
- initialize()-time hindsight-client auto-upgrade (~L1240):
  install_specs(); blocked installs log the gate reason with the manual
  command instead of a raw subprocess error, and init proceeds.

Audited every other memory plugin (supermemory, byterover, holographic,
openviking, retaindb) for direct pip/uv install subprocess calls: none
remain — their deps flow through plugin.yaml pip_dependencies or
lazy_deps.ensure().

Tests: TestClientAutoUpgradeRoutesThroughLazyDeps — upgrade goes through
install_specs with the exact spec (regression guard asserts no
subprocess.run), blocked upgrade is non-fatal and surfaces the gate
reason. Updated TestPostSetupEnvEncoding stubs to the new install path.
2026-07-28 22:40:33 -07:00
Shannon Sands 8bbd77f368 fix: route memory-provider dep installs through lazy_deps durable target
Installing a memory provider (Honcho, mem0, hindsight, ...) from the
dashboard Plugins page failed on hosted deployments with a permission
error: the setup endpoint shelled out to
`uv pip install --python sys.executable`, which targets the sealed
read-only venv under /opt/hermes (immutable hosted image, NS-579/#49113).

The correct mechanism already exists: tools/lazy_deps.py redirects
installs to the writable durable target on the data volume
(HERMES_LAZY_INSTALL_TARGET=/opt/data/lazy-packages) when the venv is
sealed (HERMES_DISABLE_LAZY_INSTALLS=1), appends the target to the END
of sys.path (core venv always wins collisions), and constrains shared
deps to core-venv versions. The dashboard installer simply never used
it.

Fix:
- tools/lazy_deps.py: new public install_specs() — installs arbitrary
  manifest-declared pip specs through the same environment routing as
  ensure(): venv-scoped by default, durable-target on sealed images,
  refused with an actionable reason when gated off (config kill switch
  or sealed venv without a target — never surfaces raw EROFS/EACCES).
  Specs are validated with _spec_is_safe(); post-install it invalidates
  import/metadata caches so availability rechecks in the same process
  see the new packages without a restart. Never raises.
- hermes_cli/web_server.py: _install_memory_provider_pip_dependencies
  now calls install_specs() instead of building its own uv/pip
  subprocess. Blocked installs surface the gate reason in the setup
  results; the response's status block reflects post-install
  availability (stale 'missing deps' state clears immediately).
- hermes_cli/memory_setup.py, plugins/memory/honcho/cli.py,
  plugins/memory/mem0/_setup.py: CLI setup wizards routed through
  install_specs() too — same sealed-venv failure mode, same fix.

No hosted setup path writes to /opt/hermes anymore; provider discovery
and installation now use the same environment (sys.path activation is
shared with the lazy-install bootstrap in hermes_bootstrap).

Tests:
- tests/tools/test_lazy_deps.py: TestInstallSpecs — gating matrix
  (sealed+no-target blocked with immutable-deployment reason, config
  kill switch, sealed+target proceeds), spec-safety rejection before
  any subprocess, venv-scoped vs --target command display, failure
  stderr passthrough, never-raises contract.
- tests/hermes_cli/test_web_server.py: setup endpoint routes pip
  through lazy_deps (regression guard asserts no direct 'pip install'
  subprocess), blocked-reason surfacing, same-response availability
  recheck clears stale missing state.

Fixes NS-605 (Plain T-1111).
2026-07-28 22:40:33 -07:00
Teknium 63fc810b95 fix(gemini): sweep hardcoded Gemini default models to gemini-3.6-flash (#32360)
gemini-2.5-flash shuts down Oct 16 2026 (Google deprecation schedule)
and gemini-3-flash-preview is superseded. Update every hardcoded
default to the current GA flash model:

- gemini_native_adapter: probe_gemini_tier + _create_chat_completion
  default params; free-tier guidance de-pinned from a specific model's
  RPD number so it doesn't stale again
- auxiliary_client: gemini/kilocode fallback aux models,
  _OPENROUTER_MODEL, _NOUS_MODEL -> google/gemini-3.6-flash
  (verified live on both OpenRouter and Nous portal /models)
- provider plugins: kilocode + vertex default_aux_model
- hindsight memory plugin: gemini provider default
- setup wizard gemini list: 3-flash-preview -> 3.6-flash (matches the
  curated picker catalog)
- tests: aux-client assertions that pinned the old default literal now
  reference the _NOUS_MODEL constant, so the next default bump can't
  break them (change-detector cleanup)

Fixes #32360.
2026-07-28 18:17:56 -07:00
spiky02plateau ab2c9289ca fix(hindsight): availability probe must cover the embedding stack for local modes
_check_local_runtime() only imported 'hindsight' and
'hindsight_embed.daemon_embed_manager', a strictly weaker import surface
than the embedded daemon actually needs: the daemon imports
sentence_transformers at startup (embeddings + reranker). When the
embedding stack is broken (e.g. a dependency conflict on a shared package
like huggingface-hub), the daemon can never start, yet is_available() and
'hermes memory status' still report Hindsight as available — every
retain/recall then fails silently.

Import sentence_transformers in the same probe so local/local_embedded
availability goes red with the real ImportError as the reason, letting
the agent degrade loudly instead of silently dropping memory. Local path
only; cloud mode is untouched and no network or model download is
triggered by the import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MN8RMDLwxCfFxwtADoEJJf
2026-07-26 19:29:18 -07:00
teknium1 0a6fda01e3 fix: restore utf-8-sig BOM tolerance at .env readers the sweep normalized
The cherry-pick auto-resolution + AST sweep applied plain utf-8 at three
.env reader sites where the salvaged PRs (#62617, #62123) deliberately
use utf-8-sig — a Notepad BOM must not hide/duplicate the first key.
Restore the contract (tests pin it).
2026-07-24 17:10:39 -07:00
teknium1 75e0d52034 fix(windows): sweep remaining bare read_text/write_text sites + linter rule
AST-driven pass over every Path.read_text()/write_text() without an
explicit encoding= across non-test code: 71 sites in 34 files
(skills_hub, hermes_cli/main+profiles+service_manager+container_boot,
mem0/hindsight/honcho plugins, achievements dashboard, release/CI
scripts, productivity+comfyui skill helpers, agent/*). Verified zero
positional-encoding collisions before insertion; per-file compile()
check after.

Adds a check-windows-footguns rule flagging bare single-line
read_text/write_text (multi-line forms stay covered by the AST guard
test from #38985). Together with the salvaged contributor commits this
retires the ~169-site bare file-I/O class (#37423's long tail).
2026-07-24 17:10:39 -07:00
solyanviktor-star f1ea4a56c2 fix(memory): cover the remaining setup-time .env reads with utf-8-sig
Follow-up to review feedback:

- mem0 _prompt_api_key read .env with the locale default, so a Notepad
  BOM hid the first key from the masked current-value lookup; read it
  with utf-8-sig + errors=replace like the canonical readers in
  hermes_cli/config.py.
- hindsight _load_simple_env used plain utf-8; it also parses the Hermes
  .env during post_setup, where a BOM stuck to the first key. Switch to
  utf-8-sig + errors=replace.
- Add hindsight regressions: BOM key matching in _load_simple_env and in
  the cloud post_setup writer, plus non-ASCII round-trip preservation,
  and a mem0 regression for the BOM'd masked-key lookup. The BOM tests
  fail without the fix on any platform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:10:39 -07:00
solyanviktor-star 75afc47baa fix(memory): read/write .env as UTF-8 in mem0 and hindsight setup
The mem0 and hindsight memory-provider setup routines round-trip the
user's ~/.hermes/.env: they read existing lines, update the keys they
manage, and rewrite the whole file preserving every other line verbatim.
Both used env_path.read_text() / write_text() with no encoding.

read_text()/write_text() with no encoding fall back to the system locale
(cp1252/GBK on Windows), so on a non-UTF-8 host the preserved lines get
mangled or the call crashes on any non-ASCII value, and — because the
reader never strips a BOM — a Notepad-edited .env makes the first key
fail the in-place match and get duplicated instead of updated.

Match the canonical .env readers in hermes_cli/config.py: read with
encoding='utf-8-sig' (BOM-tolerant) and write with encoding='utf-8'.
mem0/_setup.py already pins utf-8 for mem0.json, so this just aligns the
.env path in the same file. Fixes both memory plugins in one class fix.

Adds regression tests: a BOM'd .env updates the first key in place
(locale-independent, fails without the fix) and non-ASCII existing lines
survive the round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:10:39 -07:00
jeay e02170f54a fix(hindsight): specify UTF-8 encoding for file I/O on Windows
On Windows with CJK locales (e.g. Chinese/GBK), pathlib.Path.read_text()
defaults to the system encoding instead of UTF-8, causing UnicodeDecodeError
when reading .env or .json config files that contain non-ASCII characters.

Explicitly pass encoding='utf-8' to all read_text() and write_text() calls
in the hindsight memory provider plugin.
2026-07-24 17:10:39 -07:00
AlexFucuson9 cf35fd6de5 fix(core,cli,gateway,plugins): add encoding='utf-8' to read_text() calls
Path.read_text() without an explicit encoding uses the platform's
default encoding. On Windows this is typically cp1252 or mbcs, which
causes UnicodeDecodeError or silent data corruption when reading
UTF-8 content (JSON files, user text, config with non-ASCII chars).

This is the read-side companion to the write_text() encoding fix.
Fixed the most critical locations that read JSON data, user content,
and config files across 14 files with 31 call sites.

Pattern: .read_text() → .read_text(encoding='utf-8')
         json.loads(path.read_text()) → json.loads(path.read_text(encoding='utf-8'))
2026-07-24 17:10:39 -07:00
Stoltemberg c89481db5e fix: add explicit UTF-8 encoding to all subprocess text=True calls (#53428)
On Windows with Chinese locale (GBK), subprocess.run(text=True) without
explicit encoding causes UnicodeDecodeError crashes. This fix adds
encoding='utf-8', errors='replace' to all subprocess.run() and
subprocess.Popen() calls that use text=True across 76 non-test Python files.

Fixes #53428 (master tracker for Windows GBK locale crash).

Note: credential_pool.py and electron changes excluded per reviewer request —
those will be submitted as separate focused PRs.
2026-07-24 11:45:57 -07:00
Aakash Kattelu c0170311c9 fix(honcho): default device-code poll interval to 5s when AS omits it
RFC 8628 §3.2 makes the device-authorization `interval` optional with a
client-side default of 5 seconds. request_device_code required it, so a
compliant AS that omitted it hit the malformed-response path and the flow
could never complete. Fall back to 5s and cover it with a regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 07:53:12 -07:00
Aakash Kattelu 2aa359ea70 feat(honcho): add OAuth device-code login (RFC 8628) for headless environments
Adds a device authorization grant flow alongside the existing loopback
OAuth flow, so `hermes setup` can connect to Honcho cloud from SSH and
other no-browser environments.

- oauth.py: new HTTP seams — _http_post_form_status (non-raising, since
  RFC 8628 polling reads the OAuth error off a 400) and _http_get_json
  for the RFC 8414 metadata probe
- oauth_flow.py: DeviceCode, request_device_code, poll_for_token with
  slow_down backoff (+5s, capped at 60s) bounded by expires_in, typed
  errors (AccessDenied, DeviceCodeExpired, AuthorizationTimeout), and
  supports_device_login (fail-closed metadata gate); device flow ends in
  the same install_grant tail as loopback so refresh/status work
  unchanged
- oauth_flow.py: loopback callback now serves a "sign-in was not
  completed" page on consent cancel instead of the success page
- cli.py: cloud menu offers oauth / device / apikey; the device option
  only appears when the host advertises the grant, and becomes the
  default when no browser is detected
- 18 new tests covering the full flow against a local fake AS, backoff
  schedule, error mapping, deadline bound, metadata gate, and wizard
  branches

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 07:53:12 -07:00
kshitijk4poor 8f0da78f84 fix(openviking): cool down failed refreshes and publish conn identity atomically
Follow-up hardening on the salvaged _ensure_client() (#21130 fix):

- Failed-config cooldown: after a refresh attempt fails for a given
  resolved config, skip re-probing for 30s. Previously every provider
  access against a down endpoint paid a 3s health probe under
  _client_refresh_lock and emitted a warning (2+ per turn, some on
  user-facing threads: prefetch, tool calls, session end). Retries
  still happen after the cooldown or immediately when config changes,
  and the log message now says so instead of the false 'disabled until
  config changes'.
- Atomic connection snapshot: _conn_snapshot (5-tuple, single
  assignment) is published only after a health check passes.
  _new_client() and on_memory_write's writer read it as one load, so
  background writers can no longer observe a torn mix of old/new
  identity fields mid-refresh or target an endpoint that never passed
  health. Field writes in _ensure_client_locked keep tracking the
  attempted config for the unchanged-config dedupe.
- _env_refresh_enabled moves to the top of initialize(): an exception
  mid-initialize (swallowed by MemoryManager) can no longer leave the
  provider silently stuck in never-refresh mode.
- _search_prefetch_context reuses _new_client() and degrades to ''
  on construction failure instead of propagating.

Mutation-checked: neutering the cooldown or publishing the snapshot on
failed health makes the new regression tests fail.
2026-07-24 13:00:53 +05:30
Hao Zhe 1cfe23c6e4 fix(openviking): serialize client refresh state 2026-07-24 13:00:53 +05:30
Hao Zhe cf0bd5dd4c fix(openviking): stop pending runtime start on shutdown 2026-07-24 13:00:53 +05:30
Hao Zhe c4d0f1c1d6 fix(openviking): serialize local runtime recovery starts
Avoid spawning multiple local OpenViking server processes while a runtime autostart waiter is already active. Remote endpoints still retry on later accesses because they do not install a local waiter.
2026-07-24 13:00:53 +05:30
0xDevNinja 60a141594c fix(openviking): start local runtime after reload
Route refreshed unreachable local OpenViking configs through the existing runtime recovery path so /reload can attach to a locally starting server instead of disabling memory until restart.

(cherry picked from commit 040e18ad90)
2026-07-24 13:00:53 +05:30
Hao Zhe 8fdc9c58e0 fix(openviking): use readonly config loader 2026-07-24 13:00:53 +05:30
爪爪 5ea3abc3b3 fix(openviking): match tenant-header errors structurally instead of hard-coding strings
The _needs_trusted_identity_retry method was hard-coding specific
server-side error strings to detect when a request failed due to
missing X-OpenViking-Account / X-OpenViking-User headers.  Each new
server-side error variant required another string added to the client.

Replace the string enumeration with a structural match: the error
message mentions one of the tenant headers AND the HTTP status is 400.
This covers all current error variants:

  - "Trusted mode requests must include X-OpenViking-Account and User"
  - "ROOT requests to tenant-scoped APIs must include X-OpenViking-Account"
  - "Trusted mode requests must include X-OpenViking-Account."
  - "Trusted mode requests must include X-OpenViking-User."

The 400 status guard avoids false-positives on 403 errors such as
"USER API keys cannot override X-OpenViking-User", which must not
trigger a retry.

All 176 existing tests pass.

(cherry picked from commit 5a24d6766c)
2026-07-24 13:00:53 +05:30
Hao Zhe 36f01d2e54 fix(openviking): sanitize splitline env separators 2026-07-24 13:00:53 +05:30
koshaji d3520944c7 fix(openviking): join runtime-autostart thread on shutdown (SIGABRT-at-exit)
`OpenVikingMemoryProvider.shutdown()` joins in-flight writers, deferred-commit
threads, and prefetch threads, but not `_runtime_start_thread` — the tracked
`daemon=True` waiter that runs `_finish_runtime_openviking_start`, which blocks
on network health probes (`_wait_for_openviking_health` polling + a
`_VikingClient.health()` request).

If the local OpenViking runtime is slow or unreachable, that waiter can still
be blocked in network I/O at interpreter exit. CPython then forcibly kills it
during `Py_FinalizeEx` (`PyThread_exit_thread` -> `__pthread_unwind` ->
`abort()`), producing SIGABRT (exit 134) with no traceback — the same daemon-
thread-at-exit failure class fixed for the Honcho provider.

Fix:
- `shutdown()` now joins `_runtime_start_thread` (timeout-bounded) alongside the
  other tracked threads.
- `_wait_for_openviking_health()` gains a `should_stop` callback; the waiter
  passes `lambda: self._shutting_down` so the poll loop bails out promptly once
  `shutdown()` flips the flag, instead of lingering up to the 60s autostart
  timeout and timing out the join (which would leave the thread alive).
- Add tests/plugins/memory/test_openviking_shutdown.py covering the short-circuit
  and the shutdown-joins-runtime-thread behaviour.

(cherry picked from commit 5471ec7021)
2026-07-24 13:00:53 +05:30
0xDevNinja 7618121783 fix(openviking): refresh client from env on every access
initialize() snapshots OPENVIKING_* into the provider once, so /reload
(which only updates os.environ) leaves viking_* tools running against
stale auth — users have to restart hermes to pick up keys added to
~/.hermes/.env after startup.

Add _ensure_client(), which re-resolves the connection settings via the
same _resolve_connection_settings/_load_hermes_openviking_config path
initialize() uses and rebuilds + health-checks the client only when an
OPENVIKING_* value actually changed; otherwise it reuses the cached
client so the hot path stays at one dict comparison with no network
calls. Every `if not self._client:` guard in system_prompt_block,
queue_prefetch, sync_turn, on_session_end, on_memory_write and
handle_tool_call now goes through it.

Refreshing is gated behind a flag set at the end of initialize() so the
baseline is established before any env re-resolution happens — callers
that wire up a client directly keep the existing client untouched.

Refs #21130

(cherry picked from commit b694d21b7c4ff0330df6051e12dc8991f7ea10a6)
2026-07-24 13:00:53 +05:30
pprism13 9291b786b4 fix(openviking): sanitize embedded newlines when writing .env secrets
`_write_env_vars` in the OpenViking memory provider interpolates each
secret straight into a `KEY=VALUE` line, but the values only ever pass
through `_clean_config_value`, whose `value.strip()` trims surrounding
whitespace and leaves internal CR/LF intact. Because the file is strictly
line-oriented and is re-read via `read_text().splitlines()`, a value that
carries an embedded newline spills onto a second physical line, and the
tail is re-parsed as an independent `KEY=VALUE` entry on the next round
trip. A secret pasted with a trailing record (e.g. an `OPENVIKING_API_KEY`
copied with an extra line) therefore injects an arbitrary additional
variable into the persisted credentials file and silently corrupts it.

The fix neutralizes the line terminators at the single chokepoint where
values reach the file. A small `_env_line_safe` helper strips `\r`, `\n`,
and the NUL byte from each value, and both write sites in `_write_env_vars`
(the existing-key update branch and the appended-key branch) route through
it, so a value can only ever occupy the single line it is written on.

## What does this PR do?

Hardens the OpenViking memory provider's `.env` writer so a malformed or
pasted secret value can no longer break out of its `KEY=VALUE` line and
inject a rogue variable into the profile-scoped credentials file.

## Related Issue

N/A

## Type of Change

- [x] 🐛 Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `plugins/memory/openviking/__init__.py`: add `_env_line_safe()` which
  removes `\r`, `\n`, and `\x00` from a value, and apply it to both the
  updated-key and appended-key write branches in `_write_env_vars()`.
- `tests/plugins/memory/test_openviking_provider.py`: add two regression
  tests covering a fresh write and an in-place key update with embedded
  CR/LF, asserting no injected line survives the read-back.

## How to Test

1. Run the targeted tests:
   `pytest tests/plugins/memory/test_openviking_provider.py -k env_writer -q`
2. Reverting the `_env_line_safe` sanitization makes
   `test_openviking_env_writer_strips_embedded_newlines_in_values` and
   `test_openviking_env_writer_strips_newlines_when_updating_existing_key`
   fail with a rogue `INJECTED_KEY=`/`ROGUE=1` line appearing in the file,
   confirming the tests pin the bug.
3. `ruff check plugins/memory/openviking/__init__.py` and
   `python scripts/check-windows-footguns.py plugins/memory/openviking/__init__.py`
   both pass.

## Checklist

### Code

- [x] I've read the Contributing Guide
- [x] My commit messages follow Conventional Commits
- [x] I searched for existing PRs to make sure this isn't a duplicate
- [x] My PR contains only changes related to this fix
- [x] I've run the relevant tests and they pass
- [x] I've added tests for my changes (required for bug fixes)
- [x] I've tested on my platform: macOS 15 (Darwin 25.5)

### Documentation & Housekeeping

- [x] I've updated relevant documentation (docstrings) — or N/A
- [x] I've updated `cli-config.yaml.example` if I added/changed config keys — N/A
- [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — N/A
- [x] I've considered cross-platform impact (strips CR as well as LF) — done
- [x] I've updated tool descriptions/schemas if I changed tool behavior — N/A

(cherry picked from commit f29dd2df84)
2026-07-24 13:00:53 +05:30
wernerhp cc84af9fad fix(memory): preserve genuine pre-delimiter content in merged compaction rows
teknium1 review on #57690: harvesting logic was skipping the ENTIRE merged
row when a compaction summary was appended to the tail message, discarding
real prior user content that context_compressor retains before the
_MERGED_SUMMARY_DELIMITER. Extract and harvest that pre-delimiter segment
instead of dropping it wholesale.

Revert-to-fail: reverting plugins/memory/holographic/__init__.py alone
drops test_merged_into_tail_preserves_genuine_pre_delimiter_preference
(19 passed, 1 failed); restoring the fix returns 20/20 passed.
2026-07-22 06:59:14 -07:00
wernerhp 004de13f14 fix(memory/holographic): don't harvest compaction summaries; honor auto_extract=false string
Two compounding defects in the holographic memory provider (#57682):

1. The on_session_end gate used plain truthiness on auto_extract, but the
   plugin's own config schema declares it as a string enum with default
   "false" — and not "false" is False, so extraction ran for users who
   had it configured off. Coerce with the shared utils.is_truthy_value
   (same fix class as the merged byterover no-op fix).

2. _auto_extract_facts scanned every role=user message. Context-compaction
   handoff summaries can be inserted as role=user messages and their prose
   reliably matches the decision patterns (we decided/agreed, the project
   uses), so the compactor's own output was persisted as a durable project
   fact on every rollover following a compaction — recreated even after
   manual deletion.

Adds agent.context_compressor.is_compaction_summary_message(), a public
helper that prefers the in-process COMPRESSED_SUMMARY_METADATA_KEY marker
and falls back to _is_context_summary_content() (covers merged-into-tail
and historical prefixes), since the metadata key is stripped by wire
sanitizers and doesn't survive all session-store round-trips. The plugin
skips summary messages before pattern matching.

Fixes #57682
2026-07-22 06:59:14 -07:00
kshitij 0c76cc6c36 fix: discard both session IDs on compression for profile re-injection
The _profile_prefetched_sessions set stores whichever session_id was
passed to prefetch(), which may differ from self._session_id. On
compression, only old_session_id (self._session_id) was discarded,
missing the case where the stored key was the prefetch session_id
parameter. Discard both old and new IDs to cover all cases.
2026-07-22 14:12:50 +05:30
Hao Zhe 8af2133009 fix(openviking): align session context with shared profile contract 2026-07-22 14:12:50 +05:30
Flownium 11c1ca01c5 fix(openviking): inject session-start memory context
(cherry picked from commit 18b474d0bd)
2026-07-22 14:12:50 +05:30
kshitij c3c80e1796 refactor: cleanup follow-up for salvaged PR #58871
- Remove dead current_sid parameter from _recover_pending_sessions
- Remove dead cleanup parameter from _release_owner_run_claim (always True)
- Set _run_lock_path after flock succeeds, not before
- Collapse redundant BlockingIOError branch (covered by OSError+errno check)
- Track _pending_marked_sids to skip re-writing marker file on every sync_turn
2026-07-22 14:05:28 +05:30
Chris Korhonen 81fc424592 fix(openviking): chunk structured session sync
Preserve ordered structured turns across OpenViking's 100-message batch limit and resume retries from the first unconfirmed message.

Based on the OpenViking batching work from commit 1a567f7067 in #58981.
2026-07-22 14:05:28 +05:30
Hao Zhe 3fd96583f4 fix(openviking): serialize orphan session recovery 2026-07-22 14:05:28 +05:30
Naveen Fernando f7c198dec8 docs: clarify OpenViking local setup
(cherry picked from commit a6807170f1)
(cherry picked from commit 6fb4e9aa8a)
2026-07-22 14:05:28 +05:30
Hao Zhe 323e9baf5d fix(openviking): recover pending session commits 2026-07-22 14:05:28 +05:30
RenoMG 95c616be20 fix(supermemory): complete self-hosted endpoint routing 2026-07-20 00:40:40 -07:00
Dhravya Shah 24ac26a3da feat(supermemory): support custom base URL for self-hosted servers
The supermemory SDK already honors SUPERMEMORY_BASE_URL, but the raw
urllib call used for session-end conversation ingest hardcoded
https://api.supermemory.ai/v4/conversations, so ingest always hit the
cloud even when pointing at a self-hosted server (e.g.
http://localhost:6767).

Resolve the base URL as config (supermemory.json base_url) >
SUPERMEMORY_BASE_URL env var > https://api.supermemory.ai, strip any
trailing slash, and use it for both the SDK client and the
/v4/conversations ingest endpoint.
2026-07-20 00:40:40 -07:00
teknium1 c84c0c5277
Merge branch 'pr-51020' into lane/c3-memory-panel 2026-07-18 15:13:04 -07:00
Teknium 07f07c7b51 fix(mem0): migrate legacy OSS base URL aliases
Normalize stale api_base keys to each mem0 provider's accepted URL field before Memory.from_config, without mutating the saved config.
2026-07-17 13:49:29 -07:00
Erosika e4cdd8d9ad fix(honcho): delegate the config.yaml timeout read to load_config_readonly
The staleness check's bespoke mtime memo keyed only on the user
config.yaml, but load_config() merges the managed-scope config
(HERMES_MANAGED_DIR/config.yaml, /etc/hermes) whose leaf keys win. A
managed honcho.timeout with no user config.yaml made the memo cache
'no timeout' while _build resolved the managed value — the same
perpetual-rebuild mismatch this PR fixes for honcho.json. A managed
timeout edit was likewise invisible while the user file's mtime stayed
put.

load_config_readonly() is already cached on both files' signatures plus
the env-ref snapshot, so use it instead of duplicating that
invalidation logic; the defensive deepcopy the old memo existed to
avoid is skipped by the readonly variant. Drive the rebuild test
through a real config.yaml and add a HERMES_MANAGED_DIR regression
test covering stable reuse and managed-timeout edits.
2026-07-17 13:20:16 -07:00
Erosika 9769facae1 fix(honcho): resolve the timeout staleness check from honcho.json like the build path
The staleness check added in #66052 resolved the timeout from env,
config.yaml, and the default only, while the build path also reads the
honcho.json host block (timeout/requestTimeout). With a timeout
configured in honcho.json, the two permanently disagreed: every
no-config get_honcho_client() call — i.e. every HonchoSessionManager
.honcho property access — interpreted the mismatch as a config change
and tore down and rebuilt the client, defeating the singleton on the
hot path it was meant to protect.

Teach the check to read honcho.json through the same host-aware chain
as from_global_config, memoized on the file's mtime_ns so the per-call
cost stays one stat(). A genuine honcho.json timeout change is now also
detected, extending #57437 to that config surface.
2026-07-17 13:20:16 -07:00
Teknium 04d84df63c fix(honcho): memoize timeout staleness check + host-aware status cadence
Follow-ups for the consolidated salvage:
- Memoize the config.yaml-derived timeout on the file's mtime_ns so the
  rebuild-on-timeout-change check from PR #57437 costs one stat() per
  get_honcho_client() call instead of a full YAML load on the hot path.
- hermes honcho status now displays the host-block-resolved
  dialecticCadence (remnant from PR #63776, whose runtime fix landed
  in #62290).
- AUTHOR_MAP entries for the salvaged contributor emails.
2026-07-16 22:25:22 -07:00
liuhao1024 e5bebe2cad fix(plugins): rebuild Honcho client when timeout config changes
The Honcho client singleton cached the HTTP timeout at first build.
In long-lived processes (gateway, dashboard), changing the timeout
via config.yaml or HONCHO_TIMEOUT had no effect until restart.

Track the resolved timeout alongside the cached client and compare
on each get_honcho_client() call. When the timeout differs, reset
the singleton so the next call rebuilds with the new value.

Fixes #57347
2026-07-16 22:25:22 -07:00
Hermes Pi 73a4574ede fix(honcho): preserve profiles for local IP config 2026-07-16 22:25:22 -07:00
Hermes Pi cd268c1226 fix(honcho): read base_url and defaultHost from honcho.json host blocks
Fixes Honcho client initialization for setup-generated configs that store
connection details in a named host block (e.g. "local"). Previously:
- base_url was only read from flat config root, not from host_block.
- resolve_active_host() ignored defaultHost and always used the Hermes
  profile key ("hermes"), so the host block lookup returned {} and the
  api_key was also lost.

Fixes NousResearch/hermes-agent#61661
2026-07-16 22:25:22 -07:00
vizi0uz 602998b76c fix(honcho): warn model away from minimal reasoning_level on multi-fact queries
honcho_reasoning's minimal tier hard-caps Honcho's dialectic output at 250
tokens combined with the model's own hidden reasoning tokens. Confirmed via
direct honcho-api server logs that a multi-fact query ("summarize known
facts about this peer and communication preferences") run at
reasoning_level=minimal gets cut off mid chain-of-thought at exactly
output_tokens=250, before the model ever reaches a synthesized answer.

low/medium/high/max fall back to Honcho's much larger global dialectic
default and don't hit this cap. Since dialecticDynamic is on by default and
the calling model picks reasoning_level itself via this tool parameter, the
fix is to make the tradeoff explicit in the parameter description so the
model defaults to low unless the query is genuinely a single-fact lookup.

Schema-description-only change; the shared dialectic_max_chars truncation
cap in session.py's dialectic_query() is a separate fix (its own PR).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 22:25:22 -07:00
RainbowAndSun 9a887e7c5b fix(honcho): use _resolve_observer_target for user context in session context
_fetch_session_context was using user_peer_id directly as observer
when calling _fetch_peer_context, bypassing _resolve_observer_target.
This caused honcho_context to return empty data because the observer
perspective was wrong (user instead of hermes/assistant).

Fixed by resolving observer via _resolve_observer_target(session, 'user'),
consistent with all other call sites (get_peer_card, honcho_search, etc.).
2026-07-16 22:25:22 -07:00
fjlaowan1983 af550a7053 fix(honcho): reject whitespace-only search/reasoning queries
Strip query before validation; add regression tests (aligns upstream #11192).

Made-with: Cursor
2026-07-16 22:25:22 -07:00
Erosika 2ad6ab17e3 fix(honcho): enforce recall latency and budget contracts 2026-07-16 12:48:48 -07:00
Erosika ef68ae7ecc docs(honcho): document latency flags and updated tool contracts 2026-07-16 12:48:48 -07:00
Erosika e8957babf4 feat(honcho): make latency-adding paths configurable
queryRewrite (default off) gates the latest-message rewrite so the
extra auxiliary LLM call is opt-in. firstTurnBaseWait and
firstTurnDialecticWait expose the turn-1 bounded waits in seconds
(0 disables). All three resolve host-block-first like every other
field. Also pins per-host timeout resolution with tests.
2026-07-16 12:48:48 -07:00
Erosika e7fb51d5ac refactor(memory): make query rewrite provider-agnostic
Move query_rewrite from the honcho plugin to plugins/memory/ and
rename the auxiliary task key honcho_query_rewrite ->
memory_query_rewrite so any memory provider can use the same
rewrite path and model/timeout config block. No behavior change.
2026-07-16 12:48:48 -07:00
Erosika 8ab4cb9d0d fix(honcho): gate the stalled-init prefetch wait to the first turn 2026-07-16 12:48:48 -07:00
vizi0uz 56816f4232 fix(honcho): stop clipping honcho_reasoning tool results to the injection budget
dialecticMaxChars (default 600) is documented as the budget for the dialectic
supplement auto-injected into the system prompt every turn — a small recurring
cost that is correct to bound tightly. But dialectic_query() applied that cap
unconditionally, so explicit honcho_reasoning tool calls — where the model
deliberately spends a turn asking for a synthesized answer — were silently
truncated mid-word to 600 chars with a trailing " …", no error surfaced. The
full answer is returned by Honcho server-side; the clip happens client-side.

The auto-injection path already has its own token-based budget (contextTokens,
enforced in prefetch() via _truncate_to_budget), so the char cap's real job is a
cheap always-on guardrail for that recurring injection. Explicit tool results
are already bounded server-side by Honcho's dialectic MAX_OUTPUT_TOKENS and don't
need the injection cap — sibling tools (honcho_search, honcho_context) don't
post-clip their results either.

Add apply_injection_cap (default True, preserving current behavior) to
dialectic_query(); the honcho_reasoning tool handler passes False so it returns
Honcho's full synthesized answer. Auto-injection is unchanged. Tests cover both
the capped injection path and the uncapped tool path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 12:48:48 -07:00
Erosika 29e0471708 fix(honcho): update SDK and restore CI coverage 2026-07-16 12:48:48 -07:00
Erosika 1c051d1df9 fix(honcho): preserve delayed and rewritten recall context 2026-07-16 12:48:48 -07:00
vizi0uz f4669f34cf feat(honcho): add list mode to honcho_conclude so delete can resolve a real conclusion id
honcho_conclude's delete action was unreachable in practice: no tool ever
surfaced a real conclusion id for the model to pass as delete_id.
honcho_search only searches the separate Message resource space, and the
SDK's ConclusionScope.list()/.query() (which do return real Conclusion.id
values) were never wired into any tool.

Adds an optional list mode to honcho_conclude (query to search, omit to
browse recent conclusions), backed by a new
HonchoSessionManager.list_conclusions(). No new tool, no changes to the
create/delete signatures or their conclusions_of() routing.
2026-07-16 12:48:48 -07:00
ljy-2000 01d1a663e1 fix(honcho): ground dialectic queries in latest user message 2026-07-16 12:48:48 -07:00
k4z4n0v4 3e4e3db66d fix(honcho): don't let first-turn injection suppress dialectic
injectionFrequency='first-turn' returned empty for the entire
prefetch_context() method on turns 2+, which blocked the dialectic
supplement from being consumed and injected. The dialectic has its
own cadence (dialecticCadence) and must continue to fire and inject
independently of the base context layer.

Now first-turn mode gates only Layer 1 (base context: representation
+ card), letting Layer 2 (dialectic supplement) flow through its
normal consumption path on every turn.

Also fixes all remaining tests that passed dialecticCadence via
cfg_extra={'raw': {...}} to use the typed dialectic_cadence field.
2026-07-16 12:48:48 -07:00
k4z4n0v4 d2b6c21a3c fix(honcho): resolve cost-awareness config from host block
injectionFrequency, contextCadence, and dialecticCadence were read
only via raw.get() which checks root-level keys in honcho.json.
Settings placed inside hosts.<name> (the normal per-host location)
were silently ignored, falling back to defaults.

- Add typed dataclass fields: injection_frequency, context_cadence,
  dialectic_cadence with host-block-first resolution chains matching
  the pattern used by all other config fields.
- Update HonchoMemoryProvider.initialize() to read from typed cfg
  fields instead of cfg.raw directly.
- Fix search_context tests that mocked _honcho directly — the
  .honcho property getter calls get_honcho_client() which overwrites
  the backing field, so use patch.object on the property instead.
- Add injection_frequency and context_cadence config override tests.
2026-07-16 12:48:48 -07:00
k4z4n0v4 111ca88fab fix(honcho): honor per-host timeout in config resolution
HonchoClientConfig timeout/requestTimeout resolution skipped the per-host config
block, silently dropping a host-scoped timeout and falling through to the global
config.yaml value (or the default). Add the host block at the front of the
resolution chain, consistent with every other field (base_url, api_key, etc.).
2026-07-16 12:48:48 -07:00
k4z4n0v4 63288f1d80 fix(honcho): stop dropping dialectic results on trivial turns
Symptom: Honcho logs show a dialectic answer was generated, but Hermes never
injects it — intermittently.

Root cause: the dialectic supplement that queue_prefetch() fires at the end of
turn N is stored pending (fired_at=N) for consumption by turn N+1's prefetch().
But prefetch()'s trivial-prompt guard returned early BEFORE the consumption
block. So when turn N+1's prompt was trivial ('ok', 'yes', 'continue', a slash
command), the ready result was never consumed, and a few turns later the
stale-discard guard dropped it. Generated by Honcho, never seen by the model.
The dependence on 'is the consuming turn trivial?' is why the loss looked random.

Fix: trivial turns now consume and inject a ready, non-stale pending result while
still spending no new work (no base-context fetch, no new dialectic fire). A
trivial ack shouldn't generate context, but it shouldn't destroy an answer
already computed for that exact turn. Extracted the pop+stale-check into a shared
_consume_pending_dialectic() used by both the trivial path and the normal path so
they age-check identically.

Preserved (covered by new tests): genuinely stale results are still discarded on
trivial turns; trivial turns still fire no new work; a trivial turn with nothing
pending still injects nothing.

Adds regression tests for inject-on-trivial and discard-stale-on-trivial.
2026-07-16 12:48:48 -07:00
k4z4n0v4 bef9eea3e6 fix(honcho): inject base context on the first message of a session
A brand-new session injected no Honcho context on the user's first message —
the peer card/representation only showed up from turn 2 onward. The base-context
fetch was fired asynchronously and popped in the same synchronous pass, so it
always lost the race on turn 1 (a background thread can't finish inside one
pass), leaving the first response with zero recalled context.

Fetch the base layer (representation + card + summary) synchronously with a
bounded timeout on turn 1 so the peer card is injected immediately; subsequent
turns still consume the background-refreshed result primed by queue_prefetch().
The wait is bounded by _FIRST_TURN_BASE_TIMEOUT and tightened further by a small
configured request timeout (fail-fast deployments / tests).

Two related first-turn/dialectic reliability fixes ride along:
- first-turn dialectic no longer double-fires: if a prewarm .chat() thread is
  already in flight from session init, turn 1 waits briefly for it instead of
  firing a second (duplicate) call that also blocked the first response. The
  first-turn wait is decoupled from a large host timeout (a 60s host timeout
  must not block the first response for 60s) via _FIRST_TURN_DIALECTIC_CAP,
  while still honoring a tight configured timeout.
- empty-pass propagation guard in multi-pass dialectic: at depth > 1 each pass
  feeds the prior pass's output into the next prompt. If a pass returned empty
  (e.g. a reasoning model that spent its whole budget thinking), the next prompt
  carried a blank assessment (the "empty spot" seen in Honcho request logs). Now
  only non-empty prior results feed dependent passes; if all priors are empty,
  re-issue the base prompt instead of referencing nothing.
2026-07-16 12:48:48 -07:00
k4z4n0v4 a35bc81b3f fix(honcho): honest, non-overlapping tool descriptions + drop dead param
The five Honcho tool schemas had overlapping/misleading descriptions, making it
hard for the model to pick the right one, plus two concrete correctness bugs:

- honcho_context advertised an 'Optional focus query' parameter that the dispatch
  never read. The model could pass query= expecting filtering that never happened.
  Remove the dead parameter; honcho_context is an honest no-query snapshot. Focused
  retrieval now lives in honcho_search (see prior commit).
- honcho_conclude's peer param was described as 'Peer to query' — wrong; it's the
  peer the conclusion is ABOUT. Corrected.

Rewrite all five descriptions to give each tool a distinct mental model and
cross-reference siblings:
  profile  = read/write the compact card (cheapest, no query, no LLM)
  search   = find what was actually said, ranked, cross-session (cheap, no LLM)
  reasoning= ask a question, get a synthesized answer (the only LLM tool; expensive)
  context  = a fixed session snapshot (no query, no LLM)
  conclude = write a durable fact to the profile

Addresses the honcho_context query param half of #29402 (see PR notes for the
divergence from that issue's proposed wire-through approach).
2026-07-16 12:48:48 -07:00
k4z4n0v4 c1c59e3474 fix(honcho): make honcho_search do real cross-session message search
honcho_search routed through search_context() -> peer.context(search_query=),
which returns the peer's standing representation + card. The search_query arg
does not turn that endpoint into a search, so results were effectively
query-independent: the same representation blob regardless of the query. Factual
lookups ('what medication', 'which value did we pick') returned noise.

Rewire search_context() to call the workspace message-search endpoint
(Honcho.search) with a peer_perspective filter: RRF-ranked (hybrid semantic +
full-text) raw message excerpts spanning every session the peer was a member of,
across all authors, membership-time-scoped. This is the cross-session factual
recall primitive.

peer_perspective is chosen over the alternatives because it is the only scope
that is simultaneously (a) cross-session, (b) inclusive of assistant-authored
facts about the peer (peer-author search drops these, and they are a large part
of what you want to recall about yourself), and (c) privacy-scoped to the peer's
own sessions (plain workspace search leaks other peers' sessions).

- snippets are labeled by author so the model can tell user-stated facts from
  assistant-derived ones
- max_tokens is now an enforced budget (was accepted but meaningless)
- graceful fallback to peer-authored search if peer_perspective is unsupported
- query length clamped under the embedding input cap

Replaces 3 change-detector tests that asserted the old representation-dump
behavior with 4 that assert the message-search contract + fallback path.
2026-07-16 12:48:48 -07:00
Erosika 155a792013 Merge origin/main: keep unified declared-schema config surface
Conflict resolutions:
- web_server.py: keep the branch's unified provider-config handlers
  (declared schema served by default, profile-scoped) over main's
  surface=declared query param. Main's declared-surface helpers and the
  hermes_cli.memory_providers import are dropped — the branch deleted
  that module when provider schemas moved into their plugins, so main's
  code path could no longer import.
- hermes.ts: keep profileScoped() calls without ?surface=declared to
  match the unified backend.
- constants.ts: take main's new reasoning effort values (max, ultra),
  keep the branch's memory provider ordering.
- config-settings.tsx: take main's FallbackModelsField import, drop the
  now-unused SECTIONS import (branch replaced it with
  sectionFieldEntries).
- provider-config-panel.test.tsx: file moved to settings/memory/ on the
  branch; main's act() fixes targeted the old tests, and the rewritten
  suite produces no act() warnings, so the old path stays deleted.
- test_web_server.py: keep both sides' new tests (main's MoA endpoint
  tests plus the branch's Honcho provider tests).
2026-07-15 17:50:31 -04:00
Teknium 861d69c7bb
fix(dashboard): keep memory.provider in the config schema so Desktop's dropdown survives (#63886)
The dashboard's dedicated memory-provider UI (4b184cbe5) excluded
memory.provider from /api/config/schema server-side. Desktop's settings
page builds its field list from that schema, so the Memory Provider
dropdown silently vanished from Desktop after v0.18.1.

- web_server.py: restore memory.provider as a select in _SCHEMA_OVERRIDES,
  with options built from plugins.memory discovery (was a stale hardcoded
  [builtin, honcho] list before the removal)
- plugins/memory: add list_memory_provider_names() — directory-scan-only
  name listing, safe at module import time (no provider imports)
- web ConfigPage: hide memory.provider client-side instead — the Plugins
  page owns the dedicated provider-switching UI there
- tests: schema contract (select present, category memory, builtin
  sentinel) + invariant that every discoverable provider is selectable
2026-07-13 18:56:51 -07:00
Erosika f000fbe5c5 revert(memory): drop the provider actions extension point
No bundled provider declares actions, and the motivating request
(openviking, #56309) needs dynamic select options rather than actions.
Removing the unconsumed POST dispatch surface keeps this PR focused on
the config panel; the extension point can return as its own PR with its
first real consumer.
2026-07-10 15:41:52 -04:00
teknium1 a801046669 fix(memory): resolve() the shared-connection registry key; symlink test
Follow-ups for salvaged PR #43819: the registry key was
str(Path(db_path).expanduser()) — a symlinked or relative path to the
same DB file got its own connection, silently reintroducing the exact
multi-writer contention the registry prevents. Key on Path.resolve()
(OSError-tolerant fallback). Adds a symlink regression test and the
AUTHOR_MAP entry for adambiggs.
2026-07-09 18:17:40 -07:00
Adam Biggs b5226caff8 fix(memory): share one SQLite connection per holographic store database
Every MemoryStore instance opened its own SQLite connection guarded by
its own RLock. Several providers coexist in one process (the main agent
plus every delegate_task subagent), so instances pointing at the same
memory_store.db raced as independent WAL writers. Combined with writes
that were not rolled back on error, one connection could leave an open
write transaction that pinned the write lock and made every other
connection's writes fail with "database is locked" for the full busy
timeout.

Instances for the same database now share ONE process-wide connection
and ONE re-entrant lock, so access is fully serialized and
cross-connection contention is impossible. The shared connection is
refcounted: closing one instance never tears it out from under a live
sibling, and the last close releases it. The connection runs in
autocommit (isolation_level=None) so a write that raises mid-method can
never leave a dangling transaction holding the write lock; the existing
explicit commit() calls become harmless no-ops.

The provider's shutdown() now calls the refcount-guarded close() instead
of just dropping the reference: leaving finalization to GC kept the
connection (and its write lock) alive indefinitely on long-running
gateways, prolonging the exact contention this fix removes. The last
provider now releases the connection deterministically while siblings
stay live; regression tests fail without the wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 18:17:40 -07:00
Erosika 1508ece2d9 Merge origin/main: unify declared-schema and instance-schema provider config
Main's #60569 (dashboard memory provider switching) rewrote the same
GET/PUT /api/memory/providers/{name}/config routes this PR owns — the
dashboard and desktop share one backend. Resolve by dispatching: providers
that declare a config_schema.py get the declared path (host-block storage,
locked honcho writes, profile scoping, actions); everything else keeps
main's instance get_config_schema()/save_config path unchanged, so the
dashboard's PluginsPage behavior is preserved for instance providers.
Setup manifests, the /setup endpoint, provider switching, and name
validation from #60569 apply to both paths. Instance payloads gain the
docs_url/actions keys the desktop panel expects; declared payloads gain
the setup block the dashboard expects. Main's hindsight/honcho instance-
schema tests are updated for the dispatch, and its status test gets the
HOME pin it was missing (it read the developer's real ~/.honcho).
2026-07-08 15:03:24 -04:00
teknium1 5e51b123f3 feat(mem0): add self-hosted mode to the setup wizard
The salvaged SelfHostedBackend made self-hosted servers reachable via
mem0.json / MEM0_HOST, but the setup wizard still offered only Platform
and OSS — exactly the gap users hit (Discord report: 'At memory setup
there's only 2 options'). Adds a third wizard mode:

- interactive picker: Platform / Self-hosted server / Open Source
- non-interactive: hermes memory setup mem0 --mode selfhosted
  --host http://... [--api-key ...] [--dry-run]
- host -> mem0.json (behavioral), API key -> .env as MEM0_API_KEY
  (secret), optional key for AUTH_DISABLED servers
- best-effort reachability check against the server, non-fatal
- README + memory-providers docs updated with the wizard path
2026-07-07 14:07:16 -07:00
kshitijk4poor 9a322726ae fix(mem0): prune dead get_all, wire rerank config default, warn on MEM0_HOST env override
Review follow-ups on the salvage:

- get_all() pruned from the ABC and all three backends: mem0_list (its
  only caller) was removed by the recall-tuning commit, leaving new,
  tested, unreachable code — including SelfHostedBackend's _MAX_TOP_K
  over-fetch workaround. Tests for it dropped; fake-class stubs remain
  harmlessly. (The #52921 true-total fix lives on in the PR history if
  a lister ever returns.)
- The persisted rerank config key was write-only (setup prompted for it,
  nothing read it). initialize() now parses it into _rerank_default and
  mem0_search uses it when the model doesn't pass rerank explicitly;
  per-call args still win. Guard test added.
- Platform-mode setup now warns when MEM0_HOST is set in the environment:
  the json host-clear can't help there (_load_config seeds host from the
  env var, docs tell users to put it in .env) — the user would silently
  keep routing to the self-hosted server.
- SelfHostedBackend: connect-level retries (httpx.HTTPTransport(retries=2))
  so a single transient blip doesn't count toward the provider breaker;
  transport now injectable and the test helper uses the real __init__
  instead of mirroring it via __new__.
- plugin.yaml description no longer leads with reranking (off by default,
  platform-only); docs em-dash typo fixed.
2026-07-08 01:57:11 +05:30
kshitijk4poor 53edf6f983 fix(mem0): make prompt label + platform setup honor host routing precedence
Follow-up on the salvaged #55614. The PR added host-based routing to
_create_backend (precedence: oss > host > platform) but two sibling surfaces
didn't mirror it:

- system_prompt_block() checked host before oss, so an oss+host config ran
  OSS but told the model it was self-hosted HTTP. Reordered to match routing.
- Platform setup (hermes memory setup mem0 --mode platform) left a stale host
  in mem0.json; since host beats platform, the user kept routing to the
  self-hosted server. save_config merges (no delete), so clear host to ""
  rather than pop() so the merge actually overwrites it.

Adds regression tests for both (mutation-checked).
2026-07-08 01:57:11 +05:30
kartik-mem0 2a14205ff4 feat(mem0): self-hosted dashboard backend + recall tuning (salvage #55614)
Salvage of #55614 by @kartik-mem0 (mem0 maintainer). Adds a SelfHostedBackend
that talks to a self-hosted Mem0 Docker server over httpx (X-API-Key auth,
/search + /memories routes), gated behind `host`. Also folds in the mem0
research-team recall tuning that rides with it: rerank defaults to false across
all modes, the mem0_list tool is removed (5->4 tools), search guidance is
de-shouted, and self-hosted get_all reports the true stored total (#52921).

Supersedes the self-hosted portion of #52487 (@liuhao1024, first-submitted).

Closes #52478
Fixes #52921
2026-07-08 01:57:11 +05:30
Erosika a182ddbf9f feat(memory): per-field info tooltips + name the profile in the full-config modal
Fields can declare a longer 'info' text rendered as an (i) tooltip next to
the label in both the panel and the modal; honcho uses it to spell out the
session strategy, write frequency, and recall mode semantics. The modal
description claimed 'the active profile' without saying which — show the
active gateway profile name.
2026-07-07 11:52:25 -04:00
Erosika 220e7ca274 refactor(honcho): list per-session first among session strategies 2026-07-07 11:45:12 -04:00
Erosika 35099685be feat(memory): provider actions extension point
Providers with behavior beyond fields (validate a server, start a local
instance, link a CLI profile) previously had no mount besides forking the
panel. Let the schema declare actions and one generic endpoint run them:
ProviderAction on ProviderConfigSchema, POST
/api/memory/providers/{name}/actions/{action} dispatching to
ACTION_HANDLERS in the plugin's config_actions.py (path-loaded and
import-light, like the schema), profile-scoped and off the event loop.
Handlers get the submitted values dict, return a JSON-able result, and
raise ValueError for user-facing 400s. The panel renders declared actions
as generic buttons beside Save. No bundled provider declares actions yet.
2026-07-07 11:00:25 -04:00
Erosika 76c063b3d9 fix(memory): profile-scope the provider config endpoints
The settings page follows the desktop's active-profile switcher, but the
provider config calls didn't: no profileScoped() on the client and no
profile param on the backend, so a multi-profile desktop edited the serving
process's config while every surrounding card showed the selected profile's.

Accept ?profile= on both endpoints and resolve inside _profile_scope (the
skills/toolsets contract), spread profileScoped() into the two client calls,
and key the schema cache on the resolved config_schema.py path instead of
the provider name — user-installed plugins are per-profile, so one profile's
lookup must never answer for another's.
2026-07-07 10:53:43 -04:00
teknium1 ba865e4038 refactor(setup): route dependency installs through the canonical uv→pip→ensurepip ladder
Replace the hand-rolled ensurepip bootstrap (and five other one-off
pip-install code paths) with hermes_cli.tools_config._pip_install, which
prefers the bundled uv (fast, needs no pip in the venv), falls back to
python -m pip, and bootstraps pip via ensurepip only when missing.

Sites unified:
- hermes_cli/setup.py: _install_neutts_deps, _install_kittentts_deps,
  modal SDK install, daytona SDK install
- hermes_cli/memory_setup.py: memory-plugin pip deps (previously dead-ended
  when uv AND pip binaries were both absent)
- hermes_cli/dingtalk_auth.py: qrcode auto-install (previously invoked
  'python -m uv' which is not how uv ships)
- agent/lsp/install.py: --target LSP server installs
- plugins/google_meet/cli.py, plugins/platforms/matrix/adapter.py,
  plugins/platforms/google_chat/oauth.py, plugins/memory/honcho/cli.py

Tests updated to assert the ladder behavior (uv-first, pip fallback,
ensurepip bootstrap) instead of the removed bespoke branches.
2026-07-07 04:09:35 -07:00
Teknium 55e3ee1ab8
fix: remove dead f-string prefixes via ruff F541 (216 sites) (#52336)
ruff check --fix --select F541 . on current main. Pure prefix removals;
adjacent-string concatenations keep the f only on interpolating fragments.
No string content or live placeholder altered.
2026-07-05 13:42:46 -07:00
dsad e02cef0d0d fix(memory): guard local uploads against credential reads 2026-07-05 03:14:49 -07:00
Erosika 703305751d style(memory): flatten comment blocks to single lines
Multi-line comment blocks and JSDoc-style headers across the provider
config surface compress to one line each; the why lives in commit messages
and docstrings, not comment essays.
2026-07-02 18:50:11 -04:00
Erosika 41e59f6126 fix(memory): retry failed config schema loads instead of caching None
A syntax error in a provider's config_schema.py was cached as 'no schema'
until process restart, rendering a silent empty panel even after the file
was fixed. Cache only successful loads and genuine file-absence.
2026-07-02 18:42:31 -04:00
Erosika 2a632807e0 refactor(memory): move provider config schemas into their plugins
Each provider now declares its config surface in config_schema.py inside
its own plugin dir (plugins/memory/<name>/), loaded by file path like the
plugins themselves so plugin __init__ imports never reach the web server.
hermes_cli/memory_providers.py is gone; the shared field primitives and
loader live in plugins/memory/config_schema.py, and the schema tests move
to tests/plugins/memory/ alongside the other per-plugin suites.
2026-07-02 16:55:39 -04:00
teknium1 638d2e7bfc fix(memory/holographic): apply FTS5 sanitizer to search_facts sibling
The store-level search_facts() shared the same raw-MATCH bug class as
_fts_candidates (FTS5 AND-joins tokens, zeroing prose recall). Route it
through FactRetriever._sanitize_fts_query via a lazy import to keep the
store->retrieval layering acyclic. Also add cyb3rwr3n to release AUTHOR_MAP.
2026-06-30 15:55:11 -07:00
cyb3rwr3n cb6d6d46ab fix(memory/holographic): sanitize FTS5 queries for natural-language recall
The FactRetriever's _fts_candidates passed the raw query string directly
to FTS5's MATCH operator. FTS5 defaults to AND-between-tokens, which
means any multi-word prose query like 'what happened with the deployment
rollback' required every single token to co-occur in a fact — dropping
recall to zero on the kind of queries agents actually issue via prefetch().

Fix: add _sanitize_fts_query() that:
- tokenizes the query and drops English stopwords
- strips FTS5 operator characters per token
- OR-joins the remaining content tokens as phrase literals

For pathological inputs (all stopwords, empty), falls back to the raw
query so the caller sees zero results instead of a SQL error.

This is a pure-retrieval-quality fix — the HRR + Jaccard reranking
stages still keep precision high. Ships with 10 tests covering the
sanitizer and retrieval integration.
2026-06-30 15:55:11 -07:00
Kartik c6eb7f9e72
fix(memory/mem0): recall on the current question + stronger search guidance (#55535) 2026-06-30 15:51:08 +05:30
Ben Barclay 1289f12812 fix(memory): lazy-install supermemory + mem0 SDKs like honcho/hindsight
The supermemory and mem0 memory providers shipped third-party SDKs
(supermemory / mem0ai) that are not core dependencies, but — unlike the
honcho and hindsight providers — they imported those SDKs directly with
no tools.lazy_deps.ensure() preflight and had no LAZY_DEPS allowlist
entry. On the published Docker image the agent venv is sealed
(HERMES_DISABLE_LAZY_INSTALLS=1) and lazy installs are redirected to a
writable durable target (HERMES_LAZY_INSTALL_TARGET). honcho/hindsight
route through ensure() and install fine there; supermemory/mem0 never
called it, so their SDK was never installed on a hosted instance and the
provider silently reported itself unavailable even with the API key set.

Fixes:
- Add memory.supermemory + memory.mem0 to the LAZY_DEPS allowlist
  (tools/lazy_deps.py), pinned to current PyPI releases.
- Call ensure('memory.<x>', prompt=False) at each SDK-import chokepoint
  (_SupermemoryClient.__init__; Mem0MemoryProvider._create_backend),
  mirroring honcho's wrapped try/except shape.
- Drop the SDK-import gate from supermemory's is_available() — it was a
  chicken-and-egg trap (provider never loaded on a sealed venv, so
  ensure() never ran). Now key-presence only, like honcho/mem0.
- Add matching pyproject extras [supermemory]/[mem0]; update the
  lazy-covered-extras contract test (excluded from [all] by policy).

Tests prove each path fails without the fix and the real sealed-venv
durable-target gate accepts both features.
2026-06-29 00:25:36 -07:00
Teknium 7c0a5def58
fix(memory/holographic): close DB connection on shutdown instead of leaking to GC (#54133)
HolographicMemoryProvider.shutdown() dropped its MemoryStore reference
without calling the existing MemoryStore.close(). Since the connection is
opened check_same_thread=False (one per session), its fd was released by
refcount/GC at a non-deterministic time on a non-deterministic thread,
churning a DB fd through the kernel free pool on every session teardown.
Call close() so the fd is released deterministically.

Reported by @alfranli123 (#44037), who pinpointed the exact code location.
Note: the report's TLS-fd-recycle corruption attribution could not be
reproduced from the code — dropping a sqlite connection flushes valid
SQLite pages via the VFS, never TLS framing, and the provider is at most a
releaser of DB fds, not a TLS-flushing socket owner. This change is correct
resource hygiene that removes per-session fd churn regardless.
2026-06-28 02:41:52 -07:00
Teknium d3d621f7c3
revert(windows): roll back terminal-popup PRs #53791 #53810 #53829 (#53853)
* Revert "fix(windows): capture is not a no-window boundary; route flashing spawns through chokepoint (#53829)"

This reverts commit 2ecca1e7d3.

* Revert "fix(windows): stop terminal-window popups from background spawns (#53810)"

This reverts commit 5db1430af9.

* Revert "fix(windows): stop subprocess console-window popups + add CI guard (#53791)"

This reverts commit ef17cd204d.
2026-06-27 15:59:00 -07:00