Repository navigation
fix(embed): stop the HTTP probe clearing its caller's event loop, and make the Hermes gate test our plugin - #4575
Merged
Merged
Conversation
`probe_get` ran `asyncio.run` on the calling thread. That does not just close the loop it creates — it leaves the thread with NO current event loop. Event loop state is per-thread, so a library helper doing this quietly sabotages any sync caller that probes between its own calls: hindsight-client's `_run_async` then finds no current loop, builds a fresh one, and issues the next request through an aiohttp session still bound to the old one, which fails with "Timeout context manager should be used inside a task". That is exactly the Hermes memory plugin in local_embedded mode, where a daemon health probe sits between two client calls, so embedded memory is broken on main today. Introduced by #4318 (httpx -> aiohttp), after the last release — which is why it reproduces against working-tree wheels but not against PyPI. The probe now always runs on its own loop in a helper thread, deleting the special case rather than adding one. The regression test pins the invariant (the caller's loop survives a probe) and fails on the previous implementation. test(hermes): make the compat gate exercise the plugin, not Hermes' copy The provider left the Hermes tree, but the script still tested Hermes' bundled copy: bundled wins on a name collision, and steps 4-6 never executed a line of plugin code. Two new steps close that. Step 3 removes the bundled provider, installs this checkout's plugin into $HERMES_HOME/plugins, runs `hermes plugins validate` (the catalog admission gate, so a change that would be rejected upstream fails here instead of in their review queue), and asserts the loaded provider resolves to the external directory. It also means the script keeps working the day Nous drops the bundle. Step 7 drives the MemoryProvider contract agent_init uses — is_available, initialize, sync_turn, prefetch, recall_status, the recall tool, on_session_end, shutdown — polling for extraction rather than sleeping, so a slow runner does not flake. Step 5 additionally asserts all four Hindsight distributions are still the locally built wheels. The plugin self-installs hindsight-all from PyPI when local_embedded has no runtime, which is right for a user and fatal here: it would swap the branch under review for a published release and leave the script passing on code nobody changed. CI: run on integrations-hermes changes (a plugin edit ran nothing before), pass an LLM key so the round-trips actually execute — they never had one, which is why the bug above went unnoticed — and raise the timeout for the added step.
Step 7 exits early when there is no LLM key, but the caller printed the same "round-trips a turn through Hermes' MemoryProvider contract" line either way — so a fork PR, which never has secrets, reported coverage it had not run. The step now exits 3 for "loaded fine, round-trip skipped" and the caller prints a line that says so. Any other non-zero status still propagates and fails the script.
`./scripts/hooks/lint.sh` reformats these on a clean checkout of main, so the drift was already there and every branch that runs lint picks it up. Formatting only — no behaviour change.
`hindsight-docs/docs-integrations/hermes.md` changed in dc75038 without re-running ./scripts/generate-docs-skill.sh, so verify-generated-files has been red on main since: the job regenerates every derived file and fails on any diff. Generated output only.
allyblockcast Bot
added a commit
to Blockcast/hindsight
that referenced
this pull request
Oct 18, 2026
…26.9.21 (BLO-47316) (#33) Hermes main moved the hindsight provider out of its tree and gated every core dependency on Python 3.14, so #32 pinned the gate to v2026.9.21. Upstream (vectorize-io/hindsight) already redesigned the gate for exactly this and it passes against Hermes main (e.g. run job 117087267621, hermes-agent@848fc0ad, 2026-10-16). Port it rather than re-derive it: - scripts/test-hermes-compat.sh from vectorize-io main (vectorize-io#4575, vectorize-io#4769, vectorize-io#4838): 3.14 venv, default ref main, installs THIS checkout's hindsight-integrations/hermes as an external plugin, runs `hermes plugins validate`, asserts the loaded provider is ours, asserts the four Hindsight distributions are the locally built wheels, and drives the MemoryProvider contract (step 7; skipped with an explicit message when no LLM key is present). - hindsight-embed _http_probe fix from vectorize-io#4575: 0.10.0's probe cleared the caller's event loop, breaking local_embedded between two client calls. - test.yml job changes from vectorize-io#4575 (integrations-hermes trigger, 60m timeout, LLM key env). No PM-runtime redesign is needed: the plugin is loaded from $HERMES_HOME/plugins inside a venv where Hermes and this checkout's wheels were co-resolved, which is the compatibility property the gate exists for. Signed-off-by: PlatformSREEngineer <platformsreengineer@paperclip.blockcast.net> Co-authored-by: PlatformSREEngineer <platformsreengineer@paperclip.blockcast.net> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug this found
hindsight-embed'sprobe_get()ranasyncio.run()on the caller's thread.asyncio.rundoesn't just close the loop it creates — it leaves the thread with no current event loop. Loop state is per-thread, so a library helper doing that sabotages any sync caller that probes between its own calls:hindsight-client._run_asyncthen finds no loop, builds a fresh one, and issues the next request through an aiohttp session still bound to the old one.Same script, two builds:
That is exactly the Hermes memory plugin in
local_embeddedmode, where a daemon health probe sits between two client calls — embedded memory is broken onmaintoday. Introduced by #4318 (httpx -> aiohttp), after the last release, which is why it reproduces against working-tree wheels but not PyPI.Fix: the probe always runs on its own loop in a helper thread, never the caller's. That deletes the existing special case rather than adding one, and fixes every caller at the shared function.
The regression test pins the invariant (the caller's loop survives a probe). I verified it actually catches the bug by reverting the fix — it fails with the exact
no current event looperror.Why it went unnoticed, and the gate change that surfaced it
test-hermes-compat.shhas never had an LLM key in CI, so the retain path it guards had literally never executed. And since the provider left the Hermes tree, the script was testing Hermes' bundled copy — bundled wins on a name collision, and steps 4-6 never ran a line of plugin code.hermes plugins validate(the catalog admission gate, so a change that would be rejected upstream fails here instead of in their review queue), and asserts which copy loaded. Also means the script keeps working the day Nous drops the bundle.MemoryProvidercontractagent_inituses:is_available->initialize->sync_turn->prefetch->recall_status->hindsight_recall->on_session_end->shutdown. Polls for extraction rather than sleeping, so a slow runner doesn't flake.hindsight-allfrom PyPI whenlocal_embeddedhas no runtime (right for a user, fatal here): it would swap the branch under review for a published release and leave the gate passing on code nobody changed.CI: runs on
integrations-hermeschanges (a plugin edit triggered nothing before), passes an LLM key so the round-trips execute, timeout 45 -> 60.Verification
Full script against
hermes-agent@main, all 7 steps:Plus 226
hindsight-embedtests, lint clean.Scope note
The branch started as a CI change and now carries the
hindsight-embedfix too. I kept them together because the fix is what makes the gate pass — splitting means landing a knowingly-red CI change. Happy to split if preferred.Follow-up, deliberately not fixed here
hindsight-client._run_asyncrelies on thread-global loop state while holding a long-lived aiohttp session. Any third-partyasyncio.runbreaks it the same way; the durable fix is the client owning its own loop. Worth its own issue.