gobby 0.2.7__py3-none-any.whl → 0.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gobby/__init__.py +1 -1
- gobby/adapters/claude_code.py +99 -61
- gobby/adapters/gemini.py +140 -38
- gobby/agents/isolation.py +130 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn_executor.py +43 -13
- gobby/agents/spawners/macos.py +26 -1
- gobby/app_context.py +59 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/memory.py +185 -0
- gobby/cli/utils.py +5 -17
- gobby/clones/git.py +177 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +87 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +573 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/hook_manager.py +21 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/claude.py +377 -42
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +2 -2
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/registries.py +35 -4
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +45 -9
- gobby/mcp_proxy/tools/artifacts.py +46 -12
- gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
- gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
- gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
- gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
- gobby/mcp_proxy/tools/spawn_agent.py +44 -6
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows/__init__.py +266 -0
- gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
- gobby/mcp_proxy/tools/workflows/_import.py +112 -0
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
- gobby/mcp_proxy/tools/workflows/_query.py +207 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
- gobby/mcp_proxy/tools/worktrees.py +32 -7
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/extractor.py +15 -1
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +36 -10
- gobby/servers/http.py +186 -149
- gobby/servers/routes/admin.py +12 -0
- gobby/servers/routes/mcp/endpoints/execution.py +15 -7
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +50 -3
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +4 -4
- gobby/sessions/manager.py +9 -0
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +46 -4
- gobby/storage/sessions.py +4 -2
- gobby/storage/skills.py +87 -7
- gobby/tasks/external_validator.py +4 -17
- gobby/tasks/validation.py +13 -87
- gobby/tools/summarizer.py +18 -51
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +5 -0
- gobby/workflows/context_actions.py +21 -24
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +109 -1
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/engine.py +96 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/lifecycle_evaluator.py +2 -1
- gobby/workflows/memory_actions.py +11 -0
- gobby/workflows/safe_evaluator.py +8 -0
- gobby/workflows/summary_actions.py +123 -50
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/RECORD +99 -107
- gobby/cli/tui.py +0 -34
- gobby/hooks/event_handlers.py +0 -909
- gobby/mcp_proxy/tools/workflows.py +0 -973
- gobby/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/top_level.txt +0 -0
|
@@ -19,23 +19,35 @@ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
21
|
from gobby.storage.artifacts import LocalArtifactManager
|
|
22
|
-
from gobby.storage.database import
|
|
22
|
+
from gobby.storage.database import DatabaseProtocol
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def create_artifacts_registry(
|
|
26
|
-
db:
|
|
26
|
+
db: DatabaseProtocol | None = None,
|
|
27
27
|
artifact_manager: LocalArtifactManager | None = None,
|
|
28
|
+
session_manager: Any | None = None,
|
|
28
29
|
) -> InternalToolRegistry:
|
|
29
30
|
"""
|
|
30
31
|
Create an artifacts tool registry with all artifact-related tools.
|
|
31
32
|
|
|
32
33
|
Args:
|
|
33
|
-
db:
|
|
34
|
+
db: DatabaseProtocol instance (used to create artifact_manager if not provided)
|
|
34
35
|
artifact_manager: LocalArtifactManager instance
|
|
36
|
+
session_manager: Session manager for resolving session references
|
|
35
37
|
|
|
36
38
|
Returns:
|
|
37
39
|
InternalToolRegistry with artifact tools registered
|
|
38
40
|
"""
|
|
41
|
+
from gobby.utils.project_context import get_project_context
|
|
42
|
+
|
|
43
|
+
def _resolve_session_id(ref: str) -> str:
|
|
44
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID."""
|
|
45
|
+
if session_manager is None:
|
|
46
|
+
return ref # No resolution available, return as-is
|
|
47
|
+
ctx = get_project_context()
|
|
48
|
+
project_id = ctx.get("id") if ctx else None
|
|
49
|
+
return str(session_manager.resolve_session_reference(ref, project_id))
|
|
50
|
+
|
|
39
51
|
# Create artifact manager if not provided
|
|
40
52
|
if artifact_manager is None:
|
|
41
53
|
if db is None:
|
|
@@ -55,7 +67,7 @@ def create_artifacts_registry(
|
|
|
55
67
|
|
|
56
68
|
@registry.tool(
|
|
57
69
|
name="search_artifacts",
|
|
58
|
-
description="Search artifacts by content using full-text search.",
|
|
70
|
+
description="Search artifacts by content using full-text search. Accepts #N, N, UUID, or prefix for session_id.",
|
|
59
71
|
)
|
|
60
72
|
def search_artifacts(
|
|
61
73
|
query: str,
|
|
@@ -68,7 +80,7 @@ def create_artifacts_registry(
|
|
|
68
80
|
|
|
69
81
|
Args:
|
|
70
82
|
query: Search query text
|
|
71
|
-
session_id: Optional session
|
|
83
|
+
session_id: Optional session reference (accepts #N, N, UUID, or prefix) to filter by
|
|
72
84
|
artifact_type: Optional artifact type to filter by (code, diff, error, etc.)
|
|
73
85
|
limit: Maximum number of results (default: 50)
|
|
74
86
|
|
|
@@ -78,10 +90,18 @@ def create_artifacts_registry(
|
|
|
78
90
|
if not query or not query.strip():
|
|
79
91
|
return {"success": True, "artifacts": [], "count": 0}
|
|
80
92
|
|
|
93
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
94
|
+
resolved_session_id = session_id
|
|
95
|
+
if session_id:
|
|
96
|
+
try:
|
|
97
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
98
|
+
except ValueError as e:
|
|
99
|
+
return {"success": False, "error": str(e), "artifacts": []}
|
|
100
|
+
|
|
81
101
|
try:
|
|
82
102
|
artifacts = _artifact_manager.search_artifacts(
|
|
83
103
|
query_text=query,
|
|
84
|
-
session_id=
|
|
104
|
+
session_id=resolved_session_id,
|
|
85
105
|
artifact_type=artifact_type,
|
|
86
106
|
limit=limit,
|
|
87
107
|
)
|
|
@@ -95,7 +115,7 @@ def create_artifacts_registry(
|
|
|
95
115
|
|
|
96
116
|
@registry.tool(
|
|
97
117
|
name="list_artifacts",
|
|
98
|
-
description="List artifacts with optional filters.",
|
|
118
|
+
description="List artifacts with optional filters. Accepts #N, N, UUID, or prefix for session_id.",
|
|
99
119
|
)
|
|
100
120
|
def list_artifacts(
|
|
101
121
|
session_id: str | None = None,
|
|
@@ -107,7 +127,7 @@ def create_artifacts_registry(
|
|
|
107
127
|
List artifacts with optional filters.
|
|
108
128
|
|
|
109
129
|
Args:
|
|
110
|
-
session_id: Optional session
|
|
130
|
+
session_id: Optional session reference (accepts #N, N, UUID, or prefix) to filter by
|
|
111
131
|
artifact_type: Optional artifact type to filter by
|
|
112
132
|
limit: Maximum number of results (default: 100)
|
|
113
133
|
offset: Offset for pagination (default: 0)
|
|
@@ -115,9 +135,17 @@ def create_artifacts_registry(
|
|
|
115
135
|
Returns:
|
|
116
136
|
Dict with success status and list of artifacts
|
|
117
137
|
"""
|
|
138
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
139
|
+
resolved_session_id = session_id
|
|
140
|
+
if session_id:
|
|
141
|
+
try:
|
|
142
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
143
|
+
except ValueError as e:
|
|
144
|
+
return {"success": False, "error": str(e), "artifacts": []}
|
|
145
|
+
|
|
118
146
|
try:
|
|
119
147
|
artifacts = _artifact_manager.list_artifacts(
|
|
120
|
-
session_id=
|
|
148
|
+
session_id=resolved_session_id,
|
|
121
149
|
artifact_type=artifact_type,
|
|
122
150
|
limit=limit,
|
|
123
151
|
offset=offset,
|
|
@@ -161,7 +189,7 @@ def create_artifacts_registry(
|
|
|
161
189
|
|
|
162
190
|
@registry.tool(
|
|
163
191
|
name="get_timeline",
|
|
164
|
-
description="Get artifacts for a session in chronological order.",
|
|
192
|
+
description="Get artifacts for a session in chronological order. Accepts #N, N, UUID, or prefix for session_id.",
|
|
165
193
|
)
|
|
166
194
|
def get_timeline(
|
|
167
195
|
session_id: str | None = None,
|
|
@@ -172,7 +200,7 @@ def create_artifacts_registry(
|
|
|
172
200
|
Get artifacts for a session in chronological order (oldest first).
|
|
173
201
|
|
|
174
202
|
Args:
|
|
175
|
-
session_id: Required session
|
|
203
|
+
session_id: Required session reference (accepts #N, N, UUID, or prefix) to get timeline for
|
|
176
204
|
artifact_type: Optional artifact type to filter by
|
|
177
205
|
limit: Maximum number of results (default: 100)
|
|
178
206
|
|
|
@@ -186,10 +214,16 @@ def create_artifacts_registry(
|
|
|
186
214
|
"artifacts": [],
|
|
187
215
|
}
|
|
188
216
|
|
|
217
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
218
|
+
try:
|
|
219
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
220
|
+
except ValueError as e:
|
|
221
|
+
return {"success": False, "error": str(e), "artifacts": []}
|
|
222
|
+
|
|
189
223
|
try:
|
|
190
224
|
# Get artifacts (list_artifacts returns newest first by default)
|
|
191
225
|
artifacts = _artifact_manager.list_artifacts(
|
|
192
|
-
session_id=
|
|
226
|
+
session_id=resolved_session_id,
|
|
193
227
|
artifact_type=artifact_type,
|
|
194
228
|
limit=limit,
|
|
195
229
|
offset=0,
|
|
@@ -27,9 +27,18 @@ def register_commits_tools(
|
|
|
27
27
|
session_manager: LocalSessionManager instance for session operations
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
+
def _resolve_session_id(ref: str) -> str:
|
|
31
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID."""
|
|
32
|
+
from gobby.utils.project_context import get_project_context
|
|
33
|
+
|
|
34
|
+
project_ctx = get_project_context()
|
|
35
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
36
|
+
|
|
37
|
+
return session_manager.resolve_session_reference(ref, project_id)
|
|
38
|
+
|
|
30
39
|
@registry.tool(
|
|
31
40
|
name="get_session_commits",
|
|
32
|
-
description="Get git commits made during a session timeframe.",
|
|
41
|
+
description="Get git commits made during a session timeframe. Accepts #N, N, UUID, or prefix for session_id.",
|
|
33
42
|
)
|
|
34
43
|
def get_session_commits(
|
|
35
44
|
session_id: str,
|
|
@@ -42,7 +51,7 @@ def register_commits_tools(
|
|
|
42
51
|
git log within that timeframe.
|
|
43
52
|
|
|
44
53
|
Args:
|
|
45
|
-
session_id: Session
|
|
54
|
+
session_id: Session reference - supports #N, N (seq_num), UUID, or prefix
|
|
46
55
|
max_commits: Maximum commits to return (default 20)
|
|
47
56
|
|
|
48
57
|
Returns:
|
|
@@ -55,21 +64,15 @@ def register_commits_tools(
|
|
|
55
64
|
if session_manager is None:
|
|
56
65
|
return {"error": "Session manager not available"}
|
|
57
66
|
|
|
58
|
-
#
|
|
59
|
-
|
|
67
|
+
# Resolve session reference (#N, N, UUID, or prefix)
|
|
68
|
+
try:
|
|
69
|
+
resolved_id = _resolve_session_id(session_id)
|
|
70
|
+
session = session_manager.get(resolved_id)
|
|
71
|
+
except ValueError as e:
|
|
72
|
+
return {"error": str(e)}
|
|
73
|
+
|
|
60
74
|
if not session:
|
|
61
|
-
|
|
62
|
-
sessions = session_manager.list(limit=100)
|
|
63
|
-
matches = [s for s in sessions if s.id.startswith(session_id)]
|
|
64
|
-
if len(matches) == 1:
|
|
65
|
-
session = matches[0]
|
|
66
|
-
elif len(matches) > 1:
|
|
67
|
-
return {
|
|
68
|
-
"error": f"Ambiguous session ID prefix '{session_id}'",
|
|
69
|
-
"matches": [s.id for s in matches[:5]],
|
|
70
|
-
}
|
|
71
|
-
else:
|
|
72
|
-
return {"error": f"Session {session_id} not found"}
|
|
75
|
+
return {"error": f"Session {session_id} not found"}
|
|
73
76
|
|
|
74
77
|
# Get working directory from transcript path or project
|
|
75
78
|
cwd = None
|
|
@@ -163,12 +166,12 @@ def register_commits_tools(
|
|
|
163
166
|
|
|
164
167
|
@registry.tool(
|
|
165
168
|
name="mark_loop_complete",
|
|
166
|
-
description="""Mark the autonomous loop as complete, preventing session chaining.
|
|
169
|
+
description="""Mark the autonomous loop as complete, preventing session chaining. Accepts #N, N, UUID, or prefix for session_id.
|
|
167
170
|
|
|
168
171
|
Args:
|
|
169
|
-
session_id: (REQUIRED) Your session ID. Get it from:
|
|
170
|
-
1. Your injected context (look for 'session_id: xxx')
|
|
171
|
-
2. Or call
|
|
172
|
+
session_id: (REQUIRED) Your session ID. Accepts #N, N, UUID, or prefix. Get it from:
|
|
173
|
+
1. Your injected context (look for 'Session Ref: #N' or 'session_id: xxx')
|
|
174
|
+
2. Or call get_current_session(external_id, source) first""",
|
|
172
175
|
)
|
|
173
176
|
def mark_loop_complete(session_id: str) -> dict[str, Any]:
|
|
174
177
|
"""
|
|
@@ -184,16 +187,20 @@ Args:
|
|
|
184
187
|
- The user has explicitly asked to stop
|
|
185
188
|
|
|
186
189
|
Args:
|
|
187
|
-
session_id: Session
|
|
190
|
+
session_id: Session reference - supports #N, N (seq_num), UUID, or prefix (REQUIRED)
|
|
188
191
|
|
|
189
192
|
Returns:
|
|
190
193
|
Success status and session details
|
|
191
194
|
"""
|
|
192
195
|
if not session_manager:
|
|
193
|
-
|
|
196
|
+
return {"error": "Session manager not available"}
|
|
194
197
|
|
|
195
|
-
#
|
|
196
|
-
|
|
198
|
+
# Resolve session reference (#N, N, UUID, or prefix)
|
|
199
|
+
try:
|
|
200
|
+
resolved_id = _resolve_session_id(session_id)
|
|
201
|
+
session = session_manager.get(resolved_id)
|
|
202
|
+
except ValueError as e:
|
|
203
|
+
return {"error": str(e), "session_id": session_id}
|
|
197
204
|
|
|
198
205
|
if not session:
|
|
199
206
|
return {"error": f"Session {session_id} not found", "session_id": session_id}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This module contains MCP tools for:
|
|
4
4
|
- Getting session details (get_session)
|
|
5
|
-
- Getting current session (
|
|
5
|
+
- Getting current session (get_current_session)
|
|
6
6
|
- Listing sessions (list_sessions)
|
|
7
7
|
- Session statistics (session_stats)
|
|
8
8
|
"""
|
|
@@ -71,7 +71,7 @@ def register_crud_tools(
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
@registry.tool(
|
|
74
|
-
name="
|
|
74
|
+
name="get_current_session",
|
|
75
75
|
description="""Get YOUR current session ID - the CORRECT way to look up your session.
|
|
76
76
|
|
|
77
77
|
Use this when session_id wasn't in your injected context. Pass your external_id
|
|
@@ -79,7 +79,7 @@ Use this when session_id wasn't in your injected context. Pass your external_id
|
|
|
79
79
|
|
|
80
80
|
DO NOT use list_sessions to find your session - it won't work with multiple active sessions.""",
|
|
81
81
|
)
|
|
82
|
-
def
|
|
82
|
+
def get_current_session(
|
|
83
83
|
external_id: str,
|
|
84
84
|
source: str,
|
|
85
85
|
) -> dict[str, Any]:
|
|
@@ -148,7 +148,7 @@ DO NOT use list_sessions to find your session - it won't work with multiple acti
|
|
|
148
148
|
WARNING: Do NOT use this to find your own session_id!
|
|
149
149
|
- `list_sessions(status="active", limit=1)` will NOT reliably return YOUR session
|
|
150
150
|
- Multiple sessions can be active simultaneously (parallel agents, multiple terminals)
|
|
151
|
-
- Use `
|
|
151
|
+
- Use `get_current_session(external_id, source)` instead - it uses your unique session key
|
|
152
152
|
|
|
153
153
|
This tool is for browsing/listing sessions, not for self-identification.""",
|
|
154
154
|
)
|
|
@@ -192,7 +192,7 @@ This tool is for browsing/listing sessions, not for self-identification.""",
|
|
|
192
192
|
"warning": (
|
|
193
193
|
"list_sessions(status='active', limit=1) will NOT reliably get YOUR session_id! "
|
|
194
194
|
"Multiple sessions can be active simultaneously. "
|
|
195
|
-
"Use
|
|
195
|
+
"Use get_current_session(external_id='<your-external-id>', source='claude') instead."
|
|
196
196
|
),
|
|
197
197
|
"hint": "Your external_id is in your transcript path: /path/to/<external_id>.jsonl",
|
|
198
198
|
"sessions": [s.to_dict() for s in sessions],
|
|
@@ -123,23 +123,29 @@ def register_handoff_tools(
|
|
|
123
123
|
registry: The InternalToolRegistry to register tools with
|
|
124
124
|
session_manager: LocalSessionManager instance for session operations
|
|
125
125
|
"""
|
|
126
|
+
from gobby.utils.project_context import get_project_context
|
|
127
|
+
|
|
128
|
+
def _resolve_session_id(ref: str) -> str:
|
|
129
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID."""
|
|
130
|
+
project_ctx = get_project_context()
|
|
131
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
132
|
+
|
|
133
|
+
return session_manager.resolve_session_reference(ref, project_id)
|
|
126
134
|
|
|
127
135
|
@registry.tool(
|
|
128
136
|
name="get_handoff_context",
|
|
129
|
-
description="Get the handoff context (compact_markdown) for a session. Accepts #N, UUID, or prefix.",
|
|
137
|
+
description="Get the handoff context (compact_markdown) for a session. Accepts #N, N, UUID, or prefix.",
|
|
130
138
|
)
|
|
131
139
|
def get_handoff_context(session_id: str) -> dict[str, Any]:
|
|
132
140
|
"""
|
|
133
141
|
Retrieve stored handoff context.
|
|
134
142
|
|
|
135
143
|
Args:
|
|
136
|
-
session_id: Session reference - supports #N (
|
|
144
|
+
session_id: Session reference - supports #N, N (seq_num), UUID, or prefix
|
|
137
145
|
|
|
138
146
|
Returns:
|
|
139
147
|
Session ID, compact_markdown, and whether context exists
|
|
140
148
|
"""
|
|
141
|
-
from gobby.utils.project_context import get_project_context
|
|
142
|
-
|
|
143
149
|
if not session_manager:
|
|
144
150
|
raise RuntimeError("Session manager not available")
|
|
145
151
|
|
|
@@ -165,12 +171,12 @@ def register_handoff_tools(
|
|
|
165
171
|
|
|
166
172
|
@registry.tool(
|
|
167
173
|
name="create_handoff",
|
|
168
|
-
description="""Create handoff context by extracting structured data from the session transcript.
|
|
174
|
+
description="""Create handoff context by extracting structured data from the session transcript. Accepts #N, N, UUID, or prefix for session_id.
|
|
169
175
|
|
|
170
176
|
Args:
|
|
171
|
-
session_id: (REQUIRED) Your session ID. Get it from:
|
|
172
|
-
1. Your injected context (look for 'session_id: xxx')
|
|
173
|
-
2. Or call
|
|
177
|
+
session_id: (REQUIRED) Your session ID. Accepts #N, N, UUID, or prefix. Get it from:
|
|
178
|
+
1. Your injected context (look for 'Session Ref: #N' or 'session_id: xxx')
|
|
179
|
+
2. Or call get_current_session(external_id, source) first""",
|
|
174
180
|
)
|
|
175
181
|
async def create_handoff(
|
|
176
182
|
session_id: str,
|
|
@@ -187,7 +193,7 @@ Args:
|
|
|
187
193
|
Always saves to database. Optionally writes to file.
|
|
188
194
|
|
|
189
195
|
Args:
|
|
190
|
-
session_id: Session
|
|
196
|
+
session_id: Session reference - supports #N, N (seq_num), UUID, or prefix (REQUIRED)
|
|
191
197
|
notes: Additional notes to include in handoff
|
|
192
198
|
compact: Generate compact summary only (default: False, neither = both)
|
|
193
199
|
full: Generate full LLM summary only (default: False, neither = both)
|
|
@@ -207,19 +213,12 @@ Args:
|
|
|
207
213
|
if session_manager is None:
|
|
208
214
|
return {"success": False, "error": "Session manager not available"}
|
|
209
215
|
|
|
210
|
-
#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if len(matches) == 1:
|
|
217
|
-
session = matches[0]
|
|
218
|
-
elif len(matches) > 1:
|
|
219
|
-
return {
|
|
220
|
-
"error": f"Ambiguous session ID prefix '{session_id}'",
|
|
221
|
-
"matches": [s.id for s in matches[:5]],
|
|
222
|
-
}
|
|
216
|
+
# Resolve session reference (#N, N, UUID, or prefix)
|
|
217
|
+
try:
|
|
218
|
+
resolved_id = _resolve_session_id(session_id)
|
|
219
|
+
session = session_manager.get(resolved_id)
|
|
220
|
+
except ValueError as e:
|
|
221
|
+
return {"success": False, "error": str(e), "session_id": session_id}
|
|
223
222
|
|
|
224
223
|
if not session:
|
|
225
224
|
return {"success": False, "error": "No session found", "session_id": session_id}
|
|
@@ -397,7 +396,7 @@ Args:
|
|
|
397
396
|
|
|
398
397
|
@registry.tool(
|
|
399
398
|
name="pickup",
|
|
400
|
-
description="Restore context from a previous session's handoff. For CLIs/IDEs without hooks.",
|
|
399
|
+
description="Restore context from a previous session's handoff. For CLIs/IDEs without hooks. Accepts #N, N, UUID, or prefix for session_id.",
|
|
401
400
|
)
|
|
402
401
|
def pickup(
|
|
403
402
|
session_id: str | None = None,
|
|
@@ -413,10 +412,10 @@ Args:
|
|
|
413
412
|
for injection into a new session.
|
|
414
413
|
|
|
415
414
|
Args:
|
|
416
|
-
session_id:
|
|
415
|
+
session_id: Session reference - supports #N, N (seq_num), UUID, or prefix (optional)
|
|
417
416
|
project_id: Project ID to find parent session in (optional)
|
|
418
417
|
source: Filter by CLI source - claude_code, gemini, codex (optional)
|
|
419
|
-
link_child_session_id:
|
|
418
|
+
link_child_session_id: Session to link as child - supports #N, N, UUID, or prefix (optional)
|
|
420
419
|
|
|
421
420
|
Returns:
|
|
422
421
|
Handoff context markdown and session metadata
|
|
@@ -428,20 +427,13 @@ Args:
|
|
|
428
427
|
|
|
429
428
|
parent_session = None
|
|
430
429
|
|
|
431
|
-
# Option 1: Direct session_id lookup
|
|
430
|
+
# Option 1: Direct session_id lookup with resolution
|
|
432
431
|
if session_id:
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if len(matches) == 1:
|
|
439
|
-
parent_session = matches[0]
|
|
440
|
-
elif len(matches) > 1:
|
|
441
|
-
return {
|
|
442
|
-
"error": f"Ambiguous session ID prefix '{session_id}'",
|
|
443
|
-
"matches": [s.id for s in matches[:5]],
|
|
444
|
-
}
|
|
432
|
+
try:
|
|
433
|
+
resolved_id = _resolve_session_id(session_id)
|
|
434
|
+
parent_session = session_manager.get(resolved_id)
|
|
435
|
+
except ValueError as e:
|
|
436
|
+
return {"error": str(e)}
|
|
445
437
|
|
|
446
438
|
# Option 2: Find parent by project_id and source
|
|
447
439
|
if not parent_session and project_id:
|
|
@@ -481,9 +473,21 @@ Args:
|
|
|
481
473
|
"message": "Session found but has no handoff context",
|
|
482
474
|
}
|
|
483
475
|
|
|
484
|
-
# Optionally link child session
|
|
476
|
+
# Optionally link child session (resolve if using #N format)
|
|
477
|
+
resolved_child_id = None
|
|
485
478
|
if link_child_session_id:
|
|
486
|
-
|
|
479
|
+
try:
|
|
480
|
+
resolved_child_id = _resolve_session_id(link_child_session_id)
|
|
481
|
+
session_manager.update_parent_session_id(resolved_child_id, parent_session.id)
|
|
482
|
+
except ValueError as e:
|
|
483
|
+
# Do not fallback to raw reference - propagate the error
|
|
484
|
+
return {
|
|
485
|
+
"found": True,
|
|
486
|
+
"session_id": parent_session.id,
|
|
487
|
+
"has_context": True,
|
|
488
|
+
"error": f"Failed to resolve child session '{link_child_session_id}': {e}",
|
|
489
|
+
"context": context,
|
|
490
|
+
}
|
|
487
491
|
|
|
488
492
|
return {
|
|
489
493
|
"found": True,
|
|
@@ -495,5 +499,5 @@ Args:
|
|
|
495
499
|
),
|
|
496
500
|
"parent_title": parent_session.title,
|
|
497
501
|
"parent_status": parent_session.status,
|
|
498
|
-
"linked_child": link_child_session_id,
|
|
502
|
+
"linked_child": resolved_child_id or link_child_session_id,
|
|
499
503
|
}
|
|
@@ -12,11 +12,13 @@ from typing import TYPE_CHECKING, Any
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
14
14
|
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
15
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def register_message_tools(
|
|
18
19
|
registry: InternalToolRegistry,
|
|
19
20
|
message_manager: LocalSessionMessageManager,
|
|
21
|
+
session_manager: LocalSessionManager | None = None,
|
|
20
22
|
) -> None:
|
|
21
23
|
"""
|
|
22
24
|
Register message retrieval and search tools with a registry.
|
|
@@ -24,11 +26,31 @@ def register_message_tools(
|
|
|
24
26
|
Args:
|
|
25
27
|
registry: The InternalToolRegistry to register tools with
|
|
26
28
|
message_manager: LocalSessionMessageManager instance for message operations
|
|
29
|
+
session_manager: LocalSessionManager for resolving session references
|
|
27
30
|
"""
|
|
28
31
|
|
|
32
|
+
def _resolve_session_id(session_id: str) -> str:
|
|
33
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
str: Resolved UUID on success
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If session reference cannot be resolved (when session_manager exists)
|
|
40
|
+
"""
|
|
41
|
+
if not session_manager:
|
|
42
|
+
return session_id # Fall back to raw value if no manager (backward compat)
|
|
43
|
+
|
|
44
|
+
from gobby.utils.project_context import get_project_context
|
|
45
|
+
|
|
46
|
+
project_ctx = get_project_context()
|
|
47
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
48
|
+
|
|
49
|
+
return session_manager.resolve_session_reference(session_id, project_id)
|
|
50
|
+
|
|
29
51
|
@registry.tool(
|
|
30
52
|
name="get_session_messages",
|
|
31
|
-
description="Get messages for a session.",
|
|
53
|
+
description="Get messages for a session. Accepts #N, N, UUID, or prefix for session_id.",
|
|
32
54
|
)
|
|
33
55
|
async def get_session_messages(
|
|
34
56
|
session_id: str,
|
|
@@ -40,7 +62,7 @@ def register_message_tools(
|
|
|
40
62
|
Get messages for a session.
|
|
41
63
|
|
|
42
64
|
Args:
|
|
43
|
-
session_id:
|
|
65
|
+
session_id: Session reference - supports #N, N (seq_num), UUID, or prefix
|
|
44
66
|
limit: Max messages to return
|
|
45
67
|
offset: Offset for pagination
|
|
46
68
|
full_content: If True, returns full content. If False (default), truncates large content.
|
|
@@ -48,8 +70,10 @@ def register_message_tools(
|
|
|
48
70
|
try:
|
|
49
71
|
if not message_manager:
|
|
50
72
|
raise RuntimeError("Message manager not available")
|
|
73
|
+
|
|
74
|
+
resolved_id = _resolve_session_id(session_id)
|
|
51
75
|
messages = await message_manager.get_messages(
|
|
52
|
-
session_id=
|
|
76
|
+
session_id=resolved_id,
|
|
53
77
|
limit=limit,
|
|
54
78
|
offset=offset,
|
|
55
79
|
)
|
|
@@ -79,7 +103,7 @@ def register_message_tools(
|
|
|
79
103
|
):
|
|
80
104
|
tr["content"] = tr["content"][:200] + "... (truncated)"
|
|
81
105
|
|
|
82
|
-
session_total = await message_manager.count_messages(
|
|
106
|
+
session_total = await message_manager.count_messages(resolved_id)
|
|
83
107
|
|
|
84
108
|
return {
|
|
85
109
|
"success": True,
|
|
@@ -95,7 +119,7 @@ def register_message_tools(
|
|
|
95
119
|
|
|
96
120
|
@registry.tool(
|
|
97
121
|
name="search_messages",
|
|
98
|
-
description="Search messages using Full Text Search (FTS).",
|
|
122
|
+
description="Search messages using Full Text Search (FTS). Accepts #N, N, UUID, or prefix for session_id.",
|
|
99
123
|
)
|
|
100
124
|
async def search_messages(
|
|
101
125
|
query: str,
|
|
@@ -108,16 +132,20 @@ def register_message_tools(
|
|
|
108
132
|
|
|
109
133
|
Args:
|
|
110
134
|
query: Search query
|
|
111
|
-
session_id: Optional session filter
|
|
135
|
+
session_id: Optional session filter - supports #N, N (seq_num), UUID, or prefix
|
|
112
136
|
limit: Max results
|
|
113
137
|
full_content: If True, returns full content. If False (default), truncates large content.
|
|
114
138
|
"""
|
|
115
139
|
try:
|
|
116
140
|
if not message_manager:
|
|
117
141
|
raise RuntimeError("Message manager not available")
|
|
142
|
+
|
|
143
|
+
resolved_session_id = None
|
|
144
|
+
if session_id:
|
|
145
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
118
146
|
results = await message_manager.search_messages(
|
|
119
147
|
query_text=query,
|
|
120
|
-
session_id=
|
|
148
|
+
session_id=resolved_session_id,
|
|
121
149
|
limit=limit,
|
|
122
150
|
)
|
|
123
151
|
|