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.
Files changed (125) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/claude_code.py +99 -61
  3. gobby/adapters/gemini.py +140 -38
  4. gobby/agents/isolation.py +130 -0
  5. gobby/agents/registry.py +11 -0
  6. gobby/agents/session.py +1 -0
  7. gobby/agents/spawn_executor.py +43 -13
  8. gobby/agents/spawners/macos.py +26 -1
  9. gobby/app_context.py +59 -0
  10. gobby/cli/__init__.py +0 -2
  11. gobby/cli/memory.py +185 -0
  12. gobby/cli/utils.py +5 -17
  13. gobby/clones/git.py +177 -0
  14. gobby/config/features.py +0 -20
  15. gobby/config/skills.py +31 -0
  16. gobby/config/tasks.py +4 -0
  17. gobby/hooks/event_handlers/__init__.py +155 -0
  18. gobby/hooks/event_handlers/_agent.py +175 -0
  19. gobby/hooks/event_handlers/_base.py +87 -0
  20. gobby/hooks/event_handlers/_misc.py +66 -0
  21. gobby/hooks/event_handlers/_session.py +573 -0
  22. gobby/hooks/event_handlers/_tool.py +196 -0
  23. gobby/hooks/hook_manager.py +21 -1
  24. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  25. gobby/llm/claude.py +377 -42
  26. gobby/mcp_proxy/importer.py +4 -41
  27. gobby/mcp_proxy/instructions.py +2 -2
  28. gobby/mcp_proxy/manager.py +13 -3
  29. gobby/mcp_proxy/registries.py +35 -4
  30. gobby/mcp_proxy/services/recommendation.py +2 -28
  31. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  32. gobby/mcp_proxy/tools/agents.py +45 -9
  33. gobby/mcp_proxy/tools/artifacts.py +46 -12
  34. gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
  35. gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
  36. gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
  37. gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
  38. gobby/mcp_proxy/tools/spawn_agent.py +44 -6
  39. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  40. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  41. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  42. gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
  43. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  44. gobby/mcp_proxy/tools/workflows/__init__.py +266 -0
  45. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  46. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  47. gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
  48. gobby/mcp_proxy/tools/workflows/_query.py +207 -0
  49. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  50. gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
  51. gobby/mcp_proxy/tools/worktrees.py +32 -7
  52. gobby/memory/components/__init__.py +0 -0
  53. gobby/memory/components/ingestion.py +98 -0
  54. gobby/memory/components/search.py +108 -0
  55. gobby/memory/extractor.py +15 -1
  56. gobby/memory/manager.py +16 -25
  57. gobby/paths.py +51 -0
  58. gobby/prompts/loader.py +1 -35
  59. gobby/runner.py +36 -10
  60. gobby/servers/http.py +186 -149
  61. gobby/servers/routes/admin.py +12 -0
  62. gobby/servers/routes/mcp/endpoints/execution.py +15 -7
  63. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  64. gobby/servers/routes/mcp/hooks.py +50 -3
  65. gobby/servers/websocket.py +57 -1
  66. gobby/sessions/analyzer.py +4 -4
  67. gobby/sessions/manager.py +9 -0
  68. gobby/sessions/transcripts/gemini.py +100 -34
  69. gobby/skills/parser.py +23 -0
  70. gobby/skills/sync.py +5 -4
  71. gobby/storage/artifacts.py +19 -0
  72. gobby/storage/database.py +9 -2
  73. gobby/storage/memories.py +32 -21
  74. gobby/storage/migrations.py +46 -4
  75. gobby/storage/sessions.py +4 -2
  76. gobby/storage/skills.py +87 -7
  77. gobby/tasks/external_validator.py +4 -17
  78. gobby/tasks/validation.py +13 -87
  79. gobby/tools/summarizer.py +18 -51
  80. gobby/utils/status.py +13 -0
  81. gobby/workflows/actions.py +5 -0
  82. gobby/workflows/context_actions.py +21 -24
  83. gobby/workflows/detection_helpers.py +38 -24
  84. gobby/workflows/enforcement/__init__.py +11 -1
  85. gobby/workflows/enforcement/blocking.py +109 -1
  86. gobby/workflows/enforcement/handlers.py +35 -1
  87. gobby/workflows/engine.py +96 -0
  88. gobby/workflows/evaluator.py +110 -0
  89. gobby/workflows/hooks.py +41 -0
  90. gobby/workflows/lifecycle_evaluator.py +2 -1
  91. gobby/workflows/memory_actions.py +11 -0
  92. gobby/workflows/safe_evaluator.py +8 -0
  93. gobby/workflows/summary_actions.py +123 -50
  94. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
  95. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/RECORD +99 -107
  96. gobby/cli/tui.py +0 -34
  97. gobby/hooks/event_handlers.py +0 -909
  98. gobby/mcp_proxy/tools/workflows.py +0 -973
  99. gobby/tui/__init__.py +0 -5
  100. gobby/tui/api_client.py +0 -278
  101. gobby/tui/app.py +0 -329
  102. gobby/tui/screens/__init__.py +0 -25
  103. gobby/tui/screens/agents.py +0 -333
  104. gobby/tui/screens/chat.py +0 -450
  105. gobby/tui/screens/dashboard.py +0 -377
  106. gobby/tui/screens/memory.py +0 -305
  107. gobby/tui/screens/metrics.py +0 -231
  108. gobby/tui/screens/orchestrator.py +0 -903
  109. gobby/tui/screens/sessions.py +0 -412
  110. gobby/tui/screens/tasks.py +0 -440
  111. gobby/tui/screens/workflows.py +0 -289
  112. gobby/tui/screens/worktrees.py +0 -174
  113. gobby/tui/widgets/__init__.py +0 -21
  114. gobby/tui/widgets/chat.py +0 -210
  115. gobby/tui/widgets/conductor.py +0 -104
  116. gobby/tui/widgets/menu.py +0 -132
  117. gobby/tui/widgets/message_panel.py +0 -160
  118. gobby/tui/widgets/review_gate.py +0 -224
  119. gobby/tui/widgets/task_tree.py +0 -99
  120. gobby/tui/widgets/token_budget.py +0 -166
  121. gobby/tui/ws_client.py +0 -258
  122. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
  123. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
  124. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
  125. {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 LocalDatabase
22
+ from gobby.storage.database import DatabaseProtocol
23
23
 
24
24
 
25
25
  def create_artifacts_registry(
26
- db: LocalDatabase | None = None,
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: LocalDatabase instance (used to create artifact_manager if not provided)
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 ID to filter by
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=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 ID to filter by
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=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 ID to get timeline for
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=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 ID
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
- # Get session
59
- session = session_manager.get(session_id)
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
- # Try prefix match
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 get_current(external_id, source) first""",
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 ID (REQUIRED)
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
- raise RuntimeError("Session manager not available")
196
+ return {"error": "Session manager not available"}
194
197
 
195
- # Find session - session_id is now required
196
- session = session_manager.get(session_id)
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 (get_current)
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="get_current",
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 get_current(
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 `get_current(external_id, source)` instead - it uses your unique session key
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 get_current(external_id='<your-external-id>', source='claude') instead."
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 (project-scoped), UUID, or prefix
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 get_current(external_id, source) first""",
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 ID (REQUIRED)
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
- # Find session - session_id is now required
211
- session = session_manager.get(session_id)
212
- if not session:
213
- # Try prefix match
214
- sessions = session_manager.list(limit=100)
215
- matches = [s for s in sessions if s.id.startswith(session_id)]
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: Specific session ID to pickup from (optional)
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: If provided, links this session as a child
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
- parent_session = session_manager.get(session_id)
434
- if not parent_session:
435
- # Try prefix match
436
- sessions = session_manager.list(limit=100)
437
- matches = [s for s in sessions if s.id.startswith(session_id)]
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
- session_manager.update_parent_session_id(link_child_session_id, parent_session.id)
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: The 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=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(session_id)
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=session_id,
148
+ session_id=resolved_session_id,
121
149
  limit=limit,
122
150
  )
123
151