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,563 @@
1
+ """
2
+ Session Manager for multi-CLI session management (local-first).
3
+
4
+ Handles:
5
+ - Session registration with local SQLite storage
6
+ - Parent session lookup for context handoff
7
+ - Session status updates (active, expired, handoff_ready)
8
+ - Summary file reading (fallback when database is unavailable)
9
+
10
+ This module is CLI-agnostic and can be used by any CLI integration.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import threading
17
+ import time
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ if TYPE_CHECKING:
22
+ from gobby.config.app import DaemonConfig
23
+ from gobby.storage.artifacts import Artifact
24
+ from gobby.storage.sessions import LocalSessionManager as SessionStorage
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class SessionManager:
30
+ """
31
+ Manages session lifecycle for AI coding assistants (local-first).
32
+
33
+ Provides:
34
+ - Session registration and lookup
35
+ - Parent session discovery for context handoff
36
+ - Status management (active, expired, handoff_ready)
37
+ - Summary file reading (failover for database)
38
+
39
+ Thread-safe: Uses locks for session metadata and mapping caches.
40
+
41
+ Design Note:
42
+ `source` is a REQUIRED parameter on all session methods, not stored as instance variable.
43
+ Each adapter (Claude, Gemini, Codex) passes its source explicitly.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ session_storage: SessionStorage,
49
+ logger_instance: logging.Logger | None = None,
50
+ config: DaemonConfig | None = None,
51
+ ) -> None:
52
+ """
53
+ Initialize SessionManager.
54
+
55
+ Args:
56
+ session_storage: LocalSessionManager for SQLite operations
57
+ logger_instance: Optional logger instance
58
+ config: Optional DaemonConfig for summary file path
59
+ """
60
+ self._storage = session_storage
61
+ self.logger = logger_instance or logger
62
+ self._config = config
63
+
64
+ # Session caches with locks
65
+ # Key is (external_id, source) tuple to prevent cross-CLI collisions
66
+ self._session_mapping: dict[
67
+ tuple[str, str], str
68
+ ] = {} # (external_id, source) -> session_id
69
+ self._session_mapping_lock = threading.Lock()
70
+ self._session_metadata: dict[str, dict[str, Any]] = {} # session_id -> metadata
71
+ self._session_metadata_lock = threading.Lock()
72
+
73
+ def register_session(
74
+ self,
75
+ external_id: str,
76
+ machine_id: str,
77
+ source: str,
78
+ project_id: str,
79
+ parent_session_id: str | None = None,
80
+ jsonl_path: str | None = None,
81
+ title: str | None = None,
82
+ git_branch: str | None = None,
83
+ project_path: str | None = None,
84
+ terminal_context: dict[str, Any] | None = None,
85
+ ) -> str:
86
+ """
87
+ Register new session with local storage.
88
+
89
+ Args:
90
+ external_id: External session identifier (e.g., Claude Code session ID)
91
+ machine_id: Machine identifier
92
+ source: CLI source identifier (e.g., "claude", "gemini", "codex") - REQUIRED
93
+ project_id: Project ID (required - sessions must belong to a project)
94
+ parent_session_id: Optional parent session ID for handoff
95
+ jsonl_path: Optional path to session transcript JSONL file
96
+ title: Optional session title/summary
97
+ git_branch: Optional git branch name
98
+ project_path: Optional project path (for git extraction if git_branch not provided)
99
+
100
+ Returns:
101
+ session_id (database UUID)
102
+ """
103
+ working_dir = project_path or str(Path.cwd())
104
+
105
+ # Extract git_branch from project_path if not provided
106
+ if not git_branch:
107
+ try:
108
+ from gobby.utils.git import get_git_branch
109
+
110
+ git_branch = get_git_branch(working_dir)
111
+ if git_branch:
112
+ self.logger.debug(f"Extracted git_branch from project_path: {git_branch}")
113
+ except Exception as e:
114
+ self.logger.debug(f"Could not extract git_branch: {e}")
115
+
116
+ try:
117
+ # Register with local storage
118
+ session = self._storage.register(
119
+ external_id=external_id,
120
+ machine_id=machine_id,
121
+ source=source,
122
+ project_id=project_id,
123
+ title=title,
124
+ jsonl_path=jsonl_path,
125
+ git_branch=git_branch,
126
+ parent_session_id=parent_session_id,
127
+ terminal_context=terminal_context,
128
+ )
129
+
130
+ session_id: str = session.id
131
+
132
+ # Cache session mapping and metadata
133
+ with self._session_mapping_lock:
134
+ self._session_mapping[(external_id, source)] = session_id
135
+
136
+ with self._session_metadata_lock:
137
+ self._session_metadata[session_id] = {
138
+ "external_id": external_id,
139
+ "machine_id": machine_id,
140
+ "source": source,
141
+ "parent_session_id": parent_session_id,
142
+ "jsonl_path": jsonl_path,
143
+ "project_id": project_id,
144
+ "title": title,
145
+ "git_branch": git_branch,
146
+ }
147
+
148
+ self.logger.debug(f"Registered session {session_id} (external_id={external_id})")
149
+ return session_id
150
+
151
+ except Exception as e:
152
+ self.logger.error(f"Failed to register session: {e}", exc_info=True)
153
+ # Return a temporary session ID to allow hooks to continue
154
+ import uuid
155
+
156
+ return str(uuid.uuid4())
157
+
158
+ def find_parent_session(
159
+ self,
160
+ machine_id: str,
161
+ source: str,
162
+ project_id: str,
163
+ max_attempts: int = 30,
164
+ ) -> tuple[str, str | None] | None:
165
+ """
166
+ Find parent session marked as 'handoff_ready' for this machine and project.
167
+
168
+ Polls for up to max_attempts seconds waiting for the session-end hook
169
+ to mark the previous session as handoff_ready.
170
+
171
+ Args:
172
+ machine_id: Machine identifier
173
+ source: CLI source identifier (e.g., "claude", "gemini", "codex") - REQUIRED
174
+ project_id: Project ID (required for matching)
175
+ max_attempts: Maximum polling attempts (1 per second)
176
+
177
+ Returns:
178
+ Tuple of (parent_session_id, summary_markdown) or None if not found
179
+ """
180
+ attempt = 0
181
+
182
+ while attempt < max_attempts:
183
+ try:
184
+ # Find parent using local storage
185
+ session = self._storage.find_parent(
186
+ machine_id=machine_id,
187
+ source=source,
188
+ project_id=project_id,
189
+ )
190
+
191
+ if session:
192
+ self.logger.debug(
193
+ f"Found parent session {session.id} (attempt {attempt + 1}/{max_attempts})"
194
+ )
195
+ return (session.id, session.summary_markdown)
196
+
197
+ # Not found yet, wait and retry
198
+ attempt += 1
199
+ if attempt < max_attempts:
200
+ self.logger.debug(
201
+ f"No handoff_ready session yet, retrying in 1s (attempt {attempt}/{max_attempts})"
202
+ )
203
+ time.sleep(1)
204
+
205
+ except Exception as e:
206
+ self.logger.warning(
207
+ f"Error polling for parent session (attempt {attempt + 1}): {e}"
208
+ )
209
+ attempt += 1
210
+ if attempt < max_attempts:
211
+ time.sleep(1)
212
+ else:
213
+ self.logger.error(f"Exhausted retries finding parent session: {e}")
214
+ return None
215
+
216
+ # Exhausted retries
217
+ self.logger.debug(f"No handoff_ready session found after {max_attempts} attempts")
218
+ return None
219
+
220
+ def mark_session_expired(self, session_id: str) -> bool:
221
+ """
222
+ Mark a session as 'expired' after successful handoff.
223
+
224
+ Args:
225
+ session_id: Session ID to mark as expired
226
+
227
+ Returns:
228
+ True if updated successfully, False otherwise
229
+ """
230
+ return self.update_session_status(session_id, "expired")
231
+
232
+ def update_session_status(
233
+ self,
234
+ session_id: str,
235
+ status: str,
236
+ ) -> bool:
237
+ """
238
+ Update session status in database.
239
+
240
+ Args:
241
+ session_id: Internal database UUID (sessions.id)
242
+ status: New status value (active, paused, expired, archived, handoff_ready)
243
+
244
+ Returns:
245
+ True if updated successfully, False otherwise
246
+ """
247
+ try:
248
+ session = self._storage.update_status(session_id, status)
249
+ if session:
250
+ self.logger.debug(f"Session status updated: {session_id} -> {status}")
251
+ return True
252
+ else:
253
+ self.logger.warning(f"Session not found for status update: {session_id}")
254
+ return False
255
+
256
+ except Exception as e:
257
+ self.logger.error(f"Failed to update session status: {e}", exc_info=True)
258
+ return False
259
+
260
+ def lookup_session_id(
261
+ self, external_id: str, source: str, machine_id: str, project_id: str
262
+ ) -> str | None:
263
+ """
264
+ Look up session_id from database by full composite key.
265
+
266
+ Args:
267
+ external_id: External session identifier
268
+ source: CLI source identifier (e.g., "claude", "gemini", "codex")
269
+ machine_id: Machine identifier
270
+ project_id: Project identifier
271
+
272
+ Returns:
273
+ session_id (database PK) or None if not found
274
+ """
275
+ try:
276
+ # Check cache first (keyed by (external_id, source) to prevent cross-CLI collisions)
277
+ cache_key = (external_id, source)
278
+ with self._session_mapping_lock:
279
+ if cache_key in self._session_mapping:
280
+ return self._session_mapping[cache_key]
281
+
282
+ # Find session using full composite key (safe lookup)
283
+ session = self._storage.find_by_external_id(external_id, machine_id, project_id, source)
284
+
285
+ if session:
286
+ session_id: str = session.id
287
+ self.logger.debug(
288
+ f"Looked up session_id {session_id} for external_id {external_id}"
289
+ )
290
+ # Cache it
291
+ with self._session_mapping_lock:
292
+ self._session_mapping[cache_key] = session_id
293
+ return session_id
294
+
295
+ return None
296
+
297
+ except Exception as e:
298
+ self.logger.debug(f"Failed to lookup session_id from database: {e}", exc_info=True)
299
+ return None
300
+
301
+ def read_summary_file(self, session_id: str) -> str | None:
302
+ """
303
+ Read session summary from file (failover if database is empty).
304
+
305
+ Searches for file matching pattern: session_*_{session_id}.md
306
+
307
+ Args:
308
+ session_id: Session ID to read summary for
309
+
310
+ Returns:
311
+ Summary markdown or None if not found
312
+ """
313
+ # Get summary directory from config or use default
314
+ if self._config and self._config.session_summary:
315
+ summary_dir = Path(self._config.session_summary.summary_file_path).expanduser()
316
+ else:
317
+ summary_dir = Path.home() / ".gobby" / "session_summaries"
318
+
319
+ # Search for files matching session_*_{session_id}.md pattern
320
+ if summary_dir.exists():
321
+ for summary_file in summary_dir.glob(f"session_*_{session_id}.md"):
322
+ try:
323
+ return summary_file.read_text()
324
+ except Exception as e:
325
+ self.logger.error(
326
+ f"Failed to read summary file {summary_file}: {e}", exc_info=True
327
+ )
328
+
329
+ return None
330
+
331
+ def get_session_id(self, external_id: str, source: str) -> str | None:
332
+ """
333
+ Get cached session_id for an external_id and source.
334
+
335
+ Args:
336
+ external_id: External session identifier
337
+ source: CLI source identifier (e.g., "claude", "gemini", "codex")
338
+
339
+ Returns:
340
+ session_id or None if not cached
341
+ """
342
+ with self._session_mapping_lock:
343
+ return self._session_mapping.get((external_id, source))
344
+
345
+ def cache_session_mapping(self, external_id: str, source: str, session_id: str) -> None:
346
+ """
347
+ Cache an (external_id, source) -> session_id mapping.
348
+
349
+ Args:
350
+ external_id: External session identifier
351
+ source: CLI source identifier (e.g., "claude", "gemini", "codex")
352
+ session_id: Database session ID
353
+ """
354
+ with self._session_mapping_lock:
355
+ self._session_mapping[(external_id, source)] = session_id
356
+
357
+ def get_session(self, session_id: str) -> dict[str, Any] | None:
358
+ """
359
+ Get session data by ID.
360
+
361
+ Args:
362
+ session_id: Database session ID
363
+
364
+ Returns:
365
+ Session dict or None if not found
366
+ """
367
+ session = self._storage.get(session_id)
368
+ if session:
369
+ return {
370
+ "id": session.id,
371
+ "external_id": session.external_id,
372
+ "machine_id": session.machine_id,
373
+ "source": session.source,
374
+ "project_id": session.project_id,
375
+ "title": session.title,
376
+ "status": session.status,
377
+ "jsonl_path": session.jsonl_path,
378
+ "summary_path": session.summary_path,
379
+ "git_branch": session.git_branch,
380
+ "parent_session_id": session.parent_session_id,
381
+ }
382
+ return None
383
+
384
+ def get_session_artifacts(
385
+ self,
386
+ session_id: str,
387
+ artifact_type: str | None = None,
388
+ limit: int | None = None,
389
+ include_parent: bool | None = None,
390
+ max_lineage_depth: int | None = None,
391
+ ) -> list[Artifact]:
392
+ """
393
+ Get artifacts for a session using LocalArtifactManager.
394
+
395
+ Args:
396
+ session_id: Session ID to get artifacts for
397
+ artifact_type: Optional filter by artifact type
398
+ limit: Maximum number of artifacts to return (default from config)
399
+ include_parent: Whether to include parent session artifacts (default from config)
400
+ max_lineage_depth: Maximum depth to traverse session lineage (default from config)
401
+
402
+ Returns:
403
+ List of Artifact objects
404
+ """
405
+ from gobby.storage.artifacts import LocalArtifactManager
406
+
407
+ # Get defaults from config
408
+ config = self._config.artifact_handoff if self._config else None
409
+ if limit is None:
410
+ limit = config.max_artifacts_in_handoff if config else 10
411
+ if include_parent is None:
412
+ include_parent = config.include_parent_artifacts if config else True
413
+ if max_lineage_depth is None:
414
+ max_lineage_depth = config.max_lineage_depth if config else 3
415
+
416
+ # Get artifact manager using storage's database
417
+ artifact_manager = LocalArtifactManager(self._storage.db)
418
+
419
+ all_artifacts = []
420
+
421
+ # Get artifacts for this session
422
+ artifacts = artifact_manager.list_artifacts(
423
+ session_id=session_id,
424
+ artifact_type=artifact_type,
425
+ limit=limit,
426
+ )
427
+ all_artifacts.extend(artifacts)
428
+
429
+ # If include_parent, traverse full session lineage up to max_lineage_depth
430
+ if include_parent:
431
+ current_session = self._storage.get(session_id)
432
+ depth = 0
433
+
434
+ while (
435
+ current_session and current_session.parent_session_id and depth < max_lineage_depth
436
+ ):
437
+ parent_id = current_session.parent_session_id
438
+ parent_artifacts = artifact_manager.list_artifacts(
439
+ session_id=parent_id,
440
+ artifact_type=artifact_type,
441
+ limit=limit,
442
+ )
443
+ all_artifacts.extend(parent_artifacts)
444
+
445
+ # Move up the lineage chain
446
+ current_session = self._storage.get(parent_id)
447
+ depth += 1
448
+
449
+ # Sort by created_at (newest first) and apply limit
450
+ all_artifacts.sort(key=lambda a: a.created_at, reverse=True)
451
+ return all_artifacts[:limit]
452
+
453
+ def generate_handoff_context(
454
+ self,
455
+ session_id: str,
456
+ max_artifacts: int | None = None,
457
+ max_context_size: int | None = None,
458
+ include_parent_artifacts: bool | None = None,
459
+ ) -> str:
460
+ """
461
+ Generate handoff context including artifacts for a session.
462
+
463
+ Args:
464
+ session_id: Session ID to generate context for
465
+ max_artifacts: Maximum number of artifacts to include (default from config)
466
+ max_context_size: Maximum context size in characters (default from config)
467
+ include_parent_artifacts: Whether to include parent session artifacts (default from config)
468
+
469
+ Returns:
470
+ Formatted context string with artifacts
471
+ """
472
+ # Get defaults from config
473
+ config = self._config.artifact_handoff if self._config else None
474
+ if max_artifacts is None:
475
+ max_artifacts = config.max_artifacts_in_handoff if config else 10
476
+ if max_context_size is None:
477
+ max_context_size = config.max_context_size if config else 50000
478
+ if include_parent_artifacts is None:
479
+ include_parent_artifacts = config.include_parent_artifacts if config else True
480
+
481
+ # Get session info
482
+ session = self.get_session(session_id)
483
+ if not session:
484
+ return ""
485
+
486
+ context_parts = []
487
+
488
+ # Add session header
489
+ context_parts.append(f"## Session Context: {session.get('title', session_id)}")
490
+ if session.get("git_branch"):
491
+ context_parts.append(f"Branch: {session['git_branch']}")
492
+
493
+ # Get artifacts
494
+ artifacts = self.get_session_artifacts(
495
+ session_id=session_id,
496
+ limit=max_artifacts,
497
+ include_parent=include_parent_artifacts,
498
+ )
499
+
500
+ if artifacts:
501
+ context_parts.append("\n## Session Artifacts\n")
502
+
503
+ current_size = sum(len(p) for p in context_parts)
504
+
505
+ for artifact in artifacts:
506
+ artifact_text = self._format_artifact(artifact)
507
+
508
+ # Check size limit
509
+ if current_size + len(artifact_text) > max_context_size:
510
+ context_parts.append("\n_[Artifacts truncated due to size limit]_")
511
+ break
512
+
513
+ context_parts.append(artifact_text)
514
+ current_size += len(artifact_text)
515
+
516
+ return "\n".join(context_parts)
517
+
518
+ def _format_artifact(self, artifact: Artifact) -> str:
519
+ """
520
+ Format an artifact for inclusion in handoff context.
521
+
522
+ Args:
523
+ artifact: Artifact object to format
524
+
525
+ Returns:
526
+ Formatted artifact string
527
+ """
528
+ parts = []
529
+
530
+ # Header with type and source
531
+ header = f"### {artifact.artifact_type.upper()}"
532
+ if artifact.source_file:
533
+ location = artifact.source_file
534
+ if artifact.line_start:
535
+ location += f":{artifact.line_start}"
536
+ if artifact.line_end and artifact.line_end != artifact.line_start:
537
+ location += f"-{artifact.line_end}"
538
+ header += f" - {location}"
539
+ parts.append(header)
540
+
541
+ # Format content based on type
542
+ if artifact.artifact_type == "code":
543
+ # Use language from metadata if available
544
+ language = ""
545
+ if artifact.metadata and "language" in artifact.metadata:
546
+ language = artifact.metadata["language"]
547
+ parts.append(f"```{language}")
548
+ parts.append(artifact.content)
549
+ parts.append("```")
550
+ elif artifact.artifact_type == "diff":
551
+ parts.append("```diff")
552
+ parts.append(artifact.content)
553
+ parts.append("```")
554
+ elif artifact.artifact_type == "error":
555
+ parts.append("```")
556
+ parts.append(artifact.content)
557
+ parts.append("```")
558
+ else:
559
+ # Plain text for other types
560
+ parts.append(artifact.content)
561
+
562
+ parts.append("") # Empty line after artifact
563
+ return "\n".join(parts)