kollabor 0.4.9__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 (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. system_prompt/example_with_trender.md +47 -0
@@ -0,0 +1,473 @@
1
+ """Conversation logging system with intelligence features.
2
+
3
+ This module provides comprehensive JSONL logging for all conversations,
4
+ including message threading, session management, and intelligence features
5
+ that learn from user patterns and project context.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import subprocess
11
+ import time
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+ from uuid import uuid4
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class KollaborConversationLogger:
21
+ """Conversation logger with intelligence features.
22
+
23
+ Logs every terminal interaction as structured JSON objects with
24
+ conversation threading, user context analysis, and learning capabilities.
25
+ """
26
+
27
+ def __init__(self, conversations_dir: Path):
28
+ """Initialize the conversation logger.
29
+
30
+ Args:
31
+ conversations_dir: Directory to store conversation JSONL files
32
+ """
33
+ self.conversations_dir = conversations_dir
34
+ self.conversations_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ # Session management
37
+ timestamp = datetime.now().strftime('%Y-%m-%d_%H%M%S')
38
+ self.session_id = f"session_{timestamp}"
39
+ self.session_file = self.conversations_dir / f"{self.session_id}.jsonl"
40
+
41
+ # Conversation state
42
+ self.conversation_start_time = datetime.now()
43
+ self.message_count = 0
44
+ self.current_thread_uuid = None
45
+
46
+ # Intelligence features
47
+ self.user_patterns = []
48
+ self.project_context = {}
49
+ self.conversation_themes = []
50
+ self.file_interactions = {}
51
+
52
+ # Memory management
53
+ self.memory_dir = self.conversations_dir.parent / "conversation_memory"
54
+ self.memory_dir.mkdir(parents=True, exist_ok=True)
55
+ self._load_conversation_memory()
56
+
57
+ logger.info(f"Conversation logger initialized: {self.session_id}")
58
+
59
+ async def initialize(self):
60
+ """Initialize async resources for conversation logger."""
61
+ # Any async initialization can happen here
62
+ logger.debug("Conversation logger async initialization complete")
63
+
64
+ async def shutdown(self):
65
+ """Shutdown conversation logger and save state."""
66
+ # Save any pending data
67
+ self._save_conversation_memory()
68
+ logger.info("Conversation logger shutdown complete")
69
+
70
+ def _load_conversation_memory(self):
71
+ """Load conversation memory from previous sessions."""
72
+ try:
73
+ # Load user patterns
74
+ patterns_file = self.memory_dir / "user_patterns.json"
75
+ if patterns_file.exists():
76
+ with open(patterns_file, 'r') as f:
77
+ self.user_patterns = json.load(f)
78
+
79
+ # Load project context
80
+ context_file = self.memory_dir / "project_context.json"
81
+ if context_file.exists():
82
+ with open(context_file, 'r') as f:
83
+ self.project_context = json.load(f)
84
+
85
+ # Load solution history
86
+ solutions_file = self.memory_dir / "solution_history.json"
87
+ if solutions_file.exists():
88
+ with open(solutions_file, 'r') as f:
89
+ self.solution_history = json.load(f)
90
+ else:
91
+ self.solution_history = []
92
+
93
+ logger.info("Loaded conversation memory from previous sessions")
94
+
95
+ except Exception as e:
96
+ logger.warning(f"Failed to load conversation memory: {e}")
97
+ self.solution_history = []
98
+
99
+ def _save_conversation_memory(self):
100
+ """Save conversation memory for future sessions."""
101
+ try:
102
+ # Save user patterns
103
+ patterns_file = self.memory_dir / "user_patterns.json"
104
+ with open(patterns_file, 'w') as f:
105
+ json.dump(self.user_patterns, f, indent=2)
106
+
107
+ # Save project context
108
+ context_file = self.memory_dir / "project_context.json"
109
+ with open(context_file, 'w') as f:
110
+ json.dump(self.project_context, f, indent=2)
111
+
112
+ # Save solution history
113
+ solutions_file = self.memory_dir / "solution_history.json"
114
+ with open(solutions_file, 'w') as f:
115
+ json.dump(self.solution_history, f, indent=2)
116
+
117
+ logger.debug("Saved conversation memory for future sessions")
118
+
119
+ except Exception as e:
120
+ logger.error(f"Failed to save conversation memory: {e}")
121
+
122
+ def _get_git_branch(self) -> str:
123
+ """Get current git branch."""
124
+ try:
125
+ result = subprocess.run(
126
+ ["git", "branch", "--show-current"],
127
+ capture_output=True,
128
+ text=True,
129
+ timeout=2
130
+ )
131
+ if result.returncode == 0:
132
+ return result.stdout.strip()
133
+ except:
134
+ pass
135
+ return "unknown"
136
+
137
+ def _get_working_directory(self) -> str:
138
+ """Get current working directory."""
139
+ return str(Path.cwd())
140
+
141
+ async def _append_to_jsonl(self, message: Dict[str, Any]):
142
+ """Append message to JSONL file."""
143
+ try:
144
+ with open(self.session_file, 'a') as f:
145
+ f.write(json.dumps(message) + '\n')
146
+ self.message_count += 1
147
+ except Exception as e:
148
+ logger.error(f"Failed to write to JSONL: {e}")
149
+
150
+ def _analyze_user_context(self, content: str) -> Dict[str, Any]:
151
+ """Analyze user context from message content."""
152
+ context = {
153
+ "message_length": len(content),
154
+ "has_code": "```" in content,
155
+ "has_question": "?" in content,
156
+ "has_command": any(cmd in content.lower() for cmd in ["fix", "create", "update", "delete", "implement"]),
157
+ "detected_intent": self._detect_intent(content)
158
+ }
159
+
160
+ # Learn from patterns (deduplicated)
161
+ new_patterns = []
162
+ if context["has_command"]:
163
+ new_patterns.append("prefers_direct_commands")
164
+ if context["message_length"] > 200:
165
+ new_patterns.append("provides_detailed_context")
166
+ if context["has_code"]:
167
+ new_patterns.append("shares_code_frequently")
168
+ if context["has_question"]:
169
+ new_patterns.append("asks_clarifying_questions")
170
+
171
+ # Add new patterns (deduplicated)
172
+ for pattern in new_patterns:
173
+ if pattern not in self.user_patterns:
174
+ self.user_patterns.append(pattern)
175
+ logger.debug(f"Learned user pattern: {pattern}")
176
+
177
+ # Update project context based on content
178
+ self._update_project_context(content)
179
+
180
+ return context
181
+
182
+ def _update_project_context(self, content: str):
183
+ """Update project context based on message content."""
184
+ # Track file mentions
185
+ import re
186
+ file_mentions = re.findall(r'(?:core/|plugins/|tests/|\.py|\.json|\.md)\S*', content)
187
+ for file_path in file_mentions:
188
+ if file_path not in self.project_context:
189
+ self.project_context[file_path] = {
190
+ "mentions": 0,
191
+ "first_mentioned": datetime.now().isoformat(),
192
+ "context": "user_discussion"
193
+ }
194
+ self.project_context[file_path]["mentions"] += 1
195
+ self.project_context[file_path]["last_mentioned"] = datetime.now().isoformat()
196
+
197
+ # Track technologies mentioned
198
+ technologies = ["python", "async", "json", "mcp", "terminal", "llm", "hook", "plugin"]
199
+ mentioned_tech = [tech for tech in technologies if tech in content.lower()]
200
+ if mentioned_tech:
201
+ if "technologies" not in self.project_context:
202
+ self.project_context["technologies"] = {}
203
+ for tech in mentioned_tech:
204
+ if tech not in self.project_context["technologies"]:
205
+ self.project_context["technologies"][tech] = 0
206
+ self.project_context["technologies"][tech] += 1
207
+
208
+ def _analyze_assistant_response(self, content: str):
209
+ """Analyze assistant response to learn solution patterns."""
210
+ # Track successful solution patterns
211
+ solution_patterns = []
212
+
213
+ if "<terminal>" in content:
214
+ solution_patterns.append("uses_terminal_commands")
215
+ if "<tool" in content:
216
+ solution_patterns.append("uses_mcp_tools")
217
+ if "```" in content:
218
+ solution_patterns.append("provides_code_examples")
219
+ if len(content) > 500:
220
+ solution_patterns.append("provides_detailed_explanations")
221
+ if any(word in content.lower() for word in ["because", "therefore", "however", "first", "next", "then"]):
222
+ solution_patterns.append("explains_reasoning")
223
+
224
+ # Add to solution history
225
+ if solution_patterns:
226
+ solution_entry = {
227
+ "timestamp": datetime.now().isoformat(),
228
+ "patterns": solution_patterns,
229
+ "content_length": len(content),
230
+ "session_id": self.session_id
231
+ }
232
+ self.solution_history.append(solution_entry)
233
+
234
+ # Keep only last 100 solutions
235
+ if len(self.solution_history) > 100:
236
+ self.solution_history = self.solution_history[-100:]
237
+
238
+ def _detect_intent(self, content: str) -> str:
239
+ """Detect user intent from message."""
240
+ content_lower = content.lower()
241
+
242
+ if any(word in content_lower for word in ["fix", "bug", "error", "broken"]):
243
+ return "debugging"
244
+ elif any(word in content_lower for word in ["create", "new", "add", "implement"]):
245
+ return "feature_development"
246
+ elif any(word in content_lower for word in ["refactor", "clean", "improve", "optimize"]):
247
+ return "refactoring"
248
+ elif any(word in content_lower for word in ["help", "how", "what", "explain"]):
249
+ return "seeking_help"
250
+ elif any(word in content_lower for word in ["test", "check", "verify"]):
251
+ return "testing"
252
+ else:
253
+ return "general_conversation"
254
+
255
+ def _get_session_context(self) -> Dict[str, Any]:
256
+ """Get current session context."""
257
+ return {
258
+ "conversation_phase": self._determine_conversation_phase(),
259
+ "message_count": self.message_count,
260
+ "session_duration": (datetime.now() - self.conversation_start_time).total_seconds(),
261
+ "recurring_themes": list(set(self.conversation_themes[-10:])) if self.conversation_themes else [],
262
+ "active_files": list(self.file_interactions.keys())[-5:] if self.file_interactions else []
263
+ }
264
+
265
+ def _determine_conversation_phase(self) -> str:
266
+ """Determine current phase of conversation."""
267
+ if self.message_count < 2:
268
+ return "initiation"
269
+ elif self.message_count < 10:
270
+ return "exploration"
271
+ elif self.message_count < 30:
272
+ return "development"
273
+ else:
274
+ return "deep_work"
275
+
276
+ def _get_project_awareness(self) -> Dict[str, Any]:
277
+ """Get project awareness context."""
278
+ return {
279
+ "project_type": self.project_context.get("type", "python_terminal_app"),
280
+ "architecture": self.project_context.get("architecture", "plugin_based"),
281
+ "recent_changes": self.project_context.get("recent_changes", []),
282
+ "known_issues": self.project_context.get("known_issues", []),
283
+ "coding_standards": self.project_context.get("coding_standards", {})
284
+ }
285
+
286
+ def _get_related_sessions(self) -> List[str]:
287
+ """Find related previous sessions."""
288
+ related = []
289
+ try:
290
+ # Look for sessions with similar themes
291
+ for session_file in self.conversations_dir.glob("session_*.jsonl"):
292
+ if session_file.name != self.session_file.name:
293
+ # Simple heuristic: sessions from same day
294
+ if session_file.name[:10] == self.session_file.name[:10]:
295
+ related.append(session_file.stem)
296
+ if len(related) >= 3:
297
+ break
298
+ except Exception as e:
299
+ logger.warning(f"Failed to find related sessions: {e}")
300
+ return related
301
+
302
+ async def log_conversation_start(self):
303
+ """Log conversation root structure with metadata."""
304
+ root_message = {
305
+ "type": "conversation_metadata",
306
+ "sessionId": self.session_id,
307
+ "startTime": self.conversation_start_time.isoformat() + "Z",
308
+ "endTime": None,
309
+ "uuid": str(uuid4()),
310
+ "timestamp": datetime.now().isoformat() + "Z",
311
+ "cwd": self._get_working_directory(),
312
+ "gitBranch": self._get_git_branch(),
313
+ "version": "1.0.0",
314
+ "conversation_context": {
315
+ "project_type": "python_terminal_app",
316
+ "active_plugins": ["llm_service", "hook_system", "conversation_logger"],
317
+ "user_profile": {
318
+ "expertise_level": "advanced",
319
+ "preferred_communication": "direct",
320
+ "coding_style": "pythonic"
321
+ },
322
+ "session_goals": [],
323
+ "conversation_summary": ""
324
+ },
325
+ "kollabor_intelligence": {
326
+ "conversation_memory": {
327
+ "related_sessions": self._get_related_sessions(),
328
+ "recurring_themes": [],
329
+ "user_patterns": self.user_patterns[:10] if self.user_patterns else []
330
+ }
331
+ }
332
+ }
333
+
334
+ await self._append_to_jsonl(root_message)
335
+ logger.info(f"Logged conversation start: {self.session_id}")
336
+
337
+ async def log_user_message(self, content: str, parent_uuid: Optional[str] = None,
338
+ user_context: Optional[Dict] = None) -> str:
339
+ """Log user message with intelligence features."""
340
+ message_uuid = str(uuid4())
341
+
342
+ message = {
343
+ "parentUuid": parent_uuid,
344
+ "isSidechain": False,
345
+ "userType": "external",
346
+ "cwd": self._get_working_directory(),
347
+ "sessionId": self.session_id,
348
+ "version": "1.0.0",
349
+ "gitBranch": self._get_git_branch(),
350
+ "type": "user",
351
+ "message": {
352
+ "role": "user",
353
+ "content": content
354
+ },
355
+ "uuid": message_uuid,
356
+ "timestamp": datetime.now().isoformat() + "Z",
357
+ "kollabor_intelligence": {
358
+ "user_context": user_context or self._analyze_user_context(content),
359
+ "session_context": self._get_session_context(),
360
+ "project_awareness": self._get_project_awareness()
361
+ }
362
+ }
363
+
364
+ await self._append_to_jsonl(message)
365
+
366
+ # Update conversation themes
367
+ intent = message["kollabor_intelligence"]["user_context"].get("detected_intent")
368
+ if intent:
369
+ self.conversation_themes.append(intent)
370
+
371
+ # Save updated conversation memory
372
+ self._save_conversation_memory()
373
+
374
+ return message_uuid
375
+
376
+ async def log_assistant_message(self, content: str, parent_uuid: str,
377
+ usage_stats: Optional[Dict] = None) -> str:
378
+ """Log assistant response with usage statistics."""
379
+ message_uuid = str(uuid4())
380
+
381
+ message = {
382
+ "parentUuid": parent_uuid,
383
+ "isSidechain": False,
384
+ "userType": "external",
385
+ "cwd": self._get_working_directory(),
386
+ "sessionId": self.session_id,
387
+ "version": "1.0.0",
388
+ "gitBranch": self._get_git_branch(),
389
+ "message": {
390
+ "id": f"msg_kollabor_{int(time.time())}",
391
+ "type": "message",
392
+ "role": "assistant",
393
+ "model": "qwen/qwen3-4b",
394
+ "content": [
395
+ {
396
+ "type": "text",
397
+ "text": content
398
+ }
399
+ ],
400
+ "stop_reason": None,
401
+ "stop_sequence": None,
402
+ "usage": usage_stats or {}
403
+ },
404
+ "requestId": f"req_kollabor_{int(time.time())}",
405
+ "type": "assistant",
406
+ "uuid": message_uuid,
407
+ "timestamp": datetime.now().isoformat() + "Z"
408
+ }
409
+
410
+ await self._append_to_jsonl(message)
411
+
412
+ # Analyze assistant response for learning
413
+ self._analyze_assistant_response(content)
414
+
415
+ # Save updated conversation memory
416
+ self._save_conversation_memory()
417
+
418
+ return message_uuid
419
+
420
+ async def log_system_message(self, content: str, parent_uuid: str,
421
+ subtype: str = "informational",
422
+ tool_use_id: Optional[str] = None) -> str:
423
+ """Log system messages including hook outputs and tool calls."""
424
+ message_uuid = str(uuid4())
425
+
426
+ message = {
427
+ "parentUuid": parent_uuid,
428
+ "isSidechain": False,
429
+ "userType": "external",
430
+ "cwd": self._get_working_directory(),
431
+ "sessionId": self.session_id,
432
+ "version": "1.0.0",
433
+ "gitBranch": self._get_git_branch(),
434
+ "type": "system",
435
+ "subtype": subtype,
436
+ "content": content,
437
+ "isMeta": False,
438
+ "timestamp": datetime.now().isoformat() + "Z",
439
+ "uuid": message_uuid,
440
+ "level": "info"
441
+ }
442
+
443
+ if tool_use_id:
444
+ message["toolUseID"] = tool_use_id
445
+
446
+ await self._append_to_jsonl(message)
447
+ return message_uuid
448
+
449
+ async def log_conversation_end(self):
450
+ """Log conversation end and save memory."""
451
+ # Update the root message with end time
452
+ # Note: In production, we'd update the first line of JSONL
453
+ # For now, append an end marker
454
+ end_message = {
455
+ "type": "conversation_end",
456
+ "sessionId": self.session_id,
457
+ "endTime": datetime.now().isoformat() + "Z",
458
+ "uuid": str(uuid4()),
459
+ "timestamp": datetime.now().isoformat() + "Z",
460
+ "summary": {
461
+ "total_messages": self.message_count,
462
+ "duration": (datetime.now() - self.conversation_start_time).total_seconds(),
463
+ "themes": list(set(self.conversation_themes)) if self.conversation_themes else [],
464
+ "files_modified": list(self.file_interactions.keys()) if self.file_interactions else []
465
+ }
466
+ }
467
+
468
+ await self._append_to_jsonl(end_message)
469
+
470
+ # Save conversation memory
471
+ self._save_conversation_memory()
472
+
473
+ logger.info(f"Logged conversation end: {self.session_id}")