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.
_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.
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.
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.
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.
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
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 dca57915b9)
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 0ca5a33063)
`_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 d1e5c3dc33)
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 8346403a4b)
`_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 b49427d85f)
## 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 8fa607d0ae)
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.
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
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).
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.
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.
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>
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.
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.
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.
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>
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>
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.
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)
`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)
`_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)
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.
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
- 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
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.
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.
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).
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.