codex-autorunner 1.2.1__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/config.py +176 -59
- 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_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/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/helpers.py +30 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +54 -3
- 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 +154 -36
- codex_autorunner/static/pma.js +96 -35
- codex_autorunner/static/styles.css +415 -4
- 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/file_chat.py +109 -61
- codex_autorunner/surfaces/web/routes/flows.py +125 -67
- codex_autorunner/surfaces/web/routes/pma.py +638 -57
- 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.1.dist-info → codex_autorunner-1.3.0.dist-info}/METADATA +1 -1
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/RECORD +55 -45
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -47,6 +47,7 @@ class CodexAppServerBackend(AgentBackend):
|
|
|
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
|
notification_handler: Optional[NotificationHandler] = None,
|
|
51
52
|
logger: Optional[logging.Logger] = None,
|
|
52
53
|
):
|
|
@@ -71,6 +72,7 @@ class CodexAppServerBackend(AgentBackend):
|
|
|
71
72
|
self._restart_backoff_initial_seconds = restart_backoff_initial_seconds
|
|
72
73
|
self._restart_backoff_max_seconds = restart_backoff_max_seconds
|
|
73
74
|
self._restart_backoff_jitter_ratio = restart_backoff_jitter_ratio
|
|
75
|
+
self._output_policy = output_policy
|
|
74
76
|
self._notification_handler = notification_handler
|
|
75
77
|
self._logger = logger or _logger
|
|
76
78
|
|
|
@@ -102,6 +104,7 @@ class CodexAppServerBackend(AgentBackend):
|
|
|
102
104
|
restart_backoff_initial_seconds=self._restart_backoff_initial_seconds,
|
|
103
105
|
restart_backoff_max_seconds=self._restart_backoff_max_seconds,
|
|
104
106
|
restart_backoff_jitter_ratio=self._restart_backoff_jitter_ratio,
|
|
107
|
+
output_policy=self._output_policy,
|
|
105
108
|
logger=self._logger,
|
|
106
109
|
)
|
|
107
110
|
await self._client.start()
|
|
@@ -201,16 +204,18 @@ class CodexAppServerBackend(AgentBackend):
|
|
|
201
204
|
yield AgentEvent.stream_delta(content=message, delta_type="user_message")
|
|
202
205
|
|
|
203
206
|
result = await handle.wait(timeout=self._turn_timeout_seconds)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
final_text = str(getattr(result, "final_message", "") or "")
|
|
208
|
+
if not final_text.strip():
|
|
209
|
+
final_text = "\n\n".join(
|
|
210
|
+
msg.strip()
|
|
211
|
+
for msg in getattr(result, "agent_messages", [])
|
|
212
|
+
if isinstance(msg, str) and msg.strip()
|
|
213
|
+
)
|
|
207
214
|
|
|
208
215
|
for event_data in result.raw_events:
|
|
209
216
|
yield self._parse_raw_event(event_data)
|
|
210
217
|
|
|
211
|
-
yield AgentEvent.message_complete(
|
|
212
|
-
final_message="\n".join(result.agent_messages)
|
|
213
|
-
)
|
|
218
|
+
yield AgentEvent.message_complete(final_message=final_text)
|
|
214
219
|
|
|
215
220
|
async def run_turn_events(
|
|
216
221
|
self, session_id: str, message: str
|
|
@@ -283,11 +288,12 @@ class CodexAppServerBackend(AgentBackend):
|
|
|
283
288
|
if get_task in pending_set:
|
|
284
289
|
get_task.cancel()
|
|
285
290
|
result = wait_task.result()
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
+
final_text = str(getattr(result, "final_message", "") or "")
|
|
292
|
+
if not final_text.strip():
|
|
293
|
+
final_text = "\n\n".join(
|
|
294
|
+
msg.strip()
|
|
295
|
+
for msg in getattr(result, "agent_messages", [])
|
|
296
|
+
if isinstance(msg, str) and msg.strip()
|
|
291
297
|
)
|
|
292
298
|
# raw_events already contain the same notifications we streamed
|
|
293
299
|
# through _event_queue; skipping here avoids double-emitting.
|
|
@@ -297,7 +303,7 @@ class CodexAppServerBackend(AgentBackend):
|
|
|
297
303
|
yield extra
|
|
298
304
|
yield Completed(
|
|
299
305
|
timestamp=now_iso(),
|
|
300
|
-
final_message=
|
|
306
|
+
final_message=final_text,
|
|
301
307
|
)
|
|
302
308
|
break
|
|
303
309
|
|
|
@@ -106,6 +106,7 @@ class AgentBackendFactory:
|
|
|
106
106
|
restart_backoff_initial_seconds=self._config.app_server.client.restart_backoff_initial_seconds,
|
|
107
107
|
restart_backoff_max_seconds=self._config.app_server.client.restart_backoff_max_seconds,
|
|
108
108
|
restart_backoff_jitter_ratio=self._config.app_server.client.restart_backoff_jitter_ratio,
|
|
109
|
+
output_policy=self._config.app_server.output.policy,
|
|
109
110
|
notification_handler=notification_handler,
|
|
110
111
|
logger=self._logger,
|
|
111
112
|
)
|
|
@@ -267,6 +268,7 @@ def build_app_server_supervisor_factory(
|
|
|
267
268
|
restart_backoff_initial_seconds=config.app_server.client.restart_backoff_initial_seconds,
|
|
268
269
|
restart_backoff_max_seconds=config.app_server.client.restart_backoff_max_seconds,
|
|
269
270
|
restart_backoff_jitter_ratio=config.app_server.client.restart_backoff_jitter_ratio,
|
|
271
|
+
output_policy=config.app_server.output.policy,
|
|
270
272
|
)
|
|
271
273
|
|
|
272
274
|
return factory
|
|
@@ -62,6 +62,8 @@ _TURN_STALL_POLL_INTERVAL_SECONDS = 2.0
|
|
|
62
62
|
_TURN_STALL_RECOVERY_MIN_INTERVAL_SECONDS = 10.0
|
|
63
63
|
_MAX_TURN_RAW_EVENTS = 200
|
|
64
64
|
_INVALID_JSON_PREVIEW_BYTES = 200
|
|
65
|
+
_DEFAULT_OUTPUT_POLICY = "final_only"
|
|
66
|
+
_OUTPUT_POLICIES = {"final_only", "all_agent_messages"}
|
|
65
67
|
|
|
66
68
|
# Track live clients so tests/cleanup can cancel any background restart tasks.
|
|
67
69
|
_CLIENT_INSTANCES: weakref.WeakSet = weakref.WeakSet()
|
|
@@ -108,6 +110,7 @@ class CodexAppServerProtocolError(CodexAppServerError, PermanentError):
|
|
|
108
110
|
class TurnResult:
|
|
109
111
|
turn_id: str
|
|
110
112
|
status: Optional[str]
|
|
113
|
+
final_message: str
|
|
111
114
|
agent_messages: list[str]
|
|
112
115
|
errors: list[str]
|
|
113
116
|
raw_events: list[Dict[str, Any]]
|
|
@@ -163,6 +166,7 @@ class CodexAppServerClient:
|
|
|
163
166
|
restart_backoff_initial_seconds: Optional[float] = None,
|
|
164
167
|
restart_backoff_max_seconds: Optional[float] = None,
|
|
165
168
|
restart_backoff_jitter_ratio: Optional[float] = None,
|
|
169
|
+
output_policy: str = _DEFAULT_OUTPUT_POLICY,
|
|
166
170
|
notification_handler: Optional[NotificationHandler] = None,
|
|
167
171
|
logger: Optional[logging.Logger] = None,
|
|
168
172
|
) -> None:
|
|
@@ -217,6 +221,7 @@ class CodexAppServerClient:
|
|
|
217
221
|
and restart_backoff_jitter_ratio >= 0
|
|
218
222
|
else _RESTART_BACKOFF_JITTER_RATIO
|
|
219
223
|
)
|
|
224
|
+
self._output_policy = _normalize_output_policy(output_policy)
|
|
220
225
|
|
|
221
226
|
self._process: Optional[asyncio.subprocess.Process] = None
|
|
222
227
|
self._reader_task: Optional[asyncio.Task] = None
|
|
@@ -558,6 +563,9 @@ class CodexAppServerClient:
|
|
|
558
563
|
state.future.set_result(
|
|
559
564
|
TurnResult(
|
|
560
565
|
turn_id=state.turn_id,
|
|
566
|
+
final_message=_final_message_for_result(
|
|
567
|
+
state, policy=self._output_policy
|
|
568
|
+
),
|
|
561
569
|
agent_messages=_agent_messages_for_result(state),
|
|
562
570
|
errors=list(state.errors),
|
|
563
571
|
raw_events=list(state.raw_events),
|
|
@@ -1269,6 +1277,9 @@ class CodexAppServerClient:
|
|
|
1269
1277
|
TurnResult(
|
|
1270
1278
|
turn_id=target.turn_id,
|
|
1271
1279
|
status=target.status,
|
|
1280
|
+
final_message=_final_message_for_result(
|
|
1281
|
+
target, policy=self._output_policy
|
|
1282
|
+
),
|
|
1272
1283
|
agent_messages=_agent_messages_for_result(target),
|
|
1273
1284
|
errors=list(target.errors),
|
|
1274
1285
|
raw_events=list(target.raw_events),
|
|
@@ -1366,6 +1377,9 @@ class CodexAppServerClient:
|
|
|
1366
1377
|
TurnResult(
|
|
1367
1378
|
turn_id=state.turn_id,
|
|
1368
1379
|
status=state.status,
|
|
1380
|
+
final_message=_final_message_for_result(
|
|
1381
|
+
state, policy=self._output_policy
|
|
1382
|
+
),
|
|
1369
1383
|
agent_messages=_agent_messages_for_result(state),
|
|
1370
1384
|
errors=list(state.errors),
|
|
1371
1385
|
raw_events=list(state.raw_events),
|
|
@@ -1701,6 +1715,23 @@ def _agent_messages_for_result(state: _TurnState) -> list[str]:
|
|
|
1701
1715
|
return _agent_message_deltas_as_list(state.agent_message_deltas)
|
|
1702
1716
|
|
|
1703
1717
|
|
|
1718
|
+
def _normalize_output_policy(policy: Optional[str]) -> str:
|
|
1719
|
+
candidate = str(policy or "").strip().lower()
|
|
1720
|
+
if candidate in _OUTPUT_POLICIES:
|
|
1721
|
+
return candidate
|
|
1722
|
+
return _DEFAULT_OUTPUT_POLICY
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
def _final_message_for_result(state: _TurnState, *, policy: str) -> str:
|
|
1726
|
+
messages = _agent_messages_for_result(state)
|
|
1727
|
+
cleaned = [msg.strip() for msg in messages if isinstance(msg, str) and msg.strip()]
|
|
1728
|
+
if not cleaned:
|
|
1729
|
+
return ""
|
|
1730
|
+
if policy == "all_agent_messages":
|
|
1731
|
+
return "\n\n".join(cleaned)
|
|
1732
|
+
return cleaned[-1]
|
|
1733
|
+
|
|
1734
|
+
|
|
1704
1735
|
def _extract_status_value(value: Any) -> Optional[str]:
|
|
1705
1736
|
if isinstance(value, str):
|
|
1706
1737
|
return value
|
|
@@ -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
|
)
|
|
@@ -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,
|