codex-autorunner 1.2.0__py3-none-any.whl → 1.3.0__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.
- codex_autorunner/bootstrap.py +26 -5
- codex_autorunner/core/about_car.py +12 -12
- codex_autorunner/core/config.py +178 -61
- codex_autorunner/core/context_awareness.py +1 -0
- codex_autorunner/core/filesystem.py +24 -0
- codex_autorunner/core/flows/controller.py +50 -12
- codex_autorunner/core/flows/runtime.py +8 -3
- codex_autorunner/core/hub.py +293 -16
- codex_autorunner/core/lifecycle_events.py +44 -5
- codex_autorunner/core/pma_context.py +188 -1
- codex_autorunner/core/pma_delivery.py +81 -0
- codex_autorunner/core/pma_dispatches.py +224 -0
- codex_autorunner/core/pma_lane_worker.py +122 -0
- codex_autorunner/core/pma_queue.py +167 -18
- codex_autorunner/core/pma_reactive.py +91 -0
- codex_autorunner/core/pma_safety.py +58 -0
- codex_autorunner/core/pma_sink.py +104 -0
- codex_autorunner/core/pma_transcripts.py +183 -0
- codex_autorunner/core/safe_paths.py +117 -0
- codex_autorunner/housekeeping.py +77 -23
- codex_autorunner/integrations/agents/codex_backend.py +18 -12
- codex_autorunner/integrations/agents/wiring.py +2 -0
- codex_autorunner/integrations/app_server/client.py +31 -0
- codex_autorunner/integrations/app_server/supervisor.py +3 -0
- codex_autorunner/integrations/telegram/adapter.py +1 -1
- codex_autorunner/integrations/telegram/config.py +1 -1
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +16 -15
- codex_autorunner/integrations/telegram/handlers/commands/files.py +5 -8
- codex_autorunner/integrations/telegram/handlers/commands/github.py +10 -6
- codex_autorunner/integrations/telegram/handlers/commands/shared.py +9 -8
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +85 -2
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +29 -8
- codex_autorunner/integrations/telegram/handlers/messages.py +8 -2
- codex_autorunner/integrations/telegram/helpers.py +30 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +54 -3
- codex_autorunner/static/archive.js +274 -81
- codex_autorunner/static/archiveApi.js +21 -0
- codex_autorunner/static/constants.js +1 -1
- codex_autorunner/static/docChatCore.js +2 -0
- codex_autorunner/static/hub.js +59 -0
- codex_autorunner/static/index.html +70 -54
- codex_autorunner/static/notificationBell.js +173 -0
- codex_autorunner/static/notifications.js +187 -36
- codex_autorunner/static/pma.js +96 -35
- codex_autorunner/static/styles.css +431 -4
- codex_autorunner/static/terminalManager.js +22 -3
- codex_autorunner/static/utils.js +5 -1
- codex_autorunner/surfaces/cli/cli.py +206 -129
- codex_autorunner/surfaces/cli/template_repos.py +157 -0
- codex_autorunner/surfaces/web/app.py +193 -5
- codex_autorunner/surfaces/web/routes/archive.py +197 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +115 -87
- codex_autorunner/surfaces/web/routes/flows.py +125 -67
- codex_autorunner/surfaces/web/routes/pma.py +638 -57
- codex_autorunner/surfaces/web/schemas.py +11 -0
- codex_autorunner/tickets/agent_pool.py +6 -1
- codex_autorunner/tickets/outbox.py +27 -14
- codex_autorunner/tickets/replies.py +4 -10
- codex_autorunner/tickets/runner.py +1 -0
- codex_autorunner/workspace/paths.py +8 -3
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/METADATA +1 -1
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/RECORD +67 -57
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -47,6 +47,7 @@ class WorkspaceAppServerSupervisor:
|
|
|
47
47
|
restart_backoff_initial_seconds: Optional[float] = None,
|
|
48
48
|
restart_backoff_max_seconds: Optional[float] = None,
|
|
49
49
|
restart_backoff_jitter_ratio: Optional[float] = None,
|
|
50
|
+
output_policy: str = "final_only",
|
|
50
51
|
default_approval_decision: str = "cancel",
|
|
51
52
|
max_handles: Optional[int] = None,
|
|
52
53
|
idle_ttl_seconds: Optional[float] = None,
|
|
@@ -78,6 +79,7 @@ class WorkspaceAppServerSupervisor:
|
|
|
78
79
|
self._restart_backoff_initial_seconds = restart_backoff_initial_seconds
|
|
79
80
|
self._restart_backoff_max_seconds = restart_backoff_max_seconds
|
|
80
81
|
self._restart_backoff_jitter_ratio = restart_backoff_jitter_ratio
|
|
82
|
+
self._output_policy = output_policy
|
|
81
83
|
self._default_approval_decision = default_approval_decision
|
|
82
84
|
self._max_handles = max_handles
|
|
83
85
|
self._idle_ttl_seconds = idle_ttl_seconds
|
|
@@ -170,6 +172,7 @@ class WorkspaceAppServerSupervisor:
|
|
|
170
172
|
restart_backoff_initial_seconds=self._restart_backoff_initial_seconds,
|
|
171
173
|
restart_backoff_max_seconds=self._restart_backoff_max_seconds,
|
|
172
174
|
restart_backoff_jitter_ratio=self._restart_backoff_jitter_ratio,
|
|
175
|
+
output_policy=self._output_policy,
|
|
173
176
|
notification_handler=self._notification_handler,
|
|
174
177
|
logger=self._logger,
|
|
175
178
|
)
|
|
@@ -1370,7 +1370,7 @@ class TelegramBotClient:
|
|
|
1370
1370
|
return bool(result) if isinstance(result, bool) else False
|
|
1371
1371
|
|
|
1372
1372
|
async def download_file(
|
|
1373
|
-
self, file_path: str, max_size_bytes: int =
|
|
1373
|
+
self, file_path: str, max_size_bytes: int = 100 * 1024 * 1024
|
|
1374
1374
|
) -> bytes:
|
|
1375
1375
|
safe_path = file_path.lstrip("/")
|
|
1376
1376
|
url = f"{self._file_base_url}/{safe_path}"
|
|
@@ -44,7 +44,7 @@ DEFAULT_APP_SERVER_TURN_TIMEOUT_SECONDS = 28800
|
|
|
44
44
|
DEFAULT_APPROVAL_TIMEOUT_SECONDS = 300.0
|
|
45
45
|
DEFAULT_MEDIA_MAX_IMAGE_BYTES = 10 * 1024 * 1024
|
|
46
46
|
DEFAULT_MEDIA_MAX_VOICE_BYTES = 10 * 1024 * 1024
|
|
47
|
-
DEFAULT_MEDIA_MAX_FILE_BYTES =
|
|
47
|
+
DEFAULT_MEDIA_MAX_FILE_BYTES = 100 * 1024 * 1024
|
|
48
48
|
DEFAULT_MEDIA_IMAGE_PROMPT = (
|
|
49
49
|
"The user sent an image with no caption. Use it to continue the "
|
|
50
50
|
"conversation; if no clear task, describe the image and ask what they want."
|
|
@@ -120,7 +120,7 @@ MAX_MENTION_BYTES = 200_000
|
|
|
120
120
|
VALID_REASONING_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh"}
|
|
121
121
|
VALID_AGENT_VALUES = {"codex", "opencode"}
|
|
122
122
|
DEFAULT_AGENT_MODELS = {
|
|
123
|
-
"codex": "gpt-5.
|
|
123
|
+
"codex": "gpt-5.3-codex",
|
|
124
124
|
"opencode": "zai-coding-plan/glm-4.7",
|
|
125
125
|
}
|
|
126
126
|
LEGACY_DEFAULT_AGENT_MODELS = DEFAULT_AGENT_MODELS
|
|
@@ -81,6 +81,7 @@ from ...helpers import (
|
|
|
81
81
|
_set_thread_summary,
|
|
82
82
|
_with_conversation_id,
|
|
83
83
|
find_github_links,
|
|
84
|
+
format_public_error,
|
|
84
85
|
is_interrupt_status,
|
|
85
86
|
)
|
|
86
87
|
from ...state import topic_key as build_topic_key
|
|
@@ -340,12 +341,12 @@ def _format_opencode_exception(exc: Exception) -> Optional[str]:
|
|
|
340
341
|
if isinstance(exc, OpenCodeSupervisorError):
|
|
341
342
|
detail = str(exc).strip()
|
|
342
343
|
if detail:
|
|
343
|
-
return f"OpenCode backend unavailable ({detail})."
|
|
344
|
+
return f"OpenCode backend unavailable ({format_public_error(detail)})."
|
|
344
345
|
return "OpenCode backend unavailable."
|
|
345
346
|
if isinstance(exc, OpenCodeProtocolError):
|
|
346
347
|
detail = str(exc).strip()
|
|
347
348
|
if detail:
|
|
348
|
-
return f"OpenCode protocol error: {detail}"
|
|
349
|
+
return f"OpenCode protocol error: {format_public_error(detail)}"
|
|
349
350
|
return "OpenCode protocol error."
|
|
350
351
|
if isinstance(exc, json.JSONDecodeError):
|
|
351
352
|
return "OpenCode returned invalid JSON."
|
|
@@ -356,15 +357,15 @@ def _format_opencode_exception(exc: Exception) -> Optional[str]:
|
|
|
356
357
|
except Exception:
|
|
357
358
|
detail = None
|
|
358
359
|
if detail:
|
|
359
|
-
return f"OpenCode error: {detail}"
|
|
360
|
+
return f"OpenCode error: {format_public_error(detail)}"
|
|
360
361
|
response_text = exc.response.text.strip()
|
|
361
362
|
if response_text:
|
|
362
|
-
return f"OpenCode error: {response_text}"
|
|
363
|
+
return f"OpenCode error: {format_public_error(response_text)}"
|
|
363
364
|
return f"OpenCode request failed (HTTP {exc.response.status_code})."
|
|
364
365
|
if isinstance(exc, httpx.RequestError):
|
|
365
366
|
detail = str(exc).strip()
|
|
366
367
|
if detail:
|
|
367
|
-
return f"OpenCode request failed: {detail}"
|
|
368
|
+
return f"OpenCode request failed: {format_public_error(detail)}"
|
|
368
369
|
return "OpenCode request failed."
|
|
369
370
|
return None
|
|
370
371
|
|
|
@@ -399,15 +400,15 @@ def _format_httpx_exception(exc: Exception) -> Optional[str]:
|
|
|
399
400
|
payload.get("detail") or payload.get("message") or payload.get("error")
|
|
400
401
|
)
|
|
401
402
|
if isinstance(detail, str) and detail:
|
|
402
|
-
return detail
|
|
403
|
+
return format_public_error(detail)
|
|
403
404
|
response_text = exc.response.text.strip()
|
|
404
405
|
if response_text:
|
|
405
|
-
return response_text
|
|
406
|
+
return format_public_error(response_text)
|
|
406
407
|
return f"Request failed (HTTP {exc.response.status_code})."
|
|
407
408
|
if isinstance(exc, httpx.RequestError):
|
|
408
409
|
detail = str(exc).strip()
|
|
409
410
|
if detail:
|
|
410
|
-
return detail
|
|
411
|
+
return format_public_error(detail)
|
|
411
412
|
return "Request failed."
|
|
412
413
|
return None
|
|
413
414
|
|
|
@@ -424,10 +425,7 @@ def _iter_exception_chain(exc: BaseException) -> list[BaseException]:
|
|
|
424
425
|
|
|
425
426
|
|
|
426
427
|
def _sanitize_error_detail(detail: str, *, limit: int = 200) -> str:
|
|
427
|
-
|
|
428
|
-
if len(cleaned) > limit:
|
|
429
|
-
return f"{cleaned[: limit - 3]}..."
|
|
430
|
-
return cleaned
|
|
428
|
+
return format_public_error(detail, limit=limit)
|
|
431
429
|
|
|
432
430
|
|
|
433
431
|
def _format_telegram_download_error(exc: Exception) -> Optional[str]:
|
|
@@ -435,10 +433,10 @@ def _format_telegram_download_error(exc: Exception) -> Optional[str]:
|
|
|
435
433
|
if isinstance(current, Exception):
|
|
436
434
|
detail = _format_httpx_exception(current)
|
|
437
435
|
if detail:
|
|
438
|
-
return
|
|
436
|
+
return format_public_error(detail)
|
|
439
437
|
message = str(current).strip()
|
|
440
438
|
if message and message not in _GENERIC_TELEGRAM_ERRORS:
|
|
441
|
-
return
|
|
439
|
+
return format_public_error(message)
|
|
442
440
|
return None
|
|
443
441
|
|
|
444
442
|
|
|
@@ -2386,7 +2384,10 @@ class ExecutionCommands(SharedHelpers):
|
|
|
2386
2384
|
runtime.interrupt_requested = False
|
|
2387
2385
|
|
|
2388
2386
|
response = _compose_agent_response(
|
|
2389
|
-
result
|
|
2387
|
+
getattr(result, "final_message", None),
|
|
2388
|
+
messages=result.agent_messages,
|
|
2389
|
+
errors=result.errors,
|
|
2390
|
+
status=result.status,
|
|
2390
2391
|
)
|
|
2391
2392
|
if thread_id and result.agent_messages:
|
|
2392
2393
|
assistant_preview = _preview_from_text(
|
|
@@ -15,7 +15,7 @@ from .....core.logging_utils import log_event
|
|
|
15
15
|
from .....core.state import now_iso
|
|
16
16
|
from ...adapter import TelegramMessage
|
|
17
17
|
from ...config import TelegramMediaCandidate
|
|
18
|
-
from ...helpers import _path_within
|
|
18
|
+
from ...helpers import _path_within, format_public_error
|
|
19
19
|
from ...state import PendingVoiceRecord, TelegramTopicRecord
|
|
20
20
|
from .. import messages as message_handlers
|
|
21
21
|
from .shared import SharedHelpers
|
|
@@ -57,10 +57,7 @@ def _iter_exception_chain(exc: BaseException) -> list[BaseException]:
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def _sanitize_error_detail(detail: str, *, limit: int = 200) -> str:
|
|
60
|
-
|
|
61
|
-
if len(cleaned) > limit:
|
|
62
|
-
return f"{cleaned[: limit - 3]}..."
|
|
63
|
-
return cleaned
|
|
60
|
+
return format_public_error(detail, limit=limit)
|
|
64
61
|
|
|
65
62
|
|
|
66
63
|
@dataclass
|
|
@@ -166,10 +163,10 @@ class FilesCommands(SharedHelpers):
|
|
|
166
163
|
if isinstance(current, Exception):
|
|
167
164
|
detail = self._format_httpx_exception(current)
|
|
168
165
|
if detail:
|
|
169
|
-
return
|
|
166
|
+
return format_public_error(detail)
|
|
170
167
|
message = str(current).strip()
|
|
171
168
|
if message and message not in _GENERIC_TELEGRAM_ERRORS:
|
|
172
|
-
return
|
|
169
|
+
return format_public_error(message)
|
|
173
170
|
return None
|
|
174
171
|
|
|
175
172
|
def _format_download_failure_response(
|
|
@@ -177,7 +174,7 @@ class FilesCommands(SharedHelpers):
|
|
|
177
174
|
) -> str:
|
|
178
175
|
base = f"Failed to download {kind}."
|
|
179
176
|
if detail:
|
|
180
|
-
return f"{base} Reason: {detail}"
|
|
177
|
+
return f"{base} Reason: {format_public_error(detail)}"
|
|
181
178
|
return base
|
|
182
179
|
|
|
183
180
|
def _format_media_batch_failure(
|
|
@@ -57,6 +57,7 @@ from ...helpers import (
|
|
|
57
57
|
_preview_from_text,
|
|
58
58
|
_set_thread_summary,
|
|
59
59
|
_with_conversation_id,
|
|
60
|
+
format_public_error,
|
|
60
61
|
is_interrupt_status,
|
|
61
62
|
)
|
|
62
63
|
from ...types import ReviewCommitSelectionState, TurnContext
|
|
@@ -493,7 +494,10 @@ class GitHubCommands(SharedHelpers):
|
|
|
493
494
|
) -> None:
|
|
494
495
|
"""Handle successful Codex review completion."""
|
|
495
496
|
response = _compose_agent_response(
|
|
496
|
-
result
|
|
497
|
+
getattr(result, "final_message", None),
|
|
498
|
+
messages=result.agent_messages,
|
|
499
|
+
errors=result.errors,
|
|
500
|
+
status=result.status,
|
|
497
501
|
)
|
|
498
502
|
if thread_id and result.agent_messages:
|
|
499
503
|
assistant_preview = _preview_from_text(
|
|
@@ -1640,12 +1644,12 @@ def _format_opencode_exception(exc: Exception) -> Optional[str]:
|
|
|
1640
1644
|
if isinstance(exc, OpenCodeSupervisorError):
|
|
1641
1645
|
detail = str(exc).strip()
|
|
1642
1646
|
if detail:
|
|
1643
|
-
return f"OpenCode backend unavailable ({detail})."
|
|
1647
|
+
return f"OpenCode backend unavailable ({format_public_error(detail)})."
|
|
1644
1648
|
return "OpenCode backend unavailable."
|
|
1645
1649
|
if isinstance(exc, OpenCodeProtocolError):
|
|
1646
1650
|
detail = str(exc).strip()
|
|
1647
1651
|
if detail:
|
|
1648
|
-
return f"OpenCode protocol error: {detail}"
|
|
1652
|
+
return f"OpenCode protocol error: {format_public_error(detail)}"
|
|
1649
1653
|
return "OpenCode protocol error."
|
|
1650
1654
|
if isinstance(exc, json.JSONDecodeError):
|
|
1651
1655
|
return "OpenCode returned invalid JSON."
|
|
@@ -1656,15 +1660,15 @@ def _format_opencode_exception(exc: Exception) -> Optional[str]:
|
|
|
1656
1660
|
except Exception:
|
|
1657
1661
|
detail = None
|
|
1658
1662
|
if detail:
|
|
1659
|
-
return f"OpenCode error: {detail}"
|
|
1663
|
+
return f"OpenCode error: {format_public_error(detail)}"
|
|
1660
1664
|
response_text = exc.response.text.strip()
|
|
1661
1665
|
if response_text:
|
|
1662
|
-
return f"OpenCode error: {response_text}"
|
|
1666
|
+
return f"OpenCode error: {format_public_error(response_text)}"
|
|
1663
1667
|
return f"OpenCode request failed (HTTP {exc.response.status_code})."
|
|
1664
1668
|
if isinstance(exc, httpx.RequestError):
|
|
1665
1669
|
detail = str(exc).strip()
|
|
1666
1670
|
if detail:
|
|
1667
|
-
return f"OpenCode request failed: {detail}"
|
|
1671
|
+
return f"OpenCode request failed: {format_public_error(detail)}"
|
|
1668
1672
|
return "OpenCode request failed."
|
|
1669
1673
|
return None
|
|
1670
1674
|
|
|
@@ -11,6 +11,7 @@ import httpx
|
|
|
11
11
|
from .....agents.opencode.client import OpenCodeProtocolError
|
|
12
12
|
from .....agents.opencode.supervisor import OpenCodeSupervisorError
|
|
13
13
|
from ...adapter import InlineButton, build_inline_keyboard, encode_cancel_callback
|
|
14
|
+
from ...helpers import format_public_error
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
pass
|
|
@@ -60,15 +61,15 @@ class SharedHelpers:
|
|
|
60
61
|
or payload.get("error")
|
|
61
62
|
)
|
|
62
63
|
if isinstance(detail, str) and detail:
|
|
63
|
-
return detail
|
|
64
|
+
return format_public_error(detail)
|
|
64
65
|
response_text = exc.response.text.strip()
|
|
65
66
|
if response_text:
|
|
66
|
-
return response_text
|
|
67
|
+
return format_public_error(response_text)
|
|
67
68
|
return f"Request failed (HTTP {exc.response.status_code})."
|
|
68
69
|
if isinstance(exc, httpx.RequestError):
|
|
69
70
|
detail = str(exc).strip()
|
|
70
71
|
if detail:
|
|
71
|
-
return detail
|
|
72
|
+
return format_public_error(detail)
|
|
72
73
|
return "Request failed."
|
|
73
74
|
return None
|
|
74
75
|
|
|
@@ -84,12 +85,12 @@ class SharedHelpers:
|
|
|
84
85
|
if isinstance(exc, OpenCodeSupervisorError):
|
|
85
86
|
detail = str(exc).strip()
|
|
86
87
|
if detail:
|
|
87
|
-
return f"OpenCode backend unavailable ({detail})."
|
|
88
|
+
return f"OpenCode backend unavailable ({format_public_error(detail)})."
|
|
88
89
|
return "OpenCode backend unavailable."
|
|
89
90
|
if isinstance(exc, OpenCodeProtocolError):
|
|
90
91
|
detail = str(exc).strip()
|
|
91
92
|
if detail:
|
|
92
|
-
return f"OpenCode protocol error: {detail}"
|
|
93
|
+
return f"OpenCode protocol error: {format_public_error(detail)}"
|
|
93
94
|
return "OpenCode protocol error."
|
|
94
95
|
if isinstance(exc, json.JSONDecodeError):
|
|
95
96
|
return "OpenCode returned invalid JSON."
|
|
@@ -100,15 +101,15 @@ class SharedHelpers:
|
|
|
100
101
|
except Exception:
|
|
101
102
|
detail = None
|
|
102
103
|
if detail:
|
|
103
|
-
return f"OpenCode error: {detail}"
|
|
104
|
+
return f"OpenCode error: {format_public_error(detail)}"
|
|
104
105
|
response_text = exc.response.text.strip()
|
|
105
106
|
if response_text:
|
|
106
|
-
return f"OpenCode error: {response_text}"
|
|
107
|
+
return f"OpenCode error: {format_public_error(response_text)}"
|
|
107
108
|
return f"OpenCode request failed (HTTP {exc.response.status_code})."
|
|
108
109
|
if isinstance(exc, httpx.RequestError):
|
|
109
110
|
detail = str(exc).strip()
|
|
110
111
|
if detail:
|
|
111
|
-
return f"OpenCode request failed: {detail}"
|
|
112
|
+
return f"OpenCode request failed: {format_public_error(detail)}"
|
|
112
113
|
return "OpenCode request failed."
|
|
113
114
|
return None
|
|
114
115
|
|
|
@@ -16,6 +16,7 @@ from .....core.utils import canonicalize_path, resolve_opencode_binary
|
|
|
16
16
|
from .....manifest import load_manifest
|
|
17
17
|
from ....app_server.client import (
|
|
18
18
|
CodexAppServerClient,
|
|
19
|
+
CodexAppServerResponseError,
|
|
19
20
|
)
|
|
20
21
|
from ...adapter import (
|
|
21
22
|
TelegramCallbackQuery,
|
|
@@ -112,6 +113,12 @@ class ResumeThreadData:
|
|
|
112
113
|
|
|
113
114
|
|
|
114
115
|
class WorkspaceCommands(SharedHelpers):
|
|
116
|
+
def _is_missing_thread_error(self, exc: Exception) -> bool:
|
|
117
|
+
if not isinstance(exc, CodexAppServerResponseError):
|
|
118
|
+
return False
|
|
119
|
+
message = str(exc).lower()
|
|
120
|
+
return "thread not found" in message
|
|
121
|
+
|
|
115
122
|
def _resolve_workspace_path(
|
|
116
123
|
self,
|
|
117
124
|
record: Optional["TelegramTopicRecord"],
|
|
@@ -390,6 +397,18 @@ class WorkspaceCommands(SharedHelpers):
|
|
|
390
397
|
try:
|
|
391
398
|
result = await client.thread_resume(thread_id)
|
|
392
399
|
except Exception as exc:
|
|
400
|
+
if self._is_missing_thread_error(exc):
|
|
401
|
+
log_event(
|
|
402
|
+
self._logger,
|
|
403
|
+
logging.INFO,
|
|
404
|
+
"telegram.thread.verify_missing",
|
|
405
|
+
chat_id=message.chat_id,
|
|
406
|
+
thread_id=message.thread_id,
|
|
407
|
+
codex_thread_id=thread_id,
|
|
408
|
+
)
|
|
409
|
+
return await self._router.set_active_thread(
|
|
410
|
+
message.chat_id, message.thread_id, None
|
|
411
|
+
)
|
|
393
412
|
log_event(
|
|
394
413
|
self._logger,
|
|
395
414
|
logging.WARNING,
|
|
@@ -1113,7 +1132,25 @@ class WorkspaceCommands(SharedHelpers):
|
|
|
1113
1132
|
reply_to=message.message_id,
|
|
1114
1133
|
)
|
|
1115
1134
|
return
|
|
1116
|
-
|
|
1135
|
+
try:
|
|
1136
|
+
thread = await client.thread_start(record.workspace_path, agent=agent)
|
|
1137
|
+
except Exception as exc:
|
|
1138
|
+
log_event(
|
|
1139
|
+
self._logger,
|
|
1140
|
+
logging.WARNING,
|
|
1141
|
+
"telegram.reset.failed",
|
|
1142
|
+
chat_id=message.chat_id,
|
|
1143
|
+
thread_id=message.thread_id,
|
|
1144
|
+
workspace_path=record.workspace_path,
|
|
1145
|
+
exc=exc,
|
|
1146
|
+
)
|
|
1147
|
+
await self._send_message(
|
|
1148
|
+
message.chat_id,
|
|
1149
|
+
"Failed to reset thread; check logs for details.",
|
|
1150
|
+
thread_id=message.thread_id,
|
|
1151
|
+
reply_to=message.message_id,
|
|
1152
|
+
)
|
|
1153
|
+
return
|
|
1117
1154
|
if not await self._require_thread_workspace(
|
|
1118
1155
|
message, record.workspace_path, thread, action="thread_start"
|
|
1119
1156
|
):
|
|
@@ -1264,7 +1301,25 @@ class WorkspaceCommands(SharedHelpers):
|
|
|
1264
1301
|
reply_to=message.message_id,
|
|
1265
1302
|
)
|
|
1266
1303
|
return
|
|
1267
|
-
|
|
1304
|
+
try:
|
|
1305
|
+
thread = await client.thread_start(record.workspace_path, agent=agent)
|
|
1306
|
+
except Exception as exc:
|
|
1307
|
+
log_event(
|
|
1308
|
+
self._logger,
|
|
1309
|
+
logging.WARNING,
|
|
1310
|
+
"telegram.new.failed",
|
|
1311
|
+
chat_id=message.chat_id,
|
|
1312
|
+
thread_id=message.thread_id,
|
|
1313
|
+
workspace_path=record.workspace_path,
|
|
1314
|
+
exc=exc,
|
|
1315
|
+
)
|
|
1316
|
+
await self._send_message(
|
|
1317
|
+
message.chat_id,
|
|
1318
|
+
"Failed to start a new thread; check logs for details.",
|
|
1319
|
+
thread_id=message.thread_id,
|
|
1320
|
+
reply_to=message.message_id,
|
|
1321
|
+
)
|
|
1322
|
+
return
|
|
1268
1323
|
if not await self._require_thread_workspace(
|
|
1269
1324
|
message, record.workspace_path, thread, action="thread_start"
|
|
1270
1325
|
):
|
|
@@ -2044,6 +2099,34 @@ class WorkspaceCommands(SharedHelpers):
|
|
|
2044
2099
|
try:
|
|
2045
2100
|
result = await client.thread_resume(thread_id)
|
|
2046
2101
|
except Exception as exc:
|
|
2102
|
+
if self._is_missing_thread_error(exc):
|
|
2103
|
+
log_event(
|
|
2104
|
+
self._logger,
|
|
2105
|
+
logging.INFO,
|
|
2106
|
+
"telegram.resume.missing_thread",
|
|
2107
|
+
topic_key=key,
|
|
2108
|
+
thread_id=thread_id,
|
|
2109
|
+
)
|
|
2110
|
+
|
|
2111
|
+
def clear_stale(record: "TelegramTopicRecord") -> None:
|
|
2112
|
+
if record.active_thread_id == thread_id:
|
|
2113
|
+
record.active_thread_id = None
|
|
2114
|
+
if thread_id in record.thread_ids:
|
|
2115
|
+
record.thread_ids.remove(thread_id)
|
|
2116
|
+
record.thread_summaries.pop(thread_id, None)
|
|
2117
|
+
|
|
2118
|
+
await self._store.update_topic(key, clear_stale)
|
|
2119
|
+
await self._answer_callback(callback, "Thread missing")
|
|
2120
|
+
await self._finalize_selection(
|
|
2121
|
+
key,
|
|
2122
|
+
callback,
|
|
2123
|
+
_with_conversation_id(
|
|
2124
|
+
"Thread no longer exists. Cleared stale state; use /new to start a fresh thread.",
|
|
2125
|
+
chat_id=chat_id,
|
|
2126
|
+
thread_id=thread_id_val,
|
|
2127
|
+
),
|
|
2128
|
+
)
|
|
2129
|
+
return
|
|
2047
2130
|
log_event(
|
|
2048
2131
|
self._logger,
|
|
2049
2132
|
logging.WARNING,
|
|
@@ -18,6 +18,7 @@ import httpx
|
|
|
18
18
|
from ....agents.opencode.client import OpenCodeProtocolError
|
|
19
19
|
from ....agents.opencode.supervisor import OpenCodeSupervisorError
|
|
20
20
|
from ....core.logging_utils import log_event
|
|
21
|
+
from ....core.pma_sink import PmaActiveSinkStore
|
|
21
22
|
from ....core.state import now_iso
|
|
22
23
|
from ....core.update import _normalize_update_target, _spawn_update_process
|
|
23
24
|
from ....core.update_paths import resolve_update_paths
|
|
@@ -81,6 +82,7 @@ from ..helpers import (
|
|
|
81
82
|
_with_conversation_id,
|
|
82
83
|
derive_codex_features_command,
|
|
83
84
|
format_codex_features,
|
|
85
|
+
format_public_error,
|
|
84
86
|
parse_codex_features_list,
|
|
85
87
|
)
|
|
86
88
|
from ..state import (
|
|
@@ -158,12 +160,12 @@ def _format_opencode_exception(exc: Exception) -> Optional[str]:
|
|
|
158
160
|
if isinstance(exc, OpenCodeSupervisorError):
|
|
159
161
|
detail = str(exc).strip()
|
|
160
162
|
if detail:
|
|
161
|
-
return f"OpenCode backend unavailable ({detail})."
|
|
163
|
+
return f"OpenCode backend unavailable ({format_public_error(detail)})."
|
|
162
164
|
return "OpenCode backend unavailable."
|
|
163
165
|
if isinstance(exc, OpenCodeProtocolError):
|
|
164
166
|
detail = str(exc).strip()
|
|
165
167
|
if detail:
|
|
166
|
-
return f"OpenCode protocol error: {detail}"
|
|
168
|
+
return f"OpenCode protocol error: {format_public_error(detail)}"
|
|
167
169
|
return "OpenCode protocol error."
|
|
168
170
|
if isinstance(exc, json.JSONDecodeError):
|
|
169
171
|
return "OpenCode returned invalid JSON."
|
|
@@ -174,15 +176,15 @@ def _format_opencode_exception(exc: Exception) -> Optional[str]:
|
|
|
174
176
|
except Exception:
|
|
175
177
|
detail = None
|
|
176
178
|
if detail:
|
|
177
|
-
return f"OpenCode error: {detail}"
|
|
179
|
+
return f"OpenCode error: {format_public_error(detail)}"
|
|
178
180
|
response_text = exc.response.text.strip()
|
|
179
181
|
if response_text:
|
|
180
|
-
return f"OpenCode error: {response_text}"
|
|
182
|
+
return f"OpenCode error: {format_public_error(response_text)}"
|
|
181
183
|
return f"OpenCode request failed (HTTP {exc.response.status_code})."
|
|
182
184
|
if isinstance(exc, httpx.RequestError):
|
|
183
185
|
detail = str(exc).strip()
|
|
184
186
|
if detail:
|
|
185
|
-
return f"OpenCode request failed: {detail}"
|
|
187
|
+
return f"OpenCode request failed: {format_public_error(detail)}"
|
|
186
188
|
return "OpenCode request failed."
|
|
187
189
|
return None
|
|
188
190
|
|
|
@@ -239,15 +241,15 @@ def _format_httpx_exception(exc: Exception) -> Optional[str]:
|
|
|
239
241
|
payload.get("detail") or payload.get("message") or payload.get("error")
|
|
240
242
|
)
|
|
241
243
|
if isinstance(detail, str) and detail:
|
|
242
|
-
return detail
|
|
244
|
+
return format_public_error(detail)
|
|
243
245
|
response_text = exc.response.text.strip()
|
|
244
246
|
if response_text:
|
|
245
|
-
return response_text
|
|
247
|
+
return format_public_error(response_text)
|
|
246
248
|
return f"Request failed (HTTP {exc.response.status_code})."
|
|
247
249
|
if isinstance(exc, httpx.RequestError):
|
|
248
250
|
detail = str(exc).strip()
|
|
249
251
|
if detail:
|
|
250
|
-
return detail
|
|
252
|
+
return format_public_error(detail)
|
|
251
253
|
return "Request failed."
|
|
252
254
|
return None
|
|
253
255
|
|
|
@@ -1202,6 +1204,25 @@ class TelegramCommandHandlers(
|
|
|
1202
1204
|
message.thread_id,
|
|
1203
1205
|
apply_pma,
|
|
1204
1206
|
)
|
|
1207
|
+
try:
|
|
1208
|
+
sink_store = PmaActiveSinkStore(Path(self._hub_root))
|
|
1209
|
+
if enabled:
|
|
1210
|
+
sink_store.set_telegram(
|
|
1211
|
+
chat_id=message.chat_id,
|
|
1212
|
+
thread_id=message.thread_id,
|
|
1213
|
+
topic_key=topic_key(message.chat_id, message.thread_id),
|
|
1214
|
+
)
|
|
1215
|
+
else:
|
|
1216
|
+
sink_store.clear()
|
|
1217
|
+
except Exception:
|
|
1218
|
+
log_event(
|
|
1219
|
+
self._logger,
|
|
1220
|
+
logging.WARNING,
|
|
1221
|
+
"telegram.pma.active_sink.update_failed",
|
|
1222
|
+
chat_id=message.chat_id,
|
|
1223
|
+
thread_id=message.thread_id,
|
|
1224
|
+
enabled=enabled,
|
|
1225
|
+
)
|
|
1205
1226
|
status = "enabled" if enabled else "disabled"
|
|
1206
1227
|
if enabled:
|
|
1207
1228
|
hint = "Use /pma off to exit. Previous repo binding saved."
|
|
@@ -859,7 +859,10 @@ async def handle_media_message(
|
|
|
859
859
|
best = photos[0]
|
|
860
860
|
try:
|
|
861
861
|
file_info = await handlers._bot.get_file(best.file_id)
|
|
862
|
-
data = await handlers._bot.download_file(
|
|
862
|
+
data = await handlers._bot.download_file(
|
|
863
|
+
file_info.file_path,
|
|
864
|
+
max_size_bytes=handlers._config.media.max_image_bytes,
|
|
865
|
+
)
|
|
863
866
|
filename = f"photo_{best.file_id}.jpg"
|
|
864
867
|
files.append((filename, data))
|
|
865
868
|
except Exception as exc:
|
|
@@ -868,7 +871,10 @@ async def handle_media_message(
|
|
|
868
871
|
elif message.document:
|
|
869
872
|
try:
|
|
870
873
|
file_info = await handlers._bot.get_file(message.document.file_id)
|
|
871
|
-
data = await handlers._bot.download_file(
|
|
874
|
+
data = await handlers._bot.download_file(
|
|
875
|
+
file_info.file_path,
|
|
876
|
+
max_size_bytes=handlers._config.media.max_file_bytes,
|
|
877
|
+
)
|
|
872
878
|
filename = (
|
|
873
879
|
message.document.file_name or f"document_{message.document.file_id}"
|
|
874
880
|
)
|
|
@@ -9,6 +9,7 @@ from datetime import datetime, timedelta, timezone
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Callable, Iterable, Optional, Sequence
|
|
11
11
|
|
|
12
|
+
from ...core.redaction import redact_text
|
|
12
13
|
from ...core.state_roots import resolve_global_state_root
|
|
13
14
|
from ...core.utils import (
|
|
14
15
|
RepoNotFoundError,
|
|
@@ -2009,12 +2010,17 @@ def _extract_first_bold_span(text: str) -> Optional[str]:
|
|
|
2009
2010
|
|
|
2010
2011
|
|
|
2011
2012
|
def _compose_agent_response(
|
|
2012
|
-
|
|
2013
|
+
final_message: Optional[str] = None,
|
|
2013
2014
|
*,
|
|
2015
|
+
messages: Optional[list[str]] = None,
|
|
2014
2016
|
errors: Optional[list[str]] = None,
|
|
2015
2017
|
status: Optional[str] = None,
|
|
2016
2018
|
) -> str:
|
|
2017
|
-
|
|
2019
|
+
if isinstance(final_message, str) and final_message.strip():
|
|
2020
|
+
return final_message.strip()
|
|
2021
|
+
cleaned = [
|
|
2022
|
+
msg.strip() for msg in (messages or []) if isinstance(msg, str) and msg.strip()
|
|
2023
|
+
]
|
|
2018
2024
|
if not cleaned:
|
|
2019
2025
|
cleaned_errors = [
|
|
2020
2026
|
err.strip()
|
|
@@ -2183,3 +2189,25 @@ def _format_selection_prompt(base: str, page: int, total_pages: int) -> str:
|
|
|
2183
2189
|
return base
|
|
2184
2190
|
trimmed = base.rstrip(".")
|
|
2185
2191
|
return f"{trimmed} (page {page + 1}/{total_pages})."
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
def format_public_error(detail: str, *, limit: int = 200) -> str:
|
|
2195
|
+
"""Format error detail for public Telegram messages with redaction and truncation.
|
|
2196
|
+
|
|
2197
|
+
This helper ensures all user-visible error text sent via Telegram is:
|
|
2198
|
+
- Short and readable
|
|
2199
|
+
- Redacted for known secret patterns
|
|
2200
|
+
- Does not include raw file contents or stack traces
|
|
2201
|
+
|
|
2202
|
+
Args:
|
|
2203
|
+
detail: Error detail string to format.
|
|
2204
|
+
limit: Maximum length of output (default 200).
|
|
2205
|
+
|
|
2206
|
+
Returns:
|
|
2207
|
+
Formatted error string with secrets redacted and length limited.
|
|
2208
|
+
"""
|
|
2209
|
+
normalized = " ".join(detail.split())
|
|
2210
|
+
redacted = redact_text(normalized)
|
|
2211
|
+
if len(redacted) > limit:
|
|
2212
|
+
return f"{redacted[: limit - 3]}..."
|
|
2213
|
+
return redacted
|