fix(memory): use shared project-wide memory for cross-spec learning (#905)

* fix(memory): use shared project-wide memory for cross-spec learning

Task execution memory was isolated per spec (GroupIdMode.SPEC), preventing
learnings from one spec from benefiting other specs. Changed all GraphitiMemory
instantiations to use GroupIdMode.PROJECT for shared project-wide context.

This aligns task memory with PR review memory, which already uses shared
databases for cross-session learning.

Fixes #205

* refactor(memory): centralize GraphitiMemory instantiation via helper

Use the existing get_graphiti_memory helper function instead of directly
instantiating GraphitiMemory in multiple places. This reduces code
duplication and improves maintainability by having a single source of
truth for memory creation.

The helper already uses GroupIdMode.PROJECT for cross-spec learning,
so this refactor maintains the original fix while centralizing the logic.

Addresses review comments on #905

* refactor(memory): improve error handling for GraphitiMemory instantiation

Move get_graphiti_memory() calls inside try/except blocks to properly catch
construction errors. Also use explicit 'is None' checks instead of truthiness
to avoid surprising __bool__ behavior.

- In get_graphiti_context: Move memory creation inside try block
- In save_session_memory: Move memory creation inside try block
- In _save_to_graphiti_async: Remove redundant is_graphiti_enabled check,
  use 'is None' for explicit None checking

These changes ensure that any construction errors are caught and handled,
allowing graceful fallback to file-based memory.

Addresses additional review comments on #905

* style(memory): use explicit None checks in finally blocks

Replace truthy checks with explicit 'is not None' checks in finally blocks
for consistency with other explicit None checks in memory_manager.py.

Addresses review comment on #905

* fix(memory): use explicit None check in second finally block

Update the finally block in save_session_memory to use explicit None check
for consistency. The first finally block was updated but this one was missed.

Addresses remaining review comment on #905

* refactor(memory): use finally block for consistent cleanup in save_to_graphiti_async

Refactor save_to_graphiti_async to use a finally block with explicit None
check for resource cleanup, matching the pattern established in memory_manager.py.

Previously, close() was called in both the try and except blocks, which could
lead to resource leaks in certain edge cases. The finally block ensures cleanup
always occurs.

Addresses CodeRabbit review feedback on #905

* refactor(memory): add type hints and improve cleanup safety

- Add return type annotation to get_graphiti_memory() using TYPE_CHECKING
  to avoid circular imports while keeping call sites honest
- Wrap memory.close() in try/except within finally blocks to prevent
  close() exceptions from overriding success/failure flows
- Use explicit 'memory is not None' checks to avoid misleading warnings
  when memory isn't available
- Distinguish between 'memory is None' (not available) and
  'memory.is_enabled is False' (disabled) for clearer logging

Addresses CodeRabbit review feedback on #905

* fix(memory): wrap close() in try/except in save_to_graphiti_async finally

Wrap graphiti.close() in try/except within the finally block to prevent
close() exceptions from overriding successful outcomes. This matches the
pattern established in memory_manager.py for consistent resource cleanup.

Addresses CodeRabbit review feedback on #905

* fix(memory): address follow-up review findings

- Wrap close() in try/except in tools/memory.py finally block to match
  the pattern in graphiti_helpers.py and memory_manager.py, preventing
  close() exceptions from overriding successful outcomes
- Update get_graphiti_memory() default in integrations/graphiti/memory.py
  from GroupIdMode.SPEC to GroupIdMode.PROJECT for consistency with the
  PR's intent of enabling cross-spec learning by default
- Add deprecation note explaining the default change

Addresses follow-up review findings on #905:
- NEW-001: Inconsistent close() handling
- e87df0a37e94: Duplicate get_graphiti_memory function with different default

* refactor(memory): log close failures at debug level

Update all finally blocks to log memory.close() failures at debug level
instead of silently swallowing them. This provides useful diagnostics
while still preventing close() exceptions from overriding the main result.

Changes:
- tools/memory.py: Add logger.debug() with exc_info for close failures
- graphiti_helpers.py: Add logger.debug() with exc_info for close failures
- memory_manager.py: Add logger.debug() with exc_info for close failures (2 locations)

The debug-level logging ensures close failures are visible for debugging
but do not affect the primary operation outcome.

Addresses review feedback on #905

* fix(memory): log close failures in nested finally block

Add debug logging to the nested finally block in save_session_memory
that was missed by the previous replace_all operation due to different
indentation levels. This ensures all close failures are logged at debug
level for debugging purposes while still preventing them from overriding
the main result.

Addresses review feedback on #905

* fix(logging): use exc_info=True for proper traceback in debug logs

Change logger.debug calls to use exc_info=True instead of exc_info=e
when logging close failures. This ensures the current exception's traceback
is included in the log output for better debugging.

exc_info=e only includes the exception object but not the traceback,
while exc_info=True captures the full traceback information from the
current exception context.

Changes:
- memory_manager.py: Update both finally blocks (2 locations)
- graphiti_helpers.py: Update finally block
- tools/memory.py: Update finally block

Addresses review feedback on #905

---------

Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com>
Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
StillKnotKnown
2026-01-11 15:42:46 +02:00
committed by GitHub
parent d9e3b28695
commit 0c139add1e
4 changed files with 89 additions and 61 deletions
+51 -39
View File
@@ -24,6 +24,7 @@ from graphiti_config import get_graphiti_status, is_graphiti_enabled
# Import from parent memory package
# Now safe since this module is named memory_manager (not memory)
from memory import save_session_insights as save_file_based_memory
from memory.graphiti_helpers import get_graphiti_memory
logger = logging.getLogger(__name__)
@@ -113,24 +114,20 @@ async def get_graphiti_context(
debug("memory", "Graphiti not enabled, skipping context retrieval")
return None
memory = None
try:
from graphiti_memory import GraphitiMemory
# Create memory manager
memory = GraphitiMemory(spec_dir, project_dir)
if not memory.is_enabled:
# Use centralized helper for GraphitiMemory instantiation
memory = get_graphiti_memory(spec_dir, project_dir)
if memory is None:
if is_debug_enabled():
debug_warning("memory", "GraphitiMemory.is_enabled=False")
debug_warning("memory", "GraphitiMemory not available")
return None
# Build search query from subtask description
subtask_desc = subtask.get("description", "")
subtask_id = subtask.get("id", "")
query = f"{subtask_desc} {subtask_id}".strip()
if not query:
await memory.close()
if is_debug_enabled():
debug_warning("memory", "Empty query, skipping context retrieval")
return None
@@ -155,8 +152,6 @@ async def get_graphiti_context(
# Also get recent session history
session_history = await memory.get_session_history(limit=3)
await memory.close()
if is_debug_enabled():
debug(
"memory",
@@ -229,14 +224,20 @@ async def get_graphiti_context(
return "\n".join(sections)
except ImportError:
logger.debug("Graphiti packages not installed")
if is_debug_enabled():
debug_warning("memory", "Graphiti packages not installed")
return None
except Exception as e:
logger.warning(f"Failed to get Graphiti context: {e}")
if is_debug_enabled():
debug_error("memory", "Graphiti context retrieval failed", error=str(e))
return None
finally:
# Always close the memory connection (swallow exceptions to avoid overriding)
if memory is not None:
try:
await memory.close()
except Exception as e:
logger.debug(
"Failed to close Graphiti memory connection", exc_info=True
)
async def save_session_memory(
@@ -321,20 +322,24 @@ async def save_session_memory(
if is_debug_enabled():
debug("memory", "Attempting PRIMARY storage: Graphiti")
memory = None
try:
from graphiti_memory import GraphitiMemory
# Use centralized helper for GraphitiMemory instantiation
memory = 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(
"memory",
"GraphitiMemory instance created",
is_enabled=memory.is_enabled,
group_id=getattr(memory, "group_id", "unknown"),
)
memory = GraphitiMemory(spec_dir, project_dir)
if is_debug_enabled():
debug_detailed(
"memory",
"GraphitiMemory instance created",
is_enabled=memory.is_enabled,
group_id=getattr(memory, "group_id", "unknown"),
)
if memory.is_enabled:
if memory is not None and memory.is_enabled:
if is_debug_enabled():
debug("memory", "Saving to Graphiti...")
@@ -351,8 +356,6 @@ async def save_session_memory(
# Fallback to basic session insights
result = await memory.save_session_insights(session_num, insights)
await memory.close()
if result:
logger.info(
f"Session {session_num} insights saved to Graphiti (primary)"
@@ -373,23 +376,32 @@ async def save_session_memory(
debug_warning(
"memory", "Graphiti save returned False, using FALLBACK"
)
else:
logger.warning(
"Graphiti memory not enabled, falling back to file-based"
)
elif memory is None:
if is_debug_enabled():
debug_warning(
"memory", "GraphitiMemory.is_enabled=False, using FALLBACK"
"memory", "GraphitiMemory not available, using FALLBACK"
)
else:
# memory is not None but memory.is_enabled is False
logger.warning(
"GraphitiMemory.is_enabled=False, falling back to file-based"
)
if is_debug_enabled():
debug_warning("memory", "GraphitiMemory disabled, using FALLBACK")
except ImportError as e:
logger.debug("Graphiti packages not installed, falling back to file-based")
if is_debug_enabled():
debug_warning("memory", "Graphiti packages not installed", error=str(e))
except Exception as e:
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))
finally:
# Always close the memory connection (swallow exceptions to avoid overriding)
if memory is not None:
try:
await memory.close()
except Exception as e:
logger.debug(
"Failed to close Graphiti memory connection", exc_info=e
)
else:
if is_debug_enabled():
debug("memory", "Graphiti not enabled, skipping to FALLBACK")
+12 -10
View File
@@ -48,15 +48,14 @@ async def _save_to_graphiti_async(
True if save succeeded, False otherwise
"""
try:
# Check if Graphiti is enabled
from graphiti_config import is_graphiti_enabled
# Use centralized helper for GraphitiMemory instantiation
# The helper handles enablement checks internally
from memory.graphiti_helpers import get_graphiti_memory
if not is_graphiti_enabled():
memory = get_graphiti_memory(spec_dir, project_dir)
if memory is None:
return False
from integrations.graphiti.queries_pkg.graphiti import GraphitiMemory
memory = GraphitiMemory(spec_dir, project_dir)
try:
if save_type == "discovery":
# Save as codebase discovery
@@ -77,11 +76,14 @@ async def _save_to_graphiti_async(
result = False
return result
finally:
await memory.close()
# Always close the memory connection (swallow exceptions to avoid overriding)
try:
await memory.close()
except Exception as e:
logger.debug(
"Failed to close Graphiti memory connection", exc_info=True
)
except ImportError as e:
logger.debug(f"Graphiti not available for memory tools: {e}")
return False
except Exception as e:
logger.warning(f"Failed to save to Graphiti: {e}")
return False
+6 -2
View File
@@ -50,7 +50,7 @@ from .queries_pkg.schema import (
def get_graphiti_memory(
spec_dir: Path,
project_dir: Path,
group_id_mode: str = GroupIdMode.SPEC,
group_id_mode: str = GroupIdMode.PROJECT,
) -> GraphitiMemory:
"""
Get a GraphitiMemory instance for the given spec.
@@ -60,10 +60,14 @@ def get_graphiti_memory(
Args:
spec_dir: Spec directory
project_dir: Project root directory
group_id_mode: "spec" for isolated memory, "project" for shared
group_id_mode: "spec" for isolated memory, "project" for shared (default)
Returns:
GraphitiMemory instance
Note:
Default changed from SPEC to PROJECT to enable cross-spec learning across
the entire project. Use GroupIdMode.SPEC explicitly for isolated per-spec memory.
"""
return GraphitiMemory(spec_dir, project_dir, group_id_mode)
+20 -10
View File
@@ -10,10 +10,13 @@ Handles checking if Graphiti is available and managing async operations.
import asyncio
import logging
from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from graphiti_memory import GraphitiMemory
def is_graphiti_memory_enabled() -> bool:
"""
@@ -34,7 +37,9 @@ def is_graphiti_memory_enabled() -> bool:
return False
def get_graphiti_memory(spec_dir: Path, project_dir: Path | None = None):
def get_graphiti_memory(
spec_dir: Path, project_dir: Path | None = None
) -> "GraphitiMemory | None":
"""
Get a GraphitiMemory instance if available.
@@ -49,11 +54,12 @@ def get_graphiti_memory(spec_dir: Path, project_dir: Path | None = None):
return None
try:
from graphiti_memory import GraphitiMemory
from graphiti_memory import GraphitiMemory, GroupIdMode
if project_dir is None:
project_dir = spec_dir.parent.parent
return GraphitiMemory(spec_dir, project_dir)
# Use project-wide shared memory for cross-spec learning
return GraphitiMemory(spec_dir, project_dir, group_id_mode=GroupIdMode.PROJECT)
except ImportError:
return None
@@ -100,7 +106,7 @@ async def save_to_graphiti_async(
True if save succeeded, False otherwise
"""
graphiti = get_graphiti_memory(spec_dir, project_dir)
if not graphiti:
if graphiti is None:
return False
try:
@@ -120,13 +126,17 @@ async def save_to_graphiti_async(
for gotcha in discoveries.get("gotchas_encountered", []):
await graphiti.save_gotcha(gotcha)
await graphiti.close()
return result
except Exception as e:
logger.warning(f"Failed to save to Graphiti: {e}")
try:
await graphiti.close()
except Exception:
pass
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:
logger.debug(
"Failed to close Graphiti memory connection", exc_info=True
)