gobby 0.2.8__py3-none-any.whl → 0.2.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 (63) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/claude_code.py +3 -26
  3. gobby/app_context.py +59 -0
  4. gobby/cli/utils.py +5 -17
  5. gobby/config/features.py +0 -20
  6. gobby/config/tasks.py +4 -0
  7. gobby/hooks/event_handlers/__init__.py +155 -0
  8. gobby/hooks/event_handlers/_agent.py +175 -0
  9. gobby/hooks/event_handlers/_base.py +87 -0
  10. gobby/hooks/event_handlers/_misc.py +66 -0
  11. gobby/hooks/event_handlers/_session.py +573 -0
  12. gobby/hooks/event_handlers/_tool.py +196 -0
  13. gobby/hooks/hook_manager.py +2 -0
  14. gobby/llm/claude.py +377 -42
  15. gobby/mcp_proxy/importer.py +4 -41
  16. gobby/mcp_proxy/manager.py +13 -3
  17. gobby/mcp_proxy/registries.py +14 -0
  18. gobby/mcp_proxy/services/recommendation.py +2 -28
  19. gobby/mcp_proxy/tools/artifacts.py +3 -3
  20. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  21. gobby/mcp_proxy/tools/workflows/__init__.py +266 -0
  22. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  23. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  24. gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
  25. gobby/mcp_proxy/tools/workflows/_query.py +207 -0
  26. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  27. gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
  28. gobby/memory/components/__init__.py +0 -0
  29. gobby/memory/components/ingestion.py +98 -0
  30. gobby/memory/components/search.py +108 -0
  31. gobby/memory/manager.py +16 -25
  32. gobby/paths.py +51 -0
  33. gobby/prompts/loader.py +1 -35
  34. gobby/runner.py +23 -10
  35. gobby/servers/http.py +186 -149
  36. gobby/servers/routes/admin.py +12 -0
  37. gobby/servers/routes/mcp/endpoints/execution.py +15 -7
  38. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  39. gobby/sessions/analyzer.py +2 -2
  40. gobby/skills/parser.py +23 -0
  41. gobby/skills/sync.py +5 -4
  42. gobby/storage/artifacts.py +19 -0
  43. gobby/storage/migrations.py +25 -2
  44. gobby/storage/skills.py +47 -7
  45. gobby/tasks/external_validator.py +4 -17
  46. gobby/tasks/validation.py +13 -87
  47. gobby/tools/summarizer.py +18 -51
  48. gobby/utils/status.py +13 -0
  49. gobby/workflows/actions.py +5 -0
  50. gobby/workflows/context_actions.py +21 -24
  51. gobby/workflows/enforcement/__init__.py +11 -1
  52. gobby/workflows/enforcement/blocking.py +96 -0
  53. gobby/workflows/enforcement/handlers.py +35 -1
  54. gobby/workflows/engine.py +6 -3
  55. gobby/workflows/lifecycle_evaluator.py +2 -1
  56. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
  57. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/RECORD +61 -45
  58. gobby/hooks/event_handlers.py +0 -1008
  59. gobby/mcp_proxy/tools/workflows.py +0 -1023
  60. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
  61. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
  62. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
  63. {gobby-0.2.8.dist-info → gobby-0.2.9.dist-info}/top_level.txt +0 -0
gobby/runner.py CHANGED
@@ -9,6 +9,7 @@ from typing import Any
9
9
  import uvicorn
10
10
 
11
11
  from gobby.agents.runner import AgentRunner
12
+ from gobby.app_context import ServiceContainer
12
13
  from gobby.config.app import load_config
13
14
  from gobby.llm import LLMService, create_llm_service
14
15
  from gobby.llm.resolver import ExecutorRegistry
@@ -212,29 +213,38 @@ class GobbyRunner:
212
213
  )
213
214
 
214
215
  # HTTP Server
215
- self.http_server = HTTPServer(
216
- port=self.config.daemon_port,
217
- test_mode=self.config.test_mode,
218
- mcp_manager=self.mcp_proxy,
219
- mcp_db_manager=self.mcp_db_manager,
216
+ # Bundle services into container
217
+ services = ServiceContainer(
220
218
  config=self.config,
219
+ database=self.database,
221
220
  session_manager=self.session_manager,
222
221
  task_manager=self.task_manager,
223
222
  task_sync_manager=self.task_sync_manager,
224
- message_manager=self.message_manager,
223
+ memory_sync_manager=self.memory_sync_manager,
225
224
  memory_manager=self.memory_manager,
226
225
  llm_service=self.llm_service,
227
- message_processor=self.message_processor,
228
- memory_sync_manager=self.memory_sync_manager,
229
- task_validator=self.task_validator,
226
+ mcp_manager=self.mcp_proxy,
227
+ mcp_db_manager=self.mcp_db_manager,
230
228
  metrics_manager=self.metrics_manager,
231
229
  agent_runner=self.agent_runner,
230
+ message_processor=self.message_processor,
231
+ message_manager=self.message_manager,
232
+ task_validator=self.task_validator,
232
233
  worktree_storage=self.worktree_storage,
233
234
  clone_storage=self.clone_storage,
234
235
  git_manager=self.git_manager,
235
236
  project_id=self.project_id,
236
237
  )
237
238
 
239
+ self.http_server = HTTPServer(
240
+ services=services,
241
+ port=self.config.daemon_port,
242
+ test_mode=self.config.test_mode,
243
+ )
244
+
245
+ # Inject server into container for circular ref if needed later
246
+ # self.http_server.services = services
247
+
238
248
  # Ensure message_processor property is set (redundant but explicit):
239
249
  self.http_server.message_processor = self.message_processor
240
250
 
@@ -424,12 +434,14 @@ class GobbyRunner:
424
434
 
425
435
  # Start HTTP server
426
436
  # nosec B104: 0.0.0.0 binding is intentional - daemon serves local network
437
+ graceful_shutdown_timeout = 15
427
438
  config = uvicorn.Config(
428
439
  self.http_server.app,
429
440
  host="0.0.0.0", # nosec B104 - local daemon needs network access
430
441
  port=self.http_server.port,
431
442
  log_level="warning",
432
443
  access_log=False,
444
+ timeout_graceful_shutdown=graceful_shutdown_timeout,
433
445
  )
434
446
  server = uvicorn.Server(config)
435
447
  server_task = asyncio.create_task(server.serve())
@@ -439,9 +451,10 @@ class GobbyRunner:
439
451
  await asyncio.sleep(0.5)
440
452
 
441
453
  # Cleanup with timeouts to prevent hanging
454
+ # Use timeout slightly longer than uvicorn's graceful shutdown to let it finish
442
455
  server.should_exit = True
443
456
  try:
444
- await asyncio.wait_for(server_task, timeout=3.0)
457
+ await asyncio.wait_for(server_task, timeout=graceful_shutdown_timeout + 5)
445
458
  except TimeoutError:
446
459
  logger.warning("HTTP server shutdown timed out")
447
460
 
gobby/servers/http.py CHANGED
@@ -10,7 +10,7 @@ import logging
10
10
  import time
11
11
  from collections.abc import AsyncGenerator
12
12
  from contextlib import asynccontextmanager
13
- from typing import Any
13
+ from typing import TYPE_CHECKING, Any
14
14
 
15
15
  from fastapi import FastAPI, HTTPException, Request
16
16
  from fastapi.middleware.cors import CORSMiddleware
@@ -19,18 +19,22 @@ from fastapi.responses import JSONResponse
19
19
  from gobby.adapters.codex_impl.adapter import CodexAdapter
20
20
  from gobby.hooks.broadcaster import HookEventBroadcaster
21
21
  from gobby.hooks.hook_manager import HookManager
22
- from gobby.llm import LLMService, create_llm_service
22
+ from gobby.llm import create_llm_service
23
23
  from gobby.mcp_proxy.registries import setup_internal_registries
24
24
  from gobby.mcp_proxy.semantic_search import SemanticToolSearch
25
25
  from gobby.mcp_proxy.server import GobbyDaemonTools, create_mcp_server
26
26
  from gobby.mcp_proxy.services.tool_filter import ToolFilterService
27
- from gobby.memory.manager import MemoryManager
28
- from gobby.storage.sessions import LocalSessionManager
29
- from gobby.storage.tasks import LocalTaskManager
30
- from gobby.sync.tasks import TaskSyncManager
31
27
  from gobby.utils.metrics import get_metrics_collector
32
28
  from gobby.utils.version import get_version
33
29
 
30
+ if TYPE_CHECKING:
31
+ from gobby.app_context import ServiceContainer
32
+ from gobby.config.app import DaemonConfig
33
+ from gobby.llm import LLMService
34
+ from gobby.mcp_proxy.manager import MCPClientManager
35
+ from gobby.servers.websocket import WebSocketServer
36
+ from gobby.utils.tool_metrics import ToolMetricsManager
37
+
34
38
  logger = logging.getLogger(__name__)
35
39
 
36
40
 
@@ -44,84 +48,38 @@ class HTTPServer:
44
48
 
45
49
  def __init__(
46
50
  self,
51
+ services: "ServiceContainer",
47
52
  port: int = 8000,
48
53
  test_mode: bool = False,
49
- mcp_manager: Any | None = None,
50
- mcp_db_manager: Any | None = None,
51
- config: Any | None = None,
52
54
  codex_client: Any | None = None,
53
- session_manager: LocalSessionManager | None = None,
54
- websocket_server: Any | None = None,
55
- task_manager: LocalTaskManager | None = None,
56
- task_sync_manager: TaskSyncManager | None = None,
57
- message_processor: Any | None = None,
58
- message_manager: Any | None = None, # LocalSessionMessageManager
59
- memory_manager: "MemoryManager | None" = None,
60
- llm_service: "LLMService | None" = None,
61
- memory_sync_manager: Any | None = None,
62
- task_validator: Any | None = None,
63
- metrics_manager: Any | None = None,
64
- agent_runner: Any | None = None,
65
- worktree_storage: Any | None = None,
66
- clone_storage: Any | None = None,
67
- git_manager: Any | None = None,
68
- project_id: str | None = None,
69
55
  ) -> None:
70
56
  """
71
57
  Initialize HTTP server.
72
58
 
73
59
  Args:
60
+ services: ServiceContainer holding all dependencies
74
61
  port: Server port
75
62
  test_mode: Run in test mode (disable features that conflict with testing)
76
- mcp_manager: MCPClientManager instance for multi-server support
77
- mcp_db_manager: LocalMCPManager instance for SQLite-based storage of MCP
78
- server configurations and tool schemas. Used by ToolsHandler for
79
- progressive tool discovery. Optional; defaults to None.
80
- config: DaemonConfig instance for configuration
81
63
  codex_client: CodexAppServerClient instance for Codex integration
82
- session_manager: LocalSessionManager for session storage
83
- websocket_server: Optional WebSocketServer instance for event broadcasting
84
- task_manager: LocalTaskManager instance
85
- task_sync_manager: TaskSyncManager instance
86
- message_processor: SessionMessageProcessor instance
87
- message_manager: LocalSessionMessageManager instance for retrieval
88
- memory_manager: MemoryManager instance
89
- llm_service: LLMService instance
90
64
  """
65
+ self.services = services
91
66
  self.port = port
92
67
  self.test_mode = test_mode
93
- self.mcp_manager = mcp_manager
94
- self.config = config
95
68
  self.codex_client = codex_client
96
- self.session_manager = session_manager
97
- self.task_manager = task_manager
98
- self.task_sync_manager = task_sync_manager
99
- self.message_processor = message_processor
100
- self.message_manager = message_manager
101
- self.memory_manager = memory_manager
102
- self.websocket_server = websocket_server
103
- self.llm_service = llm_service
104
- self.memory_sync_manager = memory_sync_manager
105
- self.task_validator = task_validator
106
- self.metrics_manager = metrics_manager
107
- self.agent_runner = agent_runner
108
- self.worktree_storage = worktree_storage
109
- self.clone_storage = clone_storage
110
- self.git_manager = git_manager
111
- self.project_id = project_id
112
-
113
- # Initialize WebSocket broadcaster
114
- # Note: websocket_server might be None if disabled
115
- self.broadcaster = HookEventBroadcaster(websocket_server, config)
69
+
70
+ # WebSocket server reference (set by GobbyRunner after construction)
71
+ self.websocket_server: WebSocketServer | None = None
72
+
73
+ self.broadcaster = HookEventBroadcaster(services.websocket_server, services.config)
116
74
 
117
75
  self._start_time: float = time.time()
118
76
 
119
- # Create LLM service if not provided
120
- if not self.llm_service and config:
77
+ # Create LLM service if not provided in container (fallback)
78
+ if not services.llm_service and services.config:
121
79
  try:
122
- self.llm_service = create_llm_service(config)
80
+ services.llm_service = create_llm_service(services.config)
123
81
  logger.debug(
124
- f"LLM service initialized with providers: {self.llm_service.enabled_providers}"
82
+ f"LLM service initialized with providers: {services.llm_service.enabled_providers}"
125
83
  )
126
84
  except Exception as e:
127
85
  logger.error(f"Failed to initialize LLM service: {e}")
@@ -130,12 +88,13 @@ class HTTPServer:
130
88
  self._mcp_server = None
131
89
  self._internal_manager = None
132
90
  self._tools_handler = None
133
- self._mcp_db_manager = mcp_db_manager
134
- if mcp_manager:
91
+
92
+ if services.mcp_manager:
135
93
  # Determine WebSocket port
136
94
  ws_port = 60888
137
- if config and hasattr(config, "websocket") and config.websocket:
138
- ws_port = config.websocket.port
95
+ cfg = services.config
96
+ if cfg and hasattr(cfg, "websocket") and cfg.websocket:
97
+ ws_port = cfg.websocket.port
139
98
 
140
99
  # Create a lazy getter for tool_proxy that will be available after
141
100
  # GobbyDaemonTools is created. This allows in-process agents to route
@@ -149,36 +108,38 @@ class HTTPServer:
149
108
  merge_storage = None
150
109
  merge_resolver = None
151
110
  inter_session_message_manager = None
152
- if mcp_db_manager:
111
+ if self.services.mcp_db_manager:
153
112
  from gobby.storage.inter_session_messages import InterSessionMessageManager
154
113
  from gobby.storage.merge_resolutions import MergeResolutionManager
155
114
  from gobby.worktrees.merge.resolver import MergeResolver
156
115
 
157
- merge_storage = MergeResolutionManager(mcp_db_manager.db)
116
+ merge_storage = MergeResolutionManager(self.services.mcp_db_manager.db)
158
117
  merge_resolver = MergeResolver()
159
- merge_resolver._llm_service = self.llm_service
160
- inter_session_message_manager = InterSessionMessageManager(mcp_db_manager.db)
118
+ merge_resolver._llm_service = services.llm_service
119
+ inter_session_message_manager = InterSessionMessageManager(
120
+ self.services.mcp_db_manager.db
121
+ )
161
122
  logger.debug("Merge resolution and inter-session messaging subsystems initialized")
162
123
 
163
124
  # Setup internal registries (gobby-tasks, gobby-memory, etc.)
164
125
  self._internal_manager = setup_internal_registries(
165
- _config=config,
126
+ _config=services.config,
166
127
  _session_manager=None, # Not needed for internal registries
167
- memory_manager=memory_manager,
168
- task_manager=task_manager,
169
- sync_manager=task_sync_manager,
170
- task_validator=self.task_validator,
171
- message_manager=message_manager,
172
- local_session_manager=session_manager,
173
- metrics_manager=self.metrics_manager,
174
- llm_service=self.llm_service,
175
- agent_runner=self.agent_runner,
176
- worktree_storage=self.worktree_storage,
177
- clone_storage=self.clone_storage,
178
- git_manager=self.git_manager,
128
+ memory_manager=services.memory_manager,
129
+ task_manager=services.task_manager,
130
+ sync_manager=services.task_sync_manager,
131
+ task_validator=services.task_validator,
132
+ message_manager=services.message_manager,
133
+ local_session_manager=services.session_manager,
134
+ metrics_manager=services.metrics_manager,
135
+ llm_service=services.llm_service,
136
+ agent_runner=services.agent_runner,
137
+ worktree_storage=services.worktree_storage,
138
+ clone_storage=services.clone_storage,
139
+ git_manager=services.git_manager,
179
140
  merge_storage=merge_storage,
180
141
  merge_resolver=merge_resolver,
181
- project_id=self.project_id,
142
+ project_id=services.project_id,
182
143
  tool_proxy_getter=tool_proxy_getter,
183
144
  inter_session_message_manager=inter_session_message_manager,
184
145
  )
@@ -186,47 +147,47 @@ class HTTPServer:
186
147
  logger.debug(f"Internal registries initialized: {registry_count} registries")
187
148
 
188
149
  # Initialize tool summarizer config
189
- if config:
150
+ if services.config:
190
151
  from gobby.tools.summarizer import init_summarizer_config
191
152
 
192
- init_summarizer_config(config.tool_summarizer)
153
+ init_summarizer_config(services.config.tool_summarizer)
193
154
  logger.debug("Tool summarizer config initialized")
194
155
 
195
156
  # Create semantic search instance if db available
196
157
  semantic_search = None
197
- if mcp_db_manager:
198
- semantic_search = SemanticToolSearch(db=mcp_db_manager.db)
158
+ if services.mcp_db_manager:
159
+ semantic_search = SemanticToolSearch(db=services.mcp_db_manager.db)
199
160
  logger.debug("Semantic tool search initialized")
200
161
 
201
162
  # Create tool filter for workflow phase restrictions
202
163
  tool_filter = None
203
- if mcp_db_manager:
204
- tool_filter = ToolFilterService(db=mcp_db_manager.db)
164
+ if services.mcp_db_manager:
165
+ tool_filter = ToolFilterService(db=services.mcp_db_manager.db)
205
166
  logger.debug("Tool filter service initialized")
206
167
 
207
168
  # Create fallback resolver for alternative tool suggestions on error
208
169
  fallback_resolver = None
209
- if semantic_search and self.metrics_manager:
170
+ if semantic_search and services.metrics_manager:
210
171
  from gobby.mcp_proxy.services.fallback import ToolFallbackResolver
211
172
 
212
173
  fallback_resolver = ToolFallbackResolver(
213
174
  semantic_search=semantic_search,
214
- metrics_manager=self.metrics_manager,
175
+ metrics_manager=services.metrics_manager,
215
176
  )
216
177
  logger.debug("Fallback resolver initialized")
217
178
 
218
179
  # Create tools handler
219
180
  self._tools_handler = GobbyDaemonTools(
220
- mcp_manager=mcp_manager,
181
+ mcp_manager=services.mcp_manager,
221
182
  daemon_port=port,
222
183
  websocket_port=ws_port,
223
184
  start_time=self._start_time,
224
185
  internal_manager=self._internal_manager,
225
- config=config,
226
- llm_service=self.llm_service,
227
- session_manager=session_manager,
228
- memory_manager=memory_manager,
229
- config_manager=mcp_db_manager,
186
+ config=services.config,
187
+ llm_service=services.llm_service,
188
+ session_manager=services.session_manager,
189
+ memory_manager=services.memory_manager,
190
+ config_manager=services.mcp_db_manager,
230
191
  semantic_search=semantic_search,
231
192
  tool_filter=tool_filter,
232
193
  fallback_resolver=fallback_resolver,
@@ -240,6 +201,83 @@ class HTTPServer:
240
201
  self._metrics = get_metrics_collector()
241
202
  self._daemon: Any = None # Set externally by daemon
242
203
 
204
+ # Property accessors for services (delegate to container)
205
+ @property
206
+ def config(self) -> "DaemonConfig | None":
207
+ return self.services.config
208
+
209
+ @property
210
+ def session_manager(self) -> Any:
211
+ return self.services.session_manager
212
+
213
+ @session_manager.setter
214
+ def session_manager(self, value: Any) -> None:
215
+ self.services.session_manager = value
216
+
217
+ @property
218
+ def task_manager(self) -> Any:
219
+ return self.services.task_manager
220
+
221
+ @task_manager.setter
222
+ def task_manager(self, value: Any) -> None:
223
+ self.services.task_manager = value
224
+
225
+ @property
226
+ def mcp_manager(self) -> "MCPClientManager | None":
227
+ return self.services.mcp_manager
228
+
229
+ @mcp_manager.setter
230
+ def mcp_manager(self, value: "MCPClientManager | None") -> None:
231
+ self.services.mcp_manager = value
232
+
233
+ @property
234
+ def llm_service(self) -> "LLMService | None":
235
+ return self.services.llm_service
236
+
237
+ @llm_service.setter
238
+ def llm_service(self, value: "LLMService | None") -> None:
239
+ self.services.llm_service = value
240
+
241
+ @property
242
+ def memory_manager(self) -> Any:
243
+ return self.services.memory_manager
244
+
245
+ @memory_manager.setter
246
+ def memory_manager(self, value: Any) -> None:
247
+ self.services.memory_manager = value
248
+
249
+ @property
250
+ def message_manager(self) -> Any:
251
+ return self.services.message_manager
252
+
253
+ @message_manager.setter
254
+ def message_manager(self, value: Any) -> None:
255
+ self.services.message_manager = value
256
+
257
+ @property
258
+ def message_processor(self) -> Any:
259
+ return self.services.message_processor
260
+
261
+ @message_processor.setter
262
+ def message_processor(self, value: Any) -> None:
263
+ self.services.message_processor = value
264
+
265
+ @property
266
+ def metrics_manager(self) -> "ToolMetricsManager | None":
267
+ return self.services.metrics_manager
268
+
269
+ @metrics_manager.setter
270
+ def metrics_manager(self, value: "ToolMetricsManager | None") -> None:
271
+ self.services.metrics_manager = value
272
+
273
+ @property
274
+ def _mcp_db_manager(self) -> Any:
275
+ return self.services.mcp_db_manager
276
+
277
+ @_mcp_db_manager.setter
278
+ def _mcp_db_manager(self, value: Any) -> None:
279
+ self.services.mcp_db_manager = value
280
+
243
281
  @property
244
282
  def tool_proxy(self) -> Any:
245
283
  """Get the ToolProxyService instance for routing tool calls with error enrichment."""
@@ -314,30 +352,32 @@ class HTTPServer:
314
352
  hook_manager_kwargs: dict[str, Any] = {
315
353
  "daemon_host": "localhost",
316
354
  "daemon_port": self.port,
317
- "llm_service": self.llm_service,
318
- "config": self.config,
355
+ "llm_service": self.services.llm_service,
356
+ "config": self.services.config,
319
357
  "broadcaster": self.broadcaster,
320
- "mcp_manager": self.mcp_manager,
321
- "message_processor": self.message_processor,
322
- "memory_sync_manager": self.memory_sync_manager,
323
- "task_sync_manager": self.task_sync_manager,
358
+ "mcp_manager": self.services.mcp_manager,
359
+ "message_processor": self.services.message_processor,
360
+ "memory_sync_manager": self.services.memory_sync_manager,
361
+ "task_sync_manager": self.services.task_sync_manager,
324
362
  }
325
- if self.config:
363
+ if self.services.config:
326
364
  # Pass full log file path from config
327
- hook_manager_kwargs["log_file"] = self.config.logging.hook_manager
328
- hook_manager_kwargs["log_max_bytes"] = self.config.logging.max_size_mb * 1024 * 1024
329
- hook_manager_kwargs["log_backup_count"] = self.config.logging.backup_count
365
+ hook_manager_kwargs["log_file"] = self.services.config.logging.hook_manager
366
+ hook_manager_kwargs["log_max_bytes"] = (
367
+ self.services.config.logging.max_size_mb * 1024 * 1024
368
+ )
369
+ hook_manager_kwargs["log_backup_count"] = self.services.config.logging.backup_count
330
370
 
331
371
  app.state.hook_manager = HookManager(**hook_manager_kwargs)
332
372
  logger.debug("HookManager initialized in daemon")
333
373
 
334
374
  # Wire up stop_registry to WebSocket server for stop_request handling
335
375
  if (
336
- self.websocket_server
376
+ self.services.websocket_server
337
377
  and hasattr(app.state, "hook_manager")
338
378
  and hasattr(app.state.hook_manager, "_stop_registry")
339
379
  ):
340
- self.websocket_server.stop_registry = app.state.hook_manager._stop_registry
380
+ self.services.websocket_server.stop_registry = app.state.hook_manager._stop_registry
341
381
  logger.debug("Stop registry connected to WebSocket server")
342
382
 
343
383
  # Store server instance for dependency injection
@@ -496,45 +536,48 @@ class HTTPServer:
496
536
  try:
497
537
  logger.debug("Processing graceful shutdown")
498
538
 
499
- # Wait for pending background tasks to complete
539
+ # Cancel pending background tasks immediately instead of polling
500
540
  pending_tasks_count = len(self._background_tasks)
501
541
  if pending_tasks_count > 0:
502
542
  logger.debug(
503
- "Waiting for pending background tasks to complete",
543
+ "Cancelling pending background tasks",
504
544
  extra={"pending_tasks": pending_tasks_count},
505
545
  )
506
546
 
507
- max_wait = 30.0
508
- wait_start = time.perf_counter()
509
-
510
- while (
511
- len(self._background_tasks) > 0
512
- and (time.perf_counter() - wait_start) < max_wait
513
- ):
514
- await asyncio.sleep(0.5)
515
-
516
- completed_wait = time.perf_counter() - wait_start
517
- remaining_tasks = len(self._background_tasks)
518
-
519
- if remaining_tasks > 0:
520
- logger.warning(
521
- "Shutdown timeout - some background tasks still pending",
522
- extra={
523
- "remaining_tasks": remaining_tasks,
524
- "wait_seconds": completed_wait,
525
- },
526
- )
527
- else:
528
- logger.debug(
529
- "All background tasks completed",
530
- extra={"wait_seconds": completed_wait},
547
+ # Cancel all tasks
548
+ for task in self._background_tasks:
549
+ task.cancel()
550
+
551
+ # Wait for cancellation to complete with a short timeout
552
+ if self._background_tasks:
553
+ done, pending = await asyncio.wait(
554
+ self._background_tasks,
555
+ timeout=5.0,
556
+ return_when=asyncio.ALL_COMPLETED,
531
557
  )
532
558
 
559
+ completed_count = len(done)
560
+ remaining_count = len(pending)
561
+
562
+ if remaining_count > 0:
563
+ logger.warning(
564
+ "Some background tasks did not cancel in time",
565
+ extra={
566
+ "completed": completed_count,
567
+ "remaining": remaining_count,
568
+ },
569
+ )
570
+ else:
571
+ logger.debug(
572
+ "All background tasks cancelled",
573
+ extra={"completed": completed_count},
574
+ )
575
+
533
576
  # Disconnect all MCP servers
534
- if self.mcp_manager:
577
+ if self.services.mcp_manager:
535
578
  logger.debug("Disconnecting MCP servers...")
536
579
  try:
537
- await self.mcp_manager.disconnect_all()
580
+ await self.services.mcp_manager.disconnect_all()
538
581
  logger.debug("MCP servers disconnected")
539
582
  except Exception as e:
540
583
  logger.warning(f"Error disconnecting MCP servers: {e}")
@@ -560,31 +603,25 @@ class HTTPServer:
560
603
 
561
604
 
562
605
  async def create_server(
606
+ services: "ServiceContainer",
563
607
  port: int = 60887,
564
608
  test_mode: bool = False,
565
- mcp_manager: Any | None = None,
566
- config: Any | None = None,
567
- session_manager: LocalSessionManager | None = None,
568
609
  ) -> HTTPServer:
569
610
  """
570
611
  Create HTTP server instance.
571
612
 
572
613
  Args:
614
+ services: ServiceContainer holding dependencies
573
615
  port: Port to listen on
574
616
  test_mode: Enable test mode
575
- mcp_manager: MCP client manager
576
- config: Daemon configuration
577
- session_manager: Local session manager
578
617
 
579
618
  Returns:
580
619
  Configured HTTPServer instance
581
620
  """
582
621
  return HTTPServer(
622
+ services=services,
583
623
  port=port,
584
624
  test_mode=test_mode,
585
- mcp_manager=mcp_manager,
586
- config=config,
587
- session_manager=session_manager,
588
625
  )
589
626
 
590
627
 
@@ -175,6 +175,17 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
175
175
  except Exception as e:
176
176
  logger.warning(f"Failed to get memory stats: {e}")
177
177
 
178
+ # Get artifact statistics
179
+ artifact_stats = {"count": 0}
180
+ if server.session_manager is not None:
181
+ try:
182
+ from gobby.storage.artifacts import LocalArtifactManager
183
+
184
+ artifact_manager = LocalArtifactManager(server.session_manager.db)
185
+ artifact_stats["count"] = artifact_manager.count_artifacts()
186
+ except Exception as e:
187
+ logger.warning(f"Failed to get artifact stats: {e}")
188
+
178
189
  # Get skills statistics
179
190
  skills_stats: dict[str, Any] = {"total": 0}
180
191
  if server._internal_manager:
@@ -231,6 +242,7 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
231
242
  "sessions": session_stats,
232
243
  "tasks": task_stats,
233
244
  "memory": memory_stats,
245
+ "artifacts": artifact_stats,
234
246
  "skills": skills_stats,
235
247
  "plugins": plugin_stats,
236
248
  "response_time_ms": response_time_ms,
@@ -318,12 +318,16 @@ async def get_tool_schema(
318
318
  schema = registry.get_schema(tool_name)
319
319
  if schema:
320
320
  response_time_ms = (time.perf_counter() - start_time) * 1000
321
- return {
322
- "name": tool_name,
321
+ # Build response with description only if present
322
+ result: dict[str, Any] = {
323
+ "name": schema.get("name", tool_name),
324
+ "inputSchema": schema.get("inputSchema"),
323
325
  "server": server_name,
324
- "inputSchema": schema,
325
326
  "response_time_ms": response_time_ms,
326
327
  }
328
+ if schema.get("description"):
329
+ result["description"] = schema["description"]
330
+ return result
327
331
  raise HTTPException(
328
332
  status_code=404,
329
333
  detail={
@@ -340,15 +344,19 @@ async def get_tool_schema(
340
344
 
341
345
  # Get from external MCP server
342
346
  try:
343
- schema = await server.mcp_manager.get_tool_input_schema(server_name, tool_name)
347
+ tool_info = await server.mcp_manager.get_tool_info(server_name, tool_name)
344
348
  response_time_ms = (time.perf_counter() - start_time) * 1000
345
349
 
346
- return {
347
- "name": tool_name,
350
+ # Build response with description only if present
351
+ response: dict[str, Any] = {
352
+ "name": tool_info.get("name", tool_name),
353
+ "inputSchema": tool_info.get("inputSchema"),
348
354
  "server": server_name,
349
- "inputSchema": schema,
350
355
  "response_time_ms": response_time_ms,
351
356
  }
357
+ if tool_info.get("description"):
358
+ response["description"] = tool_info["description"]
359
+ return response
352
360
 
353
361
  except (KeyError, ValueError) as e:
354
362
  # Tool or server not found - 404