claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +9 -0
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +300 -33
- claude_mpm/cli/startup_display.py +4 -2
- claude_mpm/cli/startup_migrations.py +236 -0
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/errors.py +21 -0
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +37 -26
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +47 -5
- claude_mpm/commander/chat/commands.py +44 -16
- claude_mpm/commander/chat/repl.py +1729 -82
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +91 -1
- claude_mpm/commander/frameworks/mpm.py +9 -14
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -0
- claude_mpm/commander/instance_manager.py +546 -15
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/constants.py +5 -0
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +30 -22
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logger.py +16 -2
- claude_mpm/core/logging_utils.py +40 -16
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +37 -7
- claude_mpm/core/socketio_pool.py +47 -15
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
- claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +222 -54
- claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
- claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
- claude_mpm/init.py +21 -14
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +10 -9
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/server.py +6 -1
- claude_mpm/services/pm_skills_deployer.py +5 -3
- claude_mpm/services/skills/git_skill_source_manager.py +79 -8
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +17 -1
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
"""Conversation storage with SQLite and vector support.
|
|
2
|
+
|
|
3
|
+
This module provides CRUD operations for conversations with vector embeddings
|
|
4
|
+
for semantic search. Uses SQLite with optional sqlite-vec extension.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import sqlite3
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from ..models.project import ThreadMessage
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _utc_now() -> datetime:
|
|
22
|
+
"""Return current UTC time with timezone awareness."""
|
|
23
|
+
return datetime.now(timezone.utc)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ConversationMessage:
|
|
28
|
+
"""Single message in a conversation.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
role: Message sender role ('user', 'assistant', 'system', 'tool')
|
|
32
|
+
content: Message content
|
|
33
|
+
timestamp: Message creation timestamp
|
|
34
|
+
token_count: Approximate token count (content length / 4)
|
|
35
|
+
entities: Extracted entities (files, functions, errors)
|
|
36
|
+
metadata: Additional metadata (tool name, error type, etc.)
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> msg = ConversationMessage(
|
|
40
|
+
... role="user",
|
|
41
|
+
... content="Fix the login bug in auth.py",
|
|
42
|
+
... entities=[{"type": "file", "value": "auth.py"}]
|
|
43
|
+
... )
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
role: str
|
|
47
|
+
content: str
|
|
48
|
+
timestamp: datetime = field(default_factory=_utc_now)
|
|
49
|
+
token_count: int = field(default=0)
|
|
50
|
+
entities: List[Dict[str, Any]] = field(default_factory=list)
|
|
51
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
def __post_init__(self) -> None:
|
|
54
|
+
"""Calculate token count if not provided."""
|
|
55
|
+
if self.token_count == 0:
|
|
56
|
+
# Rough approximation: 1 token ≈ 4 characters
|
|
57
|
+
self.token_count = len(self.content) // 4
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_thread_message(cls, msg: ThreadMessage) -> "ConversationMessage":
|
|
61
|
+
"""Convert ThreadMessage to ConversationMessage.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
msg: ThreadMessage from project thread
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
ConversationMessage with extracted data
|
|
68
|
+
"""
|
|
69
|
+
return cls(
|
|
70
|
+
role=msg.role,
|
|
71
|
+
content=msg.content,
|
|
72
|
+
timestamp=msg.timestamp,
|
|
73
|
+
entities=[],
|
|
74
|
+
metadata={
|
|
75
|
+
"thread_message_id": msg.id,
|
|
76
|
+
"session_id": msg.session_id,
|
|
77
|
+
"event_id": msg.event_id,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class Conversation:
|
|
84
|
+
"""Complete conversation thread.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
id: Unique conversation identifier (UUID)
|
|
88
|
+
project_id: Parent project ID
|
|
89
|
+
instance_name: Instance name (e.g., "claude-code-1")
|
|
90
|
+
session_id: Session ID from ToolSession
|
|
91
|
+
messages: List of conversation messages
|
|
92
|
+
summary: Optional compressed summary
|
|
93
|
+
embedding: Optional vector embedding for semantic search
|
|
94
|
+
created_at: Conversation creation timestamp
|
|
95
|
+
updated_at: Last update timestamp
|
|
96
|
+
metadata: Additional metadata (framework, version, etc.)
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
>>> conversation = Conversation(
|
|
100
|
+
... id="conv-abc123",
|
|
101
|
+
... project_id="proj-xyz",
|
|
102
|
+
... instance_name="claude-code-1",
|
|
103
|
+
... session_id="sess-123",
|
|
104
|
+
... messages=[msg1, msg2, msg3]
|
|
105
|
+
... )
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
id: str
|
|
109
|
+
project_id: str
|
|
110
|
+
instance_name: str
|
|
111
|
+
session_id: str
|
|
112
|
+
messages: List[ConversationMessage] = field(default_factory=list)
|
|
113
|
+
summary: Optional[str] = None
|
|
114
|
+
embedding: Optional[List[float]] = None
|
|
115
|
+
created_at: datetime = field(default_factory=_utc_now)
|
|
116
|
+
updated_at: datetime = field(default_factory=_utc_now)
|
|
117
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def total_tokens(self) -> int:
|
|
121
|
+
"""Calculate total token count across all messages."""
|
|
122
|
+
return sum(msg.token_count for msg in self.messages)
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def message_count(self) -> int:
|
|
126
|
+
"""Return number of messages in conversation."""
|
|
127
|
+
return len(self.messages)
|
|
128
|
+
|
|
129
|
+
def get_full_text(self) -> str:
|
|
130
|
+
"""Get full conversation as text.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Formatted conversation text with role prefixes
|
|
134
|
+
"""
|
|
135
|
+
lines = []
|
|
136
|
+
for msg in self.messages:
|
|
137
|
+
lines.append(f"{msg.role.upper()}: {msg.content}")
|
|
138
|
+
return "\n\n".join(lines)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ConversationStore:
|
|
142
|
+
"""Persists conversations to SQLite with vector support.
|
|
143
|
+
|
|
144
|
+
Provides CRUD operations and vector search capabilities using
|
|
145
|
+
SQLite with optional sqlite-vec extension.
|
|
146
|
+
|
|
147
|
+
Attributes:
|
|
148
|
+
db_path: Path to SQLite database file
|
|
149
|
+
enable_vector: Whether to enable vector extension
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
>>> store = ConversationStore()
|
|
153
|
+
>>> await store.save(conversation)
|
|
154
|
+
>>> results = await store.search("login bug", limit=5)
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
db_path: Optional[Path] = None,
|
|
160
|
+
enable_vector: bool = True,
|
|
161
|
+
):
|
|
162
|
+
"""Initialize conversation store.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
db_path: Path to database file (default: ~/.claude-mpm/commander/conversations.db)
|
|
166
|
+
enable_vector: Enable vector extension for semantic search
|
|
167
|
+
"""
|
|
168
|
+
if db_path is None:
|
|
169
|
+
db_path = Path("~/.claude-mpm/commander/conversations.db").expanduser()
|
|
170
|
+
|
|
171
|
+
self.db_path = db_path
|
|
172
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
self.enable_vector = enable_vector
|
|
174
|
+
|
|
175
|
+
# Initialize schema
|
|
176
|
+
asyncio.create_task(self._init_schema())
|
|
177
|
+
|
|
178
|
+
logger.info(
|
|
179
|
+
"ConversationStore initialized at %s (vector: %s)",
|
|
180
|
+
self.db_path,
|
|
181
|
+
enable_vector,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def _init_schema(self) -> None:
|
|
185
|
+
"""Initialize database schema.
|
|
186
|
+
|
|
187
|
+
Creates tables for conversations and messages if they don't exist.
|
|
188
|
+
Optionally loads sqlite-vec extension for vector operations.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def _create_schema(conn: sqlite3.Connection) -> None:
|
|
192
|
+
"""Create schema in sync context."""
|
|
193
|
+
# Try to load sqlite-vec extension
|
|
194
|
+
if self.enable_vector:
|
|
195
|
+
try:
|
|
196
|
+
conn.enable_load_extension(True)
|
|
197
|
+
# Try common locations for sqlite-vec
|
|
198
|
+
# Users need to install: pip install sqlite-vec
|
|
199
|
+
try:
|
|
200
|
+
conn.load_extension("vec0")
|
|
201
|
+
logger.info("Loaded sqlite-vec extension")
|
|
202
|
+
except sqlite3.OperationalError:
|
|
203
|
+
logger.warning(
|
|
204
|
+
"sqlite-vec extension not found. "
|
|
205
|
+
"Install with: pip install sqlite-vec. "
|
|
206
|
+
"Falling back to non-vector search."
|
|
207
|
+
)
|
|
208
|
+
self.enable_vector = False
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.warning("Cannot load extensions: %s", e)
|
|
211
|
+
self.enable_vector = False
|
|
212
|
+
|
|
213
|
+
# Create conversations table
|
|
214
|
+
conn.execute(
|
|
215
|
+
"""
|
|
216
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
217
|
+
id TEXT PRIMARY KEY,
|
|
218
|
+
project_id TEXT NOT NULL,
|
|
219
|
+
instance_name TEXT NOT NULL,
|
|
220
|
+
session_id TEXT NOT NULL,
|
|
221
|
+
summary TEXT,
|
|
222
|
+
created_at TEXT NOT NULL,
|
|
223
|
+
updated_at TEXT NOT NULL,
|
|
224
|
+
metadata TEXT,
|
|
225
|
+
UNIQUE(session_id)
|
|
226
|
+
)
|
|
227
|
+
"""
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Create messages table
|
|
231
|
+
conn.execute(
|
|
232
|
+
"""
|
|
233
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
234
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
235
|
+
conversation_id TEXT NOT NULL,
|
|
236
|
+
role TEXT NOT NULL,
|
|
237
|
+
content TEXT NOT NULL,
|
|
238
|
+
timestamp TEXT NOT NULL,
|
|
239
|
+
token_count INTEGER NOT NULL,
|
|
240
|
+
entities TEXT,
|
|
241
|
+
metadata TEXT,
|
|
242
|
+
FOREIGN KEY(conversation_id) REFERENCES conversations(id)
|
|
243
|
+
)
|
|
244
|
+
"""
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Create embeddings table (if vector enabled)
|
|
248
|
+
if self.enable_vector:
|
|
249
|
+
conn.execute(
|
|
250
|
+
"""
|
|
251
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS conversation_embeddings
|
|
252
|
+
USING vec0(
|
|
253
|
+
conversation_id TEXT PRIMARY KEY,
|
|
254
|
+
embedding FLOAT[384]
|
|
255
|
+
)
|
|
256
|
+
"""
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Create indexes
|
|
260
|
+
conn.execute(
|
|
261
|
+
"CREATE INDEX IF NOT EXISTS idx_conversations_project "
|
|
262
|
+
"ON conversations(project_id)"
|
|
263
|
+
)
|
|
264
|
+
conn.execute(
|
|
265
|
+
"CREATE INDEX IF NOT EXISTS idx_conversations_session "
|
|
266
|
+
"ON conversations(session_id)"
|
|
267
|
+
)
|
|
268
|
+
conn.execute(
|
|
269
|
+
"CREATE INDEX IF NOT EXISTS idx_messages_conversation "
|
|
270
|
+
"ON messages(conversation_id)"
|
|
271
|
+
)
|
|
272
|
+
conn.execute(
|
|
273
|
+
"CREATE INDEX IF NOT EXISTS idx_conversations_updated "
|
|
274
|
+
"ON conversations(updated_at)"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
conn.commit()
|
|
278
|
+
|
|
279
|
+
# Run in executor to avoid blocking
|
|
280
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
281
|
+
None,
|
|
282
|
+
lambda: self._execute_sync(_create_schema),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _execute_sync(self, func: Any) -> Any:
|
|
286
|
+
"""Execute synchronous database operation.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
func: Function that takes connection and executes queries
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Result from func
|
|
293
|
+
"""
|
|
294
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
295
|
+
try:
|
|
296
|
+
return func(conn)
|
|
297
|
+
finally:
|
|
298
|
+
conn.close()
|
|
299
|
+
|
|
300
|
+
async def save(self, conversation: Conversation) -> None:
|
|
301
|
+
"""Save conversation to database.
|
|
302
|
+
|
|
303
|
+
Updates existing conversation or inserts new one.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
conversation: Conversation to persist
|
|
307
|
+
|
|
308
|
+
Example:
|
|
309
|
+
>>> await store.save(conversation)
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
def _save(conn: sqlite3.Connection) -> None:
|
|
313
|
+
"""Save in sync context."""
|
|
314
|
+
# Upsert conversation
|
|
315
|
+
conn.execute(
|
|
316
|
+
"""
|
|
317
|
+
INSERT OR REPLACE INTO conversations
|
|
318
|
+
(id, project_id, instance_name, session_id, summary,
|
|
319
|
+
created_at, updated_at, metadata)
|
|
320
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
321
|
+
""",
|
|
322
|
+
(
|
|
323
|
+
conversation.id,
|
|
324
|
+
conversation.project_id,
|
|
325
|
+
conversation.instance_name,
|
|
326
|
+
conversation.session_id,
|
|
327
|
+
conversation.summary,
|
|
328
|
+
conversation.created_at.isoformat(),
|
|
329
|
+
conversation.updated_at.isoformat(),
|
|
330
|
+
json.dumps(conversation.metadata),
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Delete old messages
|
|
335
|
+
conn.execute(
|
|
336
|
+
"DELETE FROM messages WHERE conversation_id = ?",
|
|
337
|
+
(conversation.id,),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Insert messages
|
|
341
|
+
for msg in conversation.messages:
|
|
342
|
+
conn.execute(
|
|
343
|
+
"""
|
|
344
|
+
INSERT INTO messages
|
|
345
|
+
(conversation_id, role, content, timestamp,
|
|
346
|
+
token_count, entities, metadata)
|
|
347
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
348
|
+
""",
|
|
349
|
+
(
|
|
350
|
+
conversation.id,
|
|
351
|
+
msg.role,
|
|
352
|
+
msg.content,
|
|
353
|
+
msg.timestamp.isoformat(),
|
|
354
|
+
msg.token_count,
|
|
355
|
+
json.dumps(msg.entities),
|
|
356
|
+
json.dumps(msg.metadata),
|
|
357
|
+
),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Save embedding if available
|
|
361
|
+
if self.enable_vector and conversation.embedding:
|
|
362
|
+
conn.execute(
|
|
363
|
+
"""
|
|
364
|
+
INSERT OR REPLACE INTO conversation_embeddings
|
|
365
|
+
(conversation_id, embedding)
|
|
366
|
+
VALUES (?, ?)
|
|
367
|
+
""",
|
|
368
|
+
(conversation.id, json.dumps(conversation.embedding)),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
conn.commit()
|
|
372
|
+
|
|
373
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
374
|
+
None, lambda: self._execute_sync(_save)
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
logger.debug(
|
|
378
|
+
"Saved conversation %s (%d messages)",
|
|
379
|
+
conversation.id,
|
|
380
|
+
len(conversation.messages),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
async def load(self, conversation_id: str) -> Optional[Conversation]:
|
|
384
|
+
"""Load conversation by ID.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
conversation_id: Conversation ID to load
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Conversation if found, None otherwise
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
>>> conv = await store.load("conv-abc123")
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
def _load(conn: sqlite3.Connection) -> Optional[Conversation]:
|
|
397
|
+
"""Load in sync context."""
|
|
398
|
+
# Load conversation
|
|
399
|
+
cursor = conn.execute(
|
|
400
|
+
"""
|
|
401
|
+
SELECT id, project_id, instance_name, session_id, summary,
|
|
402
|
+
created_at, updated_at, metadata
|
|
403
|
+
FROM conversations
|
|
404
|
+
WHERE id = ?
|
|
405
|
+
""",
|
|
406
|
+
(conversation_id,),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
row = cursor.fetchone()
|
|
410
|
+
if not row:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
# Load messages
|
|
414
|
+
messages_cursor = conn.execute(
|
|
415
|
+
"""
|
|
416
|
+
SELECT role, content, timestamp, token_count, entities, metadata
|
|
417
|
+
FROM messages
|
|
418
|
+
WHERE conversation_id = ?
|
|
419
|
+
ORDER BY timestamp ASC
|
|
420
|
+
""",
|
|
421
|
+
(conversation_id,),
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
messages = []
|
|
425
|
+
for msg_row in messages_cursor.fetchall():
|
|
426
|
+
messages.append(
|
|
427
|
+
ConversationMessage(
|
|
428
|
+
role=msg_row[0],
|
|
429
|
+
content=msg_row[1],
|
|
430
|
+
timestamp=datetime.fromisoformat(msg_row[2]),
|
|
431
|
+
token_count=msg_row[3],
|
|
432
|
+
entities=json.loads(msg_row[4]) if msg_row[4] else [],
|
|
433
|
+
metadata=json.loads(msg_row[5]) if msg_row[5] else {},
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Load embedding if available
|
|
438
|
+
embedding = None
|
|
439
|
+
if self.enable_vector:
|
|
440
|
+
emb_cursor = conn.execute(
|
|
441
|
+
"SELECT embedding FROM conversation_embeddings WHERE conversation_id = ?",
|
|
442
|
+
(conversation_id,),
|
|
443
|
+
)
|
|
444
|
+
emb_row = emb_cursor.fetchone()
|
|
445
|
+
if emb_row:
|
|
446
|
+
embedding = json.loads(emb_row[0])
|
|
447
|
+
|
|
448
|
+
return Conversation(
|
|
449
|
+
id=row[0],
|
|
450
|
+
project_id=row[1],
|
|
451
|
+
instance_name=row[2],
|
|
452
|
+
session_id=row[3],
|
|
453
|
+
summary=row[4],
|
|
454
|
+
created_at=datetime.fromisoformat(row[5]),
|
|
455
|
+
updated_at=datetime.fromisoformat(row[6]),
|
|
456
|
+
metadata=json.loads(row[7]) if row[7] else {},
|
|
457
|
+
messages=messages,
|
|
458
|
+
embedding=embedding,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
return await asyncio.get_event_loop().run_in_executor(
|
|
462
|
+
None, lambda: self._execute_sync(_load)
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
async def list_by_project(
|
|
466
|
+
self,
|
|
467
|
+
project_id: str,
|
|
468
|
+
limit: Optional[int] = None,
|
|
469
|
+
) -> List[Conversation]:
|
|
470
|
+
"""List conversations for a project.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
project_id: Project ID to filter by
|
|
474
|
+
limit: Maximum number of results
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
List of conversations ordered by updated_at descending
|
|
478
|
+
|
|
479
|
+
Example:
|
|
480
|
+
>>> conversations = await store.list_by_project("proj-xyz", limit=10)
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
def _list(conn: sqlite3.Connection) -> List[Conversation]:
|
|
484
|
+
"""List in sync context."""
|
|
485
|
+
query = """
|
|
486
|
+
SELECT id FROM conversations
|
|
487
|
+
WHERE project_id = ?
|
|
488
|
+
ORDER BY updated_at DESC
|
|
489
|
+
"""
|
|
490
|
+
if limit:
|
|
491
|
+
query += f" LIMIT {limit}"
|
|
492
|
+
|
|
493
|
+
cursor = conn.execute(query, (project_id,))
|
|
494
|
+
conversation_ids = [row[0] for row in cursor.fetchall()]
|
|
495
|
+
|
|
496
|
+
# Load full conversations
|
|
497
|
+
conversations = []
|
|
498
|
+
for conv_id in conversation_ids:
|
|
499
|
+
conv = self._execute_sync(lambda c: self._load_conversation(c, conv_id))
|
|
500
|
+
if conv:
|
|
501
|
+
conversations.append(conv)
|
|
502
|
+
|
|
503
|
+
return conversations
|
|
504
|
+
|
|
505
|
+
return await asyncio.get_event_loop().run_in_executor(
|
|
506
|
+
None, lambda: self._execute_sync(_list)
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
def _load_conversation(
|
|
510
|
+
self, conn: sqlite3.Connection, conversation_id: str
|
|
511
|
+
) -> Optional[Conversation]:
|
|
512
|
+
"""Load conversation in sync context (helper for list operations)."""
|
|
513
|
+
cursor = conn.execute(
|
|
514
|
+
"""
|
|
515
|
+
SELECT id, project_id, instance_name, session_id, summary,
|
|
516
|
+
created_at, updated_at, metadata
|
|
517
|
+
FROM conversations
|
|
518
|
+
WHERE id = ?
|
|
519
|
+
""",
|
|
520
|
+
(conversation_id,),
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
row = cursor.fetchone()
|
|
524
|
+
if not row:
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
# Load messages
|
|
528
|
+
messages_cursor = conn.execute(
|
|
529
|
+
"""
|
|
530
|
+
SELECT role, content, timestamp, token_count, entities, metadata
|
|
531
|
+
FROM messages
|
|
532
|
+
WHERE conversation_id = ?
|
|
533
|
+
ORDER BY timestamp ASC
|
|
534
|
+
""",
|
|
535
|
+
(conversation_id,),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
messages = []
|
|
539
|
+
for msg_row in messages_cursor.fetchall():
|
|
540
|
+
messages.append(
|
|
541
|
+
ConversationMessage(
|
|
542
|
+
role=msg_row[0],
|
|
543
|
+
content=msg_row[1],
|
|
544
|
+
timestamp=datetime.fromisoformat(msg_row[2]),
|
|
545
|
+
token_count=msg_row[3],
|
|
546
|
+
entities=json.loads(msg_row[4]) if msg_row[4] else [],
|
|
547
|
+
metadata=json.loads(msg_row[5]) if msg_row[5] else {},
|
|
548
|
+
)
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
return Conversation(
|
|
552
|
+
id=row[0],
|
|
553
|
+
project_id=row[1],
|
|
554
|
+
instance_name=row[2],
|
|
555
|
+
session_id=row[3],
|
|
556
|
+
summary=row[4],
|
|
557
|
+
created_at=datetime.fromisoformat(row[5]),
|
|
558
|
+
updated_at=datetime.fromisoformat(row[6]),
|
|
559
|
+
metadata=json.loads(row[7]) if row[7] else {},
|
|
560
|
+
messages=messages,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
async def search_by_text(
|
|
564
|
+
self,
|
|
565
|
+
query: str,
|
|
566
|
+
project_id: Optional[str] = None,
|
|
567
|
+
limit: int = 10,
|
|
568
|
+
) -> List[Conversation]:
|
|
569
|
+
"""Search conversations by text (fallback when vectors unavailable).
|
|
570
|
+
|
|
571
|
+
Uses SQLite FTS5 full-text search on conversation content.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
query: Search query
|
|
575
|
+
project_id: Optional project ID filter
|
|
576
|
+
limit: Maximum results
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
List of matching conversations
|
|
580
|
+
|
|
581
|
+
Example:
|
|
582
|
+
>>> results = await store.search_by_text("login bug fix")
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
def _search(conn: sqlite3.Connection) -> List[str]:
|
|
586
|
+
"""Search in sync context."""
|
|
587
|
+
# Simple LIKE search (can be improved with FTS5)
|
|
588
|
+
if project_id:
|
|
589
|
+
cursor = conn.execute(
|
|
590
|
+
"""
|
|
591
|
+
SELECT DISTINCT c.id
|
|
592
|
+
FROM conversations c
|
|
593
|
+
JOIN messages m ON c.id = m.conversation_id
|
|
594
|
+
WHERE c.project_id = ? AND (
|
|
595
|
+
c.summary LIKE ? OR m.content LIKE ?
|
|
596
|
+
)
|
|
597
|
+
ORDER BY c.updated_at DESC
|
|
598
|
+
LIMIT ?
|
|
599
|
+
""",
|
|
600
|
+
(project_id, f"%{query}%", f"%{query}%", limit),
|
|
601
|
+
)
|
|
602
|
+
else:
|
|
603
|
+
cursor = conn.execute(
|
|
604
|
+
"""
|
|
605
|
+
SELECT DISTINCT c.id
|
|
606
|
+
FROM conversations c
|
|
607
|
+
JOIN messages m ON c.id = m.conversation_id
|
|
608
|
+
WHERE c.summary LIKE ? OR m.content LIKE ?
|
|
609
|
+
ORDER BY c.updated_at DESC
|
|
610
|
+
LIMIT ?
|
|
611
|
+
""",
|
|
612
|
+
(f"%{query}%", f"%{query}%", limit),
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
return [row[0] for row in cursor.fetchall()]
|
|
616
|
+
|
|
617
|
+
conversation_ids = await asyncio.get_event_loop().run_in_executor(
|
|
618
|
+
None, lambda: self._execute_sync(_search)
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Load full conversations
|
|
622
|
+
conversations = []
|
|
623
|
+
for conv_id in conversation_ids:
|
|
624
|
+
conv = await self.load(conv_id)
|
|
625
|
+
if conv:
|
|
626
|
+
conversations.append(conv)
|
|
627
|
+
|
|
628
|
+
return conversations
|
|
629
|
+
|
|
630
|
+
async def delete(self, conversation_id: str) -> None:
|
|
631
|
+
"""Delete conversation and all messages.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
conversation_id: Conversation ID to delete
|
|
635
|
+
|
|
636
|
+
Example:
|
|
637
|
+
>>> await store.delete("conv-abc123")
|
|
638
|
+
"""
|
|
639
|
+
|
|
640
|
+
def _delete(conn: sqlite3.Connection) -> None:
|
|
641
|
+
"""Delete in sync context."""
|
|
642
|
+
conn.execute(
|
|
643
|
+
"DELETE FROM messages WHERE conversation_id = ?", (conversation_id,)
|
|
644
|
+
)
|
|
645
|
+
conn.execute("DELETE FROM conversations WHERE id = ?", (conversation_id,))
|
|
646
|
+
if self.enable_vector:
|
|
647
|
+
conn.execute(
|
|
648
|
+
"DELETE FROM conversation_embeddings WHERE conversation_id = ?",
|
|
649
|
+
(conversation_id,),
|
|
650
|
+
)
|
|
651
|
+
conn.commit()
|
|
652
|
+
|
|
653
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
654
|
+
None, lambda: self._execute_sync(_delete)
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
logger.debug("Deleted conversation %s", conversation_id)
|
|
@@ -26,6 +26,9 @@ class EventType(Enum):
|
|
|
26
26
|
MILESTONE = "milestone" # Significant progress
|
|
27
27
|
STATUS = "status" # General update
|
|
28
28
|
PROJECT_IDLE = "project_idle" # Project has no work
|
|
29
|
+
INSTANCE_STARTING = "instance_starting" # Instance is starting up
|
|
30
|
+
INSTANCE_READY = "instance_ready" # Instance is ready for work
|
|
31
|
+
INSTANCE_ERROR = "instance_error" # Instance encountered an error
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
class EventPriority(Enum):
|
|
@@ -62,6 +65,9 @@ DEFAULT_PRIORITIES: Dict[EventType, EventPriority] = {
|
|
|
62
65
|
EventType.MILESTONE: EventPriority.LOW,
|
|
63
66
|
EventType.STATUS: EventPriority.INFO,
|
|
64
67
|
EventType.PROJECT_IDLE: EventPriority.INFO,
|
|
68
|
+
EventType.INSTANCE_STARTING: EventPriority.INFO,
|
|
69
|
+
EventType.INSTANCE_READY: EventPriority.INFO,
|
|
70
|
+
EventType.INSTANCE_ERROR: EventPriority.HIGH,
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
|