perf(orchestrator): skip blocking Lore memory prefetch on parent agent - #3
perf(orchestrator): skip blocking Lore memory prefetch on parent agent#3davidgut1982 wants to merge 1 commit into
Conversation
The parent orchestrator calls MemoryManager.prefetch_all() ->
kb_search(top_k=30) to Lore synchronously at the start of every
run_conversation (~4.3s/request), before the first LLM call. The parent
never uses the result for routing: SOUL.md delegates ALL recall to the
`memory` child profile. The prefetch is therefore pure latency.
Pass skip_memory=True at every user-facing PARENT/orchestrator
construction point, which leaves _memory_manager=None so the prefetch is
never reached. Delegated children build their own memory manager
independently and are unaffected — recall still works when the parent
delegates to a memory-capable child.
Parent construction points changed:
- gateway/platforms/api_server.py _create_agent (HTTP /v1/chat/completions)
- gateway/run.py _run_agent (Telegram/Discord/Slack inbound)
- gateway/run.py _run_background_task (gateway background tasks)
- tui_gateway/server.py _make_agent (TUI / dashboard chat; was
conditional on HERMES_IGNORE_RULES,
now forced True for the parent)
- tui_gateway/server.py _background_agent_kwargs (TUI background tasks)
Tradeoff (passive writes): the parent's _memory_manager is also what
performs the post-turn passive transcript write
(_sync_external_memory_for_turn -> sync_all -> provider.sync_turn) and
queue_prefetch_all. With skip_memory=True these passive writes no longer
fire for PARENT-handled turns. Explicit memory(action="add") / kb_add via
the MCP knowledge server are unaffected, and delegated children still
sync their own turns. Surfaced for human review before deploy.
Tests:
- tests/gateway/test_api_server_toolset.py: assert _create_agent passes
skip_memory=True.
- tests/gateway/test_orchestrator_skip_memory.py: assert the TUI parent
helpers pass skip_memory=True; assert a real AIAgent(skip_memory=True)
yields _memory_manager is None; assert the delegated child construction
carries its own skip_memory kwarg (parent value does not leak in).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
1 |
First entries
tests/gateway/test_orchestrator_skip_memory.py:21: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
✅ Fixed issues: none
Unchanged: 4957 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
|
Closing — superseded by Lore-side performance fixes (kb_get batching + Postgres GIN/random_page_cost planner fix + semantic default recall). Those dropped the parent prefetch from ~4.3s to ~115ms (measured, 40x), so skip_memory would now save only ~115ms while losing passive memory — a bad trade. Keeping this branch for reference; the change still validly decouples the router from Lore availability if ever needed, but it should not merge. |
…NousResearch#34192) (NousResearch#34382) NousResearch#34192 reports Hostinger's 'Hermes WebUI' catalog crashes on startup with: /usr/bin/tini: No such file or directory The image moved from tini to s6-overlay as PID 1 (/init) earlier in 2026. Orchestration templates that still pin /usr/bin/tini as the entrypoint \u2014 like the Hostinger Hermes WebUI catalog \u2014 have no binary to exec and the container crashes immediately. Hermes has no control over the Hostinger catalog template, but we can make the image backward-compatible by symlinking /usr/bin/tini -> /init during the s6-overlay install step. External wrappers that exec /usr/bin/tini will land on the same s6-overlay reaper they would have landed on if they'd used the canonical /init entrypoint. The image's own ENTRYPOINT continues to be /init verbatim \u2014 the shim is purely for legacy external wrappers, not for the image's own runtime path. Once affected catalogs are updated, the symlink can be removed. Other issues NousResearch#34192 raises that are NOT addressed by this PR: * Problem #2 (UID 1024 vs 10000 mismatch): already fixed by NousResearch#33148 (S6_KEEP_ENV=1) and NousResearch#32412 (with-contenv shebangs). The Hostinger template likely needs to update its env-var propagation. * Problem #3 (incompatible session formats): RFC for pluggable SessionDB is tracked in NousResearch#23717. * Problem #4 (Telegram polling conflict): an operations problem on Hostinger's side, not in this codebase. This PR is scoped to the one issue that can be fixed inside Dockerfile: the missing /usr/bin/tini binary. Tests (3 in test_dockerfile_tini_compat_shim.py): - test_tini_compat_symlink_present Guard: the symlink line must exist in Dockerfile. - test_tini_compat_comment_explains_why The NousResearch#34192 anchor comment must be present so future readers know why the shim is there (avoid accidental removal). - test_entrypoint_still_init_not_tini Sanity check: ENTRYPOINT remains /init (s6-overlay). The shim is only for external wrappers. Refs: NousResearch#34192 Partial fix: addresses the immediate tini-binary crash. Catalog-side fixes still needed by Hostinger for the UID and session-format problems documented in the issue. Co-authored-by: Cursor <cursoragent@cursor.com>
…bes + test-leak fix (NousResearch#40909) * fix(gateway,windows): reliability — supervisor task, JOB breakaway, status --deep Three coordinated fixes for the Windows gateway reliability story: 1. CREATE_BREAKAWAY_FROM_JOB on every detached spawn The 'hermes update' triggered from the Electron Desktop GUI ran inside Electron's job object. Without breakaway, the post-update gateway watcher spawned by update — already DETACHED_PROCESS — was still reaped when Electron's job tore down, so the gateway never came back after a GUI-initiated update. Adds CREATE_BREAKAWAY_FROM_JOB (0x01000000) to: - hermes_cli/_subprocess_compat.py::windows_detach_flags() — used by every helper that calls windows_detach_popen_kwargs(), including launch_detached_profile_gateway_restart() - The watcher subprocess's own respawn snippet in hermes_cli/gateway.py (inlined flags so the watcher's child respawn also breaks away) _spawn_detached() in gateway_windows.py already had the flag; this change brings the rest of the codebase to parity. 2. Per-minute supervisor Scheduled Task — Windows equivalent of systemd Restart=always Introduces hermes_cli/gateway_supervisor.py and registers it as a second Scheduled Task ('Hermes_Gateway_Supervisor', SC MINUTE /MO 1, LIMITED rights) alongside the existing ONLOGON task. Every minute, the supervisor uses the same gateway.status.get_running_pid() probe as 'hermes gateway status' and, if no gateway is alive, calls gateway_windows._spawn_detached() (which now includes BREAKAWAY) to bring one back. Covers every crash mode, not just 'machine rebooted': taskkill, OOM, GUI update SIGTERM, parent job teardown. Cheap — one pythonw startup per minute when down, one PID-existence check per minute when up. Wired into both the schtasks-success and Startup-folder-fallback install paths via _install_supervisor_best_effort(), and removed in uninstall(). Best-effort: a failing supervisor install logs a warning but doesn't roll back the primary install. 3. 'hermes gateway status --deep' shows per-probe PASS/FAIL Replaces the existing terse '--deep' output (which only printed paths) with an actual diagnostic table: [1] PID file present [2] Lock file held by a live process [3] get_running_pid() result [4] _pid_exists(pid) — OS-level liveness [5] gateway_state.json (state + age) [6] Last lifecycle event from gateway-exit-diag.log When the high-level summary disagrees with reality, the user can see exactly which signal is lying. Test-leak fix ------------- tests/hermes_cli/test_gateway_wsl.py::TestGatewayCommandWSLMessages monkey-patched is_linux/is_wsl/supports_systemd_services to simulate WSL but did NOT stub is_windows(). On a Windows host, the dispatcher in _gateway_command_inner takes the is_windows() branch BEFORE the WSL guidance branch, so the test invoked gateway_windows.install() for real. install() writes to %APPDATA%\...\Startup\Hermes_Gateway.cmd — the REAL user Startup folder, never sandboxed by tmp_path — pointing at the test's pytest-of-<user>/pytest-<N>/.../gateway-service/ wrapper. When pytest tore down the tmp_path, every subsequent Windows login flashed a cmd.exe window that failed to find the missing target. Stubs is_windows=False on all four affected tests: test_install_wsl_no_systemd test_start_wsl_no_systemd test_status_wsl_running_manual test_status_wsl_not_running Defense-in-depth: _build_startup_launcher() now prefixes the launcher with 'if not exist <target> exit /b 0', so any future stale Startup entry silently no-ops instead of flashing a console window. Status enhancements ------------------- - status() now reports supervisor task presence alongside the existing schtasks/Startup info, and nudges the user to reinstall if the supervisor isn't registered. - Deep mode dumps both the supervisor task name + script path. * fix(gateway,windows): drop the per-minute supervisor task — keep breakaway + deep probes Earlier in this branch we added a per-minute schtasks-based supervisor to respawn the gateway after crashes / GUI-update SIGTERMs. The implementation flashed a brief console window on every firing, which stole window focus. We tried several variants: - cmd.exe wrapper invoking pythonw -> flashes (cmd.exe is console-subsystem) - schtasks /TR pointing at pythonw -> flashes (uv venv launcher pythonw is actually subsystem=Console, not GUI; it respawns the real pythonw) - schtasks /TR pointing at base uv -> still flashes (Task Scheduler-side conhost preallocation; documented Windows quirk) - XML registration with <Hidden>true> -> still flashes (<Hidden> only hides the task in the Task Scheduler UI, not the spawned window) Researched what leading projects do: - Ollama: GUI-subsystem tray exe + Startup-folder shortcut. No supervisor. - Tailscale: real Windows Service via SCM. Session 0, no console possible. - Syncthing: --no-console flag inside the binary + Startup folder. - openclaw: VBS Run(..., 0, False) wrapper. Suppresses the *window* but Super User Q971162 confirms focus-steal still occurs in some cases. None of these use a per-minute polling scheduled task. The 'auto-restart on crash' responsibility belongs INSIDE the daemon (Tailscale's in-process recovery / Ollama's monitor+worker pair) OR is delegated to the Windows Service Control Manager — not Task Scheduler. So this commit drops the supervisor entirely. The CREATE_BREAKAWAY_FROM_JOB fix in _subprocess_compat.py (from commit c1e5fa4) survives — that is the *real* fix for problem #2 (GUI-update kills gateway): the post-update watcher in launch_detached_profile_gateway_restart() now breaks out of Electron's job object, so the gateway respawn watcher survives the GUI quit and successfully respawns the gateway. Surviving from c1e5fa4: * CREATE_BREAKAWAY_FROM_JOB in hermes_cli/_subprocess_compat.py (fixes #2) * Inlined breakaway flag in the watcher respawn snippet in gateway.py * hermes gateway status --deep PASS/FAIL probes (fixes #1 — visibility) * 'if not exist <target> exit /b 0' guard in _build_startup_launcher (fixes #3 — silent no-op for stale Startup entries) * tests/hermes_cli/test_gateway_wsl.py is_windows=False stubs (root cause of #3 — pytest WSL tests no longer leak Startup entries on Win hosts) Removed in this commit: * hermes_cli/gateway_supervisor.py (entire file) * Supervisor section in hermes_cli/gateway_windows.py (~180 lines): get_supervisor_task_name, get_supervisor_script_path, _build_supervisor_cmd_script, _write_supervisor_script, _install_supervisor_task, is_supervisor_task_registered, _install_supervisor_best_effort * _install_supervisor_best_effort() calls in install() (3 spots) * supervisor cleanup block in uninstall() * supervisor display lines in status() / status(deep=True) Future direction (out of scope for this PR): the right place for Windows 'Restart=always' semantics is a real Windows Service installed via pywin32's win32serviceutil.ServiceFramework — session-0 isolation, SCM auto-restart, no console window possible. That's a meaningful next-PR project, not a band-aid. Tests: 51 pass / 2 pre-existing failures in tests/hermes_cli/test_gateway_{windows,wsl}.py (the 2 failures are TestSupportsSystemdServicesWSL cases that fail on origin/main too — unrelated to this PR).
What it does
The parent orchestrator calls
MemoryManager.prefetch_all()→kb_search(top_k=30)to Lore (CT 121) synchronously at the start of everyrun_conversation()(agent/conversation_loop.py~654), before the first LLM call. Measured ~4.3s on every request.The parent never uses that result for routing:
SOUL.mdexplicitly delegates ALL recall to thememorychild profile. The prefetch is therefore pure latency the orchestrator pays and discards.This PR passes the existing
skip_memory=Trueconstructor flag at every user-facing PARENT/orchestrator construction point. When set,agent_init.pyleaves_memory_manager=Noneand the prefetch is never reached. No change toconversation_loop.pyormemory_manager.py— the prefetch logic is untouched; we simply don't build a memory manager on the parent.Why it's safe
memorychild — the parent never consults prefetch output for routing decisions, so dropping it changes no routing behavior._memory_managerindependently (the genericdelegate_taskpath intools/delegate_tool.pyand profile-based children decide their ownskip_memory). Setting the flag on the parent does not flow into the child constructor — recall still works when the parent delegates to a memory-capable child.memory(action="add")andkb_addvia the MCP knowledge server are independent of the parent's_memory_managerprefetch path and continue to work._memory_manager=Noneacross turns (the cache stores the constructed instance;_init_cached_agent_for_turnnever rebuilds the manager), so the saving applies to every turn in a session, not just the first.Tradeoff surfaced for review — passive memory writes
The parent's
_memory_manageris also what performs the post-turn passive transcript write:conversation_loop.py(~4261) →AIAgent._sync_external_memory_for_turn(run_agent.py~2104) →MemoryManager.sync_all→provider.sync_turn, plusqueue_prefetch_allto warm the next turn.With
skip_memory=True, these passive writes no longer fire for PARENT-handled turns. Concretely, what stops accumulating in Lore from the parent path: the automatic(user, assistant)turn-pair transcript capture and the next-turn prefetch warming.What is not lost:
memory(action="add")writes andkb_addvia the MCP knowledge server.skip_memory=True(and re-binds built-inMEMORY.md/USER.mdfrom the parent), so it is unchanged.Net: passive parent-transcript accumulation into Lore stops; child-driven memory and all explicit writes continue. This is the one real behavioral change — flagged for a human to accept before deploy.
Parent construction points changed
gateway/platforms/api_server.py_create_agent/v1/chat/completionsgateway/run.py_run_agentgateway/run.py_run_background_tasktui_gateway/server.py_make_agentHERMES_IGNORE_RULES; now forcedTruefor the parent)tui_gateway/server.py_background_agent_kwargsConstruction points intentionally not changed:
tools/delegate_tool.py_build_child_agent— alreadyskip_memory=True(children); left as-is so child memory scoping is unchanged.gateway/platforms/feishu_comment.py, gateway_handle_compress_command, the gateway hygiene/compaction agent,background_review/curator— alreadyskip_memory=True.Flagged for reviewer
acp_adapter/session.py~624) is another user-facing top-level parent that currently builds a memory manager and pays the same ~4.3s prefetch. It was outside the stated scope (api_server / telegram / TUI) so it is left unchanged here — call out if you want it included.Tests
tests/gateway/test_api_server_toolset.py— assert_create_agentpassesskip_memory=True.tests/gateway/test_orchestrator_skip_memory.py(new) — assert the TUI parent helpers passskip_memory=True; assert a realAIAgent(skip_memory=True)yields_memory_manager is None; assert the delegated-child construction carries its ownskip_memorykwarg (parent value does not leak into the child).Targeted run:
17 passed. Fulltests/gateway/suite:5862 passedwith 9 pre-existing, unrelated failures (telegram-markdown-escaping + wecom-callback test-isolation flakes) that reproduce identically onmainwithout this change.🤖 Generated with Claude Code