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.
This commit is contained in:
parent
e6333719ee
commit
26944e25bc
2 changed files with 67 additions and 4 deletions
|
|
@ -209,7 +209,7 @@ def _ensure_daemon_running(config: dict, port: int, debug_fn=None):
|
||||||
config,
|
config,
|
||||||
["daemon", "--profile", PROFILE_NAME, "start"],
|
["daemon", "--profile", PROFILE_NAME, "start"],
|
||||||
daemon_env,
|
daemon_env,
|
||||||
timeout=10,
|
timeout=30,
|
||||||
)
|
)
|
||||||
if debug_fn:
|
if debug_fn:
|
||||||
debug_fn(f"Daemon start exit={result.returncode} stdout={result.stdout.strip()}")
|
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")
|
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):
|
def stop_daemon(config: dict, debug_fn=None):
|
||||||
"""Stop the daemon if it was started by this plugin.
|
"""Stop the daemon if it was started by this plugin.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
from lib.client import HindsightClient
|
from lib.client import HindsightClient
|
||||||
from lib.config import debug_log, load_config
|
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():
|
def main():
|
||||||
|
|
@ -45,8 +45,10 @@ def main():
|
||||||
client = HindsightClient(api_url, config.get("hindsightApiToken"))
|
client = HindsightClient(api_url, config.get("hindsightApiToken"))
|
||||||
debug_log(config, f"Hindsight server reachable at {api_url}")
|
debug_log(config, f"Hindsight server reachable at {api_url}")
|
||||||
except (RuntimeError, ValueError) as e:
|
except (RuntimeError, ValueError) as e:
|
||||||
# Server not available — log but don't block session
|
# Server not running — kick off background pre-start so it's ready
|
||||||
debug_log(config, f"Hindsight not available at session start: {e}")
|
# 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
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue