Debug Kanban Memory & Add Sentry Monitoring (#1380)

* auto-claude: subtask-1-1 - Add debug logging to capture memory initialization state

Added comprehensive debug logging to memory_manager.py to reveal if
_ensure_initialized() is failing silently. This captures:

- PRE-INIT STATE: Memory instance details before any save attempt
  - is_enabled, is_initialized, group_id
  - Internal component states (client, queries, search)
  - Config validation state (is_valid, providers, database)
  - State object details (initialized, episode_count, errors)

- PRE-SAVE CHECK: Initialization state right before save method
  - Logs whether _ensure_initialized() will be called

- POST-SAVE CHECK: State after save method returns
  - Confirms if initialization actually happened
  - Shows save result and component states

This logging will reveal the root cause of kanban task memory save
failures by showing exactly what state the memory system is in
during the save flow.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-2-1 - Update graphiti_helpers.py to add explicit initialize() call

- Made get_graphiti_memory() async function
- Added await memory.initialize() call following GitHub pattern
- Updated save_to_graphiti_async() to await the async helper
- Added proper error handling for initialization failures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-2-2 - Update memory_manager.py to use async get_graphiti_memory

Updated both get_graphiti_context() and save_session_memory() to properly
await the now-async get_graphiti_memory() helper, which initializes the
GraphitiMemory instance internally before returning.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-3-1 - Add Sentry capture_exception to GraphitiClient ini

* auto-claude: subtask-3-2 - Add Sentry capture_exception to GraphitiMemory class

- Import capture_exception from core.sentry
- Add Sentry tracking to initialize() method for initialization failures
- Add try/except with capture_exception to all save_* methods:
  - save_session_insights
  - save_codebase_discoveries
  - save_pattern
  - save_gotcha
  - save_task_outcome
  - save_structured_insights
- Add try/except with capture_exception to all get_* methods:
  - get_relevant_context
  - get_session_history
  - get_similar_task_outcomes
  - get_patterns_and_gotchas
- Include relevant context (component, operation, etc.) with each capture

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-3-3 - Add Sentry capture_exception to GraphitiQueries class

Track all episode save failures with operation type and content summary:
- add_session_insight: tracks group_id, spec_id, session_number
- add_codebase_discoveries: tracks group_id, spec_id, discovery_count
- add_pattern: tracks group_id, spec_id, content_summary
- add_gotcha: tracks group_id, spec_id, content_summary
- add_task_outcome: tracks group_id, spec_id, task_id, success, content_summary
- add_structured_insights: tracks group_id, spec_id, content_summary (insight types)

* auto-claude: subtask-3-4 - Add Sentry capture_exception to GraphitiSearch class

- Import capture_exception from core.sentry
- Add Sentry tracking to get_relevant_context with query_summary and group_id
- Add Sentry tracking to get_session_history with group_id context
- Add Sentry tracking to get_similar_task_outcomes with query_summary and group_id
- Add Sentry tracking to get_patterns_and_gotchas with query_summary and group_id
- All exception handlers now include operation name for better error grouping

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-4-1 - Add Sentry capture_exception to graphiti_helpers.py

Add Sentry error tracking to Graphiti helper functions:
- Import capture_exception from core.sentry
- Track get_graphiti_memory failures with spec_dir and project_dir context
- Track save_to_graphiti_async failures with spec_dir, session_num context
- Track connection close failures with context information

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-4-2 - Add Sentry capture_exception to memory_manager.py.

* auto-claude: subtask-5-1 - Create test script for memory save verification

* fix: resolve ruff lint and format errors

- Fix import block sorting (I001) in graphiti.py and test_memory_save.py
- Fix f-string without placeholders (F541) in test_memory_save.py
- Apply ruff formatting to 4 files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(memory): await async get_graphiti_memory calls to prevent AttributeError

The get_graphiti_memory function was changed to async but call sites in
patterns.py, codebase_map.py, and tools/memory.py were not updated.
This caused graphiti variables to be coroutine objects instead of
GraphitiMemory instances, resulting in AttributeError when calling
methods like save_gotcha() or save_pattern().

- Wrap sync callers with run_async() helper
- Add await for async caller in _save_to_graphiti_async
- Add graphiti.close() calls to prevent connection leaks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(memory): fix run_async returning Future in async context

- Fix ASYNC-004: run_async() now returns None when called from async
  context instead of returning a Future. This prevents AttributeError
  when callers try to use the Future as the actual result.
- This fixes ASYNC-001, ASYNC-002, ASYNC-003 in codebase_map.py and
  patterns.py since they already check `if graphiti:` which will be
  False for None.
- Close the coroutine when in async context to avoid "coroutine was
  never awaited" warning.
- Remove investigation debug logging from memory_manager.py (CMT-001)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: StillKnotKnown <192589389+StillKnotKnown@users.noreply.github.com>
This commit is contained in:
Andy
2026-01-25 12:42:35 +01:00
committed by GitHub
parent 53111dbb95
commit bc5f550ee3
10 changed files with 786 additions and 70 deletions
+43 -13
View File
@@ -10,6 +10,7 @@ Handles session memory storage using dual-layer approach:
import logging
from pathlib import Path
from core.sentry import capture_exception
from debug import (
debug,
debug_detailed,
@@ -116,12 +117,15 @@ async def get_graphiti_context(
memory = None
try:
# Use centralized helper for GraphitiMemory instantiation
memory = get_graphiti_memory(spec_dir, project_dir)
# Use centralized helper for GraphitiMemory instantiation (async)
memory = await get_graphiti_memory(spec_dir, project_dir)
if memory is None:
if is_debug_enabled():
debug_warning("memory", "GraphitiMemory not available")
debug_warning(
"memory", "GraphitiMemory not available for context retrieval"
)
return None
# Build search query from subtask description
subtask_desc = subtask.get("description", "")
subtask_id = subtask.get("id", "")
@@ -228,6 +232,15 @@ async def get_graphiti_context(
logger.warning(f"Failed to get Graphiti context: {e}")
if is_debug_enabled():
debug_error("memory", "Graphiti context retrieval failed", error=str(e))
# Capture exception to Sentry with full context
capture_exception(
e,
operation="get_graphiti_context",
subtask_id=subtask.get("id", "unknown"),
subtask_desc=subtask.get("description", "")[:200],
spec_dir=str(spec_dir),
project_dir=str(project_dir),
)
return None
finally:
# Always close the memory connection (swallow exceptions to avoid overriding)
@@ -324,21 +337,16 @@ async def save_session_memory(
memory = None
try:
# Use centralized helper for GraphitiMemory instantiation
memory = get_graphiti_memory(spec_dir, project_dir)
# Use centralized helper for GraphitiMemory instantiation (async)
memory = await get_graphiti_memory(spec_dir, project_dir)
if memory is None:
if is_debug_enabled():
debug_warning("memory", "GraphitiMemory not available")
# Continue to file-based fallback
else:
if is_debug_enabled():
debug_detailed(
debug(
"memory",
"GraphitiMemory instance created",
is_enabled=memory.is_enabled,
group_id=getattr(memory, "group_id", "unknown"),
"get_graphiti_memory() returned None - this usually means Graphiti is disabled or provider config is invalid",
)
# Continue to file-based fallback
if memory is not None and memory.is_enabled:
if is_debug_enabled():
debug("memory", "Saving to Graphiti...")
@@ -393,6 +401,17 @@ async def save_session_memory(
logger.warning(f"Graphiti save failed: {e}, falling back to file-based")
if is_debug_enabled():
debug_error("memory", "Graphiti save failed", error=str(e))
# Capture exception to Sentry with full context
capture_exception(
e,
operation="save_session_memory_graphiti",
subtask_id=subtask_id,
session_num=session_num,
success=success,
subtasks_completed=subtasks_completed,
spec_dir=str(spec_dir),
project_dir=str(project_dir),
)
finally:
# Always close the memory connection (swallow exceptions to avoid overriding)
if memory is not None:
@@ -438,6 +457,17 @@ async def save_session_memory(
logger.error(f"File-based memory save also failed: {e}")
if is_debug_enabled():
debug_error("memory", "File-based memory save FAILED", error=str(e))
# Capture exception to Sentry with full context
capture_exception(
e,
operation="save_session_memory_file",
subtask_id=subtask_id,
session_num=session_num,
success=success,
subtasks_completed=subtasks_completed,
spec_dir=str(spec_dir),
project_dir=str(project_dir),
)
return False, "none"
@@ -52,7 +52,7 @@ async def _save_to_graphiti_async(
# The helper handles enablement checks internally
from memory.graphiti_helpers import get_graphiti_memory
memory = get_graphiti_memory(spec_dir, project_dir)
memory = await get_graphiti_memory(spec_dir, project_dir)
if memory is None:
return False
@@ -9,6 +9,7 @@ import logging
import sys
from datetime import datetime, timezone
from core.sentry import capture_exception
from graphiti_config import GraphitiConfig, GraphitiState
logger = logging.getLogger(__name__)
@@ -133,9 +134,23 @@ class GraphitiClient:
)
except ProviderNotInstalled as e:
logger.warning(f"LLM provider packages not installed: {e}")
capture_exception(
e,
error_type="ProviderNotInstalled",
provider_type="llm",
llm_provider=self.config.llm_provider,
embedder_provider=self.config.embedder_provider,
)
return False
except ProviderError as e:
logger.warning(f"LLM provider configuration error: {e}")
capture_exception(
e,
error_type="ProviderError",
provider_type="llm",
llm_provider=self.config.llm_provider,
embedder_provider=self.config.embedder_provider,
)
return False
try:
@@ -145,9 +160,23 @@ class GraphitiClient:
)
except ProviderNotInstalled as e:
logger.warning(f"Embedder provider packages not installed: {e}")
capture_exception(
e,
error_type="ProviderNotInstalled",
provider_type="embedder",
llm_provider=self.config.llm_provider,
embedder_provider=self.config.embedder_provider,
)
return False
except ProviderError as e:
logger.warning(f"Embedder provider configuration error: {e}")
capture_exception(
e,
error_type="ProviderError",
provider_type="embedder",
llm_provider=self.config.llm_provider,
embedder_provider=self.config.embedder_provider,
)
return False
# Apply LadybugDB monkeypatch to use it via graphiti's KuzuDriver
@@ -173,15 +202,36 @@ class GraphitiClient:
logger.warning(
f"Failed to initialize LadybugDB driver at {db_path}: {e}"
)
capture_exception(
e,
error_type=type(e).__name__,
db_path=str(db_path),
llm_provider=self.config.llm_provider,
embedder_provider=self.config.embedder_provider,
)
return False
except Exception as e:
logger.warning(
f"Unexpected error initializing LadybugDB driver at {db_path}: {e}"
)
capture_exception(
e,
error_type=type(e).__name__,
db_path=str(db_path),
llm_provider=self.config.llm_provider,
embedder_provider=self.config.embedder_provider,
)
return False
logger.info(f"Initialized LadybugDB driver (patched) at: {db_path}")
except ImportError as e:
logger.warning(f"KuzuDriver not available: {e}")
capture_exception(
e,
error_type="ImportError",
component="kuzu_driver_patched",
llm_provider=self.config.llm_provider,
embedder_provider=self.config.embedder_provider,
)
return False
# Initialize Graphiti with the custom providers
@@ -216,10 +266,23 @@ class GraphitiClient:
f"Graphiti packages not installed: {e}. "
"Install with: pip install real_ladybug graphiti-core"
)
capture_exception(
e,
error_type="ImportError",
component="graphiti_core",
llm_provider=self.config.llm_provider,
embedder_provider=self.config.embedder_provider,
)
return False
except Exception as e:
logger.warning(f"Failed to initialize Graphiti client: {e}")
capture_exception(
e,
error_type=type(e).__name__,
llm_provider=self.config.llm_provider,
embedder_provider=self.config.embedder_provider,
)
return False
async def close(self) -> None:
@@ -13,6 +13,7 @@ import logging
from datetime import datetime, timezone
from pathlib import Path
from core.sentry import capture_exception
from graphiti_config import GraphitiConfig, GraphitiState
from .client import GraphitiClient
@@ -196,6 +197,13 @@ class GraphitiMemory:
except Exception as e:
logger.warning(f"Failed to initialize Graphiti: {e}")
self._record_error(f"Initialization failed: {e}")
capture_exception(
e,
component="graphiti",
operation="initialize",
group_id=self.group_id,
group_id_mode=self.group_id_mode,
)
self._available = False
return False
@@ -220,6 +228,7 @@ class GraphitiMemory:
if not await self._ensure_initialized():
return False
try:
result = await self._queries.add_session_insight(session_num, insights)
if result and self.state:
@@ -228,6 +237,16 @@ class GraphitiMemory:
self.state.save(self.spec_dir)
return result
except Exception as e:
logger.warning(f"Failed to save session insights: {e}")
self._record_error(f"save_session_insights failed: {e}")
capture_exception(
e,
component="graphiti",
operation="save_session_insights",
session_num=session_num,
)
return False
async def save_codebase_discoveries(
self,
@@ -237,6 +256,7 @@ class GraphitiMemory:
if not await self._ensure_initialized():
return False
try:
result = await self._queries.add_codebase_discoveries(discoveries)
if result and self.state:
@@ -244,12 +264,22 @@ class GraphitiMemory:
self.state.save(self.spec_dir)
return result
except Exception as e:
logger.warning(f"Failed to save codebase discoveries: {e}")
self._record_error(f"save_codebase_discoveries failed: {e}")
capture_exception(
e,
component="graphiti",
operation="save_codebase_discoveries",
)
return False
async def save_pattern(self, pattern: str) -> bool:
"""Save a code pattern to the knowledge graph."""
if not await self._ensure_initialized():
return False
try:
result = await self._queries.add_pattern(pattern)
if result and self.state:
@@ -257,12 +287,22 @@ class GraphitiMemory:
self.state.save(self.spec_dir)
return result
except Exception as e:
logger.warning(f"Failed to save pattern: {e}")
self._record_error(f"save_pattern failed: {e}")
capture_exception(
e,
component="graphiti",
operation="save_pattern",
)
return False
async def save_gotcha(self, gotcha: str) -> bool:
"""Save a gotcha (pitfall) to the knowledge graph."""
if not await self._ensure_initialized():
return False
try:
result = await self._queries.add_gotcha(gotcha)
if result and self.state:
@@ -270,6 +310,15 @@ class GraphitiMemory:
self.state.save(self.spec_dir)
return result
except Exception as e:
logger.warning(f"Failed to save gotcha: {e}")
self._record_error(f"save_gotcha failed: {e}")
capture_exception(
e,
component="graphiti",
operation="save_gotcha",
)
return False
async def save_task_outcome(
self,
@@ -282,6 +331,7 @@ class GraphitiMemory:
if not await self._ensure_initialized():
return False
try:
result = await self._queries.add_task_outcome(
task_id, success, outcome, metadata
)
@@ -291,12 +341,23 @@ class GraphitiMemory:
self.state.save(self.spec_dir)
return result
except Exception as e:
logger.warning(f"Failed to save task outcome: {e}")
self._record_error(f"save_task_outcome failed: {e}")
capture_exception(
e,
component="graphiti",
operation="save_task_outcome",
task_id=task_id,
)
return False
async def save_structured_insights(self, insights: dict) -> bool:
"""Save extracted insights as multiple focused episodes."""
if not await self._ensure_initialized():
return False
try:
result = await self._queries.add_structured_insights(insights)
if result and self.state:
@@ -304,6 +365,15 @@ class GraphitiMemory:
pass
return result
except Exception as e:
logger.warning(f"Failed to save structured insights: {e}")
self._record_error(f"save_structured_insights failed: {e}")
capture_exception(
e,
component="graphiti",
operation="save_structured_insights",
)
return False
# Delegate methods to search module
@@ -317,9 +387,19 @@ class GraphitiMemory:
if not await self._ensure_initialized():
return []
try:
return await self._search.get_relevant_context(
query, num_results, include_project_context
)
except Exception as e:
logger.warning(f"Failed to get relevant context: {e}")
self._record_error(f"get_relevant_context failed: {e}")
capture_exception(
e,
component="graphiti",
operation="get_relevant_context",
)
return []
async def get_session_history(
self,
@@ -330,7 +410,17 @@ class GraphitiMemory:
if not await self._ensure_initialized():
return []
try:
return await self._search.get_session_history(limit, spec_only)
except Exception as e:
logger.warning(f"Failed to get session history: {e}")
self._record_error(f"get_session_history failed: {e}")
capture_exception(
e,
component="graphiti",
operation="get_session_history",
)
return []
async def get_similar_task_outcomes(
self,
@@ -341,7 +431,17 @@ class GraphitiMemory:
if not await self._ensure_initialized():
return []
try:
return await self._search.get_similar_task_outcomes(task_description, limit)
except Exception as e:
logger.warning(f"Failed to get similar task outcomes: {e}")
self._record_error(f"get_similar_task_outcomes failed: {e}")
capture_exception(
e,
component="graphiti",
operation="get_similar_task_outcomes",
)
return []
async def get_patterns_and_gotchas(
self,
@@ -367,9 +467,19 @@ class GraphitiMemory:
if not await self._ensure_initialized():
return [], []
try:
return await self._search.get_patterns_and_gotchas(
query, num_results, min_score
)
except Exception as e:
logger.warning(f"Failed to get patterns and gotchas: {e}")
self._record_error(f"get_patterns_and_gotchas failed: {e}")
capture_exception(
e,
component="graphiti",
operation="get_patterns_and_gotchas",
)
return [], []
# Status and utility methods
@@ -8,6 +8,8 @@ import json
import logging
from datetime import datetime, timezone
from core.sentry import capture_exception
from .schema import (
EPISODE_TYPE_CODEBASE_DISCOVERY,
EPISODE_TYPE_GOTCHA,
@@ -82,6 +84,13 @@ class GraphitiQueries:
except Exception as e:
logger.warning(f"Failed to save session insights: {e}")
capture_exception(
e,
operation="add_session_insight",
group_id=self.group_id,
spec_id=self.spec_context_id,
session_number=session_num,
)
return False
async def add_codebase_discoveries(
@@ -124,6 +133,13 @@ class GraphitiQueries:
except Exception as e:
logger.warning(f"Failed to save codebase discoveries: {e}")
capture_exception(
e,
operation="add_codebase_discoveries",
group_id=self.group_id,
spec_id=self.spec_context_id,
discovery_count=len(discoveries),
)
return False
async def add_pattern(self, pattern: str) -> bool:
@@ -160,6 +176,13 @@ class GraphitiQueries:
except Exception as e:
logger.warning(f"Failed to save pattern: {e}")
capture_exception(
e,
operation="add_pattern",
group_id=self.group_id,
spec_id=self.spec_context_id,
content_summary=pattern[:100] if pattern else "",
)
return False
async def add_gotcha(self, gotcha: str) -> bool:
@@ -196,6 +219,13 @@ class GraphitiQueries:
except Exception as e:
logger.warning(f"Failed to save gotcha: {e}")
capture_exception(
e,
operation="add_gotcha",
group_id=self.group_id,
spec_id=self.spec_context_id,
content_summary=gotcha[:100] if gotcha else "",
)
return False
async def add_task_outcome(
@@ -245,6 +275,15 @@ class GraphitiQueries:
except Exception as e:
logger.warning(f"Failed to save task outcome: {e}")
capture_exception(
e,
operation="add_task_outcome",
group_id=self.group_id,
spec_id=self.spec_context_id,
task_id=task_id,
success=success,
content_summary=outcome[:100] if outcome else "",
)
return False
async def add_structured_insights(self, insights: dict) -> bool:
@@ -459,4 +498,26 @@ class GraphitiQueries:
except Exception as e:
logger.warning(f"Failed to save structured insights: {e}")
# Build content summary of insight types
insight_types = []
if insights.get("file_insights"):
insight_types.append(f"files:{len(insights['file_insights'])}")
if insights.get("patterns_discovered"):
insight_types.append(f"patterns:{len(insights['patterns_discovered'])}")
if insights.get("gotchas_discovered"):
insight_types.append(f"gotchas:{len(insights['gotchas_discovered'])}")
if insights.get("approach_outcome"):
insight_types.append("outcome:1")
if insights.get("recommendations"):
insight_types.append(
f"recommendations:{len(insights['recommendations'])}"
)
capture_exception(
e,
operation="add_structured_insights",
group_id=self.group_id,
spec_id=self.spec_context_id,
content_summary=", ".join(insight_types) if insight_types else "empty",
)
return False
@@ -9,6 +9,8 @@ import json
import logging
from pathlib import Path
from core.sentry import capture_exception
from .schema import (
EPISODE_TYPE_GOTCHA,
EPISODE_TYPE_PATTERN,
@@ -120,6 +122,12 @@ class GraphitiSearch:
except Exception as e:
logger.warning(f"Failed to search context: {e}")
capture_exception(
e,
query_summary=query[:100] if query else "",
group_id=self.group_id,
operation="get_relevant_context",
)
return []
async def get_session_history(
@@ -174,6 +182,11 @@ class GraphitiSearch:
except Exception as e:
logger.warning(f"Failed to get session history: {e}")
capture_exception(
e,
group_id=self.group_id,
operation="get_session_history",
)
return []
async def get_similar_task_outcomes(
@@ -227,6 +240,12 @@ class GraphitiSearch:
except Exception as e:
logger.warning(f"Failed to get similar task outcomes: {e}")
capture_exception(
e,
query_summary=task_description[:100] if task_description else "",
group_id=self.group_id,
operation="get_similar_task_outcomes",
)
return []
async def get_patterns_and_gotchas(
@@ -337,4 +356,10 @@ class GraphitiSearch:
except Exception as e:
logger.warning(f"Failed to get patterns/gotchas: {e}")
capture_exception(
e,
query_summary=query[:100] if query else "",
group_id=self.group_id,
operation="get_patterns_and_gotchas",
)
return [], []
+2 -1
View File
@@ -64,9 +64,10 @@ def update_codebase_map(spec_dir: Path, discoveries: dict[str, str]) -> None:
# Also save to Graphiti if enabled
if is_graphiti_memory_enabled() and discoveries:
try:
graphiti = get_graphiti_memory(spec_dir)
graphiti = run_async(get_graphiti_memory(spec_dir))
if graphiti:
run_async(graphiti.save_codebase_discoveries(discoveries))
run_async(graphiti.close())
logger.info("Codebase discoveries also saved to Graphiti")
except Exception as e:
logger.warning(f"Graphiti codebase save failed: {e}")
+57 -12
View File
@@ -12,6 +12,8 @@ import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any
from core.sentry import capture_exception
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
@@ -37,18 +39,22 @@ def is_graphiti_memory_enabled() -> bool:
return False
def get_graphiti_memory(
async def get_graphiti_memory(
spec_dir: Path, project_dir: Path | None = None
) -> "GraphitiMemory | None":
"""
Get a GraphitiMemory instance if available.
Get an initialized GraphitiMemory instance if available.
Args:
spec_dir: Spec directory
project_dir: Project root directory (defaults to spec_dir.parent.parent)
Returns:
GraphitiMemory instance or None if not available
Initialized GraphitiMemory instance or None if not available
Note:
This function is async and calls initialize() on the memory instance
before returning, following the GitHub pattern for proper initialization.
"""
if not is_graphiti_memory_enabled():
return None
@@ -59,29 +65,54 @@ def get_graphiti_memory(
if project_dir is None:
project_dir = spec_dir.parent.parent
# Use project-wide shared memory for cross-spec learning
return GraphitiMemory(spec_dir, project_dir, group_id_mode=GroupIdMode.PROJECT)
memory = GraphitiMemory(
spec_dir, project_dir, group_id_mode=GroupIdMode.PROJECT
)
# Initialize the memory instance (following GitHub pattern)
await memory.initialize()
return memory
except ImportError:
return None
except Exception as e:
logger.warning(f"Failed to initialize Graphiti memory: {e}")
capture_exception(
e,
function="get_graphiti_memory",
spec_dir=str(spec_dir),
project_dir=str(project_dir) if project_dir else None,
)
return None
def run_async(coro):
"""
Run an async coroutine synchronously.
Handles the case where we're already in an event loop.
NOTE: This should only be called from synchronous code. For async callers,
use the async function directly with await to ensure proper execution.
Args:
coro: Async coroutine to run
Returns:
Result of the coroutine or a Future if already in event loop
Result of the coroutine, or None if already in an async context
"""
try:
loop = asyncio.get_running_loop()
# Already in an event loop - create a task
return asyncio.ensure_future(coro)
asyncio.get_running_loop()
# Already in an async context - caller should use await directly
# Log a warning and return None to avoid returning a Future that
# callers would incorrectly try to use as the actual result
logger.warning(
"run_async called from async context. "
"Use await directly for proper execution."
)
# Close the coroutine to avoid "coroutine was never awaited" warning
coro.close()
return None
except RuntimeError:
# No event loop running - create one
# No event loop running - safe to create one
return asyncio.run(coro)
@@ -105,7 +136,7 @@ async def save_to_graphiti_async(
Returns:
True if save succeeded, False otherwise
"""
graphiti = get_graphiti_memory(spec_dir, project_dir)
graphiti = await get_graphiti_memory(spec_dir, project_dir)
if graphiti is None:
return False
@@ -130,13 +161,27 @@ async def save_to_graphiti_async(
except Exception as e:
logger.warning(f"Failed to save to Graphiti: {e}")
capture_exception(
e,
function="save_to_graphiti_async",
spec_dir=str(spec_dir),
session_num=session_num,
project_dir=str(project_dir) if project_dir else None,
)
return False
finally:
# Always close the graphiti connection (swallow exceptions to avoid overriding)
if graphiti is not None:
try:
await graphiti.close()
except Exception as e:
except Exception as close_error:
logger.debug(
"Failed to close Graphiti memory connection", exc_info=True
)
capture_exception(
close_error,
function="save_to_graphiti_async",
context="closing_connection",
spec_dir=str(spec_dir),
session_num=session_num,
)
+4 -2
View File
@@ -57,9 +57,10 @@ def append_gotcha(spec_dir: Path, gotcha: str) -> None:
# Also save to Graphiti if enabled
if is_graphiti_memory_enabled():
try:
graphiti = get_graphiti_memory(spec_dir)
graphiti = run_async(get_graphiti_memory(spec_dir))
if graphiti:
run_async(graphiti.save_gotcha(gotcha_stripped))
run_async(graphiti.close())
except Exception as e:
logger.warning(f"Graphiti gotcha save failed: {e}")
@@ -133,9 +134,10 @@ def append_pattern(spec_dir: Path, pattern: str) -> None:
# Also save to Graphiti if enabled
if is_graphiti_memory_enabled():
try:
graphiti = get_graphiti_memory(spec_dir)
graphiti = run_async(get_graphiti_memory(spec_dir))
if graphiti:
run_async(graphiti.save_pattern(pattern_stripped))
run_async(graphiti.close())
except Exception as e:
logger.warning(f"Graphiti pattern save failed: {e}")
+379
View File
@@ -0,0 +1,379 @@
#!/usr/bin/env python3
"""
Memory Save Verification Script
================================
Tests the memory save functionality with Graphiti enabled.
Run with DEBUG=true SENTRY_DEV=true to verify Sentry events.
Usage:
cd apps/backend
DEBUG=true python scripts/test_memory_save.py
# With Sentry enabled (for Sentry event verification):
DEBUG=true SENTRY_DEV=true python scripts/test_memory_save.py
"""
import asyncio
import logging
import os
import sys
import tempfile
from pathlib import Path
# Add the backend directory to the path so we can import modules
SCRIPT_DIR = Path(__file__).resolve().parent
BACKEND_DIR = SCRIPT_DIR.parent
if str(BACKEND_DIR) not in sys.path:
sys.path.insert(0, str(BACKEND_DIR))
# Configure logging
logging.basicConfig(
level=logging.DEBUG if os.environ.get("DEBUG") else logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
async def test_memory_imports():
"""Test that all memory-related imports work correctly."""
print("\n=== Testing Memory System Imports ===")
errors = []
# Test memory_manager imports
try:
from agents.memory_manager import (
debug_memory_system_status,
get_graphiti_context,
save_session_memory,
)
print("[OK] agents.memory_manager imports successful")
except ImportError as e:
errors.append(f"agents.memory_manager: {e}")
print(f"[FAIL] agents.memory_manager: {e}")
# Test graphiti_helpers imports
try:
from memory.graphiti_helpers import (
get_graphiti_memory,
is_graphiti_memory_enabled,
save_to_graphiti_async,
)
print("[OK] memory.graphiti_helpers imports successful")
except ImportError as e:
errors.append(f"memory.graphiti_helpers: {e}")
print(f"[FAIL] memory.graphiti_helpers: {e}")
# Test graphiti_config imports
try:
from graphiti_config import (
get_graphiti_status,
is_graphiti_enabled,
)
print("[OK] graphiti_config imports successful")
except ImportError as e:
errors.append(f"graphiti_config: {e}")
print(f"[FAIL] graphiti_config: {e}")
# Test sentry imports
try:
from core.sentry import (
capture_exception,
capture_message,
init_sentry,
)
from core.sentry import (
is_enabled as sentry_is_enabled,
)
print("[OK] core.sentry imports successful")
except ImportError as e:
errors.append(f"core.sentry: {e}")
print(f"[FAIL] core.sentry: {e}")
# Test graphiti queries_pkg imports
try:
from integrations.graphiti.queries_pkg.client import GraphitiClient
from integrations.graphiti.queries_pkg.graphiti import GraphitiMemory
from integrations.graphiti.queries_pkg.queries import GraphitiQueries
from integrations.graphiti.queries_pkg.search import GraphitiSearch
print("[OK] integrations.graphiti.queries_pkg imports successful")
except ImportError as e:
errors.append(f"integrations.graphiti.queries_pkg: {e}")
print(f"[FAIL] integrations.graphiti.queries_pkg: {e}")
if errors:
print(f"\n[FAIL] {len(errors)} import error(s) found")
return False
else:
print("\n[OK] All imports successful")
return True
async def test_graphiti_status():
"""Test Graphiti configuration status."""
print("\n=== Testing Graphiti Status ===")
try:
from graphiti_config import get_graphiti_status, is_graphiti_enabled
enabled = is_graphiti_enabled()
status = get_graphiti_status()
print(f"Graphiti Enabled: {enabled}")
print(f"Graphiti Available: {status.get('available')}")
print(f" Host: {status.get('host')}")
print(f" Port: {status.get('port')}")
print(f" Database: {status.get('database')}")
print(f" LLM Provider: {status.get('llm_provider')}")
print(f" Embedder Provider: {status.get('embedder_provider')}")
if not status.get("available"):
print(f" Reason: {status.get('reason')}")
print(f" Errors: {status.get('errors')}")
return enabled
except Exception as e:
print(f"[FAIL] Error checking Graphiti status: {e}")
return False
async def test_sentry_status():
"""Test Sentry configuration status.
Returns True if:
- Sentry is enabled and ready, OR
- Sentry is properly disabled due to configuration (no DSN, not dev mode, SDK not installed)
Only returns False if there's an unexpected error.
"""
print("\n=== Testing Sentry Status ===")
try:
from core.sentry import init_sentry, is_enabled, is_initialized
# Check if SENTRY_DEV is set
sentry_dev = os.environ.get("SENTRY_DEV", "").lower() in ("true", "1", "yes")
sentry_dsn = os.environ.get("SENTRY_DSN", "")
print(f"SENTRY_DSN set: {bool(sentry_dsn)}")
print(f"SENTRY_DEV: {sentry_dev}")
# Initialize Sentry
init_sentry(component="memory-test")
print(f"Sentry Initialized: {is_initialized()}")
print(f"Sentry Enabled: {is_enabled()}")
if is_enabled():
print("[OK] Sentry is enabled and ready to capture events")
else:
# Sentry being disabled is OK - it just means configuration requires it
if not sentry_dsn:
print("[INFO] Sentry disabled - no SENTRY_DSN configured (expected)")
elif not sentry_dev:
print(
"[INFO] Sentry disabled in dev mode - set SENTRY_DEV=true to enable"
)
else:
print("[INFO] Sentry disabled - sentry-sdk may not be installed")
print(
"[OK] Sentry integration configured correctly (disabled by configuration)"
)
# Return True even if disabled - we're testing that the integration works,
# not that Sentry is necessarily enabled
return True
except Exception as e:
print(f"[FAIL] Error checking Sentry status: {e}")
return False
async def test_memory_save_flow():
"""Test the memory save flow end-to-end."""
print("\n=== Testing Memory Save Flow ===")
# Create temporary directories for testing
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
spec_dir = tmp_path / "test_spec"
spec_dir.mkdir(parents=True)
project_dir = tmp_path / "project"
project_dir.mkdir(parents=True)
print(f"Test spec_dir: {spec_dir}")
print(f"Test project_dir: {project_dir}")
try:
from agents.memory_manager import save_session_memory
# Test memory save with sample data
subtask_id = "test-subtask-1"
session_num = 1
success = True
subtasks_completed = ["test-subtask-1"]
discoveries = {
"files_understood": {"test.py": "Test file for memory verification"},
"patterns_found": ["Test pattern: Always verify imports"],
"gotchas_encountered": ["Test gotcha: Check async/await usage"],
}
print("\nSaving test session memory...")
print(f" subtask_id: {subtask_id}")
print(f" session_num: {session_num}")
print(f" success: {success}")
result, storage_type = await save_session_memory(
spec_dir=spec_dir,
project_dir=project_dir,
subtask_id=subtask_id,
session_num=session_num,
success=success,
subtasks_completed=subtasks_completed,
discoveries=discoveries,
)
print("\nMemory Save Result:")
print(f" Success: {result}")
print(f" Storage Type: {storage_type}")
if result:
print(f"[OK] Memory save succeeded using {storage_type} storage")
# Verify file was created if file-based
if storage_type == "file":
memory_file = (
spec_dir
/ "memory"
/ "session_insights"
/ f"session_{session_num:03d}.json"
)
if memory_file.exists():
print(f"[OK] Memory file created: {memory_file}")
else:
print(f"[WARN] Memory file not found: {memory_file}")
return True
else:
print("[FAIL] Memory save failed")
return False
except Exception as e:
print(f"[FAIL] Error during memory save test: {e}")
import traceback
traceback.print_exc()
# Test Sentry capture
try:
from core.sentry import capture_exception, is_enabled
if is_enabled():
capture_exception(
e,
operation="test_memory_save",
context="verification_script",
)
print("[INFO] Exception captured to Sentry")
except Exception:
pass
return False
async def test_sentry_capture():
"""Test that Sentry capture works (only if Sentry is enabled)."""
print("\n=== Testing Sentry Capture ===")
try:
from core.sentry import (
capture_exception,
capture_message,
is_enabled,
)
if not is_enabled():
print("[SKIP] Sentry not enabled - skipping capture test")
print(" Set SENTRY_DSN and SENTRY_DEV=true to test Sentry capture")
return True
# Test capture_message
print("Sending test message to Sentry...")
capture_message(
"Memory save verification script test message",
level="info",
test_type="verification",
component="memory-test",
)
print("[OK] Test message sent to Sentry")
# Test capture_exception
print("Sending test exception to Sentry...")
try:
raise ValueError("Test exception for memory save verification")
except Exception as e:
capture_exception(
e,
operation="test_exception",
context="verification_script",
)
print("[OK] Test exception sent to Sentry")
print("\n[INFO] Check your Sentry dashboard for the test events")
return True
except Exception as e:
print(f"[FAIL] Error testing Sentry capture: {e}")
return False
async def main():
"""Run all memory save verification tests."""
print("=" * 60)
print("Memory Save Verification Script")
print("=" * 60)
print(f"DEBUG: {os.environ.get('DEBUG', 'not set')}")
print(f"SENTRY_DEV: {os.environ.get('SENTRY_DEV', 'not set')}")
print(f"GRAPHITI_ENABLED: {os.environ.get('GRAPHITI_ENABLED', 'not set')}")
results = {}
# Run tests
results["imports"] = await test_memory_imports()
results["graphiti_status"] = await test_graphiti_status()
results["sentry_status"] = await test_sentry_status()
results["memory_save"] = await test_memory_save_flow()
results["sentry_capture"] = await test_sentry_capture()
# Summary
print("\n" + "=" * 60)
print("Test Summary")
print("=" * 60)
passed = 0
failed = 0
for test_name, result in results.items():
status = "[OK]" if result else "[FAIL]"
print(f" {status} {test_name}")
if result:
passed += 1
else:
failed += 1
print(f"\nTotal: {passed} passed, {failed} failed")
if failed > 0:
print("\n[FAIL] Some tests failed")
sys.exit(1)
else:
print("\n[OK] All tests passed")
sys.exit(0)
if __name__ == "__main__":
asyncio.run(main())