mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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 mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +796 -46
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +879 -129
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +973 -73
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +2732 -0
- mcp_ticketer/adapters/linear/client.py +344 -0
- mcp_ticketer/adapters/linear/mappers.py +420 -0
- mcp_ticketer/adapters/linear/queries.py +479 -0
- mcp_ticketer/adapters/linear/types.py +360 -0
- mcp_ticketer/adapters/linear.py +10 -2315
- mcp_ticketer/analysis/__init__.py +23 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +888 -151
- mcp_ticketer/cli/diagnostics.py +400 -157
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +616 -0
- mcp_ticketer/cli/main.py +203 -1165
- mcp_ticketer/cli/mcp_configure.py +474 -90
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +418 -0
- mcp_ticketer/cli/platform_installer.py +513 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +90 -65
- mcp_ticketer/cli/ticket_commands.py +1013 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +114 -66
- mcp_ticketer/core/__init__.py +24 -1
- mcp_ticketer/core/adapter.py +250 -16
- mcp_ticketer/core/config.py +145 -37
- mcp_ticketer/core/env_discovery.py +101 -22
- mcp_ticketer/core/env_loader.py +349 -0
- mcp_ticketer/core/exceptions.py +160 -0
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/models.py +280 -28
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +183 -49
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +56 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +95 -25
- mcp_ticketer/queue/queue.py +40 -21
- mcp_ticketer/queue/run_worker.py +6 -1
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +109 -49
- mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
- mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
- mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
mcp_ticketer/queue/manager.py
CHANGED
|
@@ -4,14 +4,14 @@ import fcntl
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import subprocess
|
|
7
|
-
import sys
|
|
8
7
|
import time
|
|
9
8
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
11
10
|
|
|
12
11
|
import psutil
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
pass
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -19,8 +19,11 @@ logger = logging.getLogger(__name__)
|
|
|
19
19
|
class WorkerManager:
|
|
20
20
|
"""Manages worker process with file-based locking."""
|
|
21
21
|
|
|
22
|
-
def __init__(self):
|
|
22
|
+
def __init__(self) -> None:
|
|
23
23
|
"""Initialize worker manager."""
|
|
24
|
+
# Lazy import to avoid circular dependency
|
|
25
|
+
from .queue import Queue
|
|
26
|
+
|
|
24
27
|
self.lock_file = Path.home() / ".mcp-ticketer" / "worker.lock"
|
|
25
28
|
self.pid_file = Path.home() / ".mcp-ticketer" / "worker.pid"
|
|
26
29
|
self.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -51,7 +54,7 @@ class WorkerManager:
|
|
|
51
54
|
# Lock already held
|
|
52
55
|
return False
|
|
53
56
|
|
|
54
|
-
def _release_lock(self):
|
|
57
|
+
def _release_lock(self) -> None:
|
|
55
58
|
"""Release worker lock."""
|
|
56
59
|
if hasattr(self, "lock_fd"):
|
|
57
60
|
fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
|
|
@@ -81,12 +84,22 @@ class WorkerManager:
|
|
|
81
84
|
# Try to start worker
|
|
82
85
|
return self.start()
|
|
83
86
|
|
|
84
|
-
def start(self) -> bool:
|
|
87
|
+
def start(self, timeout: float = 30.0) -> bool:
|
|
85
88
|
"""Start the worker process.
|
|
86
89
|
|
|
90
|
+
Args:
|
|
91
|
+
timeout: Maximum seconds to wait for worker to become fully operational (default: 30)
|
|
92
|
+
|
|
87
93
|
Returns:
|
|
88
94
|
True if started successfully, False otherwise
|
|
89
95
|
|
|
96
|
+
Raises:
|
|
97
|
+
TimeoutError: If worker doesn't start within timeout period
|
|
98
|
+
|
|
99
|
+
Note:
|
|
100
|
+
If timeout occurs, a worker may already be running in another process.
|
|
101
|
+
Check for stale lock files or processes using `get_status()`.
|
|
102
|
+
|
|
90
103
|
"""
|
|
91
104
|
# Check if already running
|
|
92
105
|
if self.is_running():
|
|
@@ -96,11 +109,44 @@ class WorkerManager:
|
|
|
96
109
|
# Try to acquire lock
|
|
97
110
|
if not self._acquire_lock():
|
|
98
111
|
logger.warning("Could not acquire lock - another worker may be running")
|
|
99
|
-
|
|
112
|
+
# Wait briefly to see if worker becomes operational
|
|
113
|
+
start_time = time.time()
|
|
114
|
+
while time.time() - start_time < timeout:
|
|
115
|
+
if self.is_running():
|
|
116
|
+
logger.info("Worker started by another process")
|
|
117
|
+
return True
|
|
118
|
+
time.sleep(1)
|
|
119
|
+
|
|
120
|
+
raise TimeoutError(
|
|
121
|
+
f"Worker start timed out after {timeout}s. "
|
|
122
|
+
f"Lock is held but worker is not running. "
|
|
123
|
+
f"Check for stale lock files or zombie processes."
|
|
124
|
+
)
|
|
100
125
|
|
|
101
126
|
try:
|
|
102
|
-
# Start worker in subprocess
|
|
103
|
-
|
|
127
|
+
# Start worker in subprocess using the same Python executable as the CLI
|
|
128
|
+
# This ensures the worker can import mcp_ticketer modules
|
|
129
|
+
# Lazy import to avoid circular dependency
|
|
130
|
+
from ..cli.python_detection import get_mcp_ticketer_python
|
|
131
|
+
|
|
132
|
+
python_executable = get_mcp_ticketer_python()
|
|
133
|
+
cmd = [python_executable, "-m", "mcp_ticketer.queue.run_worker"]
|
|
134
|
+
|
|
135
|
+
# Prepare environment for subprocess
|
|
136
|
+
# Ensure the subprocess gets the same environment as the parent
|
|
137
|
+
subprocess_env = os.environ.copy()
|
|
138
|
+
|
|
139
|
+
# Explicitly load environment variables from .env.local if it exists
|
|
140
|
+
env_file = Path.cwd() / ".env.local"
|
|
141
|
+
if env_file.exists():
|
|
142
|
+
logger.debug(f"Loading environment from {env_file} for subprocess")
|
|
143
|
+
from dotenv import dotenv_values
|
|
144
|
+
|
|
145
|
+
env_vars = dotenv_values(env_file)
|
|
146
|
+
subprocess_env.update(env_vars)
|
|
147
|
+
logger.debug(
|
|
148
|
+
f"Added {len(env_vars)} environment variables from .env.local"
|
|
149
|
+
)
|
|
104
150
|
|
|
105
151
|
# Start as background process
|
|
106
152
|
process = subprocess.Popen(
|
|
@@ -108,25 +154,49 @@ class WorkerManager:
|
|
|
108
154
|
stdout=subprocess.DEVNULL,
|
|
109
155
|
stderr=subprocess.DEVNULL,
|
|
110
156
|
start_new_session=True,
|
|
157
|
+
env=subprocess_env, # Pass environment explicitly
|
|
158
|
+
cwd=str(Path.cwd()), # Ensure correct working directory
|
|
111
159
|
)
|
|
112
160
|
|
|
113
161
|
# Save PID
|
|
114
162
|
self.pid_file.write_text(str(process.pid))
|
|
115
163
|
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
164
|
+
# Wait for worker to become fully operational with timeout
|
|
165
|
+
start_time = time.time()
|
|
166
|
+
while time.time() - start_time < timeout:
|
|
167
|
+
# Check if process exists
|
|
168
|
+
if not psutil.pid_exists(process.pid):
|
|
169
|
+
logger.error("Worker process died during startup")
|
|
170
|
+
self._cleanup()
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# Check if worker is fully running (not just process exists)
|
|
174
|
+
if self.is_running():
|
|
175
|
+
elapsed = time.time() - start_time
|
|
176
|
+
logger.info(
|
|
177
|
+
f"Worker started successfully in {elapsed:.1f}s with PID {process.pid}"
|
|
178
|
+
)
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
# Log progress for long startups
|
|
182
|
+
elapsed = time.time() - start_time
|
|
183
|
+
if elapsed > 5 and int(elapsed) % 5 == 0:
|
|
184
|
+
logger.debug(
|
|
185
|
+
f"Waiting for worker to start, elapsed: {elapsed:.1f}s"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
time.sleep(0.5)
|
|
189
|
+
|
|
190
|
+
# Timeout reached
|
|
191
|
+
raise TimeoutError(
|
|
192
|
+
f"Worker start timed out after {timeout}s. "
|
|
193
|
+
f"Process spawned (PID {process.pid}) but worker did not become operational. "
|
|
194
|
+
f"Check worker logs for startup errors."
|
|
195
|
+
)
|
|
129
196
|
|
|
197
|
+
except TimeoutError:
|
|
198
|
+
# Re-raise timeout errors with context preserved
|
|
199
|
+
raise
|
|
130
200
|
except Exception as e:
|
|
131
201
|
logger.error(f"Failed to start worker: {e}")
|
|
132
202
|
self._release_lock()
|
|
@@ -217,7 +287,7 @@ class WorkerManager:
|
|
|
217
287
|
is_running = self.is_running()
|
|
218
288
|
pid = self._get_pid() if is_running else None
|
|
219
289
|
|
|
220
|
-
status = {"running": is_running, "pid": pid}
|
|
290
|
+
status: dict[str, Any] = {"running": is_running, "pid": pid}
|
|
221
291
|
|
|
222
292
|
# Add process info if running
|
|
223
293
|
if is_running and pid:
|
|
@@ -240,7 +310,7 @@ class WorkerManager:
|
|
|
240
310
|
|
|
241
311
|
return status
|
|
242
312
|
|
|
243
|
-
def _get_pid(self) ->
|
|
313
|
+
def _get_pid(self) -> int | None:
|
|
244
314
|
"""Get worker PID from file.
|
|
245
315
|
|
|
246
316
|
Returns:
|
|
@@ -256,7 +326,7 @@ class WorkerManager:
|
|
|
256
326
|
except (OSError, ValueError):
|
|
257
327
|
return None
|
|
258
328
|
|
|
259
|
-
def _cleanup(self):
|
|
329
|
+
def _cleanup(self) -> None:
|
|
260
330
|
"""Clean up lock and PID files."""
|
|
261
331
|
self._release_lock()
|
|
262
332
|
if self.pid_file.exists():
|
mcp_ticketer/queue/queue.py
CHANGED
|
@@ -8,7 +8,7 @@ from dataclasses import asdict, dataclass
|
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class QueueStatus(str, Enum):
|
|
@@ -30,11 +30,12 @@ class QueueItem:
|
|
|
30
30
|
operation: str
|
|
31
31
|
status: QueueStatus
|
|
32
32
|
created_at: datetime
|
|
33
|
-
processed_at:
|
|
34
|
-
error_message:
|
|
33
|
+
processed_at: datetime | None = None
|
|
34
|
+
error_message: str | None = None
|
|
35
35
|
retry_count: int = 0
|
|
36
|
-
result:
|
|
37
|
-
project_dir:
|
|
36
|
+
result: dict[str, Any] | None = None
|
|
37
|
+
project_dir: str | None = None
|
|
38
|
+
adapter_config: dict[str, Any] | None = None # Adapter configuration
|
|
38
39
|
|
|
39
40
|
def to_dict(self) -> dict:
|
|
40
41
|
"""Convert to dictionary for storage."""
|
|
@@ -59,13 +60,14 @@ class QueueItem:
|
|
|
59
60
|
retry_count=row[8],
|
|
60
61
|
result=json.loads(row[9]) if row[9] else None,
|
|
61
62
|
project_dir=row[10] if len(row) > 10 else None,
|
|
63
|
+
adapter_config=json.loads(row[11]) if len(row) > 11 and row[11] else None,
|
|
62
64
|
)
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
class Queue:
|
|
66
68
|
"""Thread-safe SQLite queue for ticket operations."""
|
|
67
69
|
|
|
68
|
-
def __init__(self, db_path:
|
|
70
|
+
def __init__(self, db_path: Path | None = None):
|
|
69
71
|
"""Initialize queue with database connection.
|
|
70
72
|
|
|
71
73
|
Args:
|
|
@@ -81,7 +83,7 @@ class Queue:
|
|
|
81
83
|
self._lock = threading.Lock()
|
|
82
84
|
self._init_database()
|
|
83
85
|
|
|
84
|
-
def _init_database(self):
|
|
86
|
+
def _init_database(self) -> None:
|
|
85
87
|
"""Initialize database schema."""
|
|
86
88
|
with sqlite3.connect(self.db_path) as conn:
|
|
87
89
|
conn.execute(
|
|
@@ -127,6 +129,8 @@ class Queue:
|
|
|
127
129
|
columns = [row[1] for row in cursor.fetchall()]
|
|
128
130
|
if "project_dir" not in columns:
|
|
129
131
|
conn.execute("ALTER TABLE queue ADD COLUMN project_dir TEXT")
|
|
132
|
+
if "adapter_config" not in columns:
|
|
133
|
+
conn.execute("ALTER TABLE queue ADD COLUMN adapter_config TEXT")
|
|
130
134
|
|
|
131
135
|
conn.commit()
|
|
132
136
|
|
|
@@ -135,7 +139,8 @@ class Queue:
|
|
|
135
139
|
ticket_data: dict[str, Any],
|
|
136
140
|
adapter: str,
|
|
137
141
|
operation: str,
|
|
138
|
-
project_dir:
|
|
142
|
+
project_dir: str | None = None,
|
|
143
|
+
adapter_config: dict[str, Any] | None = None,
|
|
139
144
|
) -> str:
|
|
140
145
|
"""Add item to queue.
|
|
141
146
|
|
|
@@ -144,6 +149,7 @@ class Queue:
|
|
|
144
149
|
adapter: Name of the adapter to use
|
|
145
150
|
operation: Operation to perform (create, update, delete, etc.)
|
|
146
151
|
project_dir: Project directory for config resolution (defaults to current directory)
|
|
152
|
+
adapter_config: Adapter configuration to use (optional, for explicit config passing)
|
|
147
153
|
|
|
148
154
|
Returns:
|
|
149
155
|
Queue ID for tracking
|
|
@@ -161,8 +167,8 @@ class Queue:
|
|
|
161
167
|
"""
|
|
162
168
|
INSERT INTO queue (
|
|
163
169
|
id, ticket_data, adapter, operation,
|
|
164
|
-
status, created_at, retry_count, project_dir
|
|
165
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
170
|
+
status, created_at, retry_count, project_dir, adapter_config
|
|
171
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
166
172
|
""",
|
|
167
173
|
(
|
|
168
174
|
queue_id,
|
|
@@ -173,13 +179,14 @@ class Queue:
|
|
|
173
179
|
datetime.now().isoformat(),
|
|
174
180
|
0,
|
|
175
181
|
project_dir,
|
|
182
|
+
json.dumps(adapter_config) if adapter_config else None,
|
|
176
183
|
),
|
|
177
184
|
)
|
|
178
185
|
conn.commit()
|
|
179
186
|
|
|
180
187
|
return queue_id
|
|
181
188
|
|
|
182
|
-
def get_next_pending(self) ->
|
|
189
|
+
def get_next_pending(self) -> QueueItem | None:
|
|
183
190
|
"""Get next pending item from queue atomically.
|
|
184
191
|
|
|
185
192
|
Returns:
|
|
@@ -211,7 +218,12 @@ class Queue:
|
|
|
211
218
|
SET status = ?, processed_at = ?
|
|
212
219
|
WHERE id = ? AND status = ?
|
|
213
220
|
""",
|
|
214
|
-
(
|
|
221
|
+
(
|
|
222
|
+
QueueStatus.PROCESSING.value,
|
|
223
|
+
datetime.now().isoformat(),
|
|
224
|
+
row[0],
|
|
225
|
+
QueueStatus.PENDING.value,
|
|
226
|
+
),
|
|
215
227
|
)
|
|
216
228
|
|
|
217
229
|
# Check if update was successful (prevents race conditions)
|
|
@@ -239,9 +251,9 @@ class Queue:
|
|
|
239
251
|
self,
|
|
240
252
|
queue_id: str,
|
|
241
253
|
status: QueueStatus,
|
|
242
|
-
error_message:
|
|
243
|
-
result:
|
|
244
|
-
expected_status:
|
|
254
|
+
error_message: str | None = None,
|
|
255
|
+
result: dict[str, Any] | None = None,
|
|
256
|
+
expected_status: QueueStatus | None = None,
|
|
245
257
|
) -> bool:
|
|
246
258
|
"""Update queue item status atomically.
|
|
247
259
|
|
|
@@ -254,6 +266,7 @@ class Queue:
|
|
|
254
266
|
|
|
255
267
|
Returns:
|
|
256
268
|
True if update was successful, False if item was in unexpected state
|
|
269
|
+
|
|
257
270
|
"""
|
|
258
271
|
with self._lock:
|
|
259
272
|
with sqlite3.connect(self.db_path) as conn:
|
|
@@ -314,7 +327,9 @@ class Queue:
|
|
|
314
327
|
conn.rollback()
|
|
315
328
|
raise
|
|
316
329
|
|
|
317
|
-
def increment_retry(
|
|
330
|
+
def increment_retry(
|
|
331
|
+
self, queue_id: str, expected_status: QueueStatus | None = None
|
|
332
|
+
) -> int:
|
|
318
333
|
"""Increment retry count and reset to pending atomically.
|
|
319
334
|
|
|
320
335
|
Args:
|
|
@@ -340,7 +355,11 @@ class Queue:
|
|
|
340
355
|
WHERE id = ? AND status = ?
|
|
341
356
|
RETURNING retry_count
|
|
342
357
|
""",
|
|
343
|
-
(
|
|
358
|
+
(
|
|
359
|
+
QueueStatus.PENDING.value,
|
|
360
|
+
queue_id,
|
|
361
|
+
expected_status.value,
|
|
362
|
+
),
|
|
344
363
|
)
|
|
345
364
|
else:
|
|
346
365
|
# Regular increment
|
|
@@ -368,7 +387,7 @@ class Queue:
|
|
|
368
387
|
conn.rollback()
|
|
369
388
|
raise
|
|
370
389
|
|
|
371
|
-
def get_item(self, queue_id: str) ->
|
|
390
|
+
def get_item(self, queue_id: str) -> QueueItem | None:
|
|
372
391
|
"""Get specific queue item by ID.
|
|
373
392
|
|
|
374
393
|
Args:
|
|
@@ -390,7 +409,7 @@ class Queue:
|
|
|
390
409
|
return QueueItem.from_row(row) if row else None
|
|
391
410
|
|
|
392
411
|
def list_items(
|
|
393
|
-
self, status:
|
|
412
|
+
self, status: QueueStatus | None = None, limit: int = 50
|
|
394
413
|
) -> list[QueueItem]:
|
|
395
414
|
"""List queue items.
|
|
396
415
|
|
|
@@ -443,7 +462,7 @@ class Queue:
|
|
|
443
462
|
|
|
444
463
|
return cursor.fetchone()[0]
|
|
445
464
|
|
|
446
|
-
def cleanup_old(self, days: int = 7):
|
|
465
|
+
def cleanup_old(self, days: int = 7) -> None:
|
|
447
466
|
"""Clean up old completed/failed items.
|
|
448
467
|
|
|
449
468
|
Args:
|
|
@@ -468,7 +487,7 @@ class Queue:
|
|
|
468
487
|
)
|
|
469
488
|
conn.commit()
|
|
470
489
|
|
|
471
|
-
def reset_stuck_items(self, timeout_minutes: int = 30):
|
|
490
|
+
def reset_stuck_items(self, timeout_minutes: int = 30) -> None:
|
|
472
491
|
"""Reset items stuck in processing state.
|
|
473
492
|
|
|
474
493
|
Args:
|
mcp_ticketer/queue/run_worker.py
CHANGED
|
@@ -13,9 +13,14 @@ logging.basicConfig(
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def main():
|
|
16
|
+
def main() -> None:
|
|
17
17
|
"""Run the worker process."""
|
|
18
|
+
import os
|
|
19
|
+
|
|
18
20
|
logger.info("Starting standalone worker process")
|
|
21
|
+
logger.info(f"Worker Python executable: {sys.executable}")
|
|
22
|
+
logger.info(f"Worker working directory: {os.getcwd()}")
|
|
23
|
+
logger.info(f"Worker Python path: {sys.path[:3]}...") # Show first 3 entries
|
|
19
24
|
|
|
20
25
|
try:
|
|
21
26
|
# Create queue and worker
|