"""Bounded current-query recall; background completion never publishes recall state.""" from __future__ import annotations import logging import math import threading import time from agent.memory_provider import spawn_context_thread logger = logging.getLogger("plugins.memory.honcho") def prefetch_sync(provider, query: str) -> str: timeout = provider._config.timeout if provider._config else None budget = timeout if timeout is not None and math.isfinite(timeout) and timeout > 0 else 5.0 deadline = time.monotonic() + budget if provider._is_trivial_prompt(query): return "" # No lock wait can extend the request budget. Keep the slot occupied even after # a caller times out, until the actual worker (including any HTTP call) exits. if not provider._recall_sync_lock.acquire(blocking=False): return "" cancelled = threading.Event() try: worker = provider._recall_sync_thread if worker is not None and worker.is_alive(): return "" generation = provider._recall_generation = object() session, turn = provider._session_key, provider._turn_count if not provider._session_ready(): provider._start_session_init_background(blocking=False) if provider._init_thread is not None: provider._init_thread.join(timeout=max(0.0, deadline - time.monotonic())) if not provider._session_ready(): return provider._pop_auth_notice() or provider._pop_peer_notice() manager = provider._manager if (generation is not provider._recall_generation or session != provider._session_key or turn != provider._turn_count or time.monotonic() >= deadline): return "" context_due = ( not (provider._injection_frequency == "first-turn" and turn > 1) and turn - provider._last_context_turn >= provider._context_cadence ) dialectic_due = turn - provider._last_dialectic_turn >= provider._effective_cadence() if not context_due and not dialectic_due: return "" # Resolve all mutable settings before starting the worker. The captured # manager/session remain its only I/O owner, even if the provider is reused. levels = [provider._resolve_pass_level(i, query=query) for i in range(provider._dialectic_depth)] rewriter = provider._query_rewriter if provider._query_rewrite_enabled else None holder = {} def expired() -> bool: return (cancelled.is_set() or generation is not provider._recall_generation or time.monotonic() >= deadline) def retrieve() -> None: try: if expired(): return base = provider._format_first_turn_context(manager.get_prefetch_context( session, query, current_query_only=True, ) or {}) if context_due else "" if expired(): return dialectic = "" if dialectic_due: rewritten = "" if rewriter: try: rewritten = rewriter(query).strip() except Exception as exc: logger.debug("Honcho query rewriter failed: %s", exc) results = [] for i, level in enumerate(levels): if expired(): return if results and provider._signal_sufficient(results[-1]): break prompt = (provider._build_dialectic_prompt(i, results, not base) if results else rewritten or "Recall evidence relevant to the user's current request.") prompt = f"Current user request:\n{query}\n\n{prompt}" result = manager.dialectic_query(session, prompt, reasoning_level=level, peer="user", raise_errors=True) if result and result.strip(): results.append(result) dialectic = results[-1] if results else "" holder["result"] = (base, dialectic) except Exception as exc: logger.debug("Honcho synchronous recall failed: %s", exc) worker = spawn_context_thread(retrieve, name="honcho-recall-sync", owner=provider) provider._recall_sync_thread = worker worker.start() worker.join(timeout=max(0.0, deadline - time.monotonic())) if (worker.is_alive() or time.monotonic() >= deadline or "result" not in holder or generation is not provider._recall_generation or provider._manager is not manager or provider._session_key != session or provider._turn_count != turn): return "" base, dialectic = holder["result"] if context_due: provider._last_context_turn = turn if dialectic_due: provider._last_dialectic_turn = turn if dialectic: provider._dialectic_empty_streak = 0 else: provider._dialectic_empty_streak += 1 parts = [provider._pop_auth_notice(), base, dialectic] return provider._truncate_to_budget("\n\n".join(part for part in parts if part and part.strip())) finally: # Thread.join may return before a coarse host clock crosses the deadline. # Record caller abandonment explicitly before a late HTTP call can resume. cancelled.set() provider._recall_sync_lock.release()