@@ -71,7 +71,7 @@ class HindsightEmbedded:
7171 llm_model: Model name to use
7272 llm_base_url: Optional custom base URL for LLM API
7373 database_url: Optional database URL override (default: profile-specific pg0)
74- idle_timeout: Seconds before daemon auto-exits when idle (default: 300 )
74+ idle_timeout: Seconds before daemon auto-exits when idle (default: 0, disabled )
7575 log_level: Daemon log level (default: "info")
7676 ui: Whether to start the control plane web UI alongside the daemon (default: False)
7777 ui_port: Port for the UI. Defaults to daemon_port + 10000.
@@ -86,7 +86,7 @@ def __init__(
8686 llm_model : str = "openai/gpt-oss-120b" ,
8787 llm_base_url : Optional [str ] = None ,
8888 database_url : Optional [str ] = None ,
89- idle_timeout : int = 300 ,
89+ idle_timeout : int = 0 ,
9090 log_level : str = "info" ,
9191 ui : bool = False ,
9292 ui_port : Optional [int ] = None ,
@@ -102,7 +102,7 @@ def __init__(
102102 llm_model: Model name to use
103103 llm_base_url: Optional custom base URL for LLM API
104104 database_url: Optional database URL override
105- idle_timeout: Seconds before daemon auto-exits when idle
105+ idle_timeout: Seconds before daemon auto-exits when idle (0 = disabled)
106106 log_level: Daemon log level
107107 ui: Whether to start the control plane web UI alongside the daemon
108108 ui_port: Port for the UI (defaults to daemon_port + 10000)
@@ -142,14 +142,37 @@ def __init__(
142142 self ._memories_api : Optional [MemoriesAPI ] = None
143143
144144 def _ensure_started (self ):
145- """Ensure daemon is running (thread-safe)."""
145+ """Ensure daemon is running (thread-safe), restarting if crashed ."""
146146 if self ._started and self ._client is not None :
147- return
147+ if self ._manager .is_running (self .profile ):
148+ return
149+ # Daemon crashed — reset state and fall through to restart
150+ logger .warning (
151+ "Daemon for profile '%s' is no longer responsive, restarting..." ,
152+ self .profile ,
153+ )
154+ try :
155+ self ._client .close ()
156+ except Exception :
157+ logger .debug ("Error closing stale client" , exc_info = True )
158+ self ._client = None
159+ self ._started = False
148160
149161 with self ._lock :
150162 # Double-check after acquiring lock
151163 if self ._started and self ._client is not None :
152- return
164+ if self ._manager .is_running (self .profile ):
165+ return
166+ logger .warning (
167+ "Daemon for profile '%s' is no longer responsive (lock path), restarting..." ,
168+ self .profile ,
169+ )
170+ try :
171+ self ._client .close ()
172+ except Exception :
173+ logger .debug ("Error closing stale client" , exc_info = True )
174+ self ._client = None
175+ self ._started = False
153176
154177 if self ._closed :
155178 raise RuntimeError (
@@ -253,23 +276,10 @@ def __getattr__(self, name: str):
253276 This allows HindsightEmbedded to expose all HindsightClient methods
254277 without manually wrapping each one.
255278 """
256- # Ensure server is started before proxying
279+ # Ensure server is started (and restart if crashed) before proxying
257280 self ._ensure_started ()
258281
259- # Get the attribute from the underlying client
260- attr = getattr (self ._client , name )
261-
262- # If it's a callable, wrap it to ensure server is started
263- # (shouldn't be needed since _ensure_started already called, but defensive)
264- if callable (attr ):
265-
266- def wrapper (* args , ** kwargs ):
267- self ._ensure_started ()
268- return attr (* args , ** kwargs )
269-
270- return wrapper
271-
272- return attr
282+ return getattr (self ._client , name )
273283
274284 def __enter__ (self ):
275285 """Context manager entry - ensures server is started."""
@@ -394,11 +404,8 @@ def client(self) -> Hindsight:
394404 """
395405 Get the underlying Hindsight client for direct access.
396406
397- WARNING: Using this property directly means daemon restarts won't be
398- handled automatically. Prefer using the API namespaces (banks, mental_models,
399- directives, memories) or direct method calls on HindsightEmbedded instead.
400-
401- Ensures daemon is started before returning the client.
407+ Ensures daemon is started (and restarts it if it has crashed) before
408+ returning the client.
402409
403410 Returns:
404411 Hindsight: The underlying client instance
@@ -409,9 +416,8 @@ def client(self) -> Hindsight:
409416
410417 embedded = HindsightEmbedded(profile="myapp", ...)
411418
412- # Direct access (not recommended - daemon crashes won't be handled)
413419 client = embedded.client
414- banks = client.list_banks() # If daemon crashes, this will fail
420+ banks = client.list_banks()
415421 ```
416422 """
417423 self ._ensure_started ()
@@ -425,8 +431,13 @@ def url(self) -> str:
425431
426432 @property
427433 def is_running (self ) -> bool :
428- """Check if the client is initialized."""
429- return self ._started and not self ._closed and self ._client is not None
434+ """Check if the client is initialized and the daemon is responsive."""
435+ return (
436+ self ._started
437+ and not self ._closed
438+ and self ._client is not None
439+ and self ._manager .is_running (self .profile )
440+ )
430441
431442 @property
432443 def ui_url (self ) -> str :
0 commit comments