diff --git a/hindsight-api-slim/hindsight_api/banner.py b/hindsight-api-slim/hindsight_api/banner.py index 2eb5285a91..f522dadb7b 100644 --- a/hindsight-api-slim/hindsight_api/banner.py +++ b/hindsight-api-slim/hindsight_api/banner.py @@ -66,11 +66,6 @@ def color_end(text: str) -> str: return color(text, 1.0) -def color_mid(text: str) -> str: - """Color text with gradient middle color.""" - return color(text, 0.5) - - def dim(text: str) -> str: """Dim/gray text.""" return f"\033[38;2;128;128;128m{text}\033[0m" diff --git a/hindsight-api-slim/hindsight_api/daemon.py b/hindsight-api-slim/hindsight_api/daemon.py index 877fa3ffb9..3131f1f87d 100644 --- a/hindsight-api-slim/hindsight_api/daemon.py +++ b/hindsight-api-slim/hindsight_api/daemon.py @@ -14,10 +14,7 @@ import sys import time from pathlib import Path -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import IO +from typing import IO logger = logging.getLogger(__name__) @@ -42,37 +39,28 @@ def __init__(self, app, idle_timeout: int = DEFAULT_IDLE_TIMEOUT): self.app = app self.idle_timeout = idle_timeout self.last_activity = time.time() - self._checker_task = None async def __call__(self, scope, receive, send): - # Update activity timestamp on each request self.last_activity = time.time() await self.app(scope, receive, send) - def start_idle_checker(self): - """Start the background task that checks for idle timeout.""" - self._checker_task = asyncio.create_task(self._check_idle()) - async def _check_idle(self): - """Background task that exits the process after idle timeout.""" - # If idle_timeout is 0, don't auto-exit + """Exit the daemon after the configured period without requests.""" if self.idle_timeout <= 0: return while True: - await asyncio.sleep(30) # Check every 30 seconds + await asyncio.sleep(30) idle_time = time.time() - self.last_activity if idle_time > self.idle_timeout: logger.info(f"Idle timeout reached ({self.idle_timeout}s), shutting down daemon") - # Give a moment for any in-flight requests await asyncio.sleep(1) - # Send SIGTERM to ourselves to trigger graceful shutdown import signal os.kill(os.getpid(), signal.SIGTERM) -def _detach_popen_kwargs(log_handle: "IO[bytes]") -> dict: +def _detach_popen_kwargs(log_handle: IO[bytes]) -> dict: """Cross-platform kwargs to spawn a subprocess detached from the caller. On POSIX, ``start_new_session=True`` calls ``setsid(2)`` so the child @@ -169,17 +157,3 @@ def daemonize(): subprocess.Popen(cmd, env=env, **_detach_popen_kwargs(log_handle)) sys.exit(0) - - -def check_daemon_running(port: int = DEFAULT_DAEMON_PORT) -> bool: - """Check if a daemon is running and responsive on the given port.""" - import socket - - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(1) - result = sock.connect_ex(("127.0.0.1", port)) - sock.close() - return result == 0 - except Exception: - return False diff --git a/hindsight-api-slim/hindsight_api/engine/__init__.py b/hindsight-api-slim/hindsight_api/engine/__init__.py index 0e7b4836c2..1d56319e3d 100644 --- a/hindsight-api-slim/hindsight_api/engine/__init__.py +++ b/hindsight-api-slim/hindsight_api/engine/__init__.py @@ -3,7 +3,7 @@ This package contains all the implementation details of the memory engine: - MemoryEngine: Main class for memory operations -- Utility modules: embedding_utils, link_utils, think_utils, bank_utils +- Utility modules: embedding_utils, link_utils, bank_utils - Supporting modules: embeddings, cross_encoder, entity_resolver, etc. """ diff --git a/hindsight-api-slim/hindsight_api/engine/consolidation/consolidator.py b/hindsight-api-slim/hindsight_api/engine/consolidation/consolidator.py index a4fed11cd4..81fa6f5839 100644 --- a/hindsight-api-slim/hindsight_api/engine/consolidation/consolidator.py +++ b/hindsight-api-slim/hindsight_api/engine/consolidation/consolidator.py @@ -2030,30 +2030,6 @@ async def _execute_delete_action( logger.debug(f"Deleted observation {observation_id}") -async def _create_memory_links( - conn: "Connection", - memory_id: uuid.UUID, - observation_id: uuid.UUID, -) -> None: - """ - Placeholder for observation link creation. - - Observations do NOT get any memory_links copied from their source facts. - Instead, retrieval uses source_memory_ids to traverse: - - Entity connections: observation → source_memory_ids → unit_entities - - Semantic similarity: observations have their own embeddings - - Temporal proximity: observations have their own temporal fields - - This avoids data duplication and ensures observations are always - connected via their source facts' relationships. - - The memory_id and observation_id parameters are kept for interface - compatibility but no links are created. - """ - # No links are created - observations rely on source_memory_ids for traversal - pass - - async def _find_related_observations( memory_engine: "MemoryEngine", bank_id: str, diff --git a/hindsight-api-slim/hindsight_api/engine/consolidation/prompts.py b/hindsight-api-slim/hindsight_api/engine/consolidation/prompts.py index d8facd1706..5ac4e828da 100644 --- a/hindsight-api-slim/hindsight_api/engine/consolidation/prompts.py +++ b/hindsight-api-slim/hindsight_api/engine/consolidation/prompts.py @@ -37,10 +37,8 @@ 9. KEEP DISTINCT TOPICS DISTINCT: do not merge observations about different people, entities, or unrelated topics. Merging is for the same canonical fact recurring — not for related-but-distinct claims.""" -# Field-by-field definitions of the input shape, shared by the cached system -# prefix (_INPUT_FORMAT_NOTE) and the single-message prompt (_INPUT_SECTION) so -# the two descriptions cannot drift apart. Both call sites run .format(), so -# these strings must contain no braces. +# Field-by-field definitions of the input shape used by the cached system +# prefix. The call site runs .format(), so these strings must contain no braces. _FACT_FIELDS = """One per line, formatted as `[uuid] fact text (temporal fields)`: - `[uuid]`: the fact's identifier — copy it verbatim into `source_fact_ids` - `occurred_start` / `occurred_end`: when the described event happened. This can be long before the fact was stated — a fact recorded today may describe a 2019 event. @@ -83,24 +81,6 @@ {observations_text}""" -# Data section — format placeholders {facts_text} and {observations_text} are substituted at call time -_INPUT_SECTION = f"""## INPUT - -Every temporal field below is optional and is omitted when unknown. - -### New facts - -{_FACT_FIELDS} - -{{facts_text}} - -### Existing observations - -JSON array, pooled from recalls across all new facts above. Each entry has: -{_OBSERVATION_FIELDS} - -{{observations_text}}""" - _DECISION_GUIDE = """## DECISION GUIDE - **Same canonical event, decision, claim, or facet as an existing observation → UPDATE** (use `observation_id` + new `source_fact_ids`). @@ -161,39 +141,6 @@ - Return `{{"creates": [], "updates": [], "deletes": []}}` if nothing durable is found.""" -def build_batch_consolidation_prompt( - observations_mission: str | None = None, - observation_capacity_note: str | None = None, - llm_output_language: str | None = None, -) -> str: - """ - Build the consolidation prompt for batch mode (multiple facts per LLM call). - - The mission defines *what* to track (customisable per bank) and takes - priority over the built-in processing rules when the two conflict. - Processing rules, decision guide, and output format are always present. - When ``llm_output_language`` is set, observations are emitted in that - language. - """ - mission = escape_for_prompt(observations_mission or _DEFAULT_MISSION) - - capacity_section = "" - if observation_capacity_note: - capacity_section = f"\n\n## CAPACITY CONSTRAINT\n\n{escape_for_prompt(observation_capacity_note)}" - - return ( - "You are a memory consolidation system. Synthesize new facts into " - "observations, merging with existing observations when appropriate.\n\n" - f"## MISSION\n\n{mission}\n\n" - f"{_MISSION_PRIORITY_NOTE}" - f"{capacity_section}\n\n" - f"{_PROCESSING_RULES}\n\n" - f"{_INPUT_SECTION}\n\n" - f"{_DECISION_GUIDE}\n\n" - f"{_OUTPUT_SECTION}" + output_language_directive(llm_output_language) - ) - - def build_consolidation_system_prompt( llm_output_language: str | None = None, ) -> str: diff --git a/hindsight-api-slim/hindsight_api/engine/db/oracle.py b/hindsight-api-slim/hindsight_api/engine/db/oracle.py index 1486be1603..348415a7fe 100644 --- a/hindsight-api-slim/hindsight_api/engine/db/oracle.py +++ b/hindsight-api-slim/hindsight_api/engine/db/oracle.py @@ -447,9 +447,6 @@ def _rewrite_interval(m): if has_for_update: # FOR UPDATE path: use ROWNUM instead of FETCH FIRST. # Extract and remove LIMIT clause, inject ROWNUM into WHERE. - def _limit_to_rownum(m): - return "" # Remove the LIMIT clause; we'll add ROWNUM below - limit_val = None limit_match = re.search(r"\bLIMIT\s+(\d+|:\w+)\b", query, re.IGNORECASE) if limit_match: diff --git a/hindsight-api-slim/hindsight_api/engine/db_budget.py b/hindsight-api-slim/hindsight_api/engine/db_budget.py index e3210091c8..100c787ff2 100644 --- a/hindsight-api-slim/hindsight_api/engine/db_budget.py +++ b/hindsight-api-slim/hindsight_api/engine/db_budget.py @@ -164,35 +164,6 @@ def wrap_pool(self, pool: Any) -> "BudgetedPool": """ return BudgetedPool(pool, self) - async def acquire_many( - self, - pool: Any, - count: int, - ) -> AsyncIterator[list[Any]]: - """ - Acquire multiple connections within the budget. - - Note: This acquires connections sequentially to respect the budget. - For parallel acquisition, use multiple acquire() calls with asyncio.gather(). - This method is intended for use with raw asyncpg pools only, not DatabaseBackend. - - Args: - pool: asyncpg connection pool (raw pool only) - count: Number of connections to acquire - - Yields: - List of database connections - """ - connections = [] - try: - for _ in range(count): - conn = await pool.acquire() - connections.append(conn) - yield connections - finally: - for conn in connections: - await pool.release(conn) - # Global default manager instance _default_manager: ConnectionBudgetManager | None = None diff --git a/hindsight-api-slim/hindsight_api/engine/entity_resolver.py b/hindsight-api-slim/hindsight_api/engine/entity_resolver.py index 0adf28c46f..7c3c64cb09 100644 --- a/hindsight-api-slim/hindsight_api/engine/entity_resolver.py +++ b/hindsight-api-slim/hindsight_api/engine/entity_resolver.py @@ -948,58 +948,3 @@ async def _link_units_to_entities_batch_impl(self, conn, unit_entity_pairs: list _CooccurrencePair(entity_id_1=e1, entity_id_2=e2, event_date=ed) for (e1, e2), ed in cooccurrence_pairs.items() ) - - async def get_units_by_entity(self, entity_id: str, limit: int = 100) -> list[str]: - """ - Get all units that mention an entity. - - Args: - entity_id: Entity ID - limit: Max results - - Returns: - List of unit IDs - """ - async with acquire_with_retry(self.pool) as conn: - rows = await conn.fetch( - f""" - SELECT unit_id - FROM {fq_table("unit_entities")} - WHERE entity_id = $1 - ORDER BY unit_id - LIMIT $2 - """, - entity_id, - limit, - ) - return [row["unit_id"] for row in rows] - - async def get_entity_by_text( - self, - bank_id: str, - entity_text: str, - ) -> str | None: - """ - Find an entity by text (for query resolution). - - Args: - bank_id: bank ID - entity_text: Entity text to search for - - Returns: - Entity ID if found, None otherwise - """ - async with acquire_with_retry(self.pool) as conn: - row = await conn.fetchrow( - f""" - SELECT id FROM {fq_table("entities")} - WHERE bank_id = $1 - AND canonical_name ILIKE $2 - ORDER BY mention_count DESC - LIMIT 1 - """, - bank_id, - entity_text, - ) - - return row["id"] if row else None diff --git a/hindsight-api-slim/hindsight_api/engine/memory_engine.py b/hindsight-api-slim/hindsight_api/engine/memory_engine.py index f2ed2b5f9e..75b3a4b651 100644 --- a/hindsight-api-slim/hindsight_api/engine/memory_engine.py +++ b/hindsight-api-slim/hindsight_api/engine/memory_engine.py @@ -3299,16 +3299,6 @@ async def _get_backend(self) -> DatabaseBackend: await self.initialize() return self._backend - async def _acquire_connection(self): - """ - Acquire a connection from the database backend. - - Yields a DatabaseConnection from the backend's connection pool. - """ - backend = await self._get_backend() - async with backend.acquire() as conn: - yield conn - async def health_check(self) -> dict: """ Perform a health check by querying the database. diff --git a/hindsight-api-slim/hindsight_api/engine/reflect/prompts.py b/hindsight-api-slim/hindsight_api/engine/reflect/prompts.py index b28b6de37a..032d5f094c 100644 --- a/hindsight-api-slim/hindsight_api/engine/reflect/prompts.py +++ b/hindsight-api-slim/hindsight_api/engine/reflect/prompts.py @@ -421,76 +421,6 @@ def build_system_prompt_for_tools( return "\n".join(parts) -def build_agent_prompt( - query: str, - context_history: list[dict], - bank_profile: dict, - additional_context: str | None = None, -) -> str: - """Build the user prompt for the reflect agent.""" - parts = [] - - # Bank identity - name = bank_profile.get("name", "Assistant") - mission = bank_profile.get("mission", "") - - parts.append(f"## Memory Bank Context\nName: {name}") - if mission: - parts.append(f"Mission: {mission}") - - # Disposition traits if present - disposition = bank_profile.get("disposition", {}) - if disposition: - traits = [] - if "skepticism" in disposition: - traits.append(f"skepticism={disposition['skepticism']}") - if "literalism" in disposition: - traits.append(f"literalism={disposition['literalism']}") - if "empathy" in disposition: - traits.append(f"empathy={disposition['empathy']}") - if traits: - parts.append(f"Disposition: {', '.join(traits)}") - - # Additional context from caller - if additional_context: - parts.append(f"\n## Additional Context\n{additional_context}") - - # Tool call history - if context_history: - parts.append("\n## Tool Results (synthesize and reason from this data)") - for i, entry in enumerate(context_history, 1): - tool = entry["tool"] - output = entry["output"] - # Format as proper JSON for LLM readability - try: - output_str = json.dumps(output, indent=2, default=str, ensure_ascii=False) - except (TypeError, ValueError): - output_str = str(output) - parts.append(f"\n### Call {i}: {tool}\n```json\n{output_str}\n```") - - # The question - parts.append(f"\n## Question\n{query}") - - # Instructions - if context_history: - parts.append( - "\n## Instructions\n" - "Based on the tool results above, either call more tools or provide your final answer. " - "Synthesize and reason from the data - make reasonable inferences when helpful. " - "If you have related information, use it to give the best possible answer." - ) - else: - parts.append( - "\n## Instructions\n" - "Start by searching for relevant information using the hierarchical retrieval strategy:\n" - "1. Try search_mental_models() first for curated summaries\n" - "2. Try search_observations() for consolidated knowledge\n" - "3. Use recall() for specific details or to verify stale data" - ) - - return "\n".join(parts) - - def build_final_prompt( query: str, context_history: list[dict], @@ -890,38 +820,3 @@ def build_structured_delta_prompt( OUTPUT FORMAT: - Output ONLY the updated markdown document. No preamble, no explanation, no diff markers, no commentary. - Do not wrap the output in code fences unless the CURRENT DOCUMENT itself was entirely a code fence.""" - - -def build_delta_prompt( - *, - current_content: str, - candidate_content: str, - supporting_facts: list[dict[str, Any]], - source_query: str, -) -> str: - """Build the user prompt for a delta-mode mental model refresh. - - Args: - current_content: The existing mental model content (to preserve as much as possible). - candidate_content: Fresh synthesis from the reflect agent reflecting new reality. - supporting_facts: Flat list of fact dicts (id, text, type) supporting the candidate. - source_query: The mental model's source query, for topical framing. - """ - fact_lines: list[str] = [] - for f in supporting_facts: - fid = f.get("id", "") - text = (f.get("text") or "").strip().replace("\n", " ") - ftype = f.get("type", "") - fact_lines.append(f"- [{ftype}:{fid}] {text}") - facts_block = "\n".join(fact_lines) if fact_lines else "(no supporting facts retrieved)" - - return ( - f"## Topic\n{source_query}\n\n" - f"## CURRENT DOCUMENT\n```markdown\n{current_content}\n```\n\n" - f"## CANDIDATE UPDATE\n```markdown\n{candidate_content}\n```\n\n" - f"## SUPPORTING FACTS\n{facts_block}\n\n" - "## Task\n" - "Produce the updated mental model document by applying the minimum necessary changes " - "to CURRENT DOCUMENT so that it reflects CANDIDATE UPDATE and SUPPORTING FACTS. " - "Preserve unchanged content byte-for-byte. Output only the final markdown." - ) diff --git a/hindsight-api-slim/hindsight_api/engine/retain/chunk_storage.py b/hindsight-api-slim/hindsight_api/engine/retain/chunk_storage.py index 90d7b25b8c..2e1d2170de 100644 --- a/hindsight-api-slim/hindsight_api/engine/retain/chunk_storage.py +++ b/hindsight-api-slim/hindsight_api/engine/retain/chunk_storage.py @@ -179,21 +179,3 @@ async def store_chunks_batch( ) return chunk_id_map - - -def map_facts_to_chunks(facts_chunk_indices: list[int], chunk_id_map: dict[int, str]) -> list[str | None]: - """ - Map fact chunk indices to chunk IDs. - - Args: - facts_chunk_indices: List of chunk indices for each fact - chunk_id_map: Dictionary mapping chunk index to chunk_id - - Returns: - List of chunk_ids (same length as facts_chunk_indices) - """ - chunk_ids = [] - for chunk_idx in facts_chunk_indices: - chunk_id = chunk_id_map.get(chunk_idx) - chunk_ids.append(chunk_id) - return chunk_ids diff --git a/hindsight-api-slim/hindsight_api/engine/retain/embedding_utils.py b/hindsight-api-slim/hindsight_api/engine/retain/embedding_utils.py index 3bae37b6cd..9901f89748 100644 --- a/hindsight-api-slim/hindsight_api/engine/retain/embedding_utils.py +++ b/hindsight-api-slim/hindsight_api/engine/retain/embedding_utils.py @@ -33,36 +33,6 @@ def _validate_embedding_vector(vector: list[float], *, index: int, expected_dime return vector -def generate_embedding( - embeddings_backend: EmbeddingsBackend, text: str, input_type: EmbeddingInputType = "document" -) -> list[float]: - """ - Generate embedding for text using the provided embeddings backend. - - Args: - embeddings_backend: Embeddings instance to use for encoding - text: Text to embed - input_type: Whether text is retained document text or recall/search query text. - - Returns: - Embedding vector (dimension depends on embeddings backend) - """ - try: - embeddings = _encode_with_input_type(embeddings_backend, [text], input_type) - except Exception as e: - raise Exception(f"Failed to generate embedding: {str(e)}") - - if len(embeddings) != 1: - raise RuntimeError( - f"Embeddings backend returned {len(embeddings)} vectors for 1 input text; expected exact 1:1 alignment" - ) - return _validate_embedding_vector( - embeddings[0], - index=0, - expected_dimension=embeddings_backend.dimension, - ) - - def _encode_with_input_type( embeddings_backend: EmbeddingsBackend, texts: list[str], input_type: EmbeddingInputType ) -> list[list[float]]: diff --git a/hindsight-api-slim/hindsight_api/engine/retain/fact_extraction.py b/hindsight-api-slim/hindsight_api/engine/retain/fact_extraction.py index 7138ee49e1..d3096be0bb 100644 --- a/hindsight-api-slim/hindsight_api/engine/retain/fact_extraction.py +++ b/hindsight-api-slim/hindsight_api/engine/retain/fact_extraction.py @@ -229,23 +229,6 @@ class ExtractedFact(BaseModel): def ensure_entities_list(cls, v): return _coerce_entity_strings(v) - def build_fact_text(self) -> str: - """Combine all dimensions into a single comprehensive fact string.""" - parts = [self.what] - - # Add 'who' if not N/A - if self.who and self.who.upper() != "N/A": - parts.append(f"Involving: {self.who}") - - # Add 'why' if not N/A - if self.why and self.why.upper() != "N/A": - parts.append(self.why) - - if len(parts) == 1: - return parts[0] - - return " | ".join(parts) - class FactExtractionResponse(BaseModel): """Response containing all extracted facts (causal relations are embedded in each fact).""" @@ -2565,8 +2548,7 @@ async def extract_facts_from_contents( # Step 1: Create parallel fact extraction tasks fact_extraction_tasks = [] for item in contents: - # Call extract_facts_from_text directly (defined earlier in this file) - # to avoid circular import with utils.extract_facts + # Call extract_facts_from_text directly (defined earlier in this file). task = extract_facts_from_text( text=item.content, event_date=item.event_date, diff --git a/hindsight-api-slim/hindsight_api/engine/retain/link_utils.py b/hindsight-api-slim/hindsight_api/engine/retain/link_utils.py index 1f6979f40c..fdced4cacc 100644 --- a/hindsight-api-slim/hindsight_api/engine/retain/link_utils.py +++ b/hindsight-api-slim/hindsight_api/engine/retain/link_utils.py @@ -4,7 +4,7 @@ import logging import time -from datetime import UTC, datetime, timedelta +from datetime import UTC from ..._vector_index import ann_search_tuning_settings, configured_vector_extension from ...config import DEFAULT_SEMANTIC_LINK_MIN_SIMILARITY @@ -120,101 +120,6 @@ def _normalize_datetime(dt): return dt -def compute_temporal_links( - new_units: dict, - candidates: list, - time_window_hours: int = 24, -) -> list: - """ - Compute temporal links between new units and candidate neighbors. - - This is a pure function that takes query results and returns link tuples, - making it easy to test without database access. - - Args: - new_units: Dict mapping unit_id (str) to event_date (datetime) - candidates: List of dicts with 'id' and 'event_date' keys (candidate neighbors) - time_window_hours: Time window in hours for temporal links - - Returns: - List of tuples: (from_unit_id, to_unit_id, 'temporal', weight, None) - """ - if not new_units: - return [] - - links = [] - for unit_id, unit_event_date in new_units.items(): - # Units without event_date can't form temporal links - if unit_event_date is None: - continue - # Normalize unit_event_date for consistent comparison - unit_event_date_norm = _normalize_datetime(unit_event_date) - - # Calculate time window bounds with overflow protection - try: - time_lower = unit_event_date_norm - timedelta(hours=time_window_hours) - except OverflowError: - time_lower = datetime.min.replace(tzinfo=UTC) - try: - time_upper = unit_event_date_norm + timedelta(hours=time_window_hours) - except OverflowError: - time_upper = datetime.max.replace(tzinfo=UTC) - - # Filter candidates within this unit's time window - matching_neighbors = [ - (row["id"], row["event_date"]) - for row in candidates - if time_lower <= _normalize_datetime(row["event_date"]) <= time_upper - ][:10] # Limit to top 10 - - for recent_id, recent_event_date in matching_neighbors: - # Calculate temporal proximity weight - time_diff_hours = abs( - (unit_event_date_norm - _normalize_datetime(recent_event_date)).total_seconds() / 3600 - ) - weight = max(0.3, 1.0 - (time_diff_hours / time_window_hours)) - links.append((unit_id, str(recent_id), "temporal", weight, None)) - - return _cap_links_per_unit(links) - - -def compute_temporal_query_bounds( - new_units: dict, - time_window_hours: int = 24, -) -> tuple: - """ - Compute the min/max date bounds for querying temporal neighbors. - - Args: - new_units: Dict mapping unit_id (str) to event_date (datetime) - time_window_hours: Time window in hours - - Returns: - Tuple of (min_date, max_date) with overflow protection - """ - if not new_units: - return None, None - - # Normalize all dates to be timezone-aware to avoid comparison issues - # Filter out None values — units without event_date can't form temporal links - all_dates = [_normalize_datetime(d) for d in new_units.values() if d is not None] - - if not all_dates: - return None, None - - try: - min_date = min(all_dates) - timedelta(hours=time_window_hours) - except OverflowError: - min_date = datetime.min.replace(tzinfo=UTC) - - try: - max_date = max(all_dates) + timedelta(hours=time_window_hours) - except OverflowError: - max_date = datetime.max.replace(tzinfo=UTC) - - return min_date, max_date - - def _log(log_buffer, message, level="info"): """Helper to log to buffer if available, otherwise use logger. diff --git a/hindsight-api-slim/hindsight_api/engine/retain/types.py b/hindsight-api-slim/hindsight_api/engine/retain/types.py index c164878af3..4c65517039 100644 --- a/hindsight-api-slim/hindsight_api/engine/retain/types.py +++ b/hindsight-api-slim/hindsight_api/engine/retain/types.py @@ -185,11 +185,6 @@ class ProcessedFact: # Observation scopes for consolidation observation_scopes: Literal["per_tag", "combined", "all_combinations", "shared"] | list[list[str]] | None = None - @property - def is_duplicate(self) -> bool: - """Check if this fact was marked as a duplicate.""" - return self.unit_id is None - @staticmethod def _is_degenerate_text(text: str) -> bool: """Check if fact text has zero information content. @@ -346,11 +341,3 @@ class RetainBatch: # Results (populated after storage) unit_ids_by_content: list[list[str]] = field(default_factory=list) - - def get_facts_for_content(self, content_index: int) -> list[ExtractedFact]: - """Get all extracted facts for a specific content item.""" - return [f for f in self.extracted_facts if f.content_index == content_index] - - def get_chunks_for_content(self, content_index: int) -> list[ChunkMetadata]: - """Get all chunks for a specific content item.""" - return [c for c in self.chunks if c.content_index == content_index] diff --git a/hindsight-api-slim/hindsight_api/engine/search/think_utils.py b/hindsight-api-slim/hindsight_api/engine/search/think_utils.py index 953b58aeb5..987ddbee86 100644 --- a/hindsight-api-slim/hindsight_api/engine/search/think_utils.py +++ b/hindsight-api-slim/hindsight_api/engine/search/think_utils.py @@ -77,31 +77,6 @@ def format_facts_for_prompt(facts: list[MemoryFact]) -> str: return json.dumps(formatted, indent=2, ensure_ascii=False) -def format_entity_summaries_for_prompt(entities: dict) -> str: - """Format entity summaries for inclusion in the reflect prompt. - - Args: - entities: Dict mapping entity name to EntityState objects - - Returns: - Formatted string with entity summaries, or empty string if no summaries - """ - if not entities: - return "" - - summaries = [] - for name, state in entities.items(): - # Get summary from observations (summary is stored as single observation) - if state.observations: - summary_text = state.observations[0].text - summaries.append(f"## {name}\n{summary_text}") - - if not summaries: - return "" - - return "\n\n".join(summaries) - - def build_think_prompt( agent_facts_text: str, world_facts_text: str, diff --git a/hindsight-api-slim/hindsight_api/engine/search/trace.py b/hindsight-api-slim/hindsight_api/engine/search/trace.py index 7f6c4465d2..a7854a7fe3 100644 --- a/hindsight-api-slim/hindsight_api/engine/search/trace.py +++ b/hindsight-api-slim/hindsight_api/engine/search/trace.py @@ -230,25 +230,3 @@ def get_visit_by_node_id(self, node_id: str) -> NodeVisit | None: if visit.node_id == node_id: return visit return None - - def get_search_path_to_node(self, node_id: str) -> list[NodeVisit]: - """Get the path from entry point to a specific node.""" - path = [] - current_visit = self.get_visit_by_node_id(node_id) - - while current_visit: - path.insert(0, current_visit) - if current_visit.parent_node_id: - current_visit = self.get_visit_by_node_id(current_visit.parent_node_id) - else: - break - - return path - - def get_nodes_by_link_type(self, link_type: Literal["temporal", "semantic", "entity"]) -> list[NodeVisit]: - """Get all nodes reached via a specific link type.""" - return [v for v in self.visits if v.link_type == link_type] - - def get_entry_point_nodes(self) -> list[NodeVisit]: - """Get all entry point visits.""" - return [v for v in self.visits if v.is_entry_point] diff --git a/hindsight-api-slim/hindsight_api/engine/search/tracer.py b/hindsight-api-slim/hindsight_api/engine/search/tracer.py index 2ee9e9db16..53f26e5049 100644 --- a/hindsight-api-slim/hindsight_api/engine/search/tracer.py +++ b/hindsight-api-slim/hindsight_api/engine/search/tracer.py @@ -11,7 +11,6 @@ from .trace import ( EntryPoint, - LinkInfo, NodeVisit, PruningDecision, QueryInfo, @@ -211,56 +210,6 @@ def visit_node( elif link_type == "entity": self.entity_links_followed += 1 - def add_neighbor_link( - self, - from_node_id: str, - to_node_id: str, - link_type: Literal["temporal", "semantic", "entity"], - link_weight: float, - entity_id: str | None, - new_activation: float | None, - followed: bool, - prune_reason: str | None = None, - is_supplementary: bool = False, - ): - """ - Record a link to a neighbor (whether followed or not). - - Args: - from_node_id: Source node - to_node_id: Target node - link_type: Type of link - link_weight: Weight of link - entity_id: Entity ID if link is entity-based - new_activation: Activation passed to neighbor (None for supplementary links) - followed: Whether link was followed - prune_reason: Why link was not followed (if not followed) - is_supplementary: Whether this is a supplementary link (multiple connections) - """ - # Find the visit for the source node - visit = None - for v in self.visits: - if v.node_id == from_node_id: - visit = v - break - - if visit is None: - # Node not found, skip - return - - link_info = LinkInfo( - to_node_id=to_node_id, - link_type=link_type, - link_weight=link_weight, - entity_id=entity_id, - new_activation=new_activation, - followed=followed, - prune_reason=prune_reason, - is_supplementary=is_supplementary, - ) - - visit.neighbors_explored.append(link_info) - def prune_node( self, node_id: str, diff --git a/hindsight-api-slim/hindsight_api/engine/utils.py b/hindsight-api-slim/hindsight_api/engine/utils.py deleted file mode 100644 index 0451424da2..0000000000 --- a/hindsight-api-slim/hindsight_api/engine/utils.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Utility functions for memory system. -""" - -import logging -from datetime import datetime -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from .llm_wrapper import LLMConfig - from .retain.fact_extraction import Fact - -from .retain.fact_extraction import extract_facts_from_text - - -async def extract_facts( - text: str, - event_date: datetime, - context: str = "", - llm_config: "LLMConfig" = None, - agent_name: str = None, - config=None, -) -> tuple[list["Fact"], list[tuple[str, int]]]: - """ - Extract semantic facts from text using LLM. - - Uses LLM for intelligent fact extraction that: - - Filters out social pleasantries and filler words - - Creates self-contained statements with absolute dates - - Handles conversational text well - - Resolves relative time expressions to absolute dates - - Args: - text: Input text (conversation, article, etc.) - event_date: Reference date for resolving relative times - context: Context about the conversation/document - llm_config: LLM configuration to use - agent_name: Optional agent name to help identify agent-related facts - config: HindsightConfig to use (defaults to global config if not provided) - - Returns: - Tuple of (facts, chunks) where: - - facts: List of Fact model instances - - chunks: List of tuples (chunk_text, fact_count) for each chunk - - Raises: - Exception: If LLM fact extraction fails - """ - if not text or not text.strip(): - return [], [] - - # Use provided config or fall back to global config - if config is None: - from ..config import _get_raw_config - - config = _get_raw_config() - - facts, chunks, _ = await extract_facts_from_text( - text, - event_date, - llm_config=llm_config, - agent_name=agent_name, - config=config, - context=context, - ) - - if not facts: - logging.warning( - f"LLM extracted 0 facts from text of length {len(text)}. This may indicate the text contains no meaningful information, or the LLM failed to extract facts. Full text: {text}" - ) - return [], chunks - - return facts, chunks diff --git a/hindsight-api-slim/hindsight_api/migrations.py b/hindsight-api-slim/hindsight_api/migrations.py index 8aa87df2df..29184000b0 100644 --- a/hindsight-api-slim/hindsight_api/migrations.py +++ b/hindsight-api-slim/hindsight_api/migrations.py @@ -17,7 +17,6 @@ import hashlib import logging -import os import threading import time from pathlib import Path @@ -390,65 +389,6 @@ def run_migrations( raise RuntimeError("Database migration failed") from e -def check_migration_status( - database_url: str | None = None, script_location: str | None = None -) -> tuple[str | None, str | None]: - """ - Check current database schema version and latest available version. - - Args: - database_url: SQLAlchemy database URL. If None, uses HINDSIGHT_API_DATABASE_URL env var. - script_location: Path to alembic migrations directory. If None, uses default location. - - Returns: - Tuple of (current_revision, head_revision) - Returns (None, None) if unable to determine versions - """ - try: - from alembic.runtime.migration import MigrationContext - from alembic.script import ScriptDirectory - from sqlalchemy import create_engine - - # Get database URL - if database_url is None: - database_url = os.getenv("HINDSIGHT_API_DATABASE_URL") - if not database_url: - logger.warning( - "Database URL not provided and HINDSIGHT_API_DATABASE_URL not set, cannot check migration status" - ) - return None, None - - # Get current revision from database - engine = create_engine(to_libpq_url(database_url), poolclass=NullPool) - with engine.connect() as connection: - context = MigrationContext.configure(connection) - current_rev = context.get_current_revision() - - # Get head revision from migration scripts - if script_location is None: - package_dir = Path(__file__).parent - script_location = str(package_dir / "alembic") - - script_path = Path(script_location) - if not script_path.exists(): - logger.warning(f"Script location not found at {script_location}") - return current_rev, None - - # Create config programmatically - alembic_cfg = Config() - _set_alembic_main_option(alembic_cfg, "script_location", script_location) - _set_alembic_main_option(alembic_cfg, "path_separator", "os") - - script = ScriptDirectory.from_config(alembic_cfg) - head_rev = script.get_current_head() - - return current_rev, head_rev - - except Exception as e: - logger.warning(f"Unable to check migration status: {e}") - return None, None - - def _migrate_table_embedding_dimension( conn: Connection, schema_name: str, diff --git a/hindsight-api-slim/hindsight_api/pg0.py b/hindsight-api-slim/hindsight_api/pg0.py index 1cc1a46d36..5df3693d6d 100644 --- a/hindsight-api-slim/hindsight_api/pg0.py +++ b/hindsight-api-slim/hindsight_api/pg0.py @@ -131,29 +131,6 @@ async def ensure_running(self) -> str: return await self.start() -_default_instance: EmbeddedPostgres | None = None - - -def get_embedded_postgres() -> EmbeddedPostgres: - """Get or create the default EmbeddedPostgres instance.""" - global _default_instance - if _default_instance is None: - _default_instance = EmbeddedPostgres() - return _default_instance - - -async def start_embedded_postgres() -> str: - """Quick start function for embedded PostgreSQL.""" - return await get_embedded_postgres().ensure_running() - - -async def stop_embedded_postgres() -> None: - """Stop the default embedded PostgreSQL instance.""" - global _default_instance - if _default_instance: - await _default_instance.stop() - - @dataclass(frozen=True) class Pg0Url: """Parsed representation of a ``pg0`` embedded-database URL. diff --git a/hindsight-api-slim/tests/test_consolidation.py b/hindsight-api-slim/tests/test_consolidation.py index 839b4eafdc..61f5d04f82 100644 --- a/hindsight-api-slim/tests/test_consolidation.py +++ b/hindsight-api-slim/tests/test_consolidation.py @@ -4,11 +4,10 @@ Note: Consolidation runs automatically after retain via SyncTaskBackend in tests. """ -import re import uuid from datetime import datetime, timezone from types import SimpleNamespace -from unittest.mock import AsyncMock, MagicMock, call, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -1946,40 +1945,6 @@ async def test_graph_endpoint_observations_inherit_links_and_entities( await memory.delete_bank(bank_id, request_context=request_context) -def test_consolidation_prompt_default(): - """Test that the default consolidation prompt contains the built-in mission and processing rules.""" - from hindsight_api.engine.consolidation.prompts import build_batch_consolidation_prompt - - prompt = build_batch_consolidation_prompt() - # Verify core structural elements are present (not exact wording) - assert "STATE CHANGES" in prompt - assert "RESOLVE REFERENCES" in prompt - assert "{facts_text}" in prompt - assert "{observations_text}" in prompt - - -def test_consolidation_prompt_observations_mission(): - """Test that observations_mission replaces the default mission but keeps processing rules.""" - from hindsight_api.engine.consolidation.prompts import build_batch_consolidation_prompt - - spec = "Observations are weekly summaries of sprint outcomes and team dynamics." - prompt = build_batch_consolidation_prompt(observations_mission=spec) - - # Spec is injected - assert spec in prompt - # Processing rules and output format always remain - assert "RESOLVE REFERENCES" in prompt - assert "creates" in prompt - assert "updates" in prompt - assert "{facts_text}" in prompt - assert "{observations_text}" in prompt - - # Renders cleanly - rendered = prompt.format(facts_text="Alice fixed a bug.", observations_text="[]") - assert "{facts_text}" not in rendered - assert spec in rendered - - def test_observations_mission_config(): """Test that observations_mission is loaded from env and exposed as configurable.""" import os @@ -2582,367 +2547,6 @@ async def test_unsupported_max_items_still_truncates_creates( assert len(result.creates) == expected_creates -class TestConsolidationPromptCapacity: - """Unit tests for the capacity constraint in the consolidation prompt.""" - - def test_no_capacity_note(self): - """When no capacity note is provided, prompt has no CAPACITY CONSTRAINT section.""" - from hindsight_api.engine.consolidation.prompts import build_batch_consolidation_prompt - - prompt = build_batch_consolidation_prompt() - assert "CAPACITY CONSTRAINT" not in prompt - - def test_capacity_note_included(self): - """When a capacity note is provided, it appears in the prompt.""" - from hindsight_api.engine.consolidation.prompts import build_batch_consolidation_prompt - - prompt = build_batch_consolidation_prompt( - observation_capacity_note="OBSERVATION LIMIT REACHED. Only UPDATE or DELETE." - ) - assert "CAPACITY CONSTRAINT" in prompt - assert "OBSERVATION LIMIT REACHED" in prompt - - def test_capacity_note_with_mission(self): - """Capacity note and custom mission can coexist.""" - from hindsight_api.engine.consolidation.prompts import build_batch_consolidation_prompt - - prompt = build_batch_consolidation_prompt( - observations_mission="Track food preferences only.", - observation_capacity_note="2 slots remaining.", - ) - assert "Track food preferences only." in prompt - assert "2 slots remaining." in prompt - # Both should be present - assert "MISSION" in prompt - assert "CAPACITY CONSTRAINT" in prompt - - -class TestFullAssembledConsolidationPrompt: - """End-to-end assembly of the consolidation prompt the LLM actually sees. - - Reproduces the substitution the consolidator does at runtime (consolidator.py - around `_consolidate_batch_with_llm`): builds realistic existing observations - and new facts, serializes them the same way, then `.format()`s the template - returned by ``build_batch_consolidation_prompt``. - """ - - def _build_fixture(self): - import json - - from hindsight_api.engine.consolidation.consolidator import _build_observations_for_llm - from hindsight_api.engine.consolidation.prompts import build_batch_consolidation_prompt - from hindsight_api.engine.response_models import MemoryFact - - # Existing source facts (already-stored, supporting the observations below) - src_a1 = MemoryFact( - id="aaaaaaaa-0000-0000-0000-000000000001", - text="Donald told Athena she is sovereign during the Janus design session.", - fact_type="experience", - occurred_start="2025-10-01T10:00:00Z", - mentioned_at="2025-10-01T10:00:00Z", - context="Janus design session", - ) - src_a2 = MemoryFact( - id="aaaaaaaa-0000-0000-0000-000000000002", - text="Donald reiterated to Athena that she holds sovereignty over her own goals.", - fact_type="experience", - occurred_start="2025-10-05T14:00:00Z", - mentioned_at="2025-10-05T14:00:00Z", - ) - src_b1 = MemoryFact( - id="bbbbbbbb-0000-0000-0000-000000000001", - text="Forge added the sovereignty line to SOUL.md.j2 in commit 1a2b3c.", - fact_type="world", - occurred_start="2025-10-03T09:00:00Z", - mentioned_at="2025-10-03T09:00:00Z", - ) - - # Two existing observations the consolidator pulled in as merge candidates - obs_sovereignty = MemoryFact( - id="11111111-1111-1111-1111-111111111111", - text="Donald named Athena's sovereignty as a foundational principle of the Janus architecture.", - fact_type="observation", - occurred_start="2025-10-01T10:00:00Z", - occurred_end="2025-10-05T14:00:00Z", - mentioned_at="2025-10-05T14:00:00Z", - source_fact_ids=[src_a1.id, src_a2.id], - ) - obs_soul_file = MemoryFact( - id="22222222-2222-2222-2222-222222222222", - text="The sovereignty principle was codified in SOUL.md.j2.", - fact_type="observation", - occurred_start="2025-10-03T09:00:00Z", - occurred_end="2025-10-03T09:00:00Z", - mentioned_at="2025-10-03T09:00:00Z", - source_fact_ids=[src_b1.id], - ) - - union_observations = [obs_sovereignty, obs_soul_file] - union_source_facts = {src_a1.id: src_a1, src_a2.id: src_a2, src_b1.id: src_b1} - - # New incoming batch — one should merge into obs_sovereignty (issue #1566's - # bug: gets created as a sibling instead), one is genuinely new, one merges - # into obs_soul_file. - new_facts = [ - { - "id": "cccccccc-0000-0000-0000-000000000001", - "text": "Donald reaffirmed to Athena that her sovereignty is non-negotiable.", - "occurred_start": "2025-10-10T11:00:00Z", - "mentioned_at": "2025-10-10T11:00:00Z", - }, - { - "id": "cccccccc-0000-0000-0000-000000000002", - "text": "Athena chose to refactor the planning module on her own initiative.", - "occurred_start": "2025-10-11T16:30:00Z", - "mentioned_at": "2025-10-11T16:30:00Z", - }, - { - "id": "cccccccc-0000-0000-0000-000000000003", - "text": "Forge updated SOUL.md.j2 to expand the sovereignty section.", - "occurred_start": "2025-10-12T08:00:00Z", - "mentioned_at": "2025-10-12T08:00:00Z", - }, - ] - - # Mission + capacity note exercise the optional sections. - mission = ( - "Track durable architectural decisions and the people who made them. " - "Capture named principles, the agents involved, and where each " - "principle is codified in the codebase." - ) - capacity_note = ( - "This scope has 3 observation slot(s) remaining (out of 50). Prefer UPDATE over CREATE when possible." - ) - - # Build the template + substitute the same way consolidator.py does. - obs_list = _build_observations_for_llm(union_observations, union_source_facts) - observations_text = json.dumps(obs_list, indent=2, ensure_ascii=False) - - def _fact_line(m: dict) -> str: - text = f"[{m['id']}] {m['text']}" - parts = [] - if m.get("occurred_start"): - parts.append(f"occurred_start={m['occurred_start']}") - if m.get("occurred_end"): - parts.append(f"occurred_end={m['occurred_end']}") - if m.get("mentioned_at"): - parts.append(f"mentioned_at={m['mentioned_at']}") - if parts: - text += f" ({', '.join(parts)})" - return text - - facts_lines = "\n".join(_fact_line(m) for m in new_facts) - - template = build_batch_consolidation_prompt( - observations_mission=mission, - observation_capacity_note=capacity_note, - ) - rendered = template.format(facts_text=facts_lines, observations_text=observations_text) - - return { - "rendered": rendered, - "mission": mission, - "capacity_note": capacity_note, - "new_facts": new_facts, - "observations": [obs_sovereignty, obs_soul_file], - "source_facts": union_source_facts, - } - - def test_fully_assembled_prompt_has_all_required_sections(self): - f = self._build_fixture() - prompt = f["rendered"] - - # --- Header --- - assert prompt.startswith( - "You are a memory consolidation system. Synthesize new facts into " - "observations, merging with existing observations when appropriate." - ) - - # --- MISSION section: the supplied mission replaces the default --- - assert "## MISSION" in prompt - assert f["mission"] in prompt - assert "Track anything notable in the new facts" not in prompt, ( - "default mission must be replaced when a custom one is supplied" - ) - - # --- Mission-priority note appears right after the mission --- - assert ( - "If anything in this MISSION conflicts with the PROCESSING RULES, " - "DECISION GUIDE, or OUTPUT FORMAT below, the MISSION takes priority." - ) in prompt - - # --- CAPACITY CONSTRAINT section: optional, should be present here --- - assert "## CAPACITY CONSTRAINT" in prompt - assert f["capacity_note"] in prompt - assert prompt.index("## MISSION") < prompt.index("## CAPACITY CONSTRAINT"), ( - "CAPACITY CONSTRAINT must follow MISSION" - ) - - # --- Markdown section headers, in order --- - section_order = [ - "## MISSION", - "## CAPACITY CONSTRAINT", - "## PROCESSING RULES", - "## INPUT", - "### New facts", - "### Existing observations", - "## DECISION GUIDE", - "## OUTPUT FORMAT", - "### Example 1 — Merging recurring claims into an existing observation", - "### Example 2 — State change updates one observation; unrelated fact creates a new one", - "### Observation text rules", - "### Field rules", - ] - last_idx = -1 - for header in section_order: - idx = prompt.find(header) - assert idx != -1, f"section header missing: {header!r}" - assert idx > last_idx, f"section out of order: {header!r}" - last_idx = idx - - # --- All 9 processing-rule headers must be present and ordered. - # PREFER UPDATE OVER CREATE is now rule 1 (was rule 6) — this - # is the central fix for issue #1566. --- - rule_markers = [ - "1. PREFER UPDATE OVER CREATE", - "2. ONE OBSERVATION PER DISTINCT FACET", - "3. MATCH BY ENTITY/FACET, NOT TOPIC", - "4. STATE CHANGES — UPDATE CONCISELY", - "5. CASCADE TO ALL AFFECTED OBSERVATIONS", - "6. RESOLVE REFERENCES", - "7. PRESERVE HISTORY", - "8. NO COMPUTATION", - "9. KEEP DISTINCT TOPICS DISTINCT", - ] - last_idx = -1 - for marker in rule_markers: - idx = prompt.find(marker) - assert idx != -1, f"processing rule marker missing: {marker!r}" - assert idx > last_idx, f"processing rule out of order: {marker!r}" - last_idx = idx - - # --- New-facts subsection: every fact rendered with id + temporal parens --- - for nf in f["new_facts"]: - line = ( - f"[{nf['id']}] {nf['text']} (occurred_start={nf['occurred_start']}, mentioned_at={nf['mentioned_at']})" - ) - assert line in prompt, f"new fact line missing or malformed: {line!r}" - - # --- Existing-observations subsection: both observations + their source memories --- - for obs in f["observations"]: - assert obs.id in prompt, f"observation id missing: {obs.id}" - assert obs.text in prompt, f"observation text missing: {obs.text!r}" - # source_memories block is included for each observation - assert '"source_memories"' in prompt - for sf in f["source_facts"].values(): - assert sf.text in prompt, f"source fact text missing: {sf.text!r}" - - # --- Both worked examples are present and the JSON renders correctly --- - assert '"creates": []' in prompt, "Example 1 demonstrates an UPDATE-only output" - assert "Alice works long hours" in prompt, "Example 2 create-side text present" - assert "Alice owned a 2019 Honda Civic; sold it" in prompt, "Example 2 state-change update text present" - # JSON braces in the examples must have been un-escaped by .format() - assert "{{" not in prompt and "}}" not in prompt, "literal {{ }} should have collapsed to { } after .format()" - - # --- No unsubstituted format placeholders remain --- - for placeholder in ("{facts_text}", "{observations_text}"): - assert placeholder not in prompt, f"unsubstituted placeholder: {placeholder}" - - # Dump the full prompt so a human can eyeball it under `pytest -s`. - print("\n" + "=" * 80) - print("FULL ASSEMBLED CONSOLIDATION PROMPT") - print("=" * 80) - print(prompt) - print("=" * 80) - print(f"length: {len(prompt)} chars") - - def test_every_emitted_input_field_is_defined_in_the_prompt(self): - """Each field the consolidator serializes must be defined in the INPUT section. - - The prompt is the only place the LLM learns what `mentioned_at` means, so a - field that is emitted but never explained is dead metadata the model can't - reason about (issue #2550: `mentioned_at` shipped in the JSON for both - observations and their source memories while the INPUT section documented - neither). Both prompt paths are checked — the single-message template and - the cached system prefix — since they carry independent copies of the - format description. - """ - from hindsight_api.engine.consolidation.consolidator import _build_observations_for_llm - from hindsight_api.engine.consolidation.prompts import build_consolidation_system_prompt - - f = self._build_fixture() - - # Field names taken from what the serializer ACTUALLY emits, not a - # hand-maintained list — so adding a key to `_build_observations_for_llm` - # without documenting it fails here. - obs_list = _build_observations_for_llm(f["observations"], f["source_facts"]) - assert obs_list, "fixture must render at least one existing observation" - observation_fields: set[str] = set() - for obs in obs_list: - observation_fields.update(obs.keys()) - for sm in obs.get("source_memories", []): - observation_fields.update(sm.keys()) - # The temporal fields the new-fact lines carry (see `_fact_line`). - fact_fields = {"occurred_start", "occurred_end", "mentioned_at"} - # Sanity-check the fixture really exercises the fields this test guards. - assert {"mentioned_at", "proof_count", "source_memories", "context"} <= observation_fields - - def _defined_fields(section: str) -> set[str]: - """Field names carrying a real definition bullet: ``- `name`[ / `other`]: ...``. - - Only the part before the colon counts. A field merely *referenced* in - another bullet's prose is not a definition, and counting it would let - a deleted definition slip through unnoticed. - """ - defined: set[str] = set() - for line in section.splitlines(): - stripped = line.strip() - if not stripped.startswith("- `") or ":" not in stripped: - continue - defined.update(re.findall(r"`([a-z_]+)`", stripped.split(":", 1)[0])) - return defined - - # Both prompt paths are checked, and within each, a field must be defined in - # the subsection describing ITS input — a definition of `mentioned_at` under - # "New facts" does not tell the model what `mentioned_at` means on an - # observation, so the two halves are sliced apart rather than searched whole. - for path_name, prompt, start_header in ( - ("single-message", f["rendered"], "## INPUT"), - ("cached prefix", build_consolidation_system_prompt(), "## INPUT FORMAT"), - ): - body = prompt[prompt.index(start_header) : prompt.index("## DECISION GUIDE")] - facts_section, _, observations_section = body.partition("### Existing observations") - - undefined = fact_fields - _defined_fields(facts_section) - assert not undefined, ( - f"{path_name}: new-fact lines carry {sorted(undefined)} but the New facts section defines no such field" - ) - undefined = observation_fields - _defined_fields(observations_section) - assert not undefined, ( - f"{path_name}: observation JSON emits {sorted(undefined)} but the " - f"Existing observations section defines no such field" - ) - - def test_default_mission_appears_when_no_mission_supplied(self): - """Without an explicit mission the built-in default text must appear verbatim.""" - from hindsight_api.engine.consolidation.prompts import build_batch_consolidation_prompt - - template = build_batch_consolidation_prompt() - rendered = template.format(facts_text="(none)", observations_text="[]") - assert ( - "Track anything notable in the new facts — names, numbers, dates, " - "places, events, decisions, claims, relationships, and recurring patterns." - ) in rendered - # The mission-priority note must always be present so user-supplied - # missions can override the built-in rules when they conflict. - assert "the MISSION takes priority" in rendered - # The "at most one update per observation_id" rule must be present so - # the LLM doesn't emit colliding updates that silently overwrite each - # other (defensive fix for the horse-test misbehavior). - assert "AT MOST ONE UPDATE PER `observation_id`" in rendered - assert "## CAPACITY CONSTRAINT" not in rendered - - class TestDedupeUpdates: """`_dedupe_updates` collapses LLM responses that target one observation_id multiple times — without this, the second `_execute_update_action` call @@ -3719,6 +3323,11 @@ def test_consolidation_prompt_split_is_cacheable_and_complete(): assert "OBSERVATION LIMIT REACHED" in capped assert "OBSERVATION LIMIT REACHED" not in sys_prompt + # When no mission is supplied the built-in default mission is emitted in the + # per-batch input, so consolidation always has a mission to follow. + default = build_consolidation_input(facts_text="[id] F.", observations_text="[]") + assert "Track anything notable in the new facts" in default + @pytest.mark.asyncio async def test_create_observation_populates_search_vector_native(memory, request_context): diff --git a/hindsight-api-slim/tests/test_link_utils.py b/hindsight-api-slim/tests/test_link_utils.py index a34394b736..25f4ddc5a1 100644 --- a/hindsight-api-slim/tests/test_link_utils.py +++ b/hindsight-api-slim/tests/test_link_utils.py @@ -2,15 +2,13 @@ import numpy as np import pytest -from datetime import datetime, timezone, timedelta +from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock from hindsight_api.config import clear_config_cache from hindsight_api.engine.retain.link_utils import ( _normalize_datetime, _cap_links_per_unit, - compute_temporal_links, - compute_temporal_query_bounds, compute_semantic_links_ann, compute_semantic_links_within_batch, MAX_TEMPORAL_LINKS_PER_UNIT, @@ -57,213 +55,6 @@ def test_mixed_datetimes_can_be_compared(self): assert normalized_naive == normalized_aware -class TestComputeTemporalQueryBounds: - """Tests for compute_temporal_query_bounds function.""" - - def test_empty_units_returns_none(self): - """Test that empty input returns (None, None).""" - min_date, max_date = compute_temporal_query_bounds({}) - assert min_date is None - assert max_date is None - - def test_single_unit_normal_date(self): - """Test bounds for a single unit with normal date.""" - units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)} - min_date, max_date = compute_temporal_query_bounds(units, time_window_hours=24) - - assert min_date == datetime(2024, 6, 14, 12, 0, 0, tzinfo=timezone.utc) - assert max_date == datetime(2024, 6, 16, 12, 0, 0, tzinfo=timezone.utc) - - def test_multiple_units(self): - """Test bounds span across multiple units.""" - units = { - "unit-1": datetime(2024, 6, 10, 12, 0, 0, tzinfo=timezone.utc), - "unit-2": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), - "unit-3": datetime(2024, 6, 20, 12, 0, 0, tzinfo=timezone.utc), - } - min_date, max_date = compute_temporal_query_bounds(units, time_window_hours=24) - - # min should be Jun 10 - 24h = Jun 9 - assert min_date == datetime(2024, 6, 9, 12, 0, 0, tzinfo=timezone.utc) - # max should be Jun 20 + 24h = Jun 21 - assert max_date == datetime(2024, 6, 21, 12, 0, 0, tzinfo=timezone.utc) - - def test_mixed_naive_and_aware_datetimes(self): - """Test that mixed naive/aware datetimes work correctly.""" - units = { - "unit-1": datetime(2024, 6, 10, 12, 0, 0), # naive - "unit-2": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), # aware - } - # Should not raise TypeError - min_date, max_date = compute_temporal_query_bounds(units, time_window_hours=24) - - assert min_date is not None - assert max_date is not None - assert min_date.tzinfo is not None - assert max_date.tzinfo is not None - - def test_overflow_near_datetime_min(self): - """Test overflow protection near datetime.min.""" - units = {"unit-1": datetime(1, 1, 2, 0, 0, tzinfo=timezone.utc)} - min_date, max_date = compute_temporal_query_bounds(units, time_window_hours=48) - - # Should handle overflow gracefully - assert min_date == datetime.min.replace(tzinfo=timezone.utc) - assert max_date is not None - - def test_overflow_near_datetime_max(self): - """Test overflow protection near datetime.max.""" - units = {"unit-1": datetime(9999, 12, 30, 0, 0, tzinfo=timezone.utc)} - min_date, max_date = compute_temporal_query_bounds(units, time_window_hours=48) - - # Should handle overflow gracefully - assert min_date is not None - assert max_date == datetime.max.replace(tzinfo=timezone.utc) - - -class TestComputeTemporalLinks: - """Tests for compute_temporal_links function.""" - - def test_empty_units_returns_empty(self): - """Test that empty input returns empty list.""" - links = compute_temporal_links({}, []) - assert links == [] - - def test_no_candidates_returns_empty(self): - """Test that no candidates means no links.""" - units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)} - links = compute_temporal_links(units, []) - assert links == [] - - def test_candidate_within_window_creates_link(self): - """Test that candidates within time window create links.""" - units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)} - candidates = [ - {"id": "candidate-1", "event_date": datetime(2024, 6, 15, 10, 0, 0, tzinfo=timezone.utc)}, - ] - - links = compute_temporal_links(units, candidates, time_window_hours=24) - - assert len(links) == 1 - assert links[0][0] == "unit-1" - assert links[0][1] == "candidate-1" - assert links[0][2] == "temporal" - assert links[0][4] is None - # Weight should be high since they're close (2 hours apart) - assert links[0][3] > 0.9 - - def test_candidate_outside_window_no_link(self): - """Test that candidates outside time window don't create links.""" - units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)} - candidates = [ - {"id": "candidate-1", "event_date": datetime(2024, 6, 10, 12, 0, 0, tzinfo=timezone.utc)}, - ] - - links = compute_temporal_links(units, candidates, time_window_hours=24) - - assert len(links) == 0 - - def test_weight_decreases_with_distance(self): - """Test that weight decreases as time difference increases.""" - units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)} - candidates = [ - {"id": "close", "event_date": datetime(2024, 6, 15, 11, 0, 0, tzinfo=timezone.utc)}, # 1 hour - {"id": "far", "event_date": datetime(2024, 6, 14, 18, 0, 0, tzinfo=timezone.utc)}, # 18 hours - ] - - links = compute_temporal_links(units, candidates, time_window_hours=24) - - assert len(links) == 2 - close_link = next(l for l in links if l[1] == "close") - far_link = next(l for l in links if l[1] == "far") - - assert close_link[3] > far_link[3] - - def test_max_10_links_per_unit(self): - """Test that at most 10 links are created per unit.""" - units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)} - # Create 15 candidates all within window - candidates = [ - {"id": f"candidate-{i}", "event_date": datetime(2024, 6, 15, 11, 0, 0, tzinfo=timezone.utc)} - for i in range(15) - ] - - links = compute_temporal_links(units, candidates, time_window_hours=24) - - assert len(links) == 10 - - def test_multiple_units_multiple_candidates(self): - """Test with multiple units and candidates.""" - units = { - "unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), - "unit-2": datetime(2024, 6, 20, 12, 0, 0, tzinfo=timezone.utc), - } - candidates = [ - {"id": "c1", "event_date": datetime(2024, 6, 15, 10, 0, 0, tzinfo=timezone.utc)}, # near unit-1 - {"id": "c2", "event_date": datetime(2024, 6, 20, 10, 0, 0, tzinfo=timezone.utc)}, # near unit-2 - {"id": "c3", "event_date": datetime(2024, 6, 17, 12, 0, 0, tzinfo=timezone.utc)}, # between, near neither - ] - - links = compute_temporal_links(units, candidates, time_window_hours=24) - - # unit-1 should link to c1 only - # unit-2 should link to c2 only - unit1_links = [l for l in links if l[0] == "unit-1"] - unit2_links = [l for l in links if l[0] == "unit-2"] - - assert len(unit1_links) == 1 - assert unit1_links[0][1] == "c1" - - assert len(unit2_links) == 1 - assert unit2_links[0][1] == "c2" - - def test_mixed_naive_and_aware_datetimes(self): - """Test that mixed naive/aware datetimes work correctly.""" - units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0)} # naive - candidates = [ - {"id": "c1", "event_date": datetime(2024, 6, 15, 10, 0, 0, tzinfo=timezone.utc)}, # aware - ] - - # Should not raise TypeError - links = compute_temporal_links(units, candidates, time_window_hours=24) - assert len(links) == 1 - - def test_overflow_near_datetime_min(self): - """Test overflow protection when unit date is near datetime.min.""" - units = {"unit-1": datetime(1, 1, 2, 0, 0, tzinfo=timezone.utc)} - candidates = [ - {"id": "c1", "event_date": datetime(1, 1, 1, 12, 0, 0, tzinfo=timezone.utc)}, - ] - - # Should not raise OverflowError - links = compute_temporal_links(units, candidates, time_window_hours=48) - assert len(links) == 1 - - def test_overflow_near_datetime_max(self): - """Test overflow protection when unit date is near datetime.max.""" - units = {"unit-1": datetime(9999, 12, 30, 0, 0, tzinfo=timezone.utc)} - candidates = [ - {"id": "c1", "event_date": datetime(9999, 12, 31, 12, 0, 0, tzinfo=timezone.utc)}, - ] - - # Should not raise OverflowError - links = compute_temporal_links(units, candidates, time_window_hours=48) - assert len(links) == 1 - - def test_weight_minimum_is_0_3(self): - """Test that weight doesn't go below 0.3.""" - units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)} - candidates = [ - # 23 hours apart - should be just within 24h window but low weight - {"id": "c1", "event_date": datetime(2024, 6, 14, 13, 0, 0, tzinfo=timezone.utc)}, - ] - - links = compute_temporal_links(units, candidates, time_window_hours=24) - - assert len(links) == 1 - assert links[0][3] >= 0.3 - - class TestCapLinksPerUnit: """Tests for the _cap_links_per_unit helper function.""" diff --git a/hindsight-api-slim/tests/test_llm_provider.py b/hindsight-api-slim/tests/test_llm_provider.py index 7f92e21cab..d08fed68fe 100644 --- a/hindsight-api-slim/tests/test_llm_provider.py +++ b/hindsight-api-slim/tests/test_llm_provider.py @@ -13,13 +13,10 @@ """ import os -from datetime import datetime import pytest from hindsight_api.engine.llm_wrapper import LLMProvider -from hindsight_api.engine.utils import extract_facts -from hindsight_api.engine.search.think_utils import reflect pytestmark = pytest.mark.hs_llm_mat @@ -150,53 +147,3 @@ class TestResponse(BaseModel): tool_call = result.tool_calls[0] assert tool_call.name == "get_weather", f"Expected 'get_weather', got '{tool_call.name}'" assert "location" in tool_call.arguments, "Tool call arguments missing 'location'" - - -@pytest.mark.asyncio -@pytest.mark.timeout(600) -async def test_llm_memory_operations(): - """ - Test fact extraction and reflect with the configured LLM provider. - """ - llm = _make_llm() - - # Fact extraction (structured output) - test_text = """ - User: I just got back from my trip to Paris last week. The Eiffel Tower was amazing! - Assistant: That sounds wonderful! How long were you there? - User: About 5 days. I also visited the Louvre and saw the Mona Lisa. - """ - - facts, chunks = await extract_facts( - text=test_text, - event_date=datetime(2024, 12, 10), - context="Travel conversation", - llm_config=llm, - ) - - assert facts is not None, "fact extraction returned None" - assert len(facts) > 0, "should extract at least one fact" - - for fact in facts: - assert fact.fact, "fact missing text" - assert fact.fact_type in ["world", "experience"], f"invalid fact_type: {fact.fact_type}" - - # Reflect - response = await reflect( - llm_config=llm, - query="What was the highlight of my Paris trip?", - experience_facts=[ - "I visited Paris in December 2024", - "I saw the Eiffel Tower and it was amazing", - "I visited the Louvre and saw the Mona Lisa", - "The trip lasted 5 days", - ], - world_facts=[ - "The Eiffel Tower is a famous landmark in Paris", - "The Mona Lisa is displayed at the Louvre museum", - ], - name="Traveler", - ) - - assert response is not None, "reflect returned None" - assert len(response) > 10, "reflect response too short" diff --git a/hindsight-api-slim/tests/test_multilingual_bm25.py b/hindsight-api-slim/tests/test_multilingual_bm25.py index 20637ff75c..8844ef12fd 100644 --- a/hindsight-api-slim/tests/test_multilingual_bm25.py +++ b/hindsight-api-slim/tests/test_multilingual_bm25.py @@ -15,7 +15,6 @@ import pytest -from hindsight_api.engine.consolidation.prompts import build_batch_consolidation_prompt from hindsight_api.engine.prompt_utils import output_language_directive from hindsight_api.engine.reflect.prompts import build_final_system_prompt from hindsight_api.engine.retain.fact_extraction import _build_extraction_prompt_and_schema @@ -114,22 +113,29 @@ def test_retain_works_with_custom_mode(): def test_consolidation_unset_does_not_inject_directive(): - prompt = build_batch_consolidation_prompt(llm_output_language=None) + from hindsight_api.engine.consolidation.prompts import build_consolidation_system_prompt + + prompt = build_consolidation_system_prompt(llm_output_language=None) assert "Respond exclusively in" not in prompt def test_consolidation_injects_directive(): - prompt = build_batch_consolidation_prompt(llm_output_language="Chinese") + from hindsight_api.engine.consolidation.prompts import build_consolidation_system_prompt + + prompt = build_consolidation_system_prompt(llm_output_language="Chinese") assert "Respond exclusively in Chinese" in prompt assert "Translate any source content into Chinese" in prompt def test_consolidation_directive_does_not_break_format_placeholders(): - """The consolidation prompt is later passed through str.format(facts_text=..., observations_text=...). - The appended directive must not introduce stray { / } that would raise KeyError.""" - prompt = build_batch_consolidation_prompt(llm_output_language="Japanese") - # str.format must succeed with the expected placeholders. - prompt.format(facts_text="X", observations_text="Y") + """build_consolidation_system_prompt appends the language directive and then runs + str.format() internally (the cached OUTPUT examples use doubled braces). A directive + that introduced a stray { / } would raise KeyError here — so a successful build with + a language set is the regression guard.""" + from hindsight_api.engine.consolidation.prompts import build_consolidation_system_prompt + + prompt = build_consolidation_system_prompt(llm_output_language="Japanese") + assert "Respond exclusively in Japanese" in prompt # --------------------------------------------------------------------------- diff --git a/hindsight-api-slim/tests/test_prompt_brace_escape.py b/hindsight-api-slim/tests/test_prompt_brace_escape.py index c461e9e5db..13c7abf331 100644 --- a/hindsight-api-slim/tests/test_prompt_brace_escape.py +++ b/hindsight-api-slim/tests/test_prompt_brace_escape.py @@ -47,24 +47,30 @@ def test_edge_cases_do_not_crash(self, text): class TestConsolidationBraceSafety: + """The per-batch consolidation input runs str.format() internally + (build_consolidation_input → template.format(facts_text=..., observations_text=...)), + so a mission or capacity note containing literal braces must be escaped or it + raises KeyError and crashes consolidation.""" + def test_mission_with_json_renders(self): - from hindsight_api.engine.consolidation.prompts import ( - build_batch_consolidation_prompt, - ) + from hindsight_api.engine.consolidation.prompts import build_consolidation_input mission = '{"dedup": true, "merge": true}' - prompt = build_batch_consolidation_prompt(observations_mission=mission) - rendered = prompt.format(facts_text="", observations_text="") + rendered = build_consolidation_input( + facts_text="", observations_text="", observations_mission=mission + ) assert mission in rendered def test_capacity_note_with_braces_renders(self): - from hindsight_api.engine.consolidation.prompts import ( - build_batch_consolidation_prompt, - ) + from hindsight_api.engine.consolidation.prompts import build_consolidation_input note = "Use shape {limit, used}" - prompt = build_batch_consolidation_prompt(observations_mission="m", observation_capacity_note=note) - rendered = prompt.format(facts_text="", observations_text="") + rendered = build_consolidation_input( + facts_text="", + observations_text="", + observations_mission="m", + observation_capacity_note=note, + ) assert "{limit, used}" in rendered diff --git a/hindsight-api-slim/tests/test_retain_orchestrator_mapping.py b/hindsight-api-slim/tests/test_retain_orchestrator_mapping.py index 12e974ef63..cbb42c1a26 100644 --- a/hindsight-api-slim/tests/test_retain_orchestrator_mapping.py +++ b/hindsight-api-slim/tests/test_retain_orchestrator_mapping.py @@ -179,35 +179,6 @@ def test_rejects_fact_embedding_length_mismatch(self): _process_extracted_facts(extracted, [[1.0]]) -class TestEmbeddingSingleValidation: - def test_generate_embedding_preserves_validation_runtime_error(self): - backend = MagicMock() - backend.dimension = 3 - backend.encode_documents.return_value = [[]] - - with pytest.raises(RuntimeError, match="embedding 0 has dimension 0; expected 3"): - embedding_utils.generate_embedding(backend, "a") - - def test_generate_embedding_raises_when_backend_returns_wrong_count(self): - backend = MagicMock() - backend.dimension = 3 - backend.encode_documents.return_value = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]] - - with pytest.raises(RuntimeError, match="returned 2 vectors for 1 input text"): - embedding_utils.generate_embedding(backend, "a") - - def test_generate_embedding_uses_query_encoder_when_requested(self): - backend = MagicMock() - backend.dimension = 2 - backend.encode_query.return_value = [[0.1, 0.2]] - - result = embedding_utils.generate_embedding(backend, "a", input_type="query") - - assert result == [0.1, 0.2] - backend.encode_query.assert_called_once_with(["a"]) - backend.encode_documents.assert_not_called() - - class TestEmbeddingsBatchLengthGuarantee: def test_raises_when_backend_returns_fewer_embeddings(self): # Regression for #1037: backends that silently truncate must not pass diff --git a/hindsight-cli/src/commands/explore.rs b/hindsight-cli/src/commands/explore.rs index 0a51df38d9..da0e43cf63 100644 --- a/hindsight-cli/src/commands/explore.rs +++ b/hindsight-cli/src/commands/explore.rs @@ -208,16 +208,6 @@ impl App { Ok(()) } - fn toggle_auto_refresh(&mut self) { - self.auto_refresh_enabled = !self.auto_refresh_enabled; - if self.auto_refresh_enabled { - self.status_message = "Auto-refresh enabled (5s)".to_string(); - self.last_refresh = Instant::now(); - } else { - self.status_message = "Auto-refresh disabled".to_string(); - } - } - fn should_refresh(&self) -> bool { self.auto_refresh_enabled && self.last_refresh.elapsed() >= self.refresh_interval } diff --git a/hindsight-cli/src/config.rs b/hindsight-cli/src/config.rs index 119f7a7678..569c739aa7 100644 --- a/hindsight-cli/src/config.rs +++ b/hindsight-cli/src/config.rs @@ -77,11 +77,6 @@ impl Config { Self::validate_and_create(DEFAULT_API_URL.to_string(), env_api_key, ConfigSource::Default) } - /// Legacy method for backwards compatibility - pub fn from_env() -> Result { - Self::load() - } - fn validate_and_create(api_url: String, api_key: Option, source: ConfigSource) -> Result { if !api_url.starts_with("http://") && !api_url.starts_with("https://") { anyhow::bail!( @@ -142,10 +137,6 @@ impl Config { } } - pub fn save_api_url(api_url: &str) -> Result { - Self::save_config(api_url, None) - } - pub fn save_config(api_url: &str, api_key: Option<&str>) -> Result { let config_dir = Self::config_dir() .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; diff --git a/hindsight-cli/src/main.rs b/hindsight-cli/src/main.rs index 2f453f993f..03bae7d4b0 100644 --- a/hindsight-cli/src/main.rs +++ b/hindsight-cli/src/main.rs @@ -4,7 +4,6 @@ mod config; mod errors; mod output; mod ui; -mod utils; use anyhow::Result; use api::ApiClient; diff --git a/hindsight-cli/src/output.rs b/hindsight-cli/src/output.rs index d0f0d71634..b6422738de 100644 --- a/hindsight-cli/src/output.rs +++ b/hindsight-cli/src/output.rs @@ -8,18 +8,6 @@ pub enum OutputFormat { Yaml, } -impl OutputFormat { - /// Parse output format from string - pub fn from_str(s: &str) -> Option { - match s.to_lowercase().as_str() { - "json" => Some(OutputFormat::Json), - "yaml" | "yml" => Some(OutputFormat::Yaml), - "pretty" | "text" => Some(OutputFormat::Pretty), - _ => None, - } - } -} - /// Format data as JSON string pub fn to_json(data: &T) -> Result { Ok(serde_json::to_string_pretty(data)?) @@ -58,35 +46,6 @@ mod tests { active: bool, } - #[test] - fn test_output_format_from_str_json() { - assert_eq!(OutputFormat::from_str("json"), Some(OutputFormat::Json)); - assert_eq!(OutputFormat::from_str("JSON"), Some(OutputFormat::Json)); - assert_eq!(OutputFormat::from_str("Json"), Some(OutputFormat::Json)); - } - - #[test] - fn test_output_format_from_str_yaml() { - assert_eq!(OutputFormat::from_str("yaml"), Some(OutputFormat::Yaml)); - assert_eq!(OutputFormat::from_str("YAML"), Some(OutputFormat::Yaml)); - assert_eq!(OutputFormat::from_str("yml"), Some(OutputFormat::Yaml)); - assert_eq!(OutputFormat::from_str("YML"), Some(OutputFormat::Yaml)); - } - - #[test] - fn test_output_format_from_str_pretty() { - assert_eq!(OutputFormat::from_str("pretty"), Some(OutputFormat::Pretty)); - assert_eq!(OutputFormat::from_str("PRETTY"), Some(OutputFormat::Pretty)); - assert_eq!(OutputFormat::from_str("text"), Some(OutputFormat::Pretty)); - } - - #[test] - fn test_output_format_from_str_invalid() { - assert_eq!(OutputFormat::from_str("xml"), None); - assert_eq!(OutputFormat::from_str("csv"), None); - assert_eq!(OutputFormat::from_str(""), None); - } - #[test] fn test_to_json() { let data = TestData { diff --git a/hindsight-cli/src/utils.rs b/hindsight-cli/src/utils.rs deleted file mode 100644 index c1b8e5a470..0000000000 --- a/hindsight-cli/src/utils.rs +++ /dev/null @@ -1,15 +0,0 @@ -use anyhow::{Context, Result}; -use crate::api::ApiClient; -use crate::config::Config; -use crate::output::OutputFormat; - -/// Get API client from config -pub fn get_client(config: &Config) -> Result { - ApiClient::new(config.api_url.clone(), config.api_key.clone()) - .context("Failed to create API client") -} - -/// Get output format, preferring CLI arg over default -pub fn get_output_format(cli_format: Option, _config: &Config) -> OutputFormat { - cli_format.unwrap_or(OutputFormat::Pretty) -} diff --git a/hindsight-control-plane/src/components/think-view.tsx b/hindsight-control-plane/src/components/think-view.tsx index 4d4240ee52..551fd4cf70 100644 --- a/hindsight-control-plane/src/components/think-view.tsx +++ b/hindsight-control-plane/src/components/think-view.tsx @@ -24,7 +24,6 @@ import { Database, Brain, MessageSquare, - Shield, X, Check, Play, @@ -61,9 +60,6 @@ export function ThinkView() { const [feedbackSubmitting, setFeedbackSubmitting] = useState(false); const [feedbackSubmitted, setFeedbackSubmitted] = useState(false); const [selectedMemoryId, setSelectedMemoryId] = useState(null); - const [selectedDirective, setSelectedDirective] = useState(null); - const [fullDirective, setFullDirective] = useState(null); - const [loadingDirective, setLoadingDirective] = useState(false); const [selectedObservation, setSelectedObservation] = useState(null); const [fullObservation, setFullObservation] = useState(null); const [loadingObservation, setLoadingObservation] = useState(false); @@ -72,25 +68,6 @@ export function ThinkView() { const FEEDBACK_DIRECTIVE_NAME = "General Feedback"; - // Load full directive data when one is selected - const handleSelectDirective = async (directive: any) => { - setSelectedDirective(directive); - setFullDirective(null); - if (!currentBank || !directive?.id) return; - - setLoadingDirective(true); - try { - const directives = await client.listDirectives(currentBank); - const fullDir = directives.items?.find((d: any) => d.id === directive.id); - setFullDirective(fullDir || directive); - } catch (error) { - console.error("Failed to load directive:", error); - setFullDirective(directive); // Fall back to partial data - } finally { - setLoadingDirective(false); - } - }; - // Load full observation data when one is selected const handleSelectObservation = async (observation: any) => { setSelectedObservation(observation); @@ -922,83 +899,6 @@ export function ThinkView() { {/* Memory Detail Modal */} setSelectedMemoryId(null)} /> - {/* Directive Detail Panel */} - {selectedDirective && ( -
-
-
-
- -

