claude-mpm 3.1.3__py3-none-any.whl → 3.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. claude_mpm/__init__.py +3 -3
  2. claude_mpm/__main__.py +0 -17
  3. claude_mpm/agents/INSTRUCTIONS.md +149 -17
  4. claude_mpm/agents/backups/INSTRUCTIONS.md +238 -0
  5. claude_mpm/agents/base_agent.json +1 -1
  6. claude_mpm/agents/templates/pm.json +25 -0
  7. claude_mpm/agents/templates/research.json +2 -1
  8. claude_mpm/cli/__init__.py +19 -23
  9. claude_mpm/cli/commands/__init__.py +3 -1
  10. claude_mpm/cli/commands/agents.py +7 -18
  11. claude_mpm/cli/commands/info.py +5 -10
  12. claude_mpm/cli/commands/memory.py +232 -0
  13. claude_mpm/cli/commands/run.py +501 -28
  14. claude_mpm/cli/commands/tickets.py +10 -17
  15. claude_mpm/cli/commands/ui.py +15 -37
  16. claude_mpm/cli/parser.py +91 -1
  17. claude_mpm/cli/utils.py +9 -28
  18. claude_mpm/config/socketio_config.py +256 -0
  19. claude_mpm/constants.py +9 -0
  20. claude_mpm/core/__init__.py +2 -2
  21. claude_mpm/core/agent_registry.py +4 -4
  22. claude_mpm/core/claude_runner.py +919 -0
  23. claude_mpm/core/config.py +21 -1
  24. claude_mpm/core/factories.py +1 -1
  25. claude_mpm/core/hook_manager.py +196 -0
  26. claude_mpm/core/pm_hook_interceptor.py +205 -0
  27. claude_mpm/core/service_registry.py +1 -1
  28. claude_mpm/core/simple_runner.py +323 -33
  29. claude_mpm/core/socketio_pool.py +582 -0
  30. claude_mpm/core/websocket_handler.py +233 -0
  31. claude_mpm/deployment_paths.py +261 -0
  32. claude_mpm/hooks/builtin/memory_hooks_example.py +67 -0
  33. claude_mpm/hooks/claude_hooks/hook_handler.py +667 -679
  34. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -4
  35. claude_mpm/hooks/memory_integration_hook.py +312 -0
  36. claude_mpm/models/__init__.py +9 -91
  37. claude_mpm/orchestration/__init__.py +1 -1
  38. claude_mpm/scripts/claude-mpm-socketio +32 -0
  39. claude_mpm/scripts/claude_mpm_monitor.html +567 -0
  40. claude_mpm/scripts/install_socketio_server.py +407 -0
  41. claude_mpm/scripts/launch_monitor.py +132 -0
  42. claude_mpm/scripts/launch_socketio_dashboard.py +261 -0
  43. claude_mpm/scripts/manage_version.py +479 -0
  44. claude_mpm/scripts/socketio_daemon.py +181 -0
  45. claude_mpm/scripts/socketio_server_manager.py +428 -0
  46. claude_mpm/services/__init__.py +5 -0
  47. claude_mpm/services/agent_lifecycle_manager.py +76 -25
  48. claude_mpm/services/agent_memory_manager.py +684 -0
  49. claude_mpm/services/agent_modification_tracker.py +98 -17
  50. claude_mpm/services/agent_persistence_service.py +33 -13
  51. claude_mpm/services/agent_registry.py +82 -43
  52. claude_mpm/services/hook_service.py +362 -0
  53. claude_mpm/services/socketio_client_manager.py +474 -0
  54. claude_mpm/services/socketio_server.py +922 -0
  55. claude_mpm/services/standalone_socketio_server.py +631 -0
  56. claude_mpm/services/ticket_manager.py +4 -5
  57. claude_mpm/services/{ticket_manager_dependency_injection.py → ticket_manager_di.py} +12 -39
  58. claude_mpm/services/{legacy_ticketing_service.py → ticketing_service_original.py} +9 -16
  59. claude_mpm/services/version_control/semantic_versioning.py +9 -10
  60. claude_mpm/services/websocket_server.py +376 -0
  61. claude_mpm/utils/dependency_manager.py +211 -0
  62. claude_mpm/utils/import_migration_example.py +80 -0
  63. claude_mpm/utils/path_operations.py +0 -20
  64. claude_mpm/web/open_dashboard.py +34 -0
  65. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/METADATA +20 -9
  66. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/RECORD +71 -50
  67. claude_mpm-3.3.0.dist-info/entry_points.txt +7 -0
  68. claude_mpm/cli_old.py +0 -728
  69. claude_mpm/models/common.py +0 -41
  70. claude_mpm/models/lifecycle.py +0 -97
  71. claude_mpm/models/modification.py +0 -126
  72. claude_mpm/models/persistence.py +0 -57
  73. claude_mpm/models/registry.py +0 -91
  74. claude_mpm/security/__init__.py +0 -8
  75. claude_mpm/security/bash_validator.py +0 -393
  76. claude_mpm-3.1.3.dist-info/entry_points.txt +0 -4
  77. /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
  78. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/WHEEL +0 -0
  79. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/licenses/LICENSE +0 -0
  80. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,684 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Agent Memory Manager Service
