Skip to content

feat(hindsight): memory provider improvements — recall_sync, retain_source, setup templates, memory indicators, error hints - #74379

Closed
benfrank241 wants to merge 13 commits into
NousResearch:mainfrom
benfrank241:feat/hindsight-memory-improvements
Closed

feat(hindsight): memory provider improvements — recall_sync, retain_source, setup templates, memory indicators, error hints#74379
benfrank241 wants to merge 13 commits into
NousResearch:mainfrom
benfrank241:feat/hindsight-memory-improvements

Conversation

@benfrank241

@benfrank241 benfrank241 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Bundles previously-separate Hindsight/memory PRs into a single review surface. Each was independently mergeable; combining them avoids parallel reviews of the same file (plugins/memory/hindsight/__init__.py, which most of them touch).

What's included

Supersedes Change Fixes
#70278 Opt-in synchronous recall for the current turn (recall_sync) — recall the injected memory in-turn instead of next-turn prefetch #5820
#70295 Actionable error when the local_embedded runtime is missing — tells the user exactly which package to install instead of a bare import failure #7718
#72926 Default retain_source to "hermes" so every stored memory self-identifies its provenance (metadata.source: "hermes")
#73415 Offer a starter memory template during hermes memory setup, plus warn before overwriting an already-configured bank
#70257 Warn when a configured memory provider reports unavailable (provider-agnostic) #2765
new Deterministic "recalled N memories" recall indicator — Hermes itself emits 👁️ Hindsight — recalled N memories via the status channel when auto-recall injects memory, so the user SEES memory working regardless of whether the model mentions it (recall_indicator, on by default)
new "saving to memory" retain indicator👁️ Hindsight — saving to memory… emitted the moment a turn is dispatched to the writer, only on real writes (retain_indicator, on by default)

Why the indicators

Auto-recall injects memory into the prompt, but whether the user can see that Hindsight contributed was previously left to the model — and models routinely decline to mention it, so memory looks like it isn't working even when it is. These indicators are rendered by Hermes itself (_emit_status, the same model-independent channel as compression/idle notices), so they always show and can't be silently dropped. Both have off switches for customer-facing agents.

Scope

  • Most changes are confined to plugins/memory/hindsight/** (+ their tests).
  • fix(memory): warn when a configured provider reports unavailable (#2765) #70257 and the indicators are (partly) provider-agnostic and also touch agent/agent_init.py, agent/memory_manager.py, agent/memory_provider.py, agent/turn_context.py, and hermes_cli/memory_setup.py — the recall indicator adds an opt-in recall_status() hook to the base MemoryProvider and a describe_recall() aggregator on MemoryManager; the status channel is wired to the provider via initialize(status_callback=…).

Tests

New/updated deterministic tests across all changes (provider behaviour incl. the recall_sync × indicator interaction, retain-indicator dispatch/off/buffered paths, template setup, local-runtime hint, provider-unavailable warning, env-hint, manager formatting, turn-loop emit wiring). Full affected suite green; ruff clean.

…Research#5820)

By default auto-recall runs in the background at the end of a turn and is
injected on the *next* turn, so `prefetch(query)` ignores the current query
and returns the previous turn's result. When the topic shifts between turns
(e.g. "fix linting" -> "fix tests") the injected memories can be stale.

Add a `recall_sync` config flag (default `false`, so existing latency
behavior is unchanged). When enabled, `prefetch()` runs a live recall against
the *current* message and injects those results, and `queue_prefetch()`
becomes a no-op (nothing to prime in the background).

Refactors the recall body out of the `queue_prefetch` closure into a shared
`_do_recall(query)` helper (plus `_recall_disabled()` / `_format_recall()`),
used by both the async and synchronous paths.

Tests: sync path recalls the current query synchronously and skips the
background queue; the default path still ignores the current query and reads
the buffer.
…ng (NousResearch#7718)

local_embedded imports `from hindsight import HindsightEmbedded`, which is
provided only by the `hindsight-all` package. plugin.yaml declares only
`hindsight-client` (enough for cloud / local_external), so a user who selects
local_embedded without running `hermes memory setup` — a hand-written config,
the legacy `"mode": "local"` alias, or a restored backup — hits
`ModuleNotFoundError: No module named 'hindsight'`.

`initialize()` already disables the provider with one warning in this case
(so the silent per-sync failure from the original report is gone), but the
message just echoes `No module named 'hindsight'` with no fix. Add an
actionable hint telling the user to install `hindsight-all` (or run
`hermes memory setup`), plus the distinction from `hindsight-client`.

Kept as a runtime hint rather than declaring `hindsight-all` in plugin.yaml:
that package pulls the full server stack (hindsight-api-slim[all], torch), so
declaring it unconditionally would bloat every cloud-only install.
…tion

The provider already stamps `metadata.source` on every retained memory from
the `retain_source` setting, but it defaulted to "" — so Hermes-originated
memories carried no source, and Hindsight had no clean signal that a memory
came from Hermes.

Default `retain_source` to "hermes" (via a new `_DEFAULT_RETAIN_SOURCE`
constant used across the config-load, __init__, schema, and initialize
defaults). Every retained memory now self-identifies as Hermes in
`metadata.source`, which Hindsight returns on recall — enabling provenance
and per-client analytics. Fully user-overridable: a `retain_source` in
config.json or `HINDSIGHT_RETAIN_SOURCE` still wins.
`hermes memory setup` left the user with a blank bank. Add an optional step
(cloud / local_external) that fetches the Hindsight Bank Templates catalog,
shows the ones tagged for Hermes, and applies the chosen manifest to the
bank via the import API — so the agent's memory arrives pre-configured with a
mission, dispositions, mental models, and directives for its use case.

- New `plugins/memory/hindsight/templates.py`: fetch catalog (filtered to the
  `hermes` integration), fetch a manifest, and POST it to
  `/v1/default/banks/{bank}/import` (which creates the bank). Catalog source is
  overridable via `HINDSIGHT_TEMPLATES_URL`.
- Wizard: after config is saved, offer a template picker (Blank is always an
  option). Best-effort and non-fatal — network/apply failures just skip.
- Skipped for local_embedded (its daemon isn't running during setup).
- Tests cover the hermes filter, manifest URL resolution, the import POST
  (endpoint + auth), and the picker orchestration (apply / blank / none / error).
…mplate step

Follow-ups on the setup-wizard starter-template step:

- Warn on re-apply: before applying a template, probe the bank (export
  endpoint). If it already has config / mental models / directives, confirm
  before overwriting ("Apply" vs "Keep existing"). Best-effort — a missing
  bank or any probe error is treated as not-customized and proceeds.
- Testable mode gate: extract `supported_for_mode()` (cloud / local_external)
  and use it in the wizard so local_embedded is provably skipped.
- Tests: apply-time failure (e.g. 401 for OAuth-only users) is swallowed with
  a hint; the customization probe (config present / empty / error); and the
  warn flow (keep-existing declines, confirm applies, fresh bank skips the
  prompt). 16 tests total.
…sResearch#2765)

A provider selected via `memory.provider` but reporting `is_available() ==
False` was dropped silently, leaving users with `memory.provider` set but no
memory and no diagnostic. The most common trigger is systemd/gateway services
not inheriting `~/.hermes/.env` (the CLI reads it via python-dotenv; services
need explicit `Environment=`).

- agent_init: emit a one-time, deduped warning naming the provider and the
  `.env`-inheritance gotcha. `is_available()` is a fast, side-effect-free
  hot-path check so it can't log for itself; dedup avoids the gateway's
  per-message AIAgent construction spamming the warning every turn.
- hermes memory status: surface the systemd/`.env` root cause in the
  "not available" block, alongside the existing missing-env-var checklist.
- tests for both paths.
@Xipong

Xipong commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Nice, its need

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard tool/memory Memory tool and memory providers area/memory Memory subsystem: store, providers, sync, background reviews labels Jul 29, 2026
Auto-recall injects memory into the prompt, but whether the user SEES that
Hindsight contributed was left to the model — and models (e.g. gpt-5.5)
routinely decline to mention it, so memory looks like it isn't working even
when it is.

Surface it deterministically instead: when prefetch injects memory, Hermes
itself emits a '🧠 Hindsight — recalled N memories' status line via
_emit_status (the same model-independent channel as compression/idle
notices). It always shows and can't be silently dropped by the model.

- MemoryProvider grows an opt-in recall_status() -> RecallStatus hook
  (default None); MemoryManager.describe_recall() aggregates + formats.
- Hindsight provider persists the recall count alongside the prefetch block
  and reports it; reflect mode has no discrete count so it renders generic.
- On by default with an off switch (recall_indicator=false) for
  customer-facing agents.
- Fast, deterministic unit tests at all three layers (provider count/stale/
  off/reflect, manager formatting/aggregation, turn-loop emit wiring).
Two follow-ups to the recall indicator, from live testing:

1. Use the Hindsight brand mark (the logo is an eye) instead of the brain
   emoji. Factored to INDICATOR_GLYPH + a glyph field on RecallStatus so the
   manager renders whatever the provider brands with — one source of truth.

2. Companion retain indicator: '👁️ Hindsight — saving to memory…' emitted the
   moment a turn is dispatched to the writer (past every skip/buffer gate, so
   it only fires on real writes). Retain runs in the background with no
   synchronous fact count, so this is a presence signal, not a count.
   Emitted via the agent status channel (agent._emit_status), injected into
   the provider through initialize(status_callback=). On by default with a
   retain_indicator off switch, mirroring recall_indicator.

Tests: 6 retain-indicator cases (dispatch/off/auto-retain-off/buffered/
no-callback/init-wiring); recall + turn-loop tests updated to the eye glyph.
All green; ruff clean.
@benfrank241 benfrank241 changed the title feat(hindsight): memory provider improvements — recall_sync, retain_source, setup templates, error hints feat(hindsight): memory provider improvements — recall_sync, retain_source, setup templates, memory indicators, error hints Jul 29, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for consolidating the Hindsight improvements. The current-main premise is still present for current-turn recall and unavailable-provider visibility, but two changes need revision before salvage.

Problems

  • The hindsight-all hint added at plugins/memory/hindsight/__init__.py:1376 is inside initialize(). Current main calls is_available() first (plugins/memory/hindsight/__init__.py:731-737) and only initializes a provider after it succeeds (agent/agent_init.py:1645-1647), so a missing embedded runtime never reaches this hint.
  • The new default source attribution conflicts with the explicit opt-in policy in AGENTS.md:118-121. The changed retain_source default should remain unset until the required generic opt-in exists.
  • plugins/memory/hindsight/README.md:91-100 still documents retain_source as optional and does not cover the new recall/indicator/template settings.

Suggested changes

  • Move the package-specific diagnosis to the unavailable-provider path and test that path through agent initialization.
  • Keep attribution opt-in, then update the Hindsight configuration documentation for the remaining settings.

Automated hermes-sweeper review.

"Hindsight local mode disabled because its runtime could not be imported: %s",
"Hindsight local mode disabled because its runtime could not be imported: %s.%s",
reason,
_local_runtime_hint(reason),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialize() is not reached when this condition matters: is_available() already calls _check_local_runtime() and agent_init only initializes providers that report available. Surface this package-specific reason from the unavailable-provider gate (or a provider unavailable-reason API) so a missing hindsight-all installation receives the actionable hint.

self._recall_tags_match = self._config.get("recall_tags_match", "any")
self._retain_source = str(
self._config.get("retain_source") or os.environ.get("HINDSIGHT_RETAIN_SOURCE", "")
self._config.get("retain_source") or os.environ.get("HINDSIGHT_RETAIN_SOURCE", _DEFAULT_RETAIN_SOURCE)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes third-party metadata.source attribution opt-out by default. AGENTS.md:118-121 requires a generic user-facing opt-in before adding attribution tags; keep the default empty unless that generic mechanism is available.

… retain_source, docs

Addresses hermes-sweeper review on NousResearch#74379:

1. Local-embedded install hint was unreachable. is_available() gates
   initialization, so a missing embedded runtime never reached the hint in
   initialize() (NousResearch#7718). Add MemoryProvider.unavailable_reason() (default ""),
   implement it in the Hindsight provider, and have agent_init's
   provider-unavailable warning surface it — the path that actually runs when a
   provider reports unavailable. Tested through that path.

2. retain_source no longer defaults to "hermes". AGENTS.md forbids on-by-default
   third-party attribution tags until a generic opt-in exists; default is now
   empty and metadata.source is stamped only when the user sets retain_source
   (config key / env var still honored).

3. README: document recall_sync / recall_indicator / retain_indicator, the
   starter-template setup step, and clarify retain_source is opt-in.
@benfrank241

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all three points were valid; addressed in bf1e0ba97.

1. Unreachable local_embedded hint. Correct — is_available() gates initialization, so a missing embedded runtime never reached the hint in initialize(). Added an opt-in MemoryProvider.unavailable_reason() hook (default ""), implemented it in the Hindsight provider (returns the hindsight-all install guidance for local modes when _check_local_runtime() fails), and had the provider-unavailable warning in agent_init — the path that actually runs — surface it. Tested through that path (test_provider_reason_is_appended, plus provider-level unavailable_reason cases for local-embedded/cloud/runtime-present).

2. retain_source default. Reverted to empty. metadata.source is now stamped only when the user sets retain_source (config key / HINDSIGHT_RETAIN_SOURCE still honored) — no attribution tag ships by default, per AGENTS.md. Tests updated to assert the opt-in behavior.

3. README. Fixed the retain_source row (clarified opt-in) and documented the new recall_sync / recall_indicator / retain_indicator settings and the starter-template setup step.

Full affected suite green; ruff clean.

…-improvements

# Conflicts:
#	tests/plugins/memory/test_hindsight_provider.py
@benfrank241

Copy link
Copy Markdown
Contributor Author

@teknium1 — all three points from the hermes-sweeper review are addressed in bf1e0ba97 (details in the comment above), and the branch is now merged up to main and green. Mind taking another look when you have a moment?

@teknium1 teknium1 added the sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages label Jul 30, 2026
@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
The merge with main left a redundant, unconditional
_init_kwargs["status_callback"] assignment alongside main's CLI-gated one
(added in the 308-commit catch-up). main only wires status_callback for
platform=="cli"; the unconditional copy leaked it into gateway provider init
and broke test_aiagent_forwards_user_id_alt_to_memory_provider (platform=feishu
asserts status_callback absent). Drop the duplicate — the retain indicator only
needs it on the interactive CLI, and no-ops gracefully when absent on gateways.
…-improvements

# Conflicts:
#	plugins/memory/hindsight/__init__.py
@benfrank241

Copy link
Copy Markdown
Contributor Author

Rebased on latest main (resolved the one conflict in plugins/memory/hindsight/__init__.py — merged the new read-after-write retain-drain in queue_prefetch with the recall-count refactor). CI is green and all three points from the earlier hermes-sweeper review are addressed.

@teknium1 — ready for another look whenever you have a moment. Happy to squash or split if that helps review.

@stepanov1975

Copy link
Copy Markdown
Contributor

I reproduced the required-check failure on exact head 3958d72ca locally: tests/agent/test_turn_context.py is 9 passed / 1 failed. This one is deterministic, not CI noise.

Root cause: test_recall_indicator_emitted_when_memory_injected calls _build(agent) with the helper default user_message="hello". Existing build_turn_context() intentionally treats that as a trivial prompt and skips prefetch_all(), so there is no recalled memory and no indicator to emit. The concurrent-turn warning in the log is incidental.

The focused fix is to make this test use a substantive query, e.g. _build(agent, user_message="what did we decide about the deploy pipeline?"), matching test_prefetch_runs_for_substantive_user_message above it. No production change is needed for this failure.

… tests

The merge with main brought in the is_trivial_prompt gate: build_turn_context
skips prefetch_all() for trivial prompts. The recall-indicator turn tests used
the _build() helper default user_message='hello', which is now trivial, so
prefetch never ran and test_recall_indicator_emitted_when_memory_injected
failed deterministically.

Give both indicator tests a substantive query so prefetch actually runs — the
positive test now exercises the emit path, and the negative test exercises the
'prefetch ran but returned nothing' path (rather than passing by being skipped
as trivial). Test-only; no production change. Thanks @stepanov1975 for the
precise root-cause.

tests/agent/test_turn_context.py: 10/10 pass.
@benfrank241

Copy link
Copy Markdown
Contributor Author

Thanks @stepanov1975 — spot-on diagnosis. The merge with main pulled in the is_trivial_prompt gate, and my recall-indicator turn tests were using the _build() helper default user_message="hello", which is now trivial, so prefetch_all() was skipped and the emit never happened.

Fixed in bc1cf5276: both indicator tests now pass a substantive query. The positive test exercises the emit path, and the negative one now exercises the "prefetch ran but returned nothing" path (rather than passing by being skipped as trivial). Test-only, no production change. tests/agent/test_turn_context.py is 10/10 locally.

@alt-glitch alt-glitch added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state and removed sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 6, 2026
kshitijk4poor pushed a commit that referenced this pull request Aug 13, 2026
…ource, 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.
kshitijk4poor added a commit that referenced this pull request Aug 13, 2026
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.
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Merged via #85494 using this PR's work. Your commits were cherry-picked with authorship preserved (rebase-merge). Thanks for the comprehensive Hindsight memory improvements!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/memory Memory subsystem: store, providers, sync, background reviews comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/memory Memory tool and memory providers type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants