Files
Aperant/apps/backend/memory/patterns.py
T
Andy bc5f550ee3 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>
2026-01-25 12:42:35 +01:00

170 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""
Patterns and Gotchas Management
================================
Functions for managing code patterns and gotchas (pitfalls to avoid).
"""
import logging
from pathlib import Path
from .graphiti_helpers import get_graphiti_memory, is_graphiti_memory_enabled, run_async
from .paths import get_memory_dir
logger = logging.getLogger(__name__)
def append_gotcha(spec_dir: Path, gotcha: str) -> None:
"""
Append a gotcha (pitfall to avoid) to the gotchas list.
Gotchas are deduplicated - if the same gotcha already exists,
it won't be added again.
Args:
spec_dir: Path to spec directory
gotcha: Description of the pitfall to avoid
Example:
append_gotcha(spec_dir, "Database connections must be closed in workers")
append_gotcha(spec_dir, "API rate limits: 100 req/min per IP")
"""
memory_dir = get_memory_dir(spec_dir)
gotchas_file = memory_dir / "gotchas.md"
# Load existing gotchas
existing_gotchas = set()
if gotchas_file.exists():
content = gotchas_file.read_text(encoding="utf-8")
# Extract bullet points
for line in content.split("\n"):
line = line.strip()
if line.startswith("- "):
existing_gotchas.add(line[2:].strip())
# Add new gotcha if not duplicate
gotcha_stripped = gotcha.strip()
if gotcha_stripped and gotcha_stripped not in existing_gotchas:
# Append to file
with open(gotchas_file, "a", encoding="utf-8") as f:
if gotchas_file.stat().st_size == 0:
# First entry - add header
f.write("# Gotchas and Pitfalls\n\n")
f.write("Things to watch out for in this codebase:\n\n")
f.write(f"- {gotcha_stripped}\n")
# Also save to Graphiti if enabled
if is_graphiti_memory_enabled():
try:
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}")
def load_gotchas(spec_dir: Path) -> list[str]:
"""
Load all gotchas.
Args:
spec_dir: Path to spec directory
Returns:
List of gotcha strings
"""
memory_dir = get_memory_dir(spec_dir)
gotchas_file = memory_dir / "gotchas.md"
if not gotchas_file.exists():
return []
content = gotchas_file.read_text(encoding="utf-8")
gotchas = []
for line in content.split("\n"):
line = line.strip()
if line.startswith("- "):
gotchas.append(line[2:].strip())
return gotchas
def append_pattern(spec_dir: Path, pattern: str) -> None:
"""
Append a code pattern to follow.
Patterns are deduplicated - if the same pattern already exists,
it won't be added again.
Args:
spec_dir: Path to spec directory
pattern: Description of the code pattern
Example:
append_pattern(spec_dir, "Use try/except with specific exceptions")
append_pattern(spec_dir, "All API responses use {success: bool, data: any, error: string}")
"""
memory_dir = get_memory_dir(spec_dir)
patterns_file = memory_dir / "patterns.md"
# Load existing patterns
existing_patterns = set()
if patterns_file.exists():
content = patterns_file.read_text(encoding="utf-8")
# Extract bullet points
for line in content.split("\n"):
line = line.strip()
if line.startswith("- "):
existing_patterns.add(line[2:].strip())
# Add new pattern if not duplicate
pattern_stripped = pattern.strip()
if pattern_stripped and pattern_stripped not in existing_patterns:
# Append to file
with open(patterns_file, "a", encoding="utf-8") as f:
if patterns_file.stat().st_size == 0:
# First entry - add header
f.write("# Code Patterns\n\n")
f.write("Established patterns to follow in this codebase:\n\n")
f.write(f"- {pattern_stripped}\n")
# Also save to Graphiti if enabled
if is_graphiti_memory_enabled():
try:
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}")
def load_patterns(spec_dir: Path) -> list[str]:
"""
Load all code patterns.
Args:
spec_dir: Path to spec directory
Returns:
List of pattern strings
"""
memory_dir = get_memory_dir(spec_dir)
patterns_file = memory_dir / "patterns.md"
if not patterns_file.exists():
return []
content = patterns_file.read_text(encoding="utf-8")
patterns = []
for line in content.split("\n"):
line = line.strip()
if line.startswith("- "):
patterns.append(line[2:].strip())
return patterns