4
+ ===========================
5
+
6
+ Manages agent memory files with size limits and validation.
7
+
8
+ This service provides:
9
+ - Memory file operations (load, save, validate)
10
+ - Size limit enforcement (8KB default)
11
+ - Auto-truncation when limits exceeded
12
+ - Default memory template creation
13
+ - Section management with item limits
14
+ - Timestamp updates
15
+ - Directory initialization with README
16
+
17
+ Memory files are stored in .claude-mpm/memories/ directory
18
+ following the naming convention: {agent_id}_agent.md
19
+ """
20
+
21
+ from pathlib import Path
22
+ from typing import Dict, List, Optional, Any
23
+ from datetime import datetime
24
+ import re
25
+ import logging
26
+
27
+ from claude_mpm.core import LoggerMixin
28
+ from claude_mpm.core.config import Config
29
+ from claude_mpm.utils.paths import PathResolver
30
+ from claude_mpm.services.websocket_server import get_websocket_server
31
+
32
+
33
+ class AgentMemoryManager(LoggerMixin):
34
+ """Manages agent memory files with size limits and validation.
35
+
36
+ WHY: Agents need to accumulate project-specific knowledge over time to become
37
+ more effective. This service manages persistent memory files that agents can
38
+ read before tasks and update with new learnings.
39
+
40
+ DESIGN DECISION: Memory files are stored in .claude-mpm/memories/ (not project root)
41
+ to keep them organized and separate from other project files. Files follow a
42
+ standardized markdown format with enforced size limits to prevent unbounded growth.
43
+
44
+ The 8KB limit (~2000 tokens) balances comprehensive knowledge storage with
45
+ reasonable context size for agent prompts.
46
+ """
47
+
48
+ # Default limits - will be overridden by configuration
49
+ DEFAULT_MEMORY_LIMITS = {
50
+ 'max_file_size_kb': 8,
51
+ 'max_sections': 10,
52
+ 'max_items_per_section': 15,
53
+ 'max_line_length': 120
54
+ }
55
+
56
+ REQUIRED_SECTIONS = [
57
+ 'Project Architecture',
58
+ 'Implementation Guidelines',
59
+ 'Common Mistakes to Avoid',
60
+ 'Current Technical Context'
61
+ ]
62
+
63
+ def __init__(self, config: Optional[Config] = None):
64
+ """Initialize the memory manager.
65
+
66
+ Sets up the memories directory and ensures it exists with proper README.
67
+
68
+ Args:
69
+ config: Optional Config object. If not provided, will create default Config.
70
+ """
71
+ super().__init__()
72
+ self.config = config or Config()
73
+ self.project_root = PathResolver.get_project_root()
74
+ self.memories_dir = self.project_root / ".claude-mpm" / "memories"
75
+ self._ensure_memories_directory()
76
+
77
+ # Initialize memory limits from configuration
78
+ self._init_memory_limits()
79
+
80
+ def _init_memory_limits(self):
81
+ """Initialize memory limits from configuration.
82
+
83
+ WHY: Allows configuration-driven memory limits instead of hardcoded values.
84
+ Supports agent-specific overrides for different memory requirements.
85
+ """
86
+ # Check if memory system is enabled
87
+ self.memory_enabled = self.config.get('memory.enabled', True)
88
+ self.auto_learning = self.config.get('memory.auto_learning', False)
89
+
90
+ # Load default limits from configuration
91
+ config_limits = self.config.get('memory.limits', {})
92
+ self.memory_limits = {
93
+ 'max_file_size_kb': config_limits.get('default_size_kb',
94
+ self.DEFAULT_MEMORY_LIMITS['max_file_size_kb']),
95
+ 'max_sections': config_limits.get('max_sections',
96
+ self.DEFAULT_MEMORY_LIMITS['max_sections']),
97
+ 'max_items_per_section': config_limits.get('max_items_per_section',
98
+ self.DEFAULT_MEMORY_LIMITS['max_items_per_section']),
99
+ 'max_line_length': config_limits.get('max_line_length',
100
+ self.DEFAULT_MEMORY_LIMITS['max_line_length'])
101
+ }
102
+
103
+ # Load agent-specific overrides
104
+ self.agent_overrides = self.config.get('memory.agent_overrides', {})
105
+
106
+ def _get_agent_limits(self, agent_id: str) -> Dict[str, Any]:
107
+ """Get memory limits for specific agent, including overrides.
108
+
109
+ WHY: Different agents may need different memory capacities. Research agents
110
+ might need larger memory for comprehensive findings, while simple agents
111
+ can work with smaller limits.
112
+
113
+ Args:
114
+ agent_id: The agent identifier
115
+
116
+ Returns:
117
+ Dict containing the effective limits for this agent
118
+ """
119
+ # Start with default limits
120
+ limits = self.memory_limits.copy()
121
+
122
+ # Apply agent-specific overrides if they exist
123
+ if agent_id in self.agent_overrides:
124
+ overrides = self.agent_overrides[agent_id]
125
+ if 'size_kb' in overrides:
126
+ limits['max_file_size_kb'] = overrides['size_kb']
127
+
128
+ return limits
129
+
130
+ def _get_agent_auto_learning(self, agent_id: str) -> bool:
131
+ """Check if auto-learning is enabled for specific agent.
132
+
133
+ Args:
134
+ agent_id: The agent identifier
135
+
136
+ Returns:
137
+ bool: True if auto-learning is enabled for this agent
138
+ """
139
+ # Check agent-specific override first
140
+ if agent_id in self.agent_overrides:
141
+ return self.agent_overrides[agent_id].get('auto_learning', self.auto_learning)
142
+
143
+ # Fall back to global setting
144
+ return self.auto_learning
145
+
146
+ def load_agent_memory(self, agent_id: str) -> str:
147
+ """Load agent memory file content.
148
+
149
+ WHY: Agents need to read their accumulated knowledge before starting tasks
150
+ to apply learned patterns and avoid repeated mistakes.
151
+
152
+ Args:
153
+ agent_id: The agent identifier (e.g., 'research', 'engineer')
154
+
155
+ Returns:
156
+ str: The memory file content, creating default if doesn't exist
157
+ """
158
+ memory_file = self.memories_dir / f"{agent_id}_agent.md"
159
+
160
+ if not memory_file.exists():
161
+ self.logger.info(f"Creating default memory for agent: {agent_id}")
162
+ return self._create_default_memory(agent_id)
163
+
164
+ try:
165
+ content = memory_file.read_text(encoding='utf-8')
166
+
167
+ # Emit WebSocket event for memory loaded
168
+ try:
169
+ ws_server = get_websocket_server()
170
+ file_size = len(content.encode('utf-8'))
171
+ # Count sections by looking for lines starting with ##
172
+ sections_count = sum(1 for line in content.split('\n') if line.startswith('## '))
173
+ ws_server.memory_loaded(agent_id, file_size, sections_count)
174
+ except Exception as ws_error:
175
+ self.logger.debug(f"WebSocket notification failed: {ws_error}")
176
+
177
+ return self._validate_and_repair(content, agent_id)
178
+ except Exception as e:
179
+ self.logger.error(f"Error reading memory file for {agent_id}: {e}")
180
+ # Return default memory on error - never fail
181
+ return self._create_default_memory(agent_id)
182
+
183
+ def update_agent_memory(self, agent_id: str, section: str, new_item: str) -> bool:
184
+ """Add new learning item to specified section.
185
+
186
+ WHY: Agents discover new patterns and insights during task execution that
187
+ should be preserved for future tasks. This method adds new learnings while
188
+ enforcing size limits to prevent unbounded growth.
189
+
190
+ Args:
191
+ agent_id: The agent identifier
192
+ section: The section name to add the item to
193
+ new_item: The learning item to add
194
+
195
+ Returns:
196
+ bool: True if update succeeded, False otherwise
197
+ """
198
+ try:
199
+ current_memory = self.load_agent_memory(agent_id)
200
+ updated_memory = self._add_item_to_section(current_memory, section, new_item)
201
+
202
+ # Enforce limits
203
+ if self._exceeds_limits(updated_memory, agent_id):
204
+ self.logger.debug(f"Memory for {agent_id} exceeds limits, truncating")
205
+ updated_memory = self._truncate_to_limits(updated_memory, agent_id)
206
+
207
+ # Save with timestamp
208
+ return self._save_memory_file(agent_id, updated_memory)
209
+ except Exception as e:
210
+ self.logger.error(f"Error updating memory for {agent_id}: {e}")
211
+ # Never fail on memory errors
212
+ return False
213
+
214
+ def add_learning(self, agent_id: str, learning_type: str, content: str) -> bool:
215
+ """Add structured learning to appropriate section.
216
+
217
+ WHY: Different types of learnings belong in different sections for better
218
+ organization and retrieval. This method maps learning types to appropriate
219
+ sections automatically.
220
+
221
+ Args:
222
+ agent_id: The agent identifier
223
+ learning_type: Type of learning (pattern, architecture, guideline, etc.)
224
+ content: The learning content
225
+
226
+ Returns:
227
+ bool: True if learning was added successfully
228
+ """
229
+ section_mapping = {
230
+ 'pattern': 'Coding Patterns Learned',
231
+ 'architecture': 'Project Architecture',
232
+ 'guideline': 'Implementation Guidelines',
233
+ 'mistake': 'Common Mistakes to Avoid',
234
+ 'strategy': 'Effective Strategies',
235
+ 'integration': 'Integration Points',
236
+ 'performance': 'Performance Considerations',
237
+ 'domain': 'Domain-Specific Knowledge',
238
+ 'context': 'Current Technical Context'
239
+ }
240
+
241
+ section = section_mapping.get(learning_type, 'Recent Learnings')
242
+ success = self.update_agent_memory(agent_id, section, content)
243
+
244
+ # Emit WebSocket event for memory updated
245
+ if success:
246
+ try:
247
+ ws_server = get_websocket_server()
248
+ ws_server.memory_updated(agent_id, learning_type, content, section)
249
+ except Exception as ws_error:
250
+ self.logger.debug(f"WebSocket notification failed: {ws_error}")
251
+
252
+ return success
253
+
254
+ def _create_default_memory(self, agent_id: str) -> str:
255
+ """Create default memory file for agent.
256
+
257
+ WHY: New agents need a starting template with essential project knowledge
258
+ and the correct structure for adding new learnings.
259
+
260
+ Args:
261
+ agent_id: The agent identifier
262
+
263
+ Returns:
264
+ str: The default memory template content
265
+ """
266
+ # Convert agent_id to proper name, handling cases like "test_agent" -> "Test"
267
+ agent_name = agent_id.replace('_agent', '').replace('_', ' ').title()
268
+ project_name = self.project_root.name
269
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
270
+
271
+ # Get limits for this agent
272
+ limits = self._get_agent_limits(agent_id)
273
+
274
+ template = f"""# {agent_name} Agent Memory - {project_name}
275
+
276
+ <!-- MEMORY LIMITS: {limits['max_file_size_kb']}KB max | {limits['max_sections']} sections max | {limits['max_items_per_section']} items per section -->
277
+ <!-- Last Updated: {timestamp} | Auto-updated by: {agent_id} -->
278
+
279
+ ## Project Architecture (Max: 15 items)
280
+ - Service-oriented architecture with clear module boundaries
281
+ - Three-tier agent hierarchy: project → user → system
282
+ - Agent definitions use standardized JSON schema validation
283
+
284
+ ## Coding Patterns Learned (Max: 15 items)
285
+ - Always use PathResolver for path operations, never hardcode paths
286
+ - SubprocessRunner utility for external command execution
287
+ - LoggerMixin provides consistent logging across all services
288
+
289
+ ## Implementation Guidelines (Max: 15 items)
290
+ - Check docs/STRUCTURE.md before creating new files
291
+ - Follow existing import patterns: from claude_mpm.module import Class
292
+ - Use existing utilities instead of reimplementing functionality
293
+
294
+ ## Domain-Specific Knowledge (Max: 15 items)
295
+ <!-- Agent-specific knowledge accumulates here -->
296
+
297
+ ## Effective Strategies (Max: 15 items)
298
+ <!-- Successful approaches discovered through experience -->
299
+
300
+ ## Common Mistakes to Avoid (Max: 15 items)
301
+ - Don't modify Claude Code core functionality, only extend it
302
+ - Avoid duplicating code - check utils/ for existing implementations
303
+ - Never hardcode file paths, use PathResolver utilities
304
+
305
+ ## Integration Points (Max: 15 items)
306
+ <!-- Key interfaces and integration patterns -->
307
+
308
+ ## Performance Considerations (Max: 15 items)
309
+ <!-- Performance insights and optimization patterns -->
310
+
311
+ ## Current Technical Context (Max: 15 items)
312
+ - EP-0001: Technical debt reduction in progress
313
+ - Target: 80% test coverage (current: 23.6%)
314
+ - Integration with Claude Code 1.0.60+ native agent framework
315
+
316
+ ## Recent Learnings (Max: 15 items)
317
+ <!-- Most recent discoveries and insights -->
318
+ """
319
+
320
+ # Save default file
321
+ try:
322
+ memory_file = self.memories_dir / f"{agent_id}_agent.md"
323
+ memory_file.write_text(template, encoding='utf-8')
324
+ self.logger.info(f"Created default memory file for {agent_id}")
325
+
326
+ # Emit WebSocket event for memory created
327
+ try:
328
+ ws_server = get_websocket_server()
329
+ ws_server.memory_created(agent_id, "default")
330
+ except Exception as ws_error:
331
+ self.logger.debug(f"WebSocket notification failed: {ws_error}")
332
+ except Exception as e:
333
+ self.logger.error(f"Error saving default memory for {agent_id}: {e}")
334
+
335
+ return template
336
+
337
+ def _add_item_to_section(self, content: str, section: str, new_item: str) -> str:
338
+ """Add item to specified section, respecting limits.
339
+
340
+ WHY: Each section has a maximum item limit to prevent information overload
341
+ and maintain readability. When limits are reached, oldest items are removed
342
+ to make room for new learnings (FIFO strategy).
343
+
344
+ Args:
345
+ content: Current memory file content
346
+ section: Section name to add item to
347
+ new_item: Item to add
348
+
349
+ Returns:
350
+ str: Updated content with new item added
351
+ """
352
+ lines = content.split('\n')
353
+ section_start = None
354
+ section_end = None
355
+
356
+ # Find section boundaries
357
+ for i, line in enumerate(lines):
358
+ if line.startswith(f'## {section}'):
359
+ section_start = i
360
+ elif section_start is not None and line.startswith('## '):
361
+ section_end = i
362
+ break
363
+
364
+ if section_start is None:
365
+ # Section doesn't exist, add it
366
+ return self._add_new_section(content, section, new_item)
367
+
368
+ if section_end is None:
369
+ section_end = len(lines)
370
+
371
+ # Count existing items in section and find first item index
372
+ item_count = 0
373
+ first_item_index = None
374
+ for i in range(section_start + 1, section_end):
375
+ if lines[i].strip().startswith('- '):
376
+ if first_item_index is None:
377
+ first_item_index = i
378
+ item_count += 1
379
+
380
+ # Check if we can add more items
381
+ if item_count >= self.memory_limits['max_items_per_section']:
382
+ # Remove oldest item (first one) to make room
383
+ if first_item_index is not None:
384
+ lines.pop(first_item_index)
385
+ section_end -= 1 # Adjust section end after removal
386
+
387
+ # Add new item (find insertion point after any comments)
388
+ insert_point = section_start + 1
389
+ while insert_point < section_end and (
390
+ not lines[insert_point].strip() or
391
+ lines[insert_point].strip().startswith('<!--')
392
+ ):
393
+ insert_point += 1
394
+
395
+ # Ensure line length limit
396
+ if len(new_item) > self.memory_limits['max_line_length']:
397
+ new_item = new_item[:self.memory_limits['max_line_length'] - 3] + '...'
398
+
399
+ lines.insert(insert_point, f"- {new_item}")
400
+
401
+ # Update timestamp
402
+ updated_content = '\n'.join(lines)
403
+ return self._update_timestamp(updated_content)
404
+
405
+ def _add_new_section(self, content: str, section: str, new_item: str) -> str:
406
+ """Add a new section with the given item.
407
+
408
+ WHY: When agents discover learnings that don't fit existing sections,
409
+ we need to create new sections dynamically while respecting the maximum
410
+ section limit.
411
+
412
+ Args:
413
+ content: Current memory content
414
+ section: New section name
415
+ new_item: First item for the section
416
+
417
+ Returns:
418
+ str: Updated content with new section
419
+ """
420
+ lines = content.split('\n')
421
+
422
+ # Count existing sections
423
+ section_count = sum(1 for line in lines if line.startswith('## '))
424
+
425
+ if section_count >= self.memory_limits['max_sections']:
426
+ self.logger.warning(f"Maximum sections reached, cannot add '{section}'")
427
+ # Try to add to Recent Learnings instead
428
+ return self._add_item_to_section(content, 'Recent Learnings', new_item)
429
+
430
+ # Find insertion point (before Recent Learnings or at end)
431
+ insert_point = len(lines)
432
+ for i, line in enumerate(lines):
433
+ if line.startswith('## Recent Learnings'):
434
+ insert_point = i
435
+ break
436
+
437
+ # Insert new section
438
+ new_section = [
439
+ '',
440
+ f'## {section} (Max: 15 items)',
441
+ f'- {new_item}',
442
+ ''
443
+ ]
444
+
445
+ for j, line in enumerate(new_section):
446
+ lines.insert(insert_point + j, line)
447
+
448
+ return '\n'.join(lines)
449
+
450
+ def _exceeds_limits(self, content: str, agent_id: Optional[str] = None) -> bool:
451
+ """Check if content exceeds size limits.
452
+
453
+ Args:
454
+ content: Content to check
455
+ agent_id: Optional agent ID for agent-specific limits
456
+
457
+ Returns:
458
+ bool: True if content exceeds limits
459
+ """
460
+ # Get appropriate limits based on agent
461
+ if agent_id:
462
+ limits = self._get_agent_limits(agent_id)
463
+ else:
464
+ limits = self.memory_limits
465
+
466
+ size_kb = len(content.encode('utf-8')) / 1024
467
+ return size_kb > limits['max_file_size_kb']
468
+
469
+ def _truncate_to_limits(self, content: str, agent_id: Optional[str] = None) -> str:
470
+ """Truncate content to fit within limits.
471
+
472
+ WHY: When memory files exceed size limits, we need a strategy to reduce
473
+ size while preserving the most important information. This implementation
474
+ removes items from "Recent Learnings" first as they're typically less
475
+ consolidated than other sections.
476
+
477
+ Args:
478
+ content: Content to truncate
479
+
480
+ Returns:
481
+ str: Truncated content within size limits
482
+ """
483
+ lines = content.split('\n')
484
+
485
+ # Get appropriate limits based on agent
486
+ if agent_id:
487
+ limits = self._get_agent_limits(agent_id)
488
+ else:
489
+ limits = self.memory_limits
490
+
491
+ # Strategy: Remove items from Recent Learnings first
492
+ while self._exceeds_limits('\n'.join(lines), agent_id):
493
+ removed = False
494
+
495
+ # First try Recent Learnings
496
+ for i, line in enumerate(lines):
497
+ if line.startswith('## Recent Learnings'):
498
+ # Find and remove first item in this section
499
+ for j in range(i + 1, len(lines)):
500
+ if lines[j].strip().startswith('- '):
501
+ lines.pop(j)
502
+ removed = True
503
+ break
504
+ elif lines[j].startswith('## '):
505
+ break
506
+ break
507
+
508
+ # If no Recent Learnings items, remove from other sections
509
+ if not removed:
510
+ # Remove from sections in reverse order (bottom up)
511
+ for i in range(len(lines) - 1, -1, -1):
512
+ if lines[i].strip().startswith('- '):
513
+ lines.pop(i)
514
+ removed = True
515
+ break
516
+
517
+ # Safety: If nothing removed, truncate from end
518
+ if not removed:
519
+ lines = lines[:-10]
520
+
521
+ return '\n'.join(lines)
522
+
523
+ def _update_timestamp(self, content: str) -> str:
524
+ """Update the timestamp in the file header.
525
+
526
+ Args:
527
+ content: Content to update
528
+
529
+ Returns:
530
+ str: Content with updated timestamp
531
+ """
532
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
533
+ return re.sub(
534
+ r'<!-- Last Updated: .+ \| Auto-updated by: .+ -->',
535
+ f'<!-- Last Updated: {timestamp} | Auto-updated by: system -->',
536
+ content
537
+ )
538
+
539
+ def _validate_and_repair(self, content: str, agent_id: str) -> str:
540
+ """Validate memory file and repair if needed.
541
+
542
+ WHY: Memory files might be manually edited by developers or corrupted.
543
+ This method ensures the file maintains required structure and sections.
544
+
545
+ Args:
546
+ content: Content to validate
547
+ agent_id: Agent identifier
548
+
549
+ Returns:
550
+ str: Validated and repaired content
551
+ """
552
+ lines = content.split('\n')
553
+ existing_sections = set()
554
+
555
+ # Find existing sections
556
+ for line in lines:
557
+ if line.startswith('## '):
558
+ section_name = line[3:].split('(')[0].strip()
559
+ existing_sections.add(section_name)
560
+
561
+ # Check for required sections
562
+ missing_sections = []
563
+ for required in self.REQUIRED_SECTIONS:
564
+ if required not in existing_sections:
565
+ missing_sections.append(required)
566
+
567
+ if missing_sections:
568
+ self.logger.info(f"Adding missing sections to {agent_id} memory: {missing_sections}")
569
+
570
+ # Add missing sections before Recent Learnings
571
+ insert_point = len(lines)
572
+ for i, line in enumerate(lines):
573
+ if line.startswith('## Recent Learnings'):
574
+ insert_point = i
575
+ break
576
+
577
+ for section in missing_sections:
578
+ section_content = [
579
+ '',
580
+ f'## {section} (Max: 15 items)',
581
+ '<!-- Section added by repair -->',
582
+ ''
583
+ ]
584
+ for j, line in enumerate(section_content):
585
+ lines.insert(insert_point + j, line)
586
+ insert_point += len(section_content)
587
+
588
+ return '\n'.join(lines)
589
+
590
+ def _save_memory_file(self, agent_id: str, content: str) -> bool:
591
+ """Save memory content to file.
592
+
593
+ WHY: Memory updates need to be persisted atomically to prevent corruption
594
+ and ensure learnings are preserved across agent invocations.
595
+
596
+ Args:
597
+ agent_id: Agent identifier
598
+ content: Content to save
599
+
600
+ Returns:
601
+ bool: True if save succeeded
602
+ """
603
+ try:
604
+ memory_file = self.memories_dir / f"{agent_id}_agent.md"
605
+ memory_file.write_text(content, encoding='utf-8')
606
+ self.logger.debug(f"Saved memory for {agent_id}")
607
+ return True
608
+ except Exception as e:
609
+ self.logger.error(f"Error saving memory for {agent_id}: {e}")
610
+ return False
611
+
612
+ def _ensure_memories_directory(self):
613
+ """Ensure memories directory exists with README.
614
+
615
+ WHY: The memories directory needs clear documentation so developers
616
+ understand the purpose of these files and how to interact with them.
617
+ """
618
+ try:
619
+ self.memories_dir.mkdir(parents=True, exist_ok=True)
620
+ self.logger.debug(f"Ensured memories directory exists: {self.memories_dir}")
621
+
622
+ readme_path = self.memories_dir / "README.md"
623
+ if not readme_path.exists():
624
+ readme_content = """# Agent Memory System
625
+
626
+ ## Purpose
627
+ Each agent maintains project-specific knowledge in these files. Agents read their memory file before tasks and update it when they learn something new.
628
+
629
+ ## Manual Editing
630
+ Feel free to edit these files to:
631
+ - Add project-specific guidelines
632
+ - Remove outdated information
633
+ - Reorganize for better clarity
634
+ - Add domain-specific knowledge
635
+
636
+ ## Memory Limits
637
+ - Max file size: 8KB (~2000 tokens)
638
+ - Max sections: 10
639
+ - Max items per section: 15
640
+ - Files auto-truncate when limits exceeded
641
+
642
+ ## File Format
643
+ Standard markdown with structured sections. Agents expect:
644
+ - Project Architecture
645
+ - Implementation Guidelines
646
+ - Common Mistakes to Avoid
647
+ - Current Technical Context
648
+
649
+ ## How It Works
650
+ 1. Agents read their memory file before starting tasks
651
+ 2. Agents add learnings during or after task completion
652
+ 3. Files automatically enforce size limits
653
+ 4. Developers can manually edit for accuracy
654
+
655
+ ## Memory File Lifecycle
656
+ - Created automatically when agent first runs
657
+ - Updated through hook system after delegations
658
+ - Manually editable by developers
659
+ - Version controlled with project
660
+ """
661
+ readme_path.write_text(readme_content, encoding='utf-8')
662
+ self.logger.info("Created README.md in memories directory")
663
+
664
+ except Exception as e:
665
+ self.logger.error(f"Error ensuring memories directory: {e}")
666
+ # Continue anyway - memory system should not block operations
667
+
668
+
669
+ # Convenience functions for external use
670
+ def get_memory_manager(config: Optional[Config] = None) -> AgentMemoryManager:
671
+ """Get a singleton instance of the memory manager.
672
+
673
+ WHY: The memory manager should be shared across the application to ensure
674
+ consistent file access and avoid multiple instances managing the same files.
675
+
676
+ Args:
677
+ config: Optional Config object. Only used on first instantiation.
678
+
679
+ Returns:
680
+ AgentMemoryManager: The memory manager instance
681
+ """
682
+ if not hasattr(get_memory_manager, '_instance'):
683
+ get_memory_manager._instance = AgentMemoryManager(config)
684
+ return get_memory_manager._instance