gobby 0.2.5__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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,582 @@
1
+ """
2
+ Session routes for Gobby HTTP server.
3
+
4
+ Provides session registration, listing, lookup, and update endpoints.
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from fastapi import APIRouter, HTTPException, Query, Request
12
+
13
+ from gobby.servers.models import SessionRegisterRequest
14
+ from gobby.utils.metrics import get_metrics_collector
15
+
16
+ if TYPE_CHECKING:
17
+ from gobby.servers.http import HTTPServer
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def create_sessions_router(server: "HTTPServer") -> APIRouter:
23
+ """
24
+ Create sessions router with endpoints bound to server instance.
25
+
26
+ Args:
27
+ server: HTTPServer instance for accessing state and dependencies
28
+
29
+ Returns:
30
+ Configured APIRouter with session endpoints
31
+ """
32
+ router = APIRouter(prefix="/sessions", tags=["sessions"])
33
+ metrics = get_metrics_collector()
34
+
35
+ @router.post("/register")
36
+ async def register_session(request_data: SessionRegisterRequest) -> dict[str, Any]:
37
+ """
38
+ Register session metadata in local storage.
39
+
40
+ Args:
41
+ request_data: Session registration parameters
42
+
43
+ Returns:
44
+ Registration confirmation with session ID
45
+ """
46
+ metrics.inc_counter("http_requests_total")
47
+ metrics.inc_counter("session_registrations_total")
48
+
49
+ try:
50
+ if server.session_manager is None:
51
+ raise HTTPException(status_code=503, detail="Session manager not available")
52
+
53
+ # Get machine_id from request or generate
54
+ machine_id = request_data.machine_id
55
+ if not machine_id:
56
+ from gobby.utils.machine_id import get_machine_id
57
+
58
+ machine_id = get_machine_id()
59
+
60
+ if not machine_id:
61
+ # Should unlikely happen if get_machine_id works, but type safe
62
+ machine_id = "unknown-machine"
63
+
64
+ # Extract git branch if project path exists but git_branch not provided
65
+ git_branch = request_data.git_branch
66
+ if request_data.project_path and not git_branch:
67
+ from gobby.utils.git import get_git_metadata
68
+
69
+ git_metadata = get_git_metadata(request_data.project_path)
70
+ if git_metadata.get("git_branch"):
71
+ git_branch = git_metadata.get("git_branch")
72
+
73
+ # Resolve project_id from cwd if not provided
74
+ project_id = server._resolve_project_id(request_data.project_id, request_data.cwd)
75
+
76
+ # Register session in local storage
77
+ session = server.session_manager.register(
78
+ external_id=request_data.external_id,
79
+ machine_id=machine_id,
80
+ source=request_data.source or "Claude Code",
81
+ project_id=project_id,
82
+ jsonl_path=request_data.jsonl_path,
83
+ title=request_data.title,
84
+ git_branch=git_branch,
85
+ parent_session_id=request_data.parent_session_id,
86
+ )
87
+
88
+ return {
89
+ "status": "registered",
90
+ "external_id": request_data.external_id,
91
+ "id": session.id,
92
+ "machine_id": machine_id,
93
+ }
94
+
95
+ except HTTPException:
96
+ metrics.inc_counter("http_requests_errors_total")
97
+ raise
98
+
99
+ except ValueError as e:
100
+ # ValueError from _resolve_project_id when project not initialized
101
+ metrics.inc_counter("http_requests_errors_total")
102
+ raise HTTPException(status_code=400, detail=str(e)) from e
103
+
104
+ except Exception as e:
105
+ metrics.inc_counter("http_requests_errors_total")
106
+ logger.error(f"Error registering session: {e}", exc_info=True)
107
+ raise HTTPException(
108
+ status_code=500, detail="Internal server error while registering session"
109
+ ) from e
110
+
111
+ @router.get("")
112
+ async def list_sessions(
113
+ project_id: str | None = None,
114
+ status: str | None = None,
115
+ source: str | None = None,
116
+ limit: int = Query(100, ge=1, le=1000),
117
+ ) -> dict[str, Any]:
118
+ """
119
+ List sessions with optional filtering and message counts.
120
+
121
+ Args:
122
+ project_id: Filter by project ID
123
+ status: Filter by status (active, archived, etc)
124
+ source: Filter by source (Claude Code, Gemini, etc)
125
+ limit: Max results (default 100)
126
+
127
+ Returns:
128
+ List of session objects with message counts
129
+ """
130
+ metrics.inc_counter("http_requests_total")
131
+ start_time = time.perf_counter()
132
+
133
+ try:
134
+ if server.session_manager is None:
135
+ raise HTTPException(status_code=503, detail="Session manager not available")
136
+
137
+ sessions = server.session_manager.list(
138
+ project_id=project_id,
139
+ status=status,
140
+ source=source,
141
+ limit=limit,
142
+ )
143
+
144
+ # Fetch message counts if message manager is available
145
+ message_counts = {}
146
+ if server.message_manager:
147
+ try:
148
+ message_counts = await server.message_manager.get_all_counts()
149
+ except Exception as e:
150
+ logger.warning(f"Failed to fetch message counts: {e}")
151
+
152
+ # Enrich sessions with counts
153
+ session_list = []
154
+ for session in sessions:
155
+ session_data = session.to_dict()
156
+ session_data["message_count"] = message_counts.get(session.id, 0)
157
+ session_list.append(session_data)
158
+
159
+ response_time_ms = (time.perf_counter() - start_time) * 1000
160
+
161
+ return {
162
+ "sessions": session_list,
163
+ "count": len(session_list),
164
+ "response_time_ms": response_time_ms,
165
+ }
166
+
167
+ except HTTPException:
168
+ metrics.inc_counter("http_requests_errors_total")
169
+ raise
170
+ except Exception as e:
171
+ metrics.inc_counter("http_requests_errors_total")
172
+ logger.error(f"Error listing sessions: {e}", exc_info=True)
173
+ raise HTTPException(status_code=500, detail=str(e)) from e
174
+
175
+ @router.get("/{session_id}")
176
+ async def sessions_get(session_id: str) -> dict[str, Any]:
177
+ """
178
+ Get session by ID from local storage.
179
+
180
+ Args:
181
+ session_id: Session ID (UUID)
182
+
183
+ Returns:
184
+ Session data
185
+ """
186
+ start_time = time.perf_counter()
187
+ metrics.inc_counter("http_requests_total")
188
+
189
+ try:
190
+ if server.session_manager is None:
191
+ raise HTTPException(status_code=503, detail="Session manager not available")
192
+
193
+ session = server.session_manager.get(session_id)
194
+
195
+ if session is None:
196
+ raise HTTPException(status_code=404, detail="Session not found")
197
+
198
+ response_time_ms = (time.perf_counter() - start_time) * 1000
199
+
200
+ return {
201
+ "status": "success",
202
+ "session": session.to_dict(),
203
+ "response_time_ms": response_time_ms,
204
+ }
205
+
206
+ except HTTPException:
207
+ raise
208
+ except Exception as e:
209
+ logger.error(f"Sessions get error: {e}", exc_info=True)
210
+ raise HTTPException(status_code=500, detail=str(e)) from e
211
+
212
+ @router.get("/{session_id}/messages")
213
+ async def sessions_get_messages(
214
+ session_id: str,
215
+ limit: int = 100,
216
+ offset: int = 0,
217
+ role: str | None = None,
218
+ ) -> dict[str, Any]:
219
+ """
220
+ Get messages for a session.
221
+
222
+ Args:
223
+ session_id: Session ID
224
+ limit: Max messages to return (default 100)
225
+ offset: Pagination offset
226
+ role: Filter by role (user, assistant, tool)
227
+
228
+ Returns:
229
+ List of messages and total count key
230
+ """
231
+ start_time = time.perf_counter()
232
+ metrics.inc_counter("http_requests_total")
233
+
234
+ try:
235
+ if server.message_manager is None:
236
+ raise HTTPException(status_code=503, detail="Message manager not available")
237
+
238
+ messages = await server.message_manager.get_messages(
239
+ session_id=session_id, limit=limit, offset=offset, role=role
240
+ )
241
+
242
+ count = await server.message_manager.count_messages(session_id)
243
+ response_time_ms = (time.perf_counter() - start_time) * 1000
244
+
245
+ return {
246
+ "status": "success",
247
+ "messages": messages,
248
+ "total_count": count,
249
+ "response_time_ms": response_time_ms,
250
+ }
251
+
252
+ except HTTPException:
253
+ raise
254
+ except Exception as e:
255
+ logger.error(f"Get messages error: {e}", exc_info=True)
256
+ raise HTTPException(status_code=500, detail=str(e)) from e
257
+
258
+ @router.post("/find_current")
259
+ async def find_current_session(request: Request) -> dict[str, Any]:
260
+ """
261
+ Find current active session by composite key.
262
+
263
+ Uses composite key: external_id, machine_id, source, project_id
264
+ Accepts either project_id directly or cwd (which is resolved to project_id).
265
+ """
266
+ try:
267
+ if server.session_manager is None:
268
+ raise HTTPException(status_code=503, detail="Session manager not available")
269
+
270
+ body = await request.json()
271
+ external_id = body.get("external_id")
272
+ machine_id = body.get("machine_id")
273
+ source = body.get("source")
274
+ project_id = body.get("project_id")
275
+ cwd = body.get("cwd")
276
+
277
+ if not external_id or not machine_id or not source:
278
+ raise HTTPException(
279
+ status_code=400,
280
+ detail="Required fields: external_id, machine_id, source",
281
+ )
282
+
283
+ # Resolve project_id from cwd if not provided
284
+ if not project_id and cwd:
285
+ project_id = server._resolve_project_id(None, cwd)
286
+
287
+ if not project_id:
288
+ raise HTTPException(
289
+ status_code=400,
290
+ detail="Required: project_id or cwd (to resolve project)",
291
+ )
292
+
293
+ session = server.session_manager.find_by_external_id(
294
+ external_id, machine_id, project_id, source
295
+ )
296
+
297
+ if session is None:
298
+ return {"session": None}
299
+
300
+ return {"session": session.to_dict()}
301
+
302
+ except HTTPException:
303
+ raise
304
+ except Exception as e:
305
+ logger.error(f"Find current session error: {e}", exc_info=True)
306
+ raise HTTPException(status_code=500, detail=str(e)) from e
307
+
308
+ @router.post("/find_parent")
309
+ async def find_parent_session(request: Request) -> dict[str, Any]:
310
+ """
311
+ Find parent session for handoff.
312
+
313
+ Looks for most recent session in same project with handoff_ready status.
314
+ Accepts either project_id directly or cwd (which is resolved to project_id).
315
+ """
316
+ try:
317
+ if server.session_manager is None:
318
+ raise HTTPException(status_code=503, detail="Session manager not available")
319
+
320
+ body = await request.json()
321
+ machine_id = body.get("machine_id")
322
+ source = body.get("source")
323
+ project_id = body.get("project_id")
324
+ cwd = body.get("cwd")
325
+
326
+ if not source:
327
+ raise HTTPException(status_code=400, detail="Required field: source")
328
+
329
+ if not machine_id:
330
+ from gobby.utils.machine_id import get_machine_id
331
+
332
+ machine_id = get_machine_id()
333
+
334
+ if not machine_id:
335
+ machine_id = "unknown-machine"
336
+
337
+ # Resolve project_id from cwd if not provided
338
+ if not project_id:
339
+ if not cwd:
340
+ raise HTTPException(
341
+ status_code=400,
342
+ detail="Required field: project_id or cwd",
343
+ )
344
+ project_id = server._resolve_project_id(None, cwd)
345
+
346
+ session = server.session_manager.find_parent(
347
+ machine_id=machine_id,
348
+ source=source,
349
+ project_id=project_id,
350
+ )
351
+
352
+ if session is None:
353
+ return {"session": None}
354
+
355
+ return {"session": session.to_dict()}
356
+
357
+ except HTTPException:
358
+ raise
359
+ except Exception as e:
360
+ logger.error(f"Find parent session error: {e}", exc_info=True)
361
+ raise HTTPException(status_code=500, detail=str(e)) from e
362
+
363
+ @router.post("/update_status")
364
+ async def update_session_status(request: Request) -> dict[str, Any]:
365
+ """
366
+ Update session status.
367
+ """
368
+ try:
369
+ if server.session_manager is None:
370
+ raise HTTPException(status_code=503, detail="Session manager not available")
371
+
372
+ body = await request.json()
373
+ session_id = body.get("session_id")
374
+ status = body.get("status")
375
+
376
+ if not session_id or not status:
377
+ raise HTTPException(status_code=400, detail="Required fields: session_id, status")
378
+
379
+ session = server.session_manager.update_status(session_id, status)
380
+
381
+ if session is None:
382
+ raise HTTPException(status_code=404, detail="Session not found")
383
+
384
+ return {"session": session.to_dict()}
385
+
386
+ except HTTPException:
387
+ raise
388
+ except Exception as e:
389
+ logger.error(f"Update session status error: {e}", exc_info=True)
390
+ raise HTTPException(status_code=500, detail=str(e)) from e
391
+
392
+ @router.post("/update_summary")
393
+ async def update_session_summary(request: Request) -> dict[str, Any]:
394
+ """
395
+ Update session summary path.
396
+ """
397
+ try:
398
+ if server.session_manager is None:
399
+ raise HTTPException(status_code=503, detail="Session manager not available")
400
+
401
+ body = await request.json()
402
+ session_id = body.get("session_id")
403
+ summary_path = body.get("summary_path")
404
+
405
+ if not session_id or not summary_path:
406
+ raise HTTPException(
407
+ status_code=400, detail="Required fields: session_id, summary_path"
408
+ )
409
+
410
+ session = server.session_manager.update_summary(session_id, summary_path)
411
+
412
+ if session is None:
413
+ raise HTTPException(status_code=404, detail="Session not found")
414
+
415
+ return {"session": session.to_dict()}
416
+
417
+ except HTTPException:
418
+ raise
419
+ except Exception as e:
420
+ logger.error(f"Update session summary error: {e}", exc_info=True)
421
+ raise HTTPException(status_code=500, detail=str(e)) from e
422
+
423
+ @router.post("/{session_id}/stop")
424
+ async def stop_session(session_id: str, request: Request) -> dict[str, Any]:
425
+ """
426
+ Signal a session to stop gracefully.
427
+
428
+ Allows external systems to request a graceful stop of an autonomous session.
429
+ The session will check for this signal and stop at the next opportunity.
430
+
431
+ Args:
432
+ session_id: Session ID to stop
433
+ request: Request body with optional reason and source
434
+
435
+ Returns:
436
+ Stop signal confirmation
437
+ """
438
+ metrics.inc_counter("http_requests_total")
439
+
440
+ try:
441
+ # Get HookManager from app state
442
+ if not hasattr(request.app.state, "hook_manager"):
443
+ raise HTTPException(status_code=503, detail="Hook manager not available")
444
+
445
+ hook_manager = request.app.state.hook_manager
446
+ if not hasattr(hook_manager, "_stop_registry") or not hook_manager._stop_registry:
447
+ raise HTTPException(status_code=503, detail="Stop registry not available")
448
+
449
+ stop_registry = hook_manager._stop_registry
450
+
451
+ # Parse optional body parameters
452
+ body: dict[str, Any] = {}
453
+ try:
454
+ body = await request.json()
455
+ except Exception:
456
+ pass # nosec B110 - empty body is fine
457
+
458
+ reason = body.get("reason", "External stop request")
459
+ source = body.get("source", "http_api")
460
+
461
+ # Signal the stop
462
+ signal = stop_registry.signal_stop(
463
+ session_id=session_id,
464
+ reason=reason,
465
+ source=source,
466
+ )
467
+
468
+ logger.info(f"Stop signal sent to session {session_id}: {reason}")
469
+
470
+ return {
471
+ "status": "stop_signaled",
472
+ "session_id": session_id,
473
+ "signal_id": signal.signal_id,
474
+ "reason": signal.reason,
475
+ "source": signal.source,
476
+ "signaled_at": signal.signaled_at.isoformat(),
477
+ }
478
+
479
+ except HTTPException:
480
+ metrics.inc_counter("http_requests_errors_total")
481
+ raise
482
+ except Exception as e:
483
+ metrics.inc_counter("http_requests_errors_total")
484
+ logger.error(f"Error sending stop signal: {e}", exc_info=True)
485
+ raise HTTPException(status_code=500, detail=str(e)) from e
486
+
487
+ @router.get("/{session_id}/stop")
488
+ async def get_stop_signal(session_id: str, request: Request) -> dict[str, Any]:
489
+ """
490
+ Check if a session has a pending stop signal.
491
+
492
+ Args:
493
+ session_id: Session ID to check
494
+
495
+ Returns:
496
+ Stop signal status and details if present
497
+ """
498
+ metrics.inc_counter("http_requests_total")
499
+
500
+ try:
501
+ # Get HookManager from app state
502
+ if not hasattr(request.app.state, "hook_manager"):
503
+ raise HTTPException(status_code=503, detail="Hook manager not available")
504
+
505
+ hook_manager = request.app.state.hook_manager
506
+ if not hasattr(hook_manager, "_stop_registry") or not hook_manager._stop_registry:
507
+ raise HTTPException(status_code=503, detail="Stop registry not available")
508
+
509
+ stop_registry = hook_manager._stop_registry
510
+
511
+ signal = stop_registry.get_signal(session_id)
512
+
513
+ if signal is None:
514
+ return {
515
+ "has_signal": False,
516
+ "session_id": session_id,
517
+ }
518
+
519
+ return {
520
+ "has_signal": True,
521
+ "session_id": session_id,
522
+ "signal_id": signal.signal_id,
523
+ "reason": signal.reason,
524
+ "source": signal.source,
525
+ "signaled_at": signal.signaled_at.isoformat(),
526
+ "acknowledged": signal.acknowledged,
527
+ "acknowledged_at": (
528
+ signal.acknowledged_at.isoformat() if signal.acknowledged_at else None
529
+ ),
530
+ }
531
+
532
+ except HTTPException:
533
+ metrics.inc_counter("http_requests_errors_total")
534
+ raise
535
+ except Exception as e:
536
+ metrics.inc_counter("http_requests_errors_total")
537
+ logger.error(f"Error checking stop signal: {e}", exc_info=True)
538
+ raise HTTPException(status_code=500, detail=str(e)) from e
539
+
540
+ @router.delete("/{session_id}/stop")
541
+ async def clear_stop_signal(session_id: str, request: Request) -> dict[str, Any]:
542
+ """
543
+ Clear a stop signal for a session.
544
+
545
+ Useful for resetting a session's stop state after handling.
546
+
547
+ Args:
548
+ session_id: Session ID to clear signal for
549
+
550
+ Returns:
551
+ Confirmation of signal cleared
552
+ """
553
+ metrics.inc_counter("http_requests_total")
554
+
555
+ try:
556
+ # Get HookManager from app state
557
+ if not hasattr(request.app.state, "hook_manager"):
558
+ raise HTTPException(status_code=503, detail="Hook manager not available")
559
+
560
+ hook_manager = request.app.state.hook_manager
561
+ if not hasattr(hook_manager, "_stop_registry") or not hook_manager._stop_registry:
562
+ raise HTTPException(status_code=503, detail="Stop registry not available")
563
+
564
+ stop_registry = hook_manager._stop_registry
565
+
566
+ cleared = stop_registry.clear(session_id)
567
+
568
+ return {
569
+ "status": "cleared" if cleared else "no_signal",
570
+ "session_id": session_id,
571
+ "was_present": cleared,
572
+ }
573
+
574
+ except HTTPException:
575
+ metrics.inc_counter("http_requests_errors_total")
576
+ raise
577
+ except Exception as e:
578
+ metrics.inc_counter("http_requests_errors_total")
579
+ logger.error(f"Error clearing stop signal: {e}", exc_info=True)
580
+ raise HTTPException(status_code=500, detail=str(e)) from e
581
+
582
+ return router