codex-autorunner 0.1.2__py3-none-any.whl → 1.0.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/__main__.py +4 -0
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +118 -30
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +136 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +16 -35
- codex_autorunner/cli.py +157 -139
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +390 -100
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +278 -262
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +15 -9
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/utils.py +29 -2
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +576 -92
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +141 -167
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +175 -0
- codex_autorunner/integrations/telegram/constants.py +16 -1
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
- codex_autorunner/integrations/telegram/helpers.py +88 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +214 -40
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +36 -3
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +23 -14
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +81 -109
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/system.py +6 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +1 -0
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +25 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +162 -196
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +41 -118
- codex_autorunner/static/index.html +787 -858
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -211
- codex_autorunner/static/styles.css +7567 -3865
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/app.py +269 -91
- codex_autorunner/web/middleware.py +3 -4
- codex_autorunner/web/schemas.py +89 -109
- codex_autorunner/web/static_assets.py +1 -44
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
import math
|
|
5
6
|
from contextlib import contextmanager
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
6
8
|
from typing import Any, Awaitable, Callable, Optional
|
|
7
9
|
|
|
8
10
|
from ...core.logging_utils import log_event
|
|
@@ -13,13 +15,40 @@ from .constants import (
|
|
|
13
15
|
OUTBOX_MAX_ATTEMPTS,
|
|
14
16
|
OUTBOX_RETRY_INTERVAL_SECONDS,
|
|
15
17
|
)
|
|
18
|
+
from .retry import _extract_retry_after_seconds
|
|
16
19
|
from .state import OutboxRecord, TelegramStateStore, topic_key
|
|
17
20
|
|
|
21
|
+
__all__ = ["_outbox_key", "TelegramOutboxManager"]
|
|
22
|
+
|
|
18
23
|
SendMessageFn = Callable[..., Awaitable[None]]
|
|
19
24
|
EditMessageFn = Callable[..., Awaitable[bool]]
|
|
20
25
|
DeleteMessageFn = Callable[..., Awaitable[bool]]
|
|
21
26
|
|
|
22
27
|
|
|
28
|
+
def _outbox_key(
|
|
29
|
+
chat_id: int,
|
|
30
|
+
thread_id: Optional[int],
|
|
31
|
+
message_id: Optional[int],
|
|
32
|
+
operation: Optional[str],
|
|
33
|
+
) -> str:
|
|
34
|
+
return f"{chat_id}:{thread_id if thread_id is not None else 'root'}:{message_id if message_id is not None else 'new'}:{operation or 'send'}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Keep a module-level reference so static analysis sees this helper as used in production.
|
|
38
|
+
OUTBOX_KEY_HELPER = _outbox_key
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_next_attempt_at(next_at_str: Optional[str]) -> Optional[datetime]:
|
|
42
|
+
if not next_at_str:
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
return datetime.strptime(next_at_str, "%Y-%m-%dT%H:%M:%SZ").replace(
|
|
46
|
+
tzinfo=timezone.utc
|
|
47
|
+
)
|
|
48
|
+
except (ValueError, TypeError):
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
23
52
|
class TelegramOutboxManager:
|
|
24
53
|
def __init__(
|
|
25
54
|
self,
|
|
@@ -36,27 +65,61 @@ class TelegramOutboxManager:
|
|
|
36
65
|
self._delete_message = delete_message
|
|
37
66
|
self._logger = logger
|
|
38
67
|
self._inflight: set[str] = set()
|
|
68
|
+
self._inflight_outbox_keys: set[str] = set()
|
|
39
69
|
self._lock: Optional[asyncio.Lock] = None
|
|
40
70
|
|
|
41
71
|
def start(self) -> None:
|
|
42
72
|
self._inflight = set()
|
|
73
|
+
self._inflight_outbox_keys = set()
|
|
43
74
|
self._lock = asyncio.Lock()
|
|
44
75
|
|
|
45
76
|
async def restore(self) -> None:
|
|
46
77
|
records = await self._store.list_outbox()
|
|
47
78
|
if not records:
|
|
48
79
|
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
for record in records:
|
|
81
|
+
conversation_id = None
|
|
82
|
+
try:
|
|
83
|
+
from .state import topic_key as build_topic_key
|
|
84
|
+
|
|
85
|
+
conversation_id = build_topic_key(record.chat_id, record.thread_id)
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
if conversation_id:
|
|
89
|
+
from ...core.request_context import set_conversation_id
|
|
90
|
+
|
|
91
|
+
token = set_conversation_id(conversation_id)
|
|
92
|
+
try:
|
|
93
|
+
log_event(
|
|
94
|
+
self._logger,
|
|
95
|
+
logging.INFO,
|
|
96
|
+
"telegram.outbox.restore",
|
|
97
|
+
record_id=record.record_id,
|
|
98
|
+
chat_id=record.chat_id,
|
|
99
|
+
thread_id=record.thread_id,
|
|
100
|
+
message_id=record.message_id,
|
|
101
|
+
conversation_id=conversation_id,
|
|
102
|
+
)
|
|
103
|
+
finally:
|
|
104
|
+
from ...core.request_context import reset_conversation_id
|
|
105
|
+
|
|
106
|
+
reset_conversation_id(token)
|
|
107
|
+
else:
|
|
108
|
+
log_event(
|
|
109
|
+
self._logger,
|
|
110
|
+
logging.INFO,
|
|
111
|
+
"telegram.outbox.restore",
|
|
112
|
+
record_id=record.record_id,
|
|
113
|
+
chat_id=record.chat_id,
|
|
114
|
+
thread_id=record.thread_id,
|
|
115
|
+
message_id=record.message_id,
|
|
116
|
+
)
|
|
55
117
|
await self._flush(records)
|
|
56
118
|
|
|
57
119
|
async def run_loop(self) -> None:
|
|
58
120
|
while True:
|
|
59
121
|
await asyncio.sleep(OUTBOX_RETRY_INTERVAL_SECONDS)
|
|
122
|
+
records = []
|
|
60
123
|
try:
|
|
61
124
|
records = await self._store.list_outbox()
|
|
62
125
|
if records:
|
|
@@ -67,6 +130,7 @@ class TelegramOutboxManager:
|
|
|
67
130
|
logging.WARNING,
|
|
68
131
|
"telegram.outbox.flush_failed",
|
|
69
132
|
exc=exc,
|
|
133
|
+
record_count=len(records) if records else 0,
|
|
70
134
|
)
|
|
71
135
|
|
|
72
136
|
async def send_message_with_outbox(
|
|
@@ -74,6 +138,11 @@ class TelegramOutboxManager:
|
|
|
74
138
|
record: OutboxRecord,
|
|
75
139
|
) -> bool:
|
|
76
140
|
await self._store.enqueue_outbox(record)
|
|
141
|
+
conversation_id = None
|
|
142
|
+
try:
|
|
143
|
+
conversation_id = topic_key(record.chat_id, record.thread_id)
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
77
146
|
log_event(
|
|
78
147
|
self._logger,
|
|
79
148
|
logging.INFO,
|
|
@@ -81,48 +150,120 @@ class TelegramOutboxManager:
|
|
|
81
150
|
record_id=record.record_id,
|
|
82
151
|
chat_id=record.chat_id,
|
|
83
152
|
thread_id=record.thread_id,
|
|
153
|
+
message_id=record.message_id,
|
|
154
|
+
conversation_id=conversation_id,
|
|
84
155
|
)
|
|
85
|
-
|
|
86
|
-
|
|
156
|
+
immediate_delays_iter = iter(OUTBOX_IMMEDIATE_RETRY_DELAYS)
|
|
157
|
+
immediate_delays_exhausted = False
|
|
158
|
+
while True:
|
|
159
|
+
current = await self._store.get_outbox(record.record_id)
|
|
160
|
+
if current is None:
|
|
161
|
+
return False
|
|
162
|
+
next_at = _parse_next_attempt_at(current.next_attempt_at)
|
|
163
|
+
if next_at is not None:
|
|
164
|
+
now = datetime.now(timezone.utc)
|
|
165
|
+
sleep_duration = (next_at - now).total_seconds()
|
|
166
|
+
if sleep_duration > 0.01:
|
|
167
|
+
await asyncio.sleep(sleep_duration)
|
|
168
|
+
if await self._attempt_send(current):
|
|
87
169
|
return True
|
|
88
170
|
current = await self._store.get_outbox(record.record_id)
|
|
89
171
|
if current is None:
|
|
90
172
|
return False
|
|
91
173
|
if current.attempts >= OUTBOX_MAX_ATTEMPTS:
|
|
92
174
|
return False
|
|
93
|
-
|
|
175
|
+
next_at = _parse_next_attempt_at(current.next_attempt_at)
|
|
176
|
+
if next_at is not None:
|
|
177
|
+
now = datetime.now(timezone.utc)
|
|
178
|
+
sleep_duration = (next_at - now).total_seconds()
|
|
179
|
+
if sleep_duration > 0.01:
|
|
180
|
+
await asyncio.sleep(sleep_duration)
|
|
181
|
+
continue
|
|
182
|
+
if immediate_delays_exhausted:
|
|
183
|
+
break
|
|
184
|
+
try:
|
|
185
|
+
delay = next(immediate_delays_iter)
|
|
186
|
+
except StopIteration:
|
|
187
|
+
immediate_delays_exhausted = True
|
|
188
|
+
has_next = await self._store.get_outbox(record.record_id)
|
|
189
|
+
if has_next is not None and has_next.next_attempt_at is None:
|
|
190
|
+
break
|
|
191
|
+
continue
|
|
192
|
+
if delay > 0:
|
|
193
|
+
await asyncio.sleep(delay)
|
|
94
194
|
return False
|
|
95
195
|
|
|
96
196
|
async def _flush(self, records: list[OutboxRecord]) -> None:
|
|
197
|
+
now = datetime.now(timezone.utc)
|
|
198
|
+
ready_records: list[OutboxRecord] = []
|
|
97
199
|
for record in records:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
200
|
+
next_at = _parse_next_attempt_at(record.next_attempt_at)
|
|
201
|
+
if next_at is None or now >= next_at:
|
|
202
|
+
ready_records.append(record)
|
|
203
|
+
|
|
204
|
+
# Keep only the last ready record per outbox_key, but do not drop deferred
|
|
205
|
+
# future records; we leave them for later flush cycles. Latest wins to avoid
|
|
206
|
+
# delivering stale edits.
|
|
207
|
+
coalesced_ready: dict[str, OutboxRecord] = {}
|
|
208
|
+
for record in ready_records:
|
|
209
|
+
if record.outbox_key is not None:
|
|
210
|
+
coalesced_ready[record.outbox_key] = record
|
|
211
|
+
else:
|
|
212
|
+
await self._process_record(record)
|
|
213
|
+
|
|
214
|
+
for record in coalesced_ready.values():
|
|
215
|
+
await self._process_record(record)
|
|
216
|
+
|
|
217
|
+
async def _process_record(self, record: OutboxRecord) -> None:
|
|
218
|
+
with self._conversation_context(record.chat_id, record.thread_id):
|
|
219
|
+
conversation_id = None
|
|
220
|
+
try:
|
|
221
|
+
conversation_id = topic_key(record.chat_id, record.thread_id)
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
if record.attempts >= OUTBOX_MAX_ATTEMPTS:
|
|
225
|
+
log_event(
|
|
226
|
+
self._logger,
|
|
227
|
+
logging.WARNING,
|
|
228
|
+
"telegram.outbox.gave_up",
|
|
229
|
+
record_id=record.record_id,
|
|
230
|
+
chat_id=record.chat_id,
|
|
231
|
+
thread_id=record.thread_id,
|
|
232
|
+
message_id=record.message_id,
|
|
233
|
+
attempts=record.attempts,
|
|
234
|
+
conversation_id=conversation_id,
|
|
235
|
+
)
|
|
236
|
+
if record.outbox_key:
|
|
237
|
+
records = await self._store.list_outbox()
|
|
238
|
+
for r in records:
|
|
239
|
+
if r.outbox_key == record.outbox_key:
|
|
240
|
+
await self._store.delete_outbox(r.record_id)
|
|
241
|
+
else:
|
|
109
242
|
await self._store.delete_outbox(record.record_id)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
243
|
+
if record.placeholder_message_id is not None:
|
|
244
|
+
await self._edit_message_text(
|
|
245
|
+
record.chat_id,
|
|
246
|
+
record.placeholder_message_id,
|
|
247
|
+
"Delivery failed after retries. Please resend.",
|
|
248
|
+
message_thread_id=record.thread_id,
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
await self._attempt_send(record)
|
|
118
252
|
|
|
119
253
|
async def _attempt_send(self, record: OutboxRecord) -> bool:
|
|
120
254
|
current = await self._store.get_outbox(record.record_id)
|
|
121
255
|
if current is None:
|
|
122
256
|
return False
|
|
123
257
|
record = current
|
|
124
|
-
if not await self._mark_inflight(
|
|
258
|
+
if not await self._mark_inflight(
|
|
259
|
+
record.outbox_key if record.outbox_key else record.record_id
|
|
260
|
+
):
|
|
125
261
|
return False
|
|
262
|
+
conversation_id = None
|
|
263
|
+
try:
|
|
264
|
+
conversation_id = topic_key(record.chat_id, record.thread_id)
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
126
267
|
with self._conversation_context(record.chat_id, record.thread_id):
|
|
127
268
|
try:
|
|
128
269
|
await self._send_message(
|
|
@@ -132,9 +273,19 @@ class TelegramOutboxManager:
|
|
|
132
273
|
reply_to=record.reply_to_message_id,
|
|
133
274
|
)
|
|
134
275
|
except Exception as exc:
|
|
276
|
+
retry_after = _extract_retry_after_seconds(exc)
|
|
135
277
|
record.attempts += 1
|
|
136
278
|
record.last_error = str(exc)[:500]
|
|
137
279
|
record.last_attempt_at = now_iso()
|
|
280
|
+
if retry_after is not None:
|
|
281
|
+
now = datetime.now(timezone.utc)
|
|
282
|
+
delay_seconds = max(1, math.ceil(retry_after))
|
|
283
|
+
next_at = now.replace(microsecond=0) + timedelta(
|
|
284
|
+
seconds=delay_seconds
|
|
285
|
+
)
|
|
286
|
+
if next_at <= now:
|
|
287
|
+
next_at = now + timedelta(seconds=delay_seconds)
|
|
288
|
+
record.next_attempt_at = next_at.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
138
289
|
await self._store.update_outbox(record)
|
|
139
290
|
log_event(
|
|
140
291
|
self._logger,
|
|
@@ -143,16 +294,34 @@ class TelegramOutboxManager:
|
|
|
143
294
|
record_id=record.record_id,
|
|
144
295
|
chat_id=record.chat_id,
|
|
145
296
|
thread_id=record.thread_id,
|
|
297
|
+
message_id=record.message_id,
|
|
146
298
|
attempts=record.attempts,
|
|
299
|
+
retry_after=retry_after,
|
|
147
300
|
exc=exc,
|
|
301
|
+
conversation_id=conversation_id,
|
|
148
302
|
)
|
|
149
303
|
return False
|
|
150
304
|
finally:
|
|
151
|
-
await self._clear_inflight(
|
|
152
|
-
|
|
305
|
+
await self._clear_inflight(
|
|
306
|
+
record.outbox_key if record.outbox_key else record.record_id
|
|
307
|
+
)
|
|
308
|
+
if record.outbox_key:
|
|
309
|
+
# Only delete records up to (and including) this record's created_at to
|
|
310
|
+
# avoid dropping newer queued messages for the same key.
|
|
311
|
+
records = await self._store.list_outbox()
|
|
312
|
+
for r in records:
|
|
313
|
+
if (
|
|
314
|
+
r.outbox_key == record.outbox_key
|
|
315
|
+
and r.created_at <= record.created_at
|
|
316
|
+
):
|
|
317
|
+
await self._store.delete_outbox(r.record_id)
|
|
318
|
+
else:
|
|
319
|
+
await self._store.delete_outbox(record.record_id)
|
|
153
320
|
if record.placeholder_message_id is not None:
|
|
154
321
|
await self._delete_message(
|
|
155
|
-
record.chat_id,
|
|
322
|
+
record.chat_id,
|
|
323
|
+
record.placeholder_message_id,
|
|
324
|
+
record.thread_id,
|
|
156
325
|
)
|
|
157
326
|
log_event(
|
|
158
327
|
self._logger,
|
|
@@ -161,23 +330,25 @@ class TelegramOutboxManager:
|
|
|
161
330
|
record_id=record.record_id,
|
|
162
331
|
chat_id=record.chat_id,
|
|
163
332
|
thread_id=record.thread_id,
|
|
333
|
+
message_id=record.message_id,
|
|
334
|
+
conversation_id=conversation_id,
|
|
164
335
|
)
|
|
165
336
|
return True
|
|
166
337
|
|
|
167
|
-
async def _mark_inflight(self,
|
|
338
|
+
async def _mark_inflight(self, key: str) -> bool:
|
|
168
339
|
if self._lock is None:
|
|
169
340
|
self._lock = asyncio.Lock()
|
|
170
341
|
async with self._lock:
|
|
171
|
-
if
|
|
342
|
+
if key in self._inflight_outbox_keys:
|
|
172
343
|
return False
|
|
173
|
-
self.
|
|
344
|
+
self._inflight_outbox_keys.add(key)
|
|
174
345
|
return True
|
|
175
346
|
|
|
176
|
-
async def _clear_inflight(self,
|
|
347
|
+
async def _clear_inflight(self, key: str) -> None:
|
|
177
348
|
if self._lock is None:
|
|
178
349
|
return
|
|
179
350
|
async with self._lock:
|
|
180
|
-
self.
|
|
351
|
+
self._inflight_outbox_keys.discard(key)
|
|
181
352
|
|
|
182
353
|
@contextmanager
|
|
183
354
|
def _conversation_context(self, chat_id: int, thread_id: Optional[int]) -> Any:
|
|
@@ -4,16 +4,9 @@ import time
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
|
+
from .constants import COMPACT_MAX_ACTIONS, COMPACT_MAX_TEXT_LENGTH, STATUS_ICONS
|
|
7
8
|
from .helpers import _truncate_text
|
|
8
9
|
|
|
9
|
-
STATUS_ICONS = {
|
|
10
|
-
"running": "▸",
|
|
11
|
-
"update": "↻",
|
|
12
|
-
"done": "✓",
|
|
13
|
-
"fail": "✗",
|
|
14
|
-
"warn": "⚠",
|
|
15
|
-
}
|
|
16
|
-
|
|
17
10
|
|
|
18
11
|
def format_elapsed(seconds: float) -> str:
|
|
19
12
|
total = max(int(seconds), 0)
|
|
@@ -45,8 +38,8 @@ class TurnProgressTracker:
|
|
|
45
38
|
agent: str
|
|
46
39
|
model: str
|
|
47
40
|
label: str
|
|
48
|
-
max_actions: int
|
|
49
|
-
max_output_chars: int
|
|
41
|
+
max_actions: int = COMPACT_MAX_ACTIONS
|
|
42
|
+
max_output_chars: int = COMPACT_MAX_TEXT_LENGTH
|
|
50
43
|
actions: list[ProgressAction] = field(default_factory=list)
|
|
51
44
|
step: int = 0
|
|
52
45
|
last_output_index: Optional[int] = None
|