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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- 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)
|