gobby 0.2.7__py3-none-any.whl → 0.2.8__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/adapters/claude_code.py +96 -35
- 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/cli/__init__.py +0 -2
- gobby/cli/memory.py +185 -0
- gobby/clones/git.py +177 -0
- gobby/config/skills.py +31 -0
- gobby/hooks/event_handlers.py +109 -10
- gobby/hooks/hook_manager.py +19 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/mcp_proxy/instructions.py +2 -2
- gobby/mcp_proxy/registries.py +21 -4
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +45 -9
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- 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/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.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -7
- gobby/memory/extractor.py +15 -1
- gobby/runner.py +13 -0
- gobby/servers/routes/mcp/hooks.py +50 -3
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/manager.py +9 -0
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +23 -4
- gobby/storage/sessions.py +4 -2
- gobby/storage/skills.py +43 -3
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/blocking.py +13 -1
- gobby/workflows/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/hooks.py +41 -0
- 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.8.dist-info}/METADATA +1 -1
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/RECORD +56 -80
- gobby/cli/tui.py +0 -34
- 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.8.dist-info}/WHEEL +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -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
|
|
|
@@ -12,7 +12,6 @@ One tool: spawn_agent(prompt, agent="generic", isolation="current"|"worktree"|"c
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import logging
|
|
15
|
-
import socket
|
|
16
15
|
import uuid
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
@@ -22,10 +21,12 @@ from gobby.agents.isolation import (
|
|
|
22
21
|
SpawnConfig,
|
|
23
22
|
get_isolation_handler,
|
|
24
23
|
)
|
|
24
|
+
from gobby.agents.registry import RunningAgent, get_running_agent_registry
|
|
25
25
|
from gobby.agents.sandbox import SandboxConfig
|
|
26
26
|
from gobby.agents.spawn_executor import SpawnRequest, execute_spawn
|
|
27
27
|
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
28
28
|
from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
|
|
29
|
+
from gobby.utils.machine_id import get_machine_id
|
|
29
30
|
from gobby.utils.project_context import get_project_context
|
|
30
31
|
|
|
31
32
|
if TYPE_CHECKING:
|
|
@@ -264,13 +265,30 @@ async def spawn_agent_impl(
|
|
|
264
265
|
worktree_id=isolation_ctx.worktree_id,
|
|
265
266
|
clone_id=isolation_ctx.clone_id,
|
|
266
267
|
session_manager=runner._child_session_manager,
|
|
267
|
-
machine_id=
|
|
268
|
+
machine_id=get_machine_id() or "unknown",
|
|
268
269
|
sandbox_config=effective_sandbox_config,
|
|
269
270
|
)
|
|
270
271
|
|
|
271
272
|
spawn_result = await execute_spawn(spawn_request)
|
|
272
273
|
|
|
273
|
-
# 11.
|
|
274
|
+
# 11. Register with RunningAgentRegistry for send_to_parent/child messaging
|
|
275
|
+
# Only register if spawn succeeded and we have a valid child_session_id
|
|
276
|
+
if spawn_result.success and spawn_result.child_session_id is not None:
|
|
277
|
+
agent_registry = get_running_agent_registry()
|
|
278
|
+
agent_registry.add(
|
|
279
|
+
RunningAgent(
|
|
280
|
+
run_id=spawn_result.run_id,
|
|
281
|
+
session_id=spawn_result.child_session_id,
|
|
282
|
+
parent_session_id=parent_session_id,
|
|
283
|
+
mode=effective_mode,
|
|
284
|
+
pid=spawn_result.pid,
|
|
285
|
+
provider=effective_provider,
|
|
286
|
+
workflow_name=effective_workflow,
|
|
287
|
+
worktree_id=isolation_ctx.worktree_id,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# 12. Return response with isolation metadata
|
|
274
292
|
return {
|
|
275
293
|
"success": spawn_result.success,
|
|
276
294
|
"run_id": spawn_result.run_id,
|
|
@@ -295,6 +313,7 @@ def create_spawn_agent_registry(
|
|
|
295
313
|
git_manager: Any | None = None,
|
|
296
314
|
clone_storage: Any | None = None,
|
|
297
315
|
clone_manager: Any | None = None,
|
|
316
|
+
session_manager: Any | None = None,
|
|
298
317
|
) -> InternalToolRegistry:
|
|
299
318
|
"""
|
|
300
319
|
Create a spawn_agent tool registry with the unified spawn_agent tool.
|
|
@@ -307,10 +326,20 @@ def create_spawn_agent_registry(
|
|
|
307
326
|
git_manager: Git manager for worktree operations.
|
|
308
327
|
clone_storage: Storage for clone records.
|
|
309
328
|
clone_manager: Git manager for clone operations.
|
|
329
|
+
session_manager: Session manager for resolving session references.
|
|
310
330
|
|
|
311
331
|
Returns:
|
|
312
332
|
InternalToolRegistry with spawn_agent tool registered.
|
|
313
333
|
"""
|
|
334
|
+
|
|
335
|
+
def _resolve_session_id(ref: str) -> str:
|
|
336
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID."""
|
|
337
|
+
if session_manager is None:
|
|
338
|
+
return ref # No resolution available, return as-is
|
|
339
|
+
ctx = get_project_context()
|
|
340
|
+
project_id = ctx.get("id") if ctx else None
|
|
341
|
+
return str(session_manager.resolve_session_reference(ref, project_id))
|
|
342
|
+
|
|
314
343
|
registry = InternalToolRegistry(
|
|
315
344
|
name="gobby-spawn-agent",
|
|
316
345
|
description="Unified agent spawning with isolation support",
|
|
@@ -324,7 +353,8 @@ def create_spawn_agent_registry(
|
|
|
324
353
|
description=(
|
|
325
354
|
"Spawn a subagent to execute a task. Supports isolation modes: "
|
|
326
355
|
"'current' (work in current directory), 'worktree' (create git worktree), "
|
|
327
|
-
"'clone' (create shallow clone). Can use named agent definitions or raw parameters."
|
|
356
|
+
"'clone' (create shallow clone). Can use named agent definitions or raw parameters. "
|
|
357
|
+
"Accepts #N, N, UUID, or prefix for parent_session_id."
|
|
328
358
|
),
|
|
329
359
|
)
|
|
330
360
|
async def spawn_agent(
|
|
@@ -374,12 +404,20 @@ def create_spawn_agent_registry(
|
|
|
374
404
|
sandbox_mode: Sandbox mode (permissive/restrictive). Overrides agent_def.
|
|
375
405
|
sandbox_allow_network: Allow network access. Overrides agent_def.
|
|
376
406
|
sandbox_extra_paths: Extra paths for sandbox write access.
|
|
377
|
-
parent_session_id:
|
|
407
|
+
parent_session_id: Session reference (accepts #N, N, UUID, or prefix) for the parent session
|
|
378
408
|
project_path: Project path override
|
|
379
409
|
|
|
380
410
|
Returns:
|
|
381
411
|
Dict with success status, run_id, child_session_id, isolation metadata
|
|
382
412
|
"""
|
|
413
|
+
# Resolve parent_session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
414
|
+
resolved_parent_session_id = parent_session_id
|
|
415
|
+
if parent_session_id:
|
|
416
|
+
try:
|
|
417
|
+
resolved_parent_session_id = _resolve_session_id(parent_session_id)
|
|
418
|
+
except ValueError as e:
|
|
419
|
+
return {"success": False, "error": str(e)}
|
|
420
|
+
|
|
383
421
|
# Load agent definition (defaults to "generic")
|
|
384
422
|
agent_def = loader.load(agent)
|
|
385
423
|
if agent_def is None and agent != "generic":
|
|
@@ -410,7 +448,7 @@ def create_spawn_agent_registry(
|
|
|
410
448
|
sandbox_mode=sandbox_mode,
|
|
411
449
|
sandbox_allow_network=sandbox_allow_network,
|
|
412
450
|
sandbox_extra_paths=sandbox_extra_paths,
|
|
413
|
-
parent_session_id=
|
|
451
|
+
parent_session_id=resolved_parent_session_id,
|
|
414
452
|
project_path=project_path,
|
|
415
453
|
)
|
|
416
454
|
|
|
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
|
|
|
9
9
|
|
|
10
10
|
from gobby.storage.projects import LocalProjectManager
|
|
11
11
|
from gobby.storage.session_tasks import SessionTaskManager
|
|
12
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
12
13
|
from gobby.storage.task_dependencies import TaskDependencyManager
|
|
13
14
|
from gobby.storage.tasks import LocalTaskManager
|
|
14
15
|
from gobby.utils.project_context import get_project_context
|
|
@@ -42,6 +43,7 @@ class RegistryContext:
|
|
|
42
43
|
# Derived managers (initialized in __post_init__)
|
|
43
44
|
dep_manager: TaskDependencyManager = field(init=False)
|
|
44
45
|
session_task_manager: SessionTaskManager = field(init=False)
|
|
46
|
+
session_manager: LocalSessionManager = field(init=False)
|
|
45
47
|
workflow_state_manager: WorkflowStateManager = field(init=False)
|
|
46
48
|
project_manager: LocalProjectManager = field(init=False)
|
|
47
49
|
|
|
@@ -56,6 +58,7 @@ class RegistryContext:
|
|
|
56
58
|
db = self.task_manager.db
|
|
57
59
|
self.dep_manager = TaskDependencyManager(db)
|
|
58
60
|
self.session_task_manager = SessionTaskManager(db)
|
|
61
|
+
self.session_manager = LocalSessionManager(db)
|
|
59
62
|
self.workflow_state_manager = WorkflowStateManager(db)
|
|
60
63
|
self.project_manager = LocalProjectManager(db)
|
|
61
64
|
|
|
@@ -90,3 +93,18 @@ class RegistryContext:
|
|
|
90
93
|
if not session_id:
|
|
91
94
|
return None
|
|
92
95
|
return self.workflow_state_manager.get_state(session_id)
|
|
96
|
+
|
|
97
|
+
def resolve_session_id(self, session_id: str) -> str:
|
|
98
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
session_id: Session reference string
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Resolved UUID string
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If session cannot be resolved
|
|
108
|
+
"""
|
|
109
|
+
project_id = self.get_current_project_id()
|
|
110
|
+
return self.session_manager.resolve_session_reference(session_id, project_id)
|
|
@@ -90,6 +90,13 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
90
90
|
if effective_category is None:
|
|
91
91
|
effective_category = _infer_category(title, description)
|
|
92
92
|
|
|
93
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
94
|
+
resolved_session_id = session_id
|
|
95
|
+
try:
|
|
96
|
+
resolved_session_id = ctx.resolve_session_id(session_id)
|
|
97
|
+
except ValueError:
|
|
98
|
+
pass # Fall back to raw value if resolution fails
|
|
99
|
+
|
|
93
100
|
# Create task
|
|
94
101
|
create_result = ctx.task_manager.create_task_with_decomposition(
|
|
95
102
|
project_id=project_id,
|
|
@@ -101,14 +108,14 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
101
108
|
labels=labels,
|
|
102
109
|
category=effective_category,
|
|
103
110
|
validation_criteria=validation_criteria,
|
|
104
|
-
created_in_session_id=
|
|
111
|
+
created_in_session_id=resolved_session_id,
|
|
105
112
|
)
|
|
106
113
|
|
|
107
114
|
task = ctx.task_manager.get_task(create_result["task"]["id"])
|
|
108
115
|
|
|
109
116
|
# Link task to session (best-effort) - tracks which session created the task
|
|
110
117
|
try:
|
|
111
|
-
ctx.session_task_manager.link_task(
|
|
118
|
+
ctx.session_task_manager.link_task(resolved_session_id, task.id, "created")
|
|
112
119
|
except Exception:
|
|
113
120
|
pass # nosec B110 - best-effort linking
|
|
114
121
|
|
|
@@ -116,7 +123,7 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
116
123
|
if claim:
|
|
117
124
|
updated_task = ctx.task_manager.update_task(
|
|
118
125
|
task.id,
|
|
119
|
-
assignee=
|
|
126
|
+
assignee=resolved_session_id,
|
|
120
127
|
status="in_progress",
|
|
121
128
|
)
|
|
122
129
|
if updated_task is None:
|
|
@@ -125,14 +132,14 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
125
132
|
task = updated_task
|
|
126
133
|
# Link task to session with "claimed" action (best-effort)
|
|
127
134
|
try:
|
|
128
|
-
ctx.session_task_manager.link_task(
|
|
135
|
+
ctx.session_task_manager.link_task(resolved_session_id, task.id, "claimed")
|
|
129
136
|
except Exception:
|
|
130
137
|
pass # nosec B110 - best-effort linking
|
|
131
138
|
|
|
132
139
|
# Set workflow state for Claude Code (CC doesn't include tool results in PostToolUse)
|
|
133
140
|
# This mirrors close_task behavior in _lifecycle.py:196-207
|
|
134
141
|
try:
|
|
135
|
-
state = ctx.workflow_state_manager.get_state(
|
|
142
|
+
state = ctx.workflow_state_manager.get_state(resolved_session_id)
|
|
136
143
|
if state:
|
|
137
144
|
state.variables["task_claimed"] = True
|
|
138
145
|
state.variables["claimed_task_id"] = task.id # Always use UUID
|
|
@@ -248,7 +255,7 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
248
255
|
},
|
|
249
256
|
"session_id": {
|
|
250
257
|
"type": "string",
|
|
251
|
-
"description": "Your session ID (
|
|
258
|
+
"description": "Your session ID (accepts #N, N, UUID, or prefix). Required to track which session created the task.",
|
|
252
259
|
},
|
|
253
260
|
"claim": {
|
|
254
261
|
"type": "boolean",
|