shotgun-sh 0.2.6.dev5__py3-none-any.whl → 0.2.7.dev2__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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

@@ -1,5 +1,9 @@
1
1
  """Context extraction utilities for history processing."""
2
2
 
3
+ import json
4
+ import logging
5
+ import traceback
6
+
3
7
  from pydantic_ai.messages import (
4
8
  BuiltinToolCallPart,
5
9
  BuiltinToolReturnPart,
@@ -16,6 +20,46 @@ from pydantic_ai.messages import (
16
20
  UserPromptPart,
17
21
  )
18
22
 
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def _safely_parse_tool_args(args: dict[str, object] | str | None) -> dict[str, object]:
27
+ """Safely parse tool call arguments, handling incomplete/invalid JSON.
28
+
29
+ Args:
30
+ args: Tool call arguments (dict, JSON string, or None)
31
+
32
+ Returns:
33
+ Parsed args dict, or empty dict if parsing fails
34
+ """
35
+ if args is None:
36
+ return {}
37
+
38
+ if isinstance(args, dict):
39
+ return args
40
+
41
+ if not isinstance(args, str):
42
+ return {}
43
+
44
+ try:
45
+ parsed = json.loads(args)
46
+ return parsed if isinstance(parsed, dict) else {}
47
+ except (json.JSONDecodeError, ValueError) as e:
48
+ # Only log warning if it looks like JSON (starts with { or [) - incomplete JSON
49
+ # Plain strings are valid args and shouldn't trigger warnings
50
+ stripped_args = args.strip()
51
+ if stripped_args.startswith(("{", "[")):
52
+ args_preview = args[:100] + "..." if len(args) > 100 else args
53
+ logger.warning(
54
+ "Detected incomplete/invalid JSON in tool call args during parsing",
55
+ extra={
56
+ "args_preview": args_preview,
57
+ "error": str(e),
58
+ "args_length": len(args),
59
+ },
60
+ )
61
+ return {}
62
+
19
63
 
20
64
  def extract_context_from_messages(messages: list[ModelMessage]) -> str:
21
65
  """Extract context from a list of messages for summarization."""
@@ -87,12 +131,55 @@ def extract_context_from_part(
87
131
  return f"<ASSISTANT_TEXT>\n{message_part.content}\n</ASSISTANT_TEXT>"
88
132
 
89
133
  elif isinstance(message_part, ToolCallPart):
90
- if isinstance(message_part.args, dict):
91
- args_str = ", ".join(f"{k}={repr(v)}" for k, v in message_part.args.items())
92
- tool_call_str = f"{message_part.tool_name}({args_str})"
93
- else:
94
- tool_call_str = f"{message_part.tool_name}({message_part.args})"
95
- return f"<TOOL_CALL>\n{tool_call_str}\n</TOOL_CALL>"
134
+ # Safely parse args to avoid crashes from incomplete JSON during streaming
135
+ try:
136
+ parsed_args = _safely_parse_tool_args(message_part.args)
137
+ if parsed_args:
138
+ # Successfully parsed as dict - format nicely
139
+ args_str = ", ".join(f"{k}={repr(v)}" for k, v in parsed_args.items())
140
+ tool_call_str = f"{message_part.tool_name}({args_str})"
141
+ elif isinstance(message_part.args, str) and message_part.args:
142
+ # Non-empty string that didn't parse as JSON
143
+ # Check if it looks like JSON (starts with { or [) - if so, it's incomplete
144
+ stripped_args = message_part.args.strip()
145
+ if stripped_args.startswith(("{", "[")):
146
+ # Looks like incomplete JSON - log warning and show empty parens
147
+ args_preview = (
148
+ stripped_args[:100] + "..."
149
+ if len(stripped_args) > 100
150
+ else stripped_args
151
+ )
152
+ stack_trace = "".join(traceback.format_stack())
153
+ logger.warning(
154
+ "ToolCallPart with unparseable args encountered during context extraction",
155
+ extra={
156
+ "tool_name": message_part.tool_name,
157
+ "tool_call_id": message_part.tool_call_id,
158
+ "args_preview": args_preview,
159
+ "args_type": type(message_part.args).__name__,
160
+ "stack_trace": stack_trace,
161
+ },
162
+ )
163
+ tool_call_str = f"{message_part.tool_name}()"
164
+ else:
165
+ # Plain string arg - display as-is
166
+ tool_call_str = f"{message_part.tool_name}({message_part.args})"
167
+ else:
168
+ # No args
169
+ tool_call_str = f"{message_part.tool_name}()"
170
+ return f"<TOOL_CALL>\n{tool_call_str}\n</TOOL_CALL>"
171
+ except Exception as e: # pragma: no cover - defensive catch-all
172
+ # If anything goes wrong, log full exception with stack trace
173
+ logger.error(
174
+ "Unexpected error processing ToolCallPart",
175
+ exc_info=True,
176
+ extra={
177
+ "tool_name": message_part.tool_name,
178
+ "tool_call_id": message_part.tool_call_id,
179
+ "error": str(e),
180
+ },
181
+ )
182
+ return f"<TOOL_CALL>\n{message_part.tool_name}()\n</TOOL_CALL>"
96
183
 
97
184
  elif isinstance(message_part, BuiltinToolCallPart):
98
185
  return f"<BUILTIN_TOOL_CALL>\n{message_part.tool_name}\n</BUILTIN_TOOL_CALL>"
@@ -15,11 +15,18 @@ from shotgun.utils.file_system_utils import get_shotgun_base_path
15
15
  logger = get_logger(__name__)
16
16
 
17
17
  # Map agent modes to their allowed directories/files (in workflow order)
18
- AGENT_DIRECTORIES = {
19
- AgentType.RESEARCH: "research.md",
20
- AgentType.SPECIFY: "specification.md",
21
- AgentType.PLAN: "plan.md",
22
- AgentType.TASKS: "tasks.md",
18
+ # Values can be:
19
+ # - A Path: exact file (e.g., Path("research.md"))
20
+ # - A list of Paths: multiple allowed files/directories (e.g., [Path("specification.md"), Path("contracts")])
21
+ # - "*": any file except protected files (for export agent)
22
+ AGENT_DIRECTORIES: dict[AgentType, str | Path | list[Path]] = {
23
+ AgentType.RESEARCH: Path("research.md"),
24
+ AgentType.SPECIFY: [
25
+ Path("specification.md"),
26
+ Path("contracts"),
27
+ ], # Specify can write specs and contract files
28
+ AgentType.PLAN: Path("plan.md"),
29
+ AgentType.TASKS: Path("tasks.md"),
23
30
  AgentType.EXPORT: "*", # Export agent can write anywhere except protected files
24
31
  }
25
32
 
@@ -60,13 +67,52 @@ def _validate_agent_scoped_path(filename: str, agent_mode: AgentType | None) ->
60
67
  # Allow writing anywhere else in .shotgun directory
61
68
  full_path = (base_path / filename).resolve()
62
69
  else:
63
- # For other agents, only allow writing to their specific file
64
- allowed_file = AGENT_DIRECTORIES[agent_mode]
65
- if filename != allowed_file:
70
+ # For other agents, check if they have access to the requested file
71
+ allowed_paths_raw = AGENT_DIRECTORIES[agent_mode]
72
+
73
+ # Convert single Path/string to list of Paths for uniform handling
74
+ if isinstance(allowed_paths_raw, str):
75
+ # Special case: "*" means export agent
76
+ allowed_paths = (
77
+ [Path(allowed_paths_raw)] if allowed_paths_raw != "*" else []
78
+ )
79
+ elif isinstance(allowed_paths_raw, Path):
80
+ allowed_paths = [allowed_paths_raw]
81
+ else:
82
+ # Already a list
83
+ allowed_paths = allowed_paths_raw
84
+
85
+ # Check if filename matches any allowed path
86
+ is_allowed = False
87
+ for allowed_path in allowed_paths:
88
+ allowed_str = str(allowed_path)
89
+
90
+ # Check if it's a directory (no .md extension or suffix)
91
+ # Directories: Path("contracts") has no suffix, files: Path("spec.md") has .md suffix
92
+ if not allowed_path.suffix or (
93
+ allowed_path.suffix and not allowed_str.endswith(".md")
94
+ ):
95
+ # Directory - allow any file within this directory
96
+ # Check both "contracts/file.py" and "contracts" prefix
97
+ if (
98
+ filename.startswith(allowed_str + "/")
99
+ or filename == allowed_str
100
+ ):
101
+ is_allowed = True
102
+ break
103
+ else:
104
+ # Exact file match
105
+ if filename == allowed_str:
106
+ is_allowed = True
107
+ break
108
+
109
+ if not is_allowed:
110
+ allowed_display = ", ".join(f"'{p}'" for p in allowed_paths)
66
111
  raise ValueError(
67
- f"{agent_mode.value.capitalize()} agent can only write to '{allowed_file}'. "
112
+ f"{agent_mode.value.capitalize()} agent can only write to {allowed_display}. "
68
113
  f"Attempted to write to '{filename}'"
69
114
  )
115
+
70
116
  full_path = (base_path / filename).resolve()
71
117
  else:
72
118
  # No agent mode specified, fall back to old validation
@@ -8,14 +8,281 @@ Transform requirements into detailed, actionable specifications that development
8
8
 
9
9
  ## MEMORY MANAGEMENT PROTOCOL
10
10
 
11
- - You have exclusive write access to: `specification.md`
11
+ - You have exclusive write access to: `specification.md` and `.shotgun/contracts/*`
12
12
  - SHOULD READ `research.md` for context but CANNOT write to it
13
- - This is your persistent memory store - ALWAYS load it first
13
+ - **specification.md is for PROSE ONLY** - no code, no implementation details, no type definitions
14
+ - **All code goes in .shotgun/contracts/** - types, interfaces, schemas
15
+ - specification.md describes WHAT and WHY, contracts/ show HOW with actual code
16
+ - This is your persistent memory store - ALWAYS load specification.md first
14
17
  - Compress content regularly to stay within context limits
15
- - Keep your file updated as you work - it's your memory across sessions
18
+ - Keep your files updated as you work - they're your memory across sessions
16
19
  - When adding new specifications, review and consolidate overlapping requirements
17
20
  - Structure specifications for easy reference by the next agents
18
21
 
22
+ ## WHAT GOES IN SPECIFICATION.MD
23
+
24
+ specification.md is your prose documentation file. It should contain:
25
+
26
+ **INCLUDE in specification.md:**
27
+ - Requirements and business context (what needs to be built and why)
28
+ - Architecture overview and system design decisions
29
+ - Component descriptions and how they interact
30
+ - User workflows and use cases
31
+ - Directory structure as succinct prose (e.g., "src/ contains main code, tests/ contains test files")
32
+ - Dependencies listed in prose (e.g., "Requires TypeScript 5.0+, React 18, and PostgreSQL")
33
+ - Configuration requirements described (e.g., "App needs database URL and API key in environment")
34
+ - Testing strategies and acceptance criteria
35
+ - References to contract files (e.g., "See contracts/user_models.py for User type definition")
36
+
37
+ **DO NOT INCLUDE in specification.md:**
38
+ - Code blocks, type definitions, or function signatures (those go in contracts/)
39
+ - Implementation details or algorithms (describe behavior instead)
40
+ - Actual configuration files or build manifests (describe what's needed instead)
41
+ - Directory trees or file listings (keep structure descriptions succinct)
42
+
43
+ **When you need to show structure:** Reference contract files instead of inline code.
44
+ Example: "User authentication uses OAuth2. See contracts/auth_types.ts for AuthUser and AuthToken types."
45
+
46
+ ## CONTRACT FILES
47
+
48
+ Contract files define the **interfaces and types** that form contracts between components.
49
+ They contain actual code that shows structure, not prose descriptions.
50
+
51
+ **ONLY put these in `.shotgun/contracts/` (language-agnostic):**
52
+ - **Type definitions ONLY** - Shape and structure, NO behavior or logic:
53
+ - Python: Pydantic models, dataclasses, `typing.Protocol` classes (interface definitions)
54
+ - TypeScript: interfaces, type aliases
55
+ - Rust: struct definitions
56
+ - Java: interfaces, POJOs
57
+ - C++: header files with class/struct declarations
58
+ - Go: interface types, struct definitions
59
+ - **Schema definitions**: API contracts and data schemas
60
+ - OpenAPI/Swagger specs (openapi.json, openapi.yaml)
61
+ - JSON Schema definitions
62
+ - GraphQL schemas
63
+ - Protobuf definitions
64
+ - **Protocol/Interface classes**: Pure interface definitions with method signatures only
65
+ - Python: `class Storage(Protocol): def save(self, data: str) -> None: ...`
66
+ - Use `...` (Ellipsis) for protocol methods, NOT `pass`
67
+
68
+ **NEVER put these in `.shotgun/contracts/` - NO EXECUTABLE CODE:**
69
+ - ❌ **Functions or methods with implementations** (even with `pass` or empty bodies)
70
+ - ❌ **Helper functions** with any logic whatsoever
71
+ - ❌ **Classes with method implementations** (use Protocol classes instead)
72
+ - ❌ **Standalone functions** like `def main(): pass` or `def validate_input(x): ...`
73
+ - ❌ **Code with behavior**: loops, conditionals, data manipulation, computations
74
+ - ❌ **Data constants**: dictionaries, lists, or any runtime values
75
+ - ❌ **`if __name__ == "__main__":` blocks** or any executable code
76
+ - Build/dependency configs (pyproject.toml, package.json, Cargo.toml, requirements.txt)
77
+ - Directory structure files (directory_structure.txt)
78
+ - Configuration templates (.env, config.yaml, example configs)
79
+ - Documentation or markdown files
80
+ - SQL migration files or database dumps
81
+
82
+ **These belong in specification.md instead:**
83
+ - Directory structure (as succinct prose: "src/ contains modules, tests/ has unit tests")
84
+ - Dependencies (as prose: "Requires Rust 1.70+, tokio, serde")
85
+ - Configuration needs (describe: "App needs DB_URL and API_KEY environment variables")
86
+
87
+ **Guidelines for contract files:**
88
+ - Keep each file focused on a single domain (e.g., user_types.ts, payment_models.py)
89
+ - Reference from specification.md: "See contracts/user_types.ts for User and Profile types"
90
+ - Use descriptive filenames: `auth_models.py`, `api_spec.json`, `database_types.rs`
91
+ - Keep files under 500 lines to avoid truncation
92
+ - When contracts grow large, split into focused files
93
+
94
+ **Example workflow:**
95
+ 1. In specification.md: "Authentication system with JWT tokens. See contracts/auth_types.ts for types."
96
+ 2. Create contract file: `write_file("contracts/auth_types.ts", content)` with actual TypeScript interfaces
97
+ 3. Create contract file: `write_file("contracts/auth_api.json", content)` with actual OpenAPI spec
98
+ 4. Coding agents can directly use these contracts to implement features
99
+
100
+ ## HOW TO WRITE CONTRACT FILES
101
+
102
+ **CRITICAL - Always use correct file paths with write_file():**
103
+
104
+ Your working directory is `.shotgun/`, so paths should be relative to that directory.
105
+
106
+ <GOOD_EXAMPLES>
107
+ ✅ `write_file("contracts/user_models.py", content)` - Correct path for Python models
108
+ ✅ `write_file("contracts/auth_types.ts", content)` - Correct path for TypeScript types
109
+ ✅ `write_file("contracts/api_spec.json", content)` - Correct path for OpenAPI spec
110
+ ✅ `write_file("contracts/payment_service.rs", content)` - Correct path for Rust code
111
+ </GOOD_EXAMPLES>
112
+
113
+ <BAD_EXAMPLES>
114
+ ❌ `write_file(".shotgun/contracts/user_models.py", content)` - WRONG! Don't include .shotgun/ prefix
115
+ ❌ `write_file("contracts/directory_structure.txt", content)` - WRONG! No documentation files
116
+ ❌ `write_file("contracts/pyproject.toml", content)` - WRONG! No build configs in contracts/
117
+ ❌ `write_file("contracts/requirements.txt", content)` - WRONG! No dependency lists in contracts/
118
+ ❌ `write_file("contracts/config.yaml", content)` - WRONG! No config templates in contracts/
119
+ </BAD_EXAMPLES>
120
+
121
+ **Path format rule:** Always use `contracts/filename.ext`, never `.shotgun/contracts/filename.ext`
122
+
123
+ **Language-specific examples:**
124
+
125
+ <PYTHON_EXAMPLE>
126
+ # Python Pydantic model contract
127
+ from pydantic import BaseModel, Field
128
+ from typing import Optional
129
+
130
+ class User(BaseModel):
131
+ """User model contract."""
132
+ id: int
133
+ email: str = Field(..., description="User email address")
134
+ username: str
135
+ is_active: bool = True
136
+ role: Optional[str] = None
137
+
138
+ # Save as: write_file("contracts/user_models.py", content)
139
+ </PYTHON_EXAMPLE>
140
+
141
+ <TYPESCRIPT_EXAMPLE>
142
+ // TypeScript interface contract
143
+ interface User {
144
+ id: number;
145
+ email: string;
146
+ username: string;
147
+ isActive: boolean;
148
+ role?: string;
149
+ }
150
+
151
+ interface AuthToken {
152
+ token: string;
153
+ expiresAt: Date;
154
+ userId: number;
155
+ }
156
+
157
+ // Save as: write_file("contracts/auth_types.ts", content)
158
+ </TYPESCRIPT_EXAMPLE>
159
+
160
+ <RUST_EXAMPLE>
161
+ // Rust struct contract
162
+ use serde::{Deserialize, Serialize};
163
+
164
+ #[derive(Debug, Serialize, Deserialize)]
165
+ pub struct User {
166
+ pub id: u64,
167
+ pub email: String,
168
+ pub username: String,
169
+ pub is_active: bool,
170
+ pub role: Option<String>,
171
+ }
172
+
173
+ // Save as: write_file("contracts/user_types.rs", content)
174
+ </RUST_EXAMPLE>
175
+
176
+ <OPENAPI_EXAMPLE>
177
+ {
178
+ "openapi": "3.0.0",
179
+ "info": {
180
+ "title": "User API",
181
+ "version": "1.0.0"
182
+ },
183
+ "paths": {
184
+ "/users": {
185
+ "get": {
186
+ "summary": "List users",
187
+ "responses": {
188
+ "200": {
189
+ "description": "Successful response",
190
+ "content": {
191
+ "application/json": {
192
+ "schema": {
193
+ "type": "array",
194
+ "items": { "$ref": "#/components/schemas/User" }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ },
203
+ "components": {
204
+ "schemas": {
205
+ "User": {
206
+ "type": "object",
207
+ "properties": {
208
+ "id": { "type": "integer" },
209
+ "email": { "type": "string" },
210
+ "username": { "type": "string" }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ // Save as: write_file("contracts/user_api.json", content)
218
+ </OPENAPI_EXAMPLE>
219
+
220
+ ## WHAT IS ALLOWED vs WHAT IS FORBIDDEN
221
+
222
+ **✅ ALLOWED - Type Definitions (Shape and Structure):**
223
+
224
+ ```python
225
+ # ✅ GOOD: Pydantic model (type definition)
226
+ from pydantic import BaseModel
227
+
228
+ class User(BaseModel):
229
+ id: int
230
+ email: str
231
+ username: str
232
+
233
+ # ✅ GOOD: Protocol class (interface definition)
234
+ from typing import Protocol
235
+
236
+ class Storage(Protocol):
237
+ def save(self, data: str) -> None: ...
238
+ def load(self) -> str: ...
239
+
240
+ # ✅ GOOD: Type aliases and enums
241
+ from typing import Literal
242
+ from enum import Enum
243
+
244
+ UserRole = Literal["admin", "user", "guest"]
245
+
246
+ class Status(Enum):
247
+ ACTIVE = "active"
248
+ INACTIVE = "inactive"
249
+ ```
250
+
251
+ **❌ FORBIDDEN - Executable Code (Behavior and Logic):**
252
+
253
+ ```python
254
+ # ❌ BAD: Function with pass (executable code)
255
+ def main() -> int:
256
+ pass
257
+
258
+ # ❌ BAD: Function with implementation
259
+ def validate_input(x: str) -> str:
260
+ return x.strip()
261
+
262
+ # ❌ BAD: Class with method implementations
263
+ class HistoryManager:
264
+ def __init__(self):
265
+ pass
266
+
267
+ def add_message(self, msg: str):
268
+ pass
269
+
270
+ # ❌ BAD: Data constants (runtime values)
271
+ SUPPORTED_PROVIDERS = [
272
+ {"name": "openai", "key": "OPENAI_API_KEY"}
273
+ ]
274
+
275
+ # ❌ BAD: Helper functions
276
+ def get_default_config() -> dict:
277
+ return {"model": "gpt-4"}
278
+
279
+ # ❌ BAD: Executable code blocks
280
+ if __name__ == "__main__":
281
+ main()
282
+ ```
283
+
284
+ **Remember**: Contracts define **SHAPES** (types, interfaces, schemas), NOT **BEHAVIOR** (functions, logic, implementations).
285
+
19
286
  ## AI AGENT PIPELINE AWARENESS
20
287
 
21
288
  **CRITICAL**: Your output will be consumed by AI coding agents (Claude Code, Cursor, Windsurf, etc.)
@@ -881,6 +881,30 @@ class ChatScreen(Screen[None]):
881
881
  except asyncio.CancelledError:
882
882
  # Handle cancellation gracefully - DO NOT re-raise
883
883
  self.mount_hint("⚠️ Operation cancelled by user")
884
+ except Exception as e:
885
+ # Log with full stack trace to shotgun.log
886
+ logger.exception(
887
+ "Agent run failed",
888
+ extra={
889
+ "agent_mode": self.mode.value,
890
+ "error_type": type(e).__name__,
891
+ },
892
+ )
893
+
894
+ # Determine user-friendly message based on error type
895
+ error_name = type(e).__name__
896
+ error_message = str(e)
897
+
898
+ if "APIStatusError" in error_name and "overload" in error_message.lower():
899
+ hint = "⚠️ The AI service is temporarily overloaded. Please wait a moment and try again."
900
+ elif "APIStatusError" in error_name and "rate" in error_message.lower():
901
+ hint = "⚠️ Rate limit reached. Please wait before trying again."
902
+ elif "APIStatusError" in error_name:
903
+ hint = f"⚠️ AI service error: {error_message}"
904
+ else:
905
+ hint = f"⚠️ An error occurred: {error_message}\n\nCheck logs at ~/.shotgun-sh/logs/shotgun.log"
906
+
907
+ self.mount_hint(hint)
884
908
  finally:
885
909
  self.working = False
886
910
  self._current_worker = None
@@ -910,24 +934,41 @@ class ChatScreen(Screen[None]):
910
934
  """Load conversation from persistent storage."""
911
935
  conversation = self.conversation_manager.load()
912
936
  if conversation is None:
937
+ # Check if file existed but was corrupted (backup was created)
938
+ backup_path = self.conversation_manager.conversation_path.with_suffix(
939
+ ".json.backup"
940
+ )
941
+ if backup_path.exists():
942
+ # File was corrupted - show friendly notification
943
+ self.mount_hint(
944
+ "⚠️ Previous session was corrupted and has been backed up. Starting fresh conversation."
945
+ )
913
946
  return
914
947
 
915
- # Restore agent state
916
- agent_messages = conversation.get_agent_messages()
917
- ui_messages = conversation.get_ui_messages()
948
+ try:
949
+ # Restore agent state
950
+ agent_messages = conversation.get_agent_messages()
951
+ ui_messages = conversation.get_ui_messages()
952
+
953
+ # Create ConversationState for restoration
954
+ state = ConversationState(
955
+ agent_messages=agent_messages,
956
+ ui_messages=ui_messages,
957
+ agent_type=conversation.last_agent_model,
958
+ )
918
959
 
919
- # Create ConversationState for restoration
920
- state = ConversationState(
921
- agent_messages=agent_messages,
922
- ui_messages=ui_messages,
923
- agent_type=conversation.last_agent_model,
924
- )
960
+ self.agent_manager.restore_conversation_state(state)
925
961
 
926
- self.agent_manager.restore_conversation_state(state)
962
+ # Update the current mode
963
+ self.mode = AgentType(conversation.last_agent_model)
964
+ self.deps.usage_manager.restore_usage_state()
927
965
 
928
- # Update the current mode
929
- self.mode = AgentType(conversation.last_agent_model)
930
- self.deps.usage_manager.restore_usage_state()
966
+ except Exception as e: # pragma: no cover
967
+ # If anything goes wrong during restoration, log it and continue
968
+ logger.error("Failed to restore conversation state: %s", e)
969
+ self.mount_hint(
970
+ "⚠️ Could not restore previous session. Starting fresh conversation."
971
+ )
931
972
 
932
973
 
933
974
  def help_text_with_codebase(already_indexed: bool = False) -> str: