Commit Graph

158 Commits

Author SHA1 Message Date
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
Ben
34c727c5c2 feat(hindsight): memory provider improvements — recall_sync, retain_source, setup templates, memory indicators, error hints
Bundles previously-separate Hindsight/memory PRs into a single review surface:
- opt-in synchronous recall (recall_sync) — recall the injected memory in-turn instead of next-turn prefetch (#5820)
- actionable error when local_embedded runtime is missing — tells the user which package to install (#7718)
- default retain_source to 'hermes' so every stored memory self-identifies its provenance
- offer a starter memory template during hermes memory setup, plus warn before overwriting an already-configured bank
- warn when a configured memory provider reports unavailable (#2765)
- deterministic 'recalled N memories' recall indicator — Hermes itself emits a status line when auto-recall injects memory
- 'saving to memory' retain indicator — emitted the moment a turn is dispatched to the writer

Authored by @benfrank241 (ben.bartholomew@vectorize.io).
Salvaged from PR #74379.
2026-08-13 23:15:25 +05:30
kshitij
6f3dcabfeb refactor(openviking): reuse _headers() and _status_code_from_error()
Simplify-code findings:
- _authenticated_json: replace manual header construction with
  self._headers(include_tenant=False) — eliminates duplication with
  _headers() and includes Content-Type consistently.
- _health_requires_credentials: replace getattr(exc, 'status_code')
  with _status_code_from_error(exc) for consistency with the existing
  error-classification utility. Drop the fragile string-matching
  fallback — _parse_response always sets status_code on
  _OpenVikingHTTPError, so 401/403 check is sufficient.
- Relax test header assertions to check presence/absence of specific
  headers rather than exact dict equality, so they survive the
  header-construction refactor.
2026-08-13 15:06:22 +05:30
Slobaka
d976670081 fix(memory): authenticate OpenViking cloud /health when anonymous probe fails
Hosted OpenViking (Volcengine) rejects anonymous GET /health with
AuthenticationError, which made the provider look unhealthy and silently
disabled automatic memory mirroring. Keep the anonymous probe first for
identity safety, then retry once with the configured API key only when
the server demands credentials.

Fixes #78410
2026-08-13 15:06:22 +05:30
ehz0ah
a49a9e5e37 fix(openviking): verify servers before sending credentials 2026-08-03 20:35:47 +05:30
ehz0ah
e443d32718 test(retaindb): guard scoped secret config resolution 2026-08-03 20:35:47 +05:30
ehz0ah
e43bc0b7aa fix(openviking): integrate reliability and configuration hardening 2026-08-03 20:35:47 +05:30
PRATHAMESH75
5396dd8f02 fix(memory): read non-secret provider config from config.yaml for OpenViking and RetainDB
OpenViking is_available() only consulted env vars and use_ovcli_config, so an
endpoint saved to config.yaml (e.g. by the Dashboard) reported needs_config;
_resolve_connection_settings() likewise never folded config.yaml's non-secret
fields into its chain. RetainDB initialize() read base_url/project from the
environment only, ignoring the values the Dashboard writes to config.yaml.

Both now resolve non-secret fields as env -> (ovcli ->) config.yaml -> default;
secrets still come from the environment. Adds regression tests for both.

Fixes #68209

(cherry picked from commit dca57915b97b5705b30927a062e1d0f2f23d3841)
2026-08-03 20:35:47 +05:30
Jeff Mettel
f94914f773 test(openviking): cover the compression lifecycle, not a hand-set latch
Review feedback: the previous test called _mark_session_committed
directly, so it verified the guard's behavior but not the wiring that
sets it — a future break in the commit_memory_session -> same-id
compression-boundary path would not be caught.

Add a lifecycle regression that drives the real sequence: on_session_end
commits through the actual path, on_session_switch(same id,
reason="compression") crosses the boundary, sync_turn records a genuinely
new turn, and a second on_session_end must produce a second commit POST.

Without the fix it fails showing exactly one commit call, which is the
reported data loss: every turn after the first compression is dropped.
The rotation and /undo tests stay as scope guards.

(cherry picked from commit 0ca5a330630a30b105cbbc32e8a23f2c5ffe0eab)
2026-08-03 20:35:47 +05:30
Jeff Mettel
f0cb219e5e fix(openviking): re-arm the commit guard after in-place compression
`_committed_session_ids` is a permanent per-sid latch, and
`_session_needs_commit` checks it before the turn counter by design — a
racing sync_turn can re-increment `_turn_count` after commit+reset, so
the guard must win to stop a double-commit.

That is correct for a session being left behind. It is wrong for one
that keeps its id. `compress_context()` commits before rewriting the
transcript in both modes, and with `compression.in_place: true` (the
default) `on_session_switch` receives the same id and does not rotate.
The latch then rejects every later commit for a still-live session — the
next compression, /new, normal session end, startup recovery — so every
post-compression turn is silently never extracted.

Rotation mode is unaffected because a fresh child id is minted and
starts clean, which is what confirms the latch's intent was only ever to
dedupe the departing id.

Clear the latch when compression completes without rotation. Turns
arriving after that point are genuinely new, and this is a defined
moment rather than a race. The rotation path is untouched, so the old
id stays latched and its _finalize_session_async still dedupes against
the compression commit.

Fixes #74695

(cherry picked from commit d1e5c3dc33ef0d43d021662674e1a7cd5e43eecd)
2026-08-03 20:35:47 +05:30
ddy4633
9014aa0263 fix(openviking): drop stale "disabled for this Hermes run" warnings
The provider used to disable OpenViking permanently when the server was
unreachable. That was fixed: `_ensure_client()` now reconnects lazily,
with a 30s cooldown gate in `_ensure_client_locked`.

Only one of the seven user-facing warnings was updated to match. The
other six still told the user memory was "disabled for this Hermes run",
which is no longer true — every one of those paths is retried on the next
access. A user who reads the old message has no reason to retry, which is
very likely how #5721 ("never recovers") came to be filed against
behaviour that already recovers.

All six sites were traced to confirm none is terminal for the run: the
`initialize()`-time and waiter-thread failures never arm `_failed_refresh`
(only line 2439 does), so they retry on the very next access with no
cooldown at all.

The replacement wording deliberately omits the "(after cooldown)"
parenthetical used at the already-correct site — that detail is only
accurate where `_failed_refresh` was just armed. The neutral phrasing is
true at all six.

Also promotes two clause separators to periods to avoid "…; …disabled;"
collisions.

(cherry picked from commit 8346403a4b97af503d26b0f7905ff513828d821e)
2026-08-03 20:35:47 +05:30
Jeff Mettel
a3f6953f1a fix(openviking): don't spawn a second server onto a live port
`_start_local_openviking_server()` spawned `openviking-server`
unconditionally. Both callers — `initialize()` and the runtime
unreachable handler — reach it from a health probe, and that probe can
time out client-side while the server is up and serving. The spawned
process then loses the data-directory lock and exits immediately with
`DataDirectoryLocked`; because the probe keeps timing out, the cycle
repeats every cooldown window (~5 min observed).

The existing 30s `_failed_refresh` cooldown paces the loop but cannot
stop it, since it expires while the underlying condition persists.

Probe the target host:port before spawning and treat an occupied port as
already-started. This guards both call sites at their single convergence
point. The probe deliberately tests only that a listener owns the port —
enough to know a second server would lose the lock — and says nothing
about that listener's health.

The parse/probe now precedes the PATH lookup, so a reachable server is
reported as running even when `openviking-server` is not on PATH.

Fixes #74846

(cherry picked from commit b49427d85fd6628eb4a7fe099e5c390c5c4cc935)
2026-08-03 20:35:47 +05:30
峯岸 亮
c7fd21add3 fix(security): reject always-blocked OpenViking endpoints
## Summary
- Normalize OpenViking endpoints through `is_always_blocked_url` and fall back to the default local endpoint when poisoned.
- Keep intentional loopback / LAN self-host working.
- Add focused unit tests.

## Salvage / credit
Memory-provider endpoint floor sibling of RetainDB/Supermemory always-blocked hardening (avoids over-broad #4984-style private-IP bans).

(cherry picked from commit 8fa607d0aedb8c5fca398d7f112b1b25ade54fa2)
2026-08-03 20:35:47 +05:30
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
kshitij
a2f95e4c0e test(memory): hoisted-retriever fixture uses a real tmp db, not :memory:
Review follow-up on the #76142 salvage: MemoryStore path-resolves and
shares one process-wide connection per file, so MemoryStore(":memory:")
creates a literal ./:memory: FILE whose state leaks across test runs —
the second run of the file failed all three spy tests because the
NULL-vector test had permanently wiped hrr_vector in the leaked db.
tmp_path isolates each run; verified two consecutive runs green + full
tests/plugins/memory/ green.
2026-08-02 21:16:34 +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
Kyzcreig
9c28600e77 test: prove concurrency with barriers/witnesses instead of wall-clock bounds
Replace OS-scheduler-dependent elapsed-time ceilings with deterministic
concurrency proofs in three tests:

- test_context_refs_concurrent: asyncio.Barrier rendezvous — all 3 URL
  fetches must be in flight simultaneously before any returns.
- test_memory_boundary_commit: positive non-blocking witness — the
  provider call list must still be empty when the async commit returns.
- test_mem0_v3 slow-prefetch: threading.Event park/release — prefetch
  must return while the backend search is still parked.

The tests/tools/test_mcp_tool.py hunk from the original PR is dropped:
main no longer carries the 'elapsed < 2.5' assertion it targeted
(superseded by a delay-relative bound).

Salvaged from #71913.
2026-07-29 21:30:53 -07:00
webtecnica
66c4c9c0b1 fix(tests): forward Windows location vars through the hermetic runner; patch Path.home() in hindsight _clean_env
Combines the Windows-hermeticity cluster (#67512 by @webtecnica, earliest;
#71112 by @Sanjays2402; #67196 by @anatolijlaptev1991-ctrl) into one fix:

- scripts/run_tests.sh: env -i forwarded only HOME, but native Windows
  CPython resolves Path.home() from USERPROFILE (or HOMEDRIVE+HOMEPATH),
  stdlib paths from LOCALAPPDATA/APPDATA, ssl/sockets need SYSTEMROOT,
  tempfile needs TEMP/TMP — the strip broke collection tree-wide on
  native Windows (issues #67385, #70813). Location vars (never
  credentials) are now forwarded, each only when actually set, so
  POSIX runs are byte-for-byte unchanged (probe-verified both ways).
  PYTHONUTF8=1 added for legacy-codepage consoles printing the
  runner's glyphs.
- tests/plugins/memory/test_hindsight_provider.py: _clean_env patched
  HOME only; on Windows Path.home() ignores HOME. Now patches
  Path.home directly into tmp_path (from #67196).

Not ported: #71112's guard test — it regex-reads run_tests.sh source,
which the test policy bans (never read source code in tests).

Fixes #67385. Fixes #70813.
2026-07-29 18:55:10 -07:00
Teknium
6b81590c55 test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00