From 26944e25bc0302e1f96e7689e08806f3782c68ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Mon, 23 Mar 2026 16:11:57 +0100 Subject: [PATCH] fix(claude-code): pre-start daemon in background on SessionStart hook (#663) Daemon cold start takes ~25s but hooks have short timeouts, causing retain to time out on first use. Fix by firing daemon startup as a detached background process in SessionStart so it warms up before the first recall/retain hook fires. Also bumps the daemon start timeout in _ensure_daemon_running from 10s to 30s as a fallback for when retain fires before pre-start completes. --- .../claude-code/scripts/lib/daemon.py | 63 ++++++++++++++++++- .../claude-code/scripts/session_start.py | 8 ++- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/hindsight-integrations/claude-code/scripts/lib/daemon.py b/hindsight-integrations/claude-code/scripts/lib/daemon.py index 26c63e02..9d8a2585 100644 --- a/hindsight-integrations/claude-code/scripts/lib/daemon.py +++ b/hindsight-integrations/claude-code/scripts/lib/daemon.py @@ -209,7 +209,7 @@ def _ensure_daemon_running(config: dict, port: int, debug_fn=None): config, ["daemon", "--profile", PROFILE_NAME, "start"], daemon_env, - timeout=10, + timeout=30, ) if debug_fn: debug_fn(f"Daemon start exit={result.returncode} stdout={result.stdout.strip()}") @@ -242,6 +242,67 @@ def _ensure_daemon_running(config: dict, port: int, debug_fn=None): raise RuntimeError("Daemon failed to become ready within 30 seconds") +def prestart_daemon_background(config: dict, debug_fn=None): + """Fire off daemon startup in the background — non-blocking. + + Called from SessionStart hook to warm up the daemon before the first + recall or retain hook fires. Returns immediately; the daemon starts + asynchronously as a detached OS process. + """ + if config.get("hindsightApiUrl"): + return # External API mode — no local daemon needed + + port = config.get("apiPort", 9077) + if _check_health(f"http://127.0.0.1:{port}"): + if debug_fn: + debug_fn(f"Daemon already running on port {port}, skipping pre-start") + return + + if not _is_embed_available(config): + if debug_fn: + debug_fn("hindsight-embed not available, skipping pre-start") + return + + try: + llm_config = detect_llm_config(config) + except RuntimeError as e: + if debug_fn: + debug_fn(f"No LLM configured, skipping daemon pre-start: {e}") + return + + llm_env = get_llm_env_vars(llm_config) + daemon_env = dict(os.environ) + daemon_env.update(llm_env) + idle_timeout = config.get("daemonIdleTimeout", 300) + daemon_env["HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT"] = str(idle_timeout) + if platform.system() == "Darwin": + daemon_env["HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU"] = "1" + daemon_env["HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU"] = "1" + + embed_cmd = _get_embed_command(config) + + profile_args = ["profile", "create", PROFILE_NAME, "--merge", "--port", str(port)] + for env_name, env_val in llm_env.items(): + if env_val: + profile_args.extend(["--env", f"{env_name}={env_val}"]) + + import shlex + profile_str = shlex.join(embed_cmd + profile_args) + daemon_str = shlex.join(embed_cmd + ["daemon", "--profile", PROFILE_NAME, "start"]) + + import subprocess as _sp + _sp.Popen( + f"{profile_str} && {daemon_str}", + shell=True, + env=daemon_env, + stdout=_sp.DEVNULL, + stderr=_sp.DEVNULL, + start_new_session=True, + ) + if debug_fn: + debug_fn(f"Daemon pre-start initiated in background (port {port})") + + def stop_daemon(config: dict, debug_fn=None): """Stop the daemon if it was started by this plugin. diff --git a/hindsight-integrations/claude-code/scripts/session_start.py b/hindsight-integrations/claude-code/scripts/session_start.py index 2d8e308a..9e7fee0f 100755 --- a/hindsight-integrations/claude-code/scripts/session_start.py +++ b/hindsight-integrations/claude-code/scripts/session_start.py @@ -17,7 +17,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from lib.client import HindsightClient from lib.config import debug_log, load_config -from lib.daemon import get_api_url +from lib.daemon import get_api_url, prestart_daemon_background def main(): @@ -45,8 +45,10 @@ def main(): client = HindsightClient(api_url, config.get("hindsightApiToken")) debug_log(config, f"Hindsight server reachable at {api_url}") except (RuntimeError, ValueError) as e: - # Server not available — log but don't block session - debug_log(config, f"Hindsight not available at session start: {e}") + # Server not running — kick off background pre-start so it's ready + # by the time the first recall or retain hook fires. + debug_log(config, f"Hindsight not running, initiating background pre-start: {e}") + prestart_daemon_background(config, debug_fn=_dbg) return