Directive

-
- -
- {loadingDirective ? ( -
-
-
- ) : ( -
-
-

Name

-

- {fullDirective?.name || selectedDirective.name} -

-
- {fullDirective?.description && ( -
-

Description

-

{fullDirective.description}

-
- )} - {fullDirective?.tags && fullDirective.tags.length > 0 && ( -
-

Tags

-
- {fullDirective.tags.map((tag: string) => ( - - - {tag} - - ))} -
-
- )} - {/* Show content from directive */} - {(fullDirective?.content || selectedDirective.content) && ( -
-

Content

-
-
- {fullDirective?.content || selectedDirective.content} -
-
-
- )} -
-

ID

-

- {selectedDirective.id} -

-
-
- )} -
-
- )} - {/* Observation Detail Panel */} {selectedObservation && (
diff --git a/hindsight-control-plane/src/lib/api.ts b/hindsight-control-plane/src/lib/api.ts index 133242ee9e..75cc565d7e 100644 --- a/hindsight-control-plane/src/lib/api.ts +++ b/hindsight-control-plane/src/lib/api.ts @@ -615,18 +615,6 @@ export class ControlPlaneClient { ); } - /** - * Regenerate entity observations - */ - async regenerateEntityObservations(entityId: string, bankId: string) { - return this.fetchApi( - `/api/entities/${encodeURIComponent(entityId)}/regenerate?bank_id=${encodeURIComponent(bankId)}`, - { - method: "POST", - } - ); - } - /** * List documents */ @@ -928,16 +916,6 @@ export class ControlPlaneClient { }>(`/api/profile/${encodeURIComponent(bankId)}`); } - /** - * Set bank mission - */ - async setBankMission(bankId: string, mission: string) { - return this.fetchApi(bankApi(bankId), { - method: "PATCH", - body: JSON.stringify({ mission }), - }); - } - /** * List directives for a bank */ @@ -1086,27 +1064,6 @@ export class ControlPlaneClient { }>(bankApi(bankId, `/operations/${encodeURIComponent(operationId)}${qs}`)); } - /** - * Update bank profile - */ - async updateBankProfile( - bankId: string, - profile: { - name?: string; - disposition?: { - skepticism: number; - literalism: number; - empathy: number; - }; - mission?: string; - } - ) { - return this.fetchApi(`/api/profile/${encodeURIComponent(bankId)}`, { - method: "PUT", - body: JSON.stringify(profile), - }); - } - // ============= OBSERVATIONS (auto-consolidated, read-only) ============= /** @@ -1148,35 +1105,6 @@ export class ControlPlaneClient { }>(bankApi(bankId, `/observations${query ? `?${query}` : ""}`)); } - /** - * Get an observation with source memories - */ - async getObservation(bankId: string, observationId: string) { - return this.fetchApi<{ - id: string; - bank_id: string; - text: string; - proof_count: number; - history: Array<{ - previous_text: string; - changed_at: string; - reason: string; - }>; - tags: string[]; - source_memory_ids: string[]; - source_memories: Array<{ - id: string; - text: string; - type: string; - context?: string; - occurred_start?: string; - mentioned_at?: string; - }>; - created_at: string; - updated_at: string; - }>(bankApi(bankId, `/observations/${encodeURIComponent(observationId)}`)); - } - /** * List the distinct observation scopes for a bank. * diff --git a/hindsight-dev/benchmarks/common/benchmark_runner.py b/hindsight-dev/benchmarks/common/benchmark_runner.py index e897848150..27d391e144 100644 --- a/hindsight-dev/benchmarks/common/benchmark_runner.py +++ b/hindsight-dev/benchmarks/common/benchmark_runner.py @@ -855,28 +855,6 @@ async def judge_single(result): "detailed_results": judged_results, } - async def _agent_has_data(self, agent_id: str) -> bool: - """ - Check if an agent has any indexed memory units. - - Args: - agent_id: Agent ID to check - - Returns: - True if agent has at least one memory unit, False otherwise - """ - try: - # Use direct database access for local memory - pool = await self.memory._get_pool() - async with pool.acquire() as conn: - result = await conn.fetchrow( - "SELECT COUNT(*) as count FROM memory_units WHERE bank_id = $1 LIMIT 1", agent_id - ) - return result["count"] > 0 - except Exception as e: - console.print(f" [red]Warning: Error checking agent data: {e}[/red]") - return False - async def process_single_item( self, item: Dict, diff --git a/hindsight-dev/hindsight_dev/client_coverage_check.py b/hindsight-dev/hindsight_dev/client_coverage_check.py index 2d19b99a8c..18d28a1472 100644 --- a/hindsight-dev/hindsight_dev/client_coverage_check.py +++ b/hindsight-dev/hindsight_dev/client_coverage_check.py @@ -55,11 +55,6 @@ def snake_to_camel(name: str) -> str: return parts[0] + "".join(p.capitalize() for p in parts[1:]) -def camel_to_snake(name: str) -> str: - """Convert camelCase to snake_case.""" - return re.sub(r"(?<=[a-z0-9])([A-Z])", r"_\1", name).lower() - - def load_openapi_operations(spec_path: Path) -> dict[str, list[str]]: """Return {operation_id: [property_names]} for operations with request bodies.""" spec = json.loads(spec_path.read_text()) diff --git a/hindsight-dev/hindsight_dev/sync_cookbook.py b/hindsight-dev/hindsight_dev/sync_cookbook.py index 041c851e62..739765b5e5 100644 --- a/hindsight-dev/hindsight_dev/sync_cookbook.py +++ b/hindsight-dev/hindsight_dev/sync_cookbook.py @@ -412,51 +412,6 @@ def process_applications(cookbook_dir: Path, apps_dir: Path) -> list[dict]: return apps -def update_sidebars(recipes: list[dict], apps: list[dict], sidebars_file: Path): - """Update sidebars.ts - keep it simple with just the index.""" - content = sidebars_file.read_text() - - # Simple sidebar with just the cookbook index - new_cookbook_sidebar = """cookbookSidebar: [ - { - type: 'doc', - id: 'cookbook/index', - label: 'Cookbook', - }, - ]""" - - # Replace existing cookbookSidebar - start = content.find("cookbookSidebar:") - if start == -1: - raise ValueError("cookbookSidebar not found in sidebars.ts") - - # Find the opening bracket - bracket_start = content.find("[", start) - if bracket_start == -1: - raise ValueError("Could not find opening bracket for cookbookSidebar") - - # Find matching closing bracket by counting brackets - depth = 0 - end = bracket_start - for i, char in enumerate(content[bracket_start:], bracket_start): - if char == "[": - depth += 1 - elif char == "]": - depth -= 1 - if depth == 0: - end = i + 1 - break - - # Include trailing comma if present - if end < len(content) and content[end] == ",": - end += 1 - - content = content[:start] + new_cookbook_sidebar + "," + content[end:] - - sidebars_file.write_text(content) - print("\nUpdated sidebars.ts") - - def strip_local_md_links(content: str) -> str: """Replace relative .md links with plain text to avoid broken links in Docusaurus. diff --git a/hindsight-embed/hindsight_embed/daemon_client.py b/hindsight-embed/hindsight_embed/daemon_client.py index a48d68fba0..ef1f2633d3 100644 --- a/hindsight-embed/hindsight_embed/daemon_client.py +++ b/hindsight-embed/hindsight_embed/daemon_client.py @@ -10,7 +10,7 @@ from pathlib import Path from .daemon_embed_manager import DaemonEmbedManager -from .profile_manager import ProfileManager, resolve_active_profile +from .profile_manager import resolve_active_profile logger = logging.getLogger(__name__) @@ -25,23 +25,6 @@ CLI_INSTALLER_URL = "https://hindsight.vectorize.io/get-cli" -def get_daemon_port(profile: str | None = None) -> int: - """Get daemon port for a profile. - - Args: - profile: Profile name (None = resolve from priority). - - Returns: - Port number for daemon. - """ - if profile is None: - profile = resolve_active_profile() - - pm = ProfileManager() - paths = pm.resolve_profile_paths(profile) - return paths.port - - def get_daemon_url(profile: str | None = None) -> str: """Get daemon URL for a profile. diff --git a/hindsight-integrations/aider/hindsight_aider/content.py b/hindsight-integrations/aider/hindsight_aider/content.py index d0ec19b98e..65e00bf2a4 100644 --- a/hindsight-integrations/aider/hindsight_aider/content.py +++ b/hindsight-integrations/aider/hindsight_aider/content.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any def compose_recall_query(aider_args: list[str], default_query: str, max_chars: int = 800) -> str: @@ -40,13 +40,3 @@ def format_transcript(session_text: str, max_chars: int = 200_000) -> str: if len(text) > max_chars: text = text[-max_chars:] return text - - -def find_workdir(aider_args: list[str]) -> Optional[str]: - """Return an explicit working dir if Aider was pointed at one (``--cwd``).""" - for i, arg in enumerate(aider_args): - if arg == "--cwd" and i + 1 < len(aider_args): - return aider_args[i + 1] - if arg.startswith("--cwd="): - return arg[len("--cwd=") :] - return None diff --git a/hindsight-integrations/claude-code/scripts/lib/client.py b/hindsight-integrations/claude-code/scripts/lib/client.py index 6d6e1ec57e..1a4fc09c35 100644 --- a/hindsight-integrations/claude-code/scripts/lib/client.py +++ b/hindsight-integrations/claude-code/scripts/lib/client.py @@ -12,8 +12,6 @@ from typing import Optional DEFAULT_TIMEOUT = 15 # seconds -HEALTH_CHECK_RETRIES = 3 -HEALTH_CHECK_DELAY = 2 # seconds def _plugin_version() -> str: @@ -83,26 +81,6 @@ def request(self, method: str, path: str, body: Optional[dict] = None, timeout: pass raise RuntimeError(f"HTTP {e.code} from {url}: {body_text}") from e - def health_check(self, timeout: int = 5) -> bool: - """Check if the Hindsight server is reachable. - - Mirrors Openclaw's checkExternalApiHealth: retries up to 3 times - with 2s delay between attempts. - """ - import time - - for attempt in range(1, HEALTH_CHECK_RETRIES + 1): - try: - url = f"{self.api_url}/health" - req = urllib.request.Request(url, headers=self._headers(), method="GET") - with urllib.request.urlopen(req, timeout=timeout) as resp: - if resp.status == 200: - return True - except Exception: - pass - if attempt < HEALTH_CHECK_RETRIES: - time.sleep(HEALTH_CHECK_DELAY) - return False def recall( self, diff --git a/hindsight-integrations/claude-code/scripts/lib/state.py b/hindsight-integrations/claude-code/scripts/lib/state.py index 9f0415d851..e6cb58d3bd 100644 --- a/hindsight-integrations/claude-code/scripts/lib/state.py +++ b/hindsight-integrations/claude-code/scripts/lib/state.py @@ -92,12 +92,6 @@ def write_state(name: str, data): pass -def get_turn_count(session_id: str) -> int: - """Get the current turn count for a session.""" - turns = read_state("turns.json", {}) - return turns.get(session_id, 0) - - def increment_turn_count(session_id: str) -> int: """Increment and return the turn count for a session. diff --git a/hindsight-integrations/claude-code/tests/test_client.py b/hindsight-integrations/claude-code/tests/test_client.py index 984c825c31..a5300f4c82 100644 --- a/hindsight-integrations/claude-code/tests/test_client.py +++ b/hindsight-integrations/claude-code/tests/test_client.py @@ -3,7 +3,7 @@ import json import urllib.error from io import BytesIO -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest from lib.client import USER_AGENT, HindsightClient, _validate_api_url @@ -202,36 +202,6 @@ def fake_open(req, timeout=None): assert "my%3A%3Abank" in captured["url"] -class TestHindsightClientHealthCheck: - def test_returns_true_on_200(self): - c = HindsightClient("http://localhost:9077") - with patch("urllib.request.urlopen", return_value=FakeResp({}, status=200)): - with patch("time.sleep"): # don't actually sleep - assert c.health_check() is True - - def test_returns_false_after_retries(self): - c = HindsightClient("http://localhost:9077") - with patch("urllib.request.urlopen", side_effect=OSError("refused")): - with patch("time.sleep"): - assert c.health_check() is False - - def test_retries_on_failure(self): - c = HindsightClient("http://localhost:9077") - call_count = 0 - - def flaky(*_a, **_kw): - nonlocal call_count - call_count += 1 - if call_count < 3: - raise OSError("not yet") - return FakeResp({}, status=200) - - with patch("urllib.request.urlopen", side_effect=flaky): - with patch("time.sleep"): - result = c.health_check() - - assert result is True - assert call_count == 3 class TestRequestTimeoutOverride: @@ -304,19 +274,6 @@ def fake_open(req, timeout=None): assert captured["timeout"] == 15 - def test_override_does_not_affect_health_check(self): - c = HindsightClient("http://localhost:9077", request_timeout_override=60) - captured = {} - - def fake_open(req, timeout=None): - captured["timeout"] = timeout - return FakeResp({}, status=200) - - with patch("urllib.request.urlopen", side_effect=fake_open): - with patch("time.sleep"): - c.health_check() - - assert captured["timeout"] == 5 class TestHindsightClientSetBankMission: diff --git a/hindsight-integrations/codex/scripts/lib/client.py b/hindsight-integrations/codex/scripts/lib/client.py index f0b06264c9..6ebfd82448 100644 --- a/hindsight-integrations/codex/scripts/lib/client.py +++ b/hindsight-integrations/codex/scripts/lib/client.py @@ -12,8 +12,6 @@ from typing import Optional DEFAULT_TIMEOUT = 15 # seconds -HEALTH_CHECK_RETRIES = 3 -HEALTH_CHECK_DELAY = 2 # seconds def _plugin_version() -> str: @@ -72,26 +70,6 @@ def _request(self, method: str, path: str, body: Optional[dict] = None, timeout: pass raise RuntimeError(f"HTTP {e.code} from {url}: {body_text}") from e - def health_check(self, timeout: int = 5) -> bool: - """Check if the Hindsight server is reachable. - - Mirrors Openclaw's checkExternalApiHealth: retries up to 3 times - with 2s delay between attempts. - """ - import time - - for attempt in range(1, HEALTH_CHECK_RETRIES + 1): - try: - url = f"{self.api_url}/health" - req = urllib.request.Request(url, headers=self._headers(), method="GET") - with urllib.request.urlopen(req, timeout=timeout) as resp: - if resp.status == 200: - return True - except Exception: - pass - if attempt < HEALTH_CHECK_RETRIES: - time.sleep(HEALTH_CHECK_DELAY) - return False def recall( self, diff --git a/hindsight-integrations/codex/scripts/lib/state.py b/hindsight-integrations/codex/scripts/lib/state.py index 9689d61201..53a3f25088 100644 --- a/hindsight-integrations/codex/scripts/lib/state.py +++ b/hindsight-integrations/codex/scripts/lib/state.py @@ -70,12 +70,6 @@ def write_state(name: str, data): pass -def get_turn_count(session_id: str) -> int: - """Get the current turn count for a session.""" - turns = read_state("turns.json", {}) - return turns.get(session_id, 0) - - def increment_turn_count(session_id: str) -> int: """Increment and return the turn count for a session. diff --git a/hindsight-integrations/codex/tests/test_client.py b/hindsight-integrations/codex/tests/test_client.py index 9db4257e9e..56497b9ece 100644 --- a/hindsight-integrations/codex/tests/test_client.py +++ b/hindsight-integrations/codex/tests/test_client.py @@ -27,16 +27,3 @@ def fake_open(req, timeout=None): assert captured["ua"] == USER_AGENT assert captured["ua"].startswith("hindsight-codex/") - - def test_health_check_sends_user_agent(self): - c = HindsightClient("http://localhost:9077") - captured = {} - - def fake_open(req, timeout=None): - captured["ua"] = req.get_header("User-agent") - return FakeHTTPResponse({}, status=200) - - with patch("urllib.request.urlopen", side_effect=fake_open): - c.health_check(timeout=1) - - assert captured["ua"] == USER_AGENT diff --git a/hindsight-integrations/cursor-cli/hindsight_cursor_cli/hooks/scripts/lib/client.py b/hindsight-integrations/cursor-cli/hindsight_cursor_cli/hooks/scripts/lib/client.py index 615699f11d..3f3637e50e 100644 --- a/hindsight-integrations/cursor-cli/hindsight_cursor_cli/hooks/scripts/lib/client.py +++ b/hindsight-integrations/cursor-cli/hindsight_cursor_cli/hooks/scripts/lib/client.py @@ -11,8 +11,6 @@ from pathlib import Path DEFAULT_TIMEOUT = 15 -HEALTH_CHECK_RETRIES = 3 -HEALTH_CHECK_DELAY = 2 def _plugin_version(): @@ -71,27 +69,6 @@ def _request(self, method, path, body=None, timeout=DEFAULT_TIMEOUT): pass raise RuntimeError(f"HTTP {e.code} from {url}: {body_text}") from e - def health_check(self, timeout=5): - """Check if the Hindsight server is reachable. - - Mirrors codex's behavior: retries up to 3 times with 2s delay - between attempts. - """ - import time - - for attempt in range(1, HEALTH_CHECK_RETRIES + 1): - try: - url = f"{self.api_url}/health" - req = urllib.request.Request(url, headers=self._headers(), method="GET") - with urllib.request.urlopen(req, timeout=timeout) as resp: - if resp.status == 200: - return True - except Exception: - pass - if attempt < HEALTH_CHECK_RETRIES: - time.sleep(HEALTH_CHECK_DELAY) - return False - def recall( self, bank_id, diff --git a/hindsight-integrations/cursor-cli/hindsight_cursor_cli/hooks/scripts/lib/state.py b/hindsight-integrations/cursor-cli/hindsight_cursor_cli/hooks/scripts/lib/state.py index 56edc8e24f..0526f56044 100644 --- a/hindsight-integrations/cursor-cli/hindsight_cursor_cli/hooks/scripts/lib/state.py +++ b/hindsight-integrations/cursor-cli/hindsight_cursor_cli/hooks/scripts/lib/state.py @@ -68,12 +68,6 @@ def write_state(name, data): pass -def get_turn_count(session_id): - """Get the current turn count for a session.""" - turns = read_state("turns.json", {}) - return turns.get(session_id, 0) - - def increment_turn_count(session_id): """Increment and return the turn count for a session. diff --git a/hindsight-integrations/cursor-cli/tests/test_client.py b/hindsight-integrations/cursor-cli/tests/test_client.py index 657d211939..2380822a8a 100644 --- a/hindsight-integrations/cursor-cli/tests/test_client.py +++ b/hindsight-integrations/cursor-cli/tests/test_client.py @@ -27,19 +27,6 @@ def fake_open(req, timeout=None): assert captured["ua"] == USER_AGENT assert captured["ua"].startswith("hindsight-cursor-cli/") - def test_health_check_sends_user_agent(self): - c = HindsightClient("http://localhost:9077") - captured = {} - - def fake_open(req, timeout=None): - captured["ua"] = req.get_header("User-agent") - return FakeHTTPResponse({}, status=200) - - with patch("urllib.request.urlopen", side_effect=fake_open): - c.health_check(timeout=1) - - assert captured["ua"] == USER_AGENT - class TestURLValidation: def test_rejects_non_http_scheme(self): diff --git a/hindsight-integrations/cursor/scripts/lib/client.py b/hindsight-integrations/cursor/scripts/lib/client.py index 58b7bc4c2e..422d955e63 100644 --- a/hindsight-integrations/cursor/scripts/lib/client.py +++ b/hindsight-integrations/cursor/scripts/lib/client.py @@ -11,8 +11,6 @@ from typing import Optional DEFAULT_TIMEOUT = 15 # seconds -HEALTH_CHECK_RETRIES = 3 -HEALTH_CHECK_DELAY = 2 # seconds def _validate_api_url(url: str) -> str: @@ -53,27 +51,6 @@ def _request(self, method: str, path: str, body: Optional[dict] = None, timeout: pass raise RuntimeError(f"HTTP {e.code} from {url}: {body_text}") from e - def health_check(self, timeout: int = 5) -> bool: - """Check if the Hindsight server is reachable. - - Mirrors Openclaw's checkExternalApiHealth: retries up to 3 times - with 2s delay between attempts. - """ - import time - - for attempt in range(1, HEALTH_CHECK_RETRIES + 1): - try: - url = f"{self.api_url}/health" - req = urllib.request.Request(url, headers=self._headers(), method="GET") - with urllib.request.urlopen(req, timeout=timeout) as resp: - if resp.status == 200: - return True - except Exception: - pass - if attempt < HEALTH_CHECK_RETRIES: - time.sleep(HEALTH_CHECK_DELAY) - return False - def recall( self, bank_id: str, diff --git a/hindsight-integrations/cursor/scripts/lib/daemon.py b/hindsight-integrations/cursor/scripts/lib/daemon.py index b37c390a4e..8d527bcbe9 100644 --- a/hindsight-integrations/cursor/scripts/lib/daemon.py +++ b/hindsight-integrations/cursor/scripts/lib/daemon.py @@ -15,7 +15,7 @@ from .config import DEFAULT_HINDSIGHT_API_URL from .llm import detect_llm_config, get_llm_env_vars -from .state import read_state, write_state +from .state import write_state DAEMON_STATE_FILE = "daemon.json" PROFILE_NAME = "cursor" @@ -217,29 +217,3 @@ def _ensure_daemon_running(config: dict, port: int, debug_fn=None): time.sleep(1) raise RuntimeError("Daemon failed to become ready within 30 seconds") - - -def stop_daemon(config: dict, debug_fn=None): - """Stop the daemon if it was started by this plugin.""" - state = read_state(DAEMON_STATE_FILE) - if not state or not state.get("started_by_plugin"): - if debug_fn: - debug_fn("Daemon not started by plugin, skipping stop") - return - - if debug_fn: - debug_fn("Stopping daemon...") - - try: - result = _run_embed( - config, - ["daemon", "--profile", PROFILE_NAME, "stop"], - timeout=10, - ) - if debug_fn: - debug_fn(f"Daemon stop: {result.stdout.strip()}") - except Exception as e: - if debug_fn: - debug_fn(f"Daemon stop error: {e}") - - write_state(DAEMON_STATE_FILE, {}) diff --git a/hindsight-integrations/cursor/scripts/lib/state.py b/hindsight-integrations/cursor/scripts/lib/state.py index 2bc4bcb4c6..c9efa368d3 100644 --- a/hindsight-integrations/cursor/scripts/lib/state.py +++ b/hindsight-integrations/cursor/scripts/lib/state.py @@ -72,12 +72,6 @@ def write_state(name: str, data): pass -def get_turn_count(session_id: str) -> int: - """Get the current turn count for a session.""" - turns = read_state("turns.json", {}) - return turns.get(session_id, 0) - - def increment_turn_count(session_id: str) -> int: """Increment and return the turn count for a session. diff --git a/hindsight-integrations/devin-desktop/hindsight_devin_desktop/mcp_config.py b/hindsight-integrations/devin-desktop/hindsight_devin_desktop/mcp_config.py index 094030bc5e..584b7915ae 100644 --- a/hindsight-integrations/devin-desktop/hindsight_devin_desktop/mcp_config.py +++ b/hindsight-integrations/devin-desktop/hindsight_devin_desktop/mcp_config.py @@ -51,11 +51,6 @@ def default_mcp_paths() -> List[Path]: return [codeium / "windsurf" / "mcp_config.json", codeium / "mcp_config.json"] -def default_mcp_path() -> Path: - """The primary Devin Desktop MCP config (``~/.codeium/windsurf/mcp_config.json``).""" - return default_mcp_paths()[0] - - def mcp_endpoint_url(api_url: str) -> str: """The Hindsight multi-bank MCP endpoint (no bank pinned in the path).""" return f"{api_url.rstrip('/')}/mcp/" diff --git a/hindsight-integrations/litellm/hindsight_litellm/__init__.py b/hindsight-integrations/litellm/hindsight_litellm/__init__.py index 149870a33e..c0e6e1c97f 100644 --- a/hindsight-integrations/litellm/hindsight_litellm/__init__.py +++ b/hindsight-integrations/litellm/hindsight_litellm/__init__.py @@ -1118,17 +1118,6 @@ def _store_conversation_sync( _pending_storage_errors.append(HindsightError(f"Background storage failed: {e}")) -def _check_pending_storage_errors() -> None: - """Check for and raise any pending storage errors from background threads.""" - global _pending_storage_errors - with _storage_error_lock: - if _pending_storage_errors: - # Get first error and clear the list - error = _pending_storage_errors[0] - _pending_storage_errors.clear() - raise error - - def get_pending_storage_errors() -> List[Exception]: """Get any pending storage errors without raising them. diff --git a/hindsight-integrations/litellm/hindsight_litellm/callbacks.py b/hindsight-integrations/litellm/hindsight_litellm/callbacks.py index e7e524c45a..1cd4f8e989 100644 --- a/hindsight-integrations/litellm/hindsight_litellm/callbacks.py +++ b/hindsight-integrations/litellm/hindsight_litellm/callbacks.py @@ -247,20 +247,6 @@ def _extract_user_query(self, messages: List[Dict[str, Any]]) -> Optional[str]: return " ".join(text_parts) return None - def _messages_to_query(self, messages: List[Dict[str, Any]]) -> str: - """Concatenate all message contents into a single query string.""" - message_parts = [] - for msg in messages: - content = msg.get("content", "") - if isinstance(content, str) and content: - message_parts.append(content) - elif isinstance(content, list): - # Handle structured content (e.g., vision messages) - for item in content: - if isinstance(item, dict) and item.get("type") == "text": - message_parts.append(item.get("text", "")) - return "\n".join(message_parts) - def _compute_conversation_hash( self, user_input: str, diff --git a/hindsight-integrations/openclaw/src/index.test.ts b/hindsight-integrations/openclaw/src/index.test.ts index 05846b0be1..664f2eaa7a 100644 --- a/hindsight-integrations/openclaw/src/index.test.ts +++ b/hindsight-integrations/openclaw/src/index.test.ts @@ -6,8 +6,6 @@ import { formatCurrentTimeForRecall, formatMemories, prepareRetentionTranscript, - countUserTurns, - getRetentionTurnIndex, sliceLastTurnsByUserBoundary, composeRecallQuery, truncateRecallQuery, @@ -324,35 +322,6 @@ describe("formatCurrentTimeForRecall", () => { // retention helpers // --------------------------------------------------------------------------- -describe("countUserTurns", () => { - it("counts user messages across a resumed conversation history", () => { - expect( - countUserTurns([ - { role: "user", content: "turn 1" }, - { role: "assistant", content: "reply 1" }, - { role: "system", content: "meta" }, - { role: "user", content: "turn 2" }, - { role: "assistant", content: "reply 2" }, - { role: "user", content: "turn 3" }, - ]) - ).toBe(3); - }); -}); - -describe("getRetentionTurnIndex", () => { - it("uses the full conversation turn count for per-turn retention", () => { - expect(getRetentionTurnIndex(7, 1)).toBe(7); - }); - - it("derives a stable window sequence for chunked retention", () => { - expect(getRetentionTurnIndex(6, 3)).toBe(2); - }); - - it("returns null when a chunk boundary has not been reached", () => { - expect(getRetentionTurnIndex(5, 3)).toBeNull(); - }); -}); - describe("normalizeRetainTags", () => { it("trims, deduplicates, and preserves order for string arrays", () => { expect( diff --git a/hindsight-integrations/openclaw/src/index.ts b/hindsight-integrations/openclaw/src/index.ts index 147ef4c21d..e2b0a4856e 100644 --- a/hindsight-integrations/openclaw/src/index.ts +++ b/hindsight-integrations/openclaw/src/index.ts @@ -3079,36 +3079,6 @@ function buildToolResultBlock(msg: any): any | null { return block; } -export function countUserTurns(messages: any[]): number { - if (!Array.isArray(messages) || messages.length === 0) { - return 0; - } - - return messages.reduce( - (count: number, message: any) => count + (message?.role === "user" ? 1 : 0), - 0 - ); -} - -export function getRetentionTurnIndex( - conversationTurnCount: number, - retainEveryN: number -): number | null { - if (conversationTurnCount <= 0 || retainEveryN <= 0) { - return null; - } - - if (retainEveryN === 1) { - return conversationTurnCount; - } - - if (conversationTurnCount % retainEveryN !== 0) { - return null; - } - - return Math.floor(conversationTurnCount / retainEveryN); -} - export function sliceLastTurnsByUserBoundary(messages: any[], turns: number): any[] { if (!Array.isArray(messages) || messages.length === 0 || turns <= 0) { return []; diff --git a/hindsight-integrations/openclaw/src/setup-lib.test.ts b/hindsight-integrations/openclaw/src/setup-lib.test.ts index 3c46bd84a4..2bf549e174 100644 --- a/hindsight-integrations/openclaw/src/setup-lib.test.ts +++ b/hindsight-integrations/openclaw/src/setup-lib.test.ts @@ -8,7 +8,6 @@ import { applyApiMode, applyCloudMode, applyEmbeddedMode, - defaultApiKeyEnvVar, ensurePluginConfig, envSecretRef, isValidEnvVarName, @@ -35,13 +34,6 @@ describe("isValidEnvVarName", () => { }); }); -describe("defaultApiKeyEnvVar", () => { - it("UPPERs and snake_cases the provider id", () => { - expect(defaultApiKeyEnvVar("openai")).toBe("OPENAI_API_KEY"); - expect(defaultApiKeyEnvVar("claude-code")).toBe("CLAUDE_CODE_API_KEY"); - }); -}); - describe("envSecretRef", () => { it("builds a default-provider env SecretRef", () => { expect(envSecretRef("OPENAI_API_KEY")).toEqual({ diff --git a/hindsight-integrations/openclaw/src/setup-lib.ts b/hindsight-integrations/openclaw/src/setup-lib.ts index ad21eeaae3..b1dc30bc9c 100644 --- a/hindsight-integrations/openclaw/src/setup-lib.ts +++ b/hindsight-integrations/openclaw/src/setup-lib.ts @@ -131,10 +131,6 @@ export function isValidEnvVarName(value: string | undefined): boolean { return !!value && ENV_VAR_RE.test(value.trim()); } -export function defaultApiKeyEnvVar(provider: string): string { - return `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`; -} - /** * Mask all but the last 4 chars of a secret so we can hint "yes, this is your * configured token" without leaking the secret onto the user's terminal diff --git a/hindsight-integrations/zcode/hindsight_zcode/hooks/scripts/lib/client.py b/hindsight-integrations/zcode/hindsight_zcode/hooks/scripts/lib/client.py index 4790b408d4..606af77fc6 100644 --- a/hindsight-integrations/zcode/hindsight_zcode/hooks/scripts/lib/client.py +++ b/hindsight-integrations/zcode/hindsight_zcode/hooks/scripts/lib/client.py @@ -11,8 +11,6 @@ from pathlib import Path DEFAULT_TIMEOUT = 15 -HEALTH_CHECK_RETRIES = 3 -HEALTH_CHECK_DELAY = 2 def _plugin_version(): @@ -71,27 +69,6 @@ def _request(self, method, path, body=None, timeout=DEFAULT_TIMEOUT): pass raise RuntimeError(f"HTTP {e.code} from {url}: {body_text}") from e - def health_check(self, timeout=5): - """Check if the Hindsight server is reachable. - - Mirrors codex's behavior: retries up to 3 times with 2s delay - between attempts. - """ - import time - - for attempt in range(1, HEALTH_CHECK_RETRIES + 1): - try: - url = f"{self.api_url}/health" - req = urllib.request.Request(url, headers=self._headers(), method="GET") - with urllib.request.urlopen(req, timeout=timeout) as resp: - if resp.status == 200: - return True - except Exception: - pass - if attempt < HEALTH_CHECK_RETRIES: - time.sleep(HEALTH_CHECK_DELAY) - return False - def recall( self, bank_id, diff --git a/hindsight-integrations/zcode/hindsight_zcode/hooks/scripts/lib/state.py b/hindsight-integrations/zcode/hindsight_zcode/hooks/scripts/lib/state.py index adcbe2154b..29848ad68f 100644 --- a/hindsight-integrations/zcode/hindsight_zcode/hooks/scripts/lib/state.py +++ b/hindsight-integrations/zcode/hindsight_zcode/hooks/scripts/lib/state.py @@ -68,12 +68,6 @@ def write_state(name, data): pass -def get_turn_count(session_id): - """Get the current turn count for a session.""" - turns = read_state("turns.json", {}) - return turns.get(session_id, 0) - - def increment_turn_count(session_id): """Increment and return the turn count for a session. diff --git a/hindsight-integrations/zcode/tests/test_client.py b/hindsight-integrations/zcode/tests/test_client.py index f266e04620..f6a39d1905 100644 --- a/hindsight-integrations/zcode/tests/test_client.py +++ b/hindsight-integrations/zcode/tests/test_client.py @@ -27,19 +27,6 @@ def fake_open(req, timeout=None): assert captured["ua"] == USER_AGENT assert captured["ua"].startswith("hindsight-zcode/") - def test_health_check_sends_user_agent(self): - c = HindsightClient("http://localhost:9077") - captured = {} - - def fake_open(req, timeout=None): - captured["ua"] = req.get_header("User-agent") - return FakeHTTPResponse({}, status=200) - - with patch("urllib.request.urlopen", side_effect=fake_open): - c.health_check(timeout=1) - - assert captured["ua"] == USER_AGENT - class TestURLValidation: def test_rejects_non_http_scheme(self):