minion-code 0.1.0__py3-none-any.whl → 0.1.2__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.
- examples/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.2.dist-info/METADATA +476 -0
- minion_code-0.1.2.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
- minion_code-0.1.2.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Session Manager for Web API.
|
|
5
|
+
|
|
6
|
+
Manages web sessions, agent instances, and the relationship between them.
|
|
7
|
+
Supports two history modes:
|
|
8
|
+
- full: Each request creates new Agent, loads full history (stateless, scalable)
|
|
9
|
+
- incremental: Reuse Agent, only send new message (stateful, low latency)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, Optional, Literal, List, Any
|
|
19
|
+
|
|
20
|
+
from ..adapters.web_adapter import WebOutputAdapter, TaskState
|
|
21
|
+
from minion_code.utils.session_storage import (
|
|
22
|
+
Session,
|
|
23
|
+
SessionStorage,
|
|
24
|
+
SessionMessage,
|
|
25
|
+
create_session,
|
|
26
|
+
load_session,
|
|
27
|
+
save_session,
|
|
28
|
+
add_message,
|
|
29
|
+
restore_agent_history,
|
|
30
|
+
)
|
|
31
|
+
from minion_code.agents.hooks import (
|
|
32
|
+
HookConfig,
|
|
33
|
+
HookMatcher,
|
|
34
|
+
create_confirm_writes_hook,
|
|
35
|
+
create_dangerous_command_check_hook,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
HistoryMode = Literal["full", "incremental"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class WebSession:
|
|
45
|
+
"""Web session containing agent and adapter."""
|
|
46
|
+
|
|
47
|
+
session_id: str
|
|
48
|
+
project_path: str
|
|
49
|
+
adapter: WebOutputAdapter
|
|
50
|
+
created_at: float = field(default_factory=time.time)
|
|
51
|
+
last_activity: float = field(default_factory=time.time)
|
|
52
|
+
history_mode: HistoryMode = "full"
|
|
53
|
+
|
|
54
|
+
# Agent instance (only used in incremental mode)
|
|
55
|
+
_agent: Optional[Any] = field(default=None, repr=False)
|
|
56
|
+
|
|
57
|
+
# Abort event for cancelling current task
|
|
58
|
+
abort_event: asyncio.Event = field(default_factory=asyncio.Event)
|
|
59
|
+
|
|
60
|
+
# Current task ID
|
|
61
|
+
current_task_id: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
# Session storage (persisted)
|
|
64
|
+
_storage_session: Optional[Session] = field(default=None, repr=False)
|
|
65
|
+
|
|
66
|
+
def update_activity(self):
|
|
67
|
+
"""Update last activity timestamp."""
|
|
68
|
+
self.last_activity = time.time()
|
|
69
|
+
|
|
70
|
+
def generate_task_id(self) -> str:
|
|
71
|
+
"""Generate unique task ID."""
|
|
72
|
+
return f"task_{self.session_id}_{int(time.time() * 1000)}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SessionManager:
|
|
76
|
+
"""
|
|
77
|
+
Web session manager.
|
|
78
|
+
|
|
79
|
+
Manages the lifecycle of web sessions and their associated resources.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
max_sessions: int = 100,
|
|
85
|
+
session_timeout: int = 3600, # 1 hour
|
|
86
|
+
default_history_mode: HistoryMode = "full",
|
|
87
|
+
):
|
|
88
|
+
"""
|
|
89
|
+
Initialize session manager.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
max_sessions: Maximum number of concurrent sessions
|
|
93
|
+
session_timeout: Session timeout in seconds
|
|
94
|
+
default_history_mode: Default history mode for new sessions
|
|
95
|
+
"""
|
|
96
|
+
self.max_sessions = max_sessions
|
|
97
|
+
self.session_timeout = session_timeout
|
|
98
|
+
self.default_history_mode = default_history_mode
|
|
99
|
+
|
|
100
|
+
self.sessions: Dict[str, WebSession] = {}
|
|
101
|
+
self.storage = SessionStorage()
|
|
102
|
+
self._lock = asyncio.Lock()
|
|
103
|
+
|
|
104
|
+
async def create_session(
|
|
105
|
+
self, project_path: str = ".", history_mode: Optional[HistoryMode] = None
|
|
106
|
+
) -> WebSession:
|
|
107
|
+
"""
|
|
108
|
+
Create a new web session.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
project_path: Working directory for the session
|
|
112
|
+
history_mode: History mode ('full' or 'incremental')
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
New WebSession instance
|
|
116
|
+
"""
|
|
117
|
+
async with self._lock:
|
|
118
|
+
# Cleanup expired sessions
|
|
119
|
+
await self._cleanup_expired_sessions()
|
|
120
|
+
|
|
121
|
+
if len(self.sessions) >= self.max_sessions:
|
|
122
|
+
raise Exception("Maximum sessions reached")
|
|
123
|
+
|
|
124
|
+
# Generate session ID
|
|
125
|
+
session_id = str(uuid.uuid4())[:8]
|
|
126
|
+
|
|
127
|
+
# Create adapter
|
|
128
|
+
adapter = WebOutputAdapter(session_id=session_id)
|
|
129
|
+
|
|
130
|
+
# Create web session
|
|
131
|
+
session = WebSession(
|
|
132
|
+
session_id=session_id,
|
|
133
|
+
project_path=str(Path(project_path).resolve()),
|
|
134
|
+
adapter=adapter,
|
|
135
|
+
history_mode=history_mode or self.default_history_mode,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Create persistent storage session
|
|
139
|
+
storage_session = self.storage.create_session(session.project_path)
|
|
140
|
+
# Override session_id to match web session
|
|
141
|
+
storage_session.metadata.session_id = session_id
|
|
142
|
+
session._storage_session = storage_session
|
|
143
|
+
|
|
144
|
+
self.sessions[session_id] = session
|
|
145
|
+
logger.info(
|
|
146
|
+
f"Created session {session_id} with history_mode={session.history_mode}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return session
|
|
150
|
+
|
|
151
|
+
async def get_session(self, session_id: str) -> Optional[WebSession]:
|
|
152
|
+
"""
|
|
153
|
+
Get a session by ID.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
session_id: Session identifier
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
WebSession if found, None otherwise
|
|
160
|
+
"""
|
|
161
|
+
session = self.sessions.get(session_id)
|
|
162
|
+
if session:
|
|
163
|
+
session.update_activity()
|
|
164
|
+
return session
|
|
165
|
+
|
|
166
|
+
async def get_or_create_session(
|
|
167
|
+
self,
|
|
168
|
+
session_id: Optional[str] = None,
|
|
169
|
+
project_path: str = ".",
|
|
170
|
+
history_mode: Optional[HistoryMode] = None,
|
|
171
|
+
) -> WebSession:
|
|
172
|
+
"""
|
|
173
|
+
Get existing session or create new one.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
session_id: Optional session ID to look up
|
|
177
|
+
project_path: Working directory for new session
|
|
178
|
+
history_mode: History mode for new session
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
WebSession instance
|
|
182
|
+
"""
|
|
183
|
+
if session_id:
|
|
184
|
+
session = await self.get_session(session_id)
|
|
185
|
+
if session:
|
|
186
|
+
return session
|
|
187
|
+
|
|
188
|
+
return await self.create_session(project_path, history_mode)
|
|
189
|
+
|
|
190
|
+
async def delete_session(self, session_id: str) -> bool:
|
|
191
|
+
"""
|
|
192
|
+
Delete a session.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
session_id: Session identifier
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
True if session was deleted, False if not found
|
|
199
|
+
"""
|
|
200
|
+
async with self._lock:
|
|
201
|
+
session = self.sessions.pop(session_id, None)
|
|
202
|
+
if session:
|
|
203
|
+
# Abort any running task
|
|
204
|
+
session.abort_event.set()
|
|
205
|
+
logger.info(f"Deleted session {session_id}")
|
|
206
|
+
return True
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
def find_session_by_interaction(self, interaction_id: str) -> Optional[WebSession]:
|
|
210
|
+
"""
|
|
211
|
+
Find session containing a pending interaction.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
interaction_id: Interaction identifier
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
WebSession if found, None otherwise
|
|
218
|
+
"""
|
|
219
|
+
for session in self.sessions.values():
|
|
220
|
+
if session.adapter.get_pending_interaction(interaction_id):
|
|
221
|
+
return session
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def _create_hooks_for_session(self, session: WebSession) -> HookConfig:
|
|
225
|
+
"""
|
|
226
|
+
Create hook configuration for a session.
|
|
227
|
+
|
|
228
|
+
Configures:
|
|
229
|
+
- Dangerous command blocking for bash
|
|
230
|
+
- User confirmation for write operations via adapter.confirm()
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
session: WebSession with adapter for confirmations
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
HookConfig for the session
|
|
237
|
+
"""
|
|
238
|
+
return HookConfig(
|
|
239
|
+
pre_tool_use=[
|
|
240
|
+
# Block dangerous bash commands
|
|
241
|
+
HookMatcher("bash", create_dangerous_command_check_hook()),
|
|
242
|
+
# Confirm non-readonly tools via WebOutputAdapter
|
|
243
|
+
HookMatcher("*", create_confirm_writes_hook(session.adapter)),
|
|
244
|
+
]
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def _create_compaction_callback(self, session: WebSession):
|
|
248
|
+
"""
|
|
249
|
+
Create a callback function for syncing compacted history to storage.
|
|
250
|
+
|
|
251
|
+
When BaseAgent compacts history, this callback updates session storage
|
|
252
|
+
with the compacted agent_history.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
session: WebSession to sync compacted history for
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Callback function for on_compaction hook
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def sync_compaction(compacted_messages: List[Dict[str, Any]]):
|
|
262
|
+
if session._storage_session:
|
|
263
|
+
session._storage_session.agent_history = compacted_messages
|
|
264
|
+
session._storage_session.compaction_count += 1
|
|
265
|
+
save_session(session._storage_session)
|
|
266
|
+
logger.info(
|
|
267
|
+
f"Session {session.session_id}: Synced compacted history "
|
|
268
|
+
f"({len(compacted_messages)} messages, "
|
|
269
|
+
f"compaction #{session._storage_session.compaction_count})"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return sync_compaction
|
|
273
|
+
|
|
274
|
+
async def get_or_create_agent(self, session: WebSession):
|
|
275
|
+
"""
|
|
276
|
+
Get or create agent for session based on history mode.
|
|
277
|
+
|
|
278
|
+
For 'full' mode: Creates new agent, restores history from storage
|
|
279
|
+
For 'incremental' mode: Reuses existing agent or creates new one
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
session: WebSession to get agent for
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
MinionCodeAgent instance
|
|
286
|
+
"""
|
|
287
|
+
from minion_code.agents import MinionCodeAgent
|
|
288
|
+
|
|
289
|
+
# Create hooks for permission control
|
|
290
|
+
hooks = self._create_hooks_for_session(session)
|
|
291
|
+
|
|
292
|
+
if session.history_mode == "full":
|
|
293
|
+
# Full mode: Always create new agent and restore history
|
|
294
|
+
agent = await MinionCodeAgent.create(
|
|
295
|
+
name=f"WebAgent-{session.session_id}",
|
|
296
|
+
llm="sonnet",
|
|
297
|
+
workdir=session.project_path,
|
|
298
|
+
hooks=hooks,
|
|
299
|
+
# History decay: save large outputs to file after N steps
|
|
300
|
+
decay_enabled=True,
|
|
301
|
+
decay_ttl_steps=3,
|
|
302
|
+
decay_min_size=100_000, # 100KB
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Inject on_compaction callback to sync compacted history
|
|
306
|
+
agent.on_compaction = self._create_compaction_callback(session)
|
|
307
|
+
|
|
308
|
+
# Restore history from storage (prefers agent_history if available)
|
|
309
|
+
if session._storage_session:
|
|
310
|
+
restore_agent_history(agent, session._storage_session, verbose=False)
|
|
311
|
+
|
|
312
|
+
return agent
|
|
313
|
+
|
|
314
|
+
else: # incremental mode
|
|
315
|
+
# Incremental mode: Reuse agent if available
|
|
316
|
+
if session._agent is None:
|
|
317
|
+
session._agent = await MinionCodeAgent.create(
|
|
318
|
+
name=f"WebAgent-{session.session_id}",
|
|
319
|
+
llm="sonnet",
|
|
320
|
+
workdir=session.project_path,
|
|
321
|
+
hooks=hooks,
|
|
322
|
+
# History decay: save large outputs to file after N steps
|
|
323
|
+
decay_enabled=True,
|
|
324
|
+
decay_ttl_steps=3,
|
|
325
|
+
decay_min_size=100_000, # 100KB
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Inject on_compaction callback to sync compacted history
|
|
329
|
+
session._agent.on_compaction = self._create_compaction_callback(session)
|
|
330
|
+
|
|
331
|
+
# Restore history on first creation (prefers agent_history if available)
|
|
332
|
+
if session._storage_session:
|
|
333
|
+
restore_agent_history(
|
|
334
|
+
session._agent, session._storage_session, verbose=False
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return session._agent
|
|
338
|
+
|
|
339
|
+
def save_message(self, session: WebSession, role: str, content: str):
|
|
340
|
+
"""
|
|
341
|
+
Save a message to session storage.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
session: WebSession to save message for
|
|
345
|
+
role: 'user' or 'assistant'
|
|
346
|
+
content: Message content
|
|
347
|
+
"""
|
|
348
|
+
if session._storage_session:
|
|
349
|
+
add_message(session._storage_session, role, content, auto_save=True)
|
|
350
|
+
|
|
351
|
+
def get_messages(self, session: WebSession) -> List[Dict[str, str]]:
|
|
352
|
+
"""
|
|
353
|
+
Get all messages from session storage.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
session: WebSession to get messages for
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
List of message dicts with 'role' and 'content'
|
|
360
|
+
"""
|
|
361
|
+
if session._storage_session:
|
|
362
|
+
return [
|
|
363
|
+
{"role": msg.role, "content": msg.content}
|
|
364
|
+
for msg in session._storage_session.messages
|
|
365
|
+
]
|
|
366
|
+
return []
|
|
367
|
+
|
|
368
|
+
async def abort_task(self, session_id: str) -> bool:
|
|
369
|
+
"""
|
|
370
|
+
Abort the current task in a session.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
session_id: Session identifier
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
True if abort signal was sent, False if session not found
|
|
377
|
+
"""
|
|
378
|
+
session = self.sessions.get(session_id)
|
|
379
|
+
if session:
|
|
380
|
+
session.abort_event.set()
|
|
381
|
+
# Reset for next task
|
|
382
|
+
session.abort_event = asyncio.Event()
|
|
383
|
+
logger.info(f"Aborted task in session {session_id}")
|
|
384
|
+
return True
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
async def _cleanup_expired_sessions(self):
|
|
388
|
+
"""Remove expired sessions."""
|
|
389
|
+
current_time = time.time()
|
|
390
|
+
expired = [
|
|
391
|
+
sid
|
|
392
|
+
for sid, session in self.sessions.items()
|
|
393
|
+
if current_time - session.last_activity > self.session_timeout
|
|
394
|
+
]
|
|
395
|
+
for sid in expired:
|
|
396
|
+
del self.sessions[sid]
|
|
397
|
+
logger.info(f"Cleaned up expired session {sid}")
|
|
398
|
+
|
|
399
|
+
def list_sessions(self) -> List[Dict[str, Any]]:
|
|
400
|
+
"""
|
|
401
|
+
List all active sessions.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
List of session info dicts
|
|
405
|
+
"""
|
|
406
|
+
return [
|
|
407
|
+
{
|
|
408
|
+
"session_id": s.session_id,
|
|
409
|
+
"project_path": s.project_path,
|
|
410
|
+
"history_mode": s.history_mode,
|
|
411
|
+
"created_at": s.created_at,
|
|
412
|
+
"last_activity": s.last_activity,
|
|
413
|
+
"has_pending_interactions": s.adapter.has_pending_interactions(),
|
|
414
|
+
}
|
|
415
|
+
for s in self.sessions.values()
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# Global instance
|
|
420
|
+
session_manager = SessionManager()
|