codex-autorunner 0.1.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/__init__.py +3 -0
- codex_autorunner/bootstrap.py +151 -0
- codex_autorunner/cli.py +886 -0
- codex_autorunner/codex_cli.py +79 -0
- codex_autorunner/codex_runner.py +17 -0
- codex_autorunner/core/__init__.py +1 -0
- codex_autorunner/core/about_car.py +125 -0
- codex_autorunner/core/codex_runner.py +100 -0
- codex_autorunner/core/config.py +1465 -0
- codex_autorunner/core/doc_chat.py +547 -0
- codex_autorunner/core/docs.py +37 -0
- codex_autorunner/core/engine.py +720 -0
- codex_autorunner/core/git_utils.py +206 -0
- codex_autorunner/core/hub.py +756 -0
- codex_autorunner/core/injected_context.py +9 -0
- codex_autorunner/core/locks.py +57 -0
- codex_autorunner/core/logging_utils.py +158 -0
- codex_autorunner/core/notifications.py +465 -0
- codex_autorunner/core/optional_dependencies.py +41 -0
- codex_autorunner/core/prompt.py +107 -0
- codex_autorunner/core/prompts.py +275 -0
- codex_autorunner/core/request_context.py +21 -0
- codex_autorunner/core/runner_controller.py +116 -0
- codex_autorunner/core/runner_process.py +29 -0
- codex_autorunner/core/snapshot.py +576 -0
- codex_autorunner/core/state.py +156 -0
- codex_autorunner/core/update.py +567 -0
- codex_autorunner/core/update_runner.py +44 -0
- codex_autorunner/core/usage.py +1221 -0
- codex_autorunner/core/utils.py +108 -0
- codex_autorunner/discovery.py +102 -0
- codex_autorunner/housekeeping.py +423 -0
- codex_autorunner/integrations/__init__.py +1 -0
- codex_autorunner/integrations/app_server/__init__.py +6 -0
- codex_autorunner/integrations/app_server/client.py +1386 -0
- codex_autorunner/integrations/app_server/supervisor.py +206 -0
- codex_autorunner/integrations/github/__init__.py +10 -0
- codex_autorunner/integrations/github/service.py +889 -0
- codex_autorunner/integrations/telegram/__init__.py +1 -0
- codex_autorunner/integrations/telegram/adapter.py +1401 -0
- codex_autorunner/integrations/telegram/commands_registry.py +104 -0
- codex_autorunner/integrations/telegram/config.py +450 -0
- codex_autorunner/integrations/telegram/constants.py +154 -0
- codex_autorunner/integrations/telegram/dispatch.py +162 -0
- codex_autorunner/integrations/telegram/handlers/__init__.py +0 -0
- codex_autorunner/integrations/telegram/handlers/approvals.py +241 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +72 -0
- codex_autorunner/integrations/telegram/handlers/commands.py +160 -0
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +5262 -0
- codex_autorunner/integrations/telegram/handlers/messages.py +477 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +545 -0
- codex_autorunner/integrations/telegram/helpers.py +2084 -0
- codex_autorunner/integrations/telegram/notifications.py +164 -0
- codex_autorunner/integrations/telegram/outbox.py +174 -0
- codex_autorunner/integrations/telegram/rendering.py +102 -0
- codex_autorunner/integrations/telegram/retry.py +37 -0
- codex_autorunner/integrations/telegram/runtime.py +270 -0
- codex_autorunner/integrations/telegram/service.py +921 -0
- codex_autorunner/integrations/telegram/state.py +1223 -0
- codex_autorunner/integrations/telegram/transport.py +318 -0
- codex_autorunner/integrations/telegram/types.py +57 -0
- codex_autorunner/integrations/telegram/voice.py +413 -0
- codex_autorunner/manifest.py +150 -0
- codex_autorunner/routes/__init__.py +53 -0
- codex_autorunner/routes/base.py +470 -0
- codex_autorunner/routes/docs.py +275 -0
- codex_autorunner/routes/github.py +197 -0
- codex_autorunner/routes/repos.py +121 -0
- codex_autorunner/routes/sessions.py +137 -0
- codex_autorunner/routes/shared.py +137 -0
- codex_autorunner/routes/system.py +175 -0
- codex_autorunner/routes/terminal_images.py +107 -0
- codex_autorunner/routes/voice.py +128 -0
- codex_autorunner/server.py +23 -0
- codex_autorunner/spec_ingest.py +113 -0
- codex_autorunner/static/app.js +95 -0
- codex_autorunner/static/autoRefresh.js +209 -0
- codex_autorunner/static/bootstrap.js +105 -0
- codex_autorunner/static/bus.js +23 -0
- codex_autorunner/static/cache.js +52 -0
- codex_autorunner/static/constants.js +48 -0
- codex_autorunner/static/dashboard.js +795 -0
- codex_autorunner/static/docs.js +1514 -0
- codex_autorunner/static/env.js +99 -0
- codex_autorunner/static/github.js +168 -0
- codex_autorunner/static/hub.js +1511 -0
- codex_autorunner/static/index.html +622 -0
- codex_autorunner/static/loader.js +28 -0
- codex_autorunner/static/logs.js +690 -0
- codex_autorunner/static/mobileCompact.js +300 -0
- codex_autorunner/static/snapshot.js +116 -0
- codex_autorunner/static/state.js +87 -0
- codex_autorunner/static/styles.css +4966 -0
- codex_autorunner/static/tabs.js +50 -0
- codex_autorunner/static/terminal.js +21 -0
- codex_autorunner/static/terminalManager.js +3535 -0
- codex_autorunner/static/todoPreview.js +25 -0
- codex_autorunner/static/types.d.ts +8 -0
- codex_autorunner/static/utils.js +597 -0
- codex_autorunner/static/vendor/LICENSE.xterm +24 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-greek.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-vietnamese.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-greek.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-vietnamese.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-greek.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-vietnamese.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/OFL.txt +93 -0
- codex_autorunner/static/vendor/xterm-addon-fit.js +2 -0
- codex_autorunner/static/vendor/xterm.css +209 -0
- codex_autorunner/static/vendor/xterm.js +2 -0
- codex_autorunner/static/voice.js +591 -0
- codex_autorunner/voice/__init__.py +39 -0
- codex_autorunner/voice/capture.py +349 -0
- codex_autorunner/voice/config.py +167 -0
- codex_autorunner/voice/provider.py +66 -0
- codex_autorunner/voice/providers/__init__.py +7 -0
- codex_autorunner/voice/providers/openai_whisper.py +345 -0
- codex_autorunner/voice/resolver.py +36 -0
- codex_autorunner/voice/service.py +210 -0
- codex_autorunner/web/__init__.py +1 -0
- codex_autorunner/web/app.py +1037 -0
- codex_autorunner/web/hub_jobs.py +181 -0
- codex_autorunner/web/middleware.py +552 -0
- codex_autorunner/web/pty_session.py +357 -0
- codex_autorunner/web/runner_manager.py +25 -0
- codex_autorunner/web/schemas.py +253 -0
- codex_autorunner/web/static_assets.py +430 -0
- codex_autorunner/web/terminal_sessions.py +78 -0
- codex_autorunner/workspace.py +16 -0
- codex_autorunner-0.1.0.dist-info/METADATA +240 -0
- codex_autorunner-0.1.0.dist-info/RECORD +147 -0
- codex_autorunner-0.1.0.dist-info/WHEEL +5 -0
- codex_autorunner-0.1.0.dist-info/entry_points.txt +3 -0
- codex_autorunner-0.1.0.dist-info/licenses/LICENSE +21 -0
- codex_autorunner-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ...core.logging_utils import log_event
|
|
9
|
+
from ...core.utils import canonicalize_path
|
|
10
|
+
from ...workspace import canonical_workspace_root, workspace_id_for_path
|
|
11
|
+
from ..app_server.client import CodexAppServerClient
|
|
12
|
+
from .constants import (
|
|
13
|
+
APP_SERVER_START_BACKOFF_INITIAL_SECONDS,
|
|
14
|
+
APP_SERVER_START_BACKOFF_MAX_SECONDS,
|
|
15
|
+
TELEGRAM_MAX_MESSAGE_LENGTH,
|
|
16
|
+
TurnKey,
|
|
17
|
+
)
|
|
18
|
+
from .helpers import _app_server_env, _seed_codex_home
|
|
19
|
+
from .rendering import _format_telegram_html, _format_telegram_markdown
|
|
20
|
+
from .state import TOPIC_ROOT, parse_topic_key
|
|
21
|
+
from .types import TurnContext
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TelegramRuntimeHelpers:
|
|
25
|
+
def _resolve_topic_key(self, chat_id: int, thread_id: Optional[int]) -> str:
|
|
26
|
+
return self._router.resolve_key(chat_id, thread_id)
|
|
27
|
+
|
|
28
|
+
def _canonical_workspace_root(
|
|
29
|
+
self, workspace_path: Optional[str]
|
|
30
|
+
) -> Optional[Path]:
|
|
31
|
+
if not isinstance(workspace_path, str) or not workspace_path.strip():
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
return canonical_workspace_root(Path(workspace_path))
|
|
35
|
+
except Exception:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def _workspace_id_for_path(self, workspace_path: Optional[str]) -> Optional[str]:
|
|
39
|
+
root = self._canonical_workspace_root(workspace_path)
|
|
40
|
+
if root is None:
|
|
41
|
+
return None
|
|
42
|
+
return workspace_id_for_path(root)
|
|
43
|
+
|
|
44
|
+
def _refresh_workspace_id(self, key: str, record) -> Optional[str]:
|
|
45
|
+
if record.workspace_id or not record.workspace_path:
|
|
46
|
+
return record.workspace_id
|
|
47
|
+
workspace_id = self._workspace_id_for_path(record.workspace_path)
|
|
48
|
+
if workspace_id:
|
|
49
|
+
self._store.update_topic(
|
|
50
|
+
key, lambda stored: setattr(stored, "workspace_id", workspace_id)
|
|
51
|
+
)
|
|
52
|
+
record.workspace_id = workspace_id
|
|
53
|
+
return record.workspace_id
|
|
54
|
+
|
|
55
|
+
def _build_workspace_env(
|
|
56
|
+
self, workspace_root: Path, workspace_id: str, state_dir: Path
|
|
57
|
+
) -> dict[str, str]:
|
|
58
|
+
env = _app_server_env(self._config.app_server_command, workspace_root)
|
|
59
|
+
codex_home = state_dir / "codex_home"
|
|
60
|
+
codex_home.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
_seed_codex_home(codex_home, logger=self._logger)
|
|
62
|
+
env["CODEX_HOME"] = str(codex_home)
|
|
63
|
+
return env
|
|
64
|
+
|
|
65
|
+
async def _client_for_workspace(
|
|
66
|
+
self, workspace_path: Optional[str]
|
|
67
|
+
) -> Optional[CodexAppServerClient]:
|
|
68
|
+
workspace_root = self._canonical_workspace_root(workspace_path)
|
|
69
|
+
if workspace_root is None:
|
|
70
|
+
return None
|
|
71
|
+
delay = APP_SERVER_START_BACKOFF_INITIAL_SECONDS
|
|
72
|
+
while True:
|
|
73
|
+
try:
|
|
74
|
+
return await self._app_server_supervisor.get_client(workspace_root)
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
self._log_app_server_start_failure(workspace_root, exc)
|
|
77
|
+
await asyncio.sleep(delay)
|
|
78
|
+
delay = min(delay * 2, APP_SERVER_START_BACKOFF_MAX_SECONDS)
|
|
79
|
+
|
|
80
|
+
def _log_app_server_start_failure(
|
|
81
|
+
self, workspace_root: Path, exc: Exception
|
|
82
|
+
) -> None:
|
|
83
|
+
log_event(
|
|
84
|
+
self._logger,
|
|
85
|
+
logging.WARNING,
|
|
86
|
+
"telegram.app_server.start_failed",
|
|
87
|
+
workspace_path=str(workspace_root),
|
|
88
|
+
exc=exc,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _topic_scope_id(
|
|
92
|
+
self, repo_id: Optional[str], workspace_path: Optional[str]
|
|
93
|
+
) -> Optional[str]:
|
|
94
|
+
normalized_repo = repo_id.strip() if isinstance(repo_id, str) else ""
|
|
95
|
+
normalized_path = (
|
|
96
|
+
workspace_path.strip() if isinstance(workspace_path, str) else ""
|
|
97
|
+
)
|
|
98
|
+
if normalized_path:
|
|
99
|
+
try:
|
|
100
|
+
normalized_path = str(canonicalize_path(Path(normalized_path)))
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
if normalized_repo and normalized_path:
|
|
104
|
+
return f"{normalized_repo}@{normalized_path}"
|
|
105
|
+
if normalized_repo:
|
|
106
|
+
return normalized_repo
|
|
107
|
+
if normalized_path:
|
|
108
|
+
return normalized_path
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def _turn_key(
|
|
112
|
+
self, thread_id: Optional[str], turn_id: Optional[str]
|
|
113
|
+
) -> Optional[TurnKey]:
|
|
114
|
+
if not isinstance(thread_id, str) or not thread_id:
|
|
115
|
+
return None
|
|
116
|
+
if not isinstance(turn_id, str) or not turn_id:
|
|
117
|
+
return None
|
|
118
|
+
return (thread_id, turn_id)
|
|
119
|
+
|
|
120
|
+
def _resolve_turn_key(
|
|
121
|
+
self, turn_id: Optional[str], *, thread_id: Optional[str] = None
|
|
122
|
+
) -> Optional[TurnKey]:
|
|
123
|
+
if not isinstance(turn_id, str) or not turn_id:
|
|
124
|
+
return None
|
|
125
|
+
key: Optional[tuple[str, str]] = None
|
|
126
|
+
if thread_id is not None:
|
|
127
|
+
if not isinstance(thread_id, str) or not thread_id:
|
|
128
|
+
return None
|
|
129
|
+
key = (thread_id, turn_id)
|
|
130
|
+
if self._turn_contexts.get(key) is not None:
|
|
131
|
+
return key
|
|
132
|
+
matches = [
|
|
133
|
+
candidate_key
|
|
134
|
+
for candidate_key in self._turn_contexts
|
|
135
|
+
if candidate_key[1] == turn_id
|
|
136
|
+
]
|
|
137
|
+
if len(matches) == 1:
|
|
138
|
+
candidate = matches[0]
|
|
139
|
+
if key is not None and candidate != key:
|
|
140
|
+
log_event(
|
|
141
|
+
self._logger,
|
|
142
|
+
logging.WARNING,
|
|
143
|
+
"telegram.turn.thread_mismatch",
|
|
144
|
+
turn_id=turn_id,
|
|
145
|
+
requested_thread_id=thread_id,
|
|
146
|
+
actual_thread_id=candidate[0],
|
|
147
|
+
)
|
|
148
|
+
return candidate
|
|
149
|
+
if len(matches) > 1:
|
|
150
|
+
log_event(
|
|
151
|
+
self._logger,
|
|
152
|
+
logging.WARNING,
|
|
153
|
+
"telegram.turn.ambiguous",
|
|
154
|
+
turn_id=turn_id,
|
|
155
|
+
matches=len(matches),
|
|
156
|
+
)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def _resolve_turn_context(
|
|
160
|
+
self, turn_id: Optional[str], *, thread_id: Optional[str] = None
|
|
161
|
+
) -> Optional[TurnContext]:
|
|
162
|
+
key = self._resolve_turn_key(turn_id, thread_id=thread_id)
|
|
163
|
+
if key is None:
|
|
164
|
+
return None
|
|
165
|
+
return self._turn_contexts.get(key)
|
|
166
|
+
|
|
167
|
+
def _register_turn_context(
|
|
168
|
+
self, turn_key: TurnKey, turn_id: str, ctx: TurnContext
|
|
169
|
+
) -> bool:
|
|
170
|
+
existing = self._turn_contexts.get(turn_key)
|
|
171
|
+
if existing and existing.topic_key != ctx.topic_key:
|
|
172
|
+
log_event(
|
|
173
|
+
self._logger,
|
|
174
|
+
logging.ERROR,
|
|
175
|
+
"telegram.turn.context.collision",
|
|
176
|
+
turn_id=turn_id,
|
|
177
|
+
existing_topic=existing.topic_key,
|
|
178
|
+
new_topic=ctx.topic_key,
|
|
179
|
+
)
|
|
180
|
+
return False
|
|
181
|
+
self._turn_contexts[turn_key] = ctx
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
def _clear_thinking_preview(self, turn_key: TurnKey) -> None:
|
|
185
|
+
self._turn_preview_text.pop(turn_key, None)
|
|
186
|
+
self._turn_preview_updated_at.pop(turn_key, None)
|
|
187
|
+
|
|
188
|
+
def _build_debug_prefix(
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
chat_id: int,
|
|
192
|
+
thread_id: Optional[int],
|
|
193
|
+
reply_to: Optional[int] = None,
|
|
194
|
+
topic_key: Optional[str] = None,
|
|
195
|
+
workspace_path: Optional[str] = None,
|
|
196
|
+
codex_thread_id: Optional[str] = None,
|
|
197
|
+
) -> str:
|
|
198
|
+
if not self._config.debug_prefix_context:
|
|
199
|
+
return ""
|
|
200
|
+
resolved_key = topic_key
|
|
201
|
+
if not resolved_key:
|
|
202
|
+
try:
|
|
203
|
+
resolved_key = self._resolve_topic_key(chat_id, thread_id)
|
|
204
|
+
except Exception:
|
|
205
|
+
resolved_key = None
|
|
206
|
+
scope = None
|
|
207
|
+
if resolved_key:
|
|
208
|
+
try:
|
|
209
|
+
_, _, scope = parse_topic_key(resolved_key)
|
|
210
|
+
except Exception:
|
|
211
|
+
scope = None
|
|
212
|
+
record = None
|
|
213
|
+
if workspace_path is None or codex_thread_id is None:
|
|
214
|
+
record = self._router.get_topic(resolved_key) if resolved_key else None
|
|
215
|
+
if workspace_path is None and record is not None:
|
|
216
|
+
workspace_path = record.workspace_path
|
|
217
|
+
if codex_thread_id is None and record is not None:
|
|
218
|
+
codex_thread_id = record.active_thread_id
|
|
219
|
+
parts = [f"chat={chat_id}"]
|
|
220
|
+
thread_label = str(thread_id) if thread_id is not None else TOPIC_ROOT
|
|
221
|
+
parts.append(f"thread={thread_label}")
|
|
222
|
+
if scope:
|
|
223
|
+
parts.append(f"scope={scope}")
|
|
224
|
+
if workspace_path:
|
|
225
|
+
parts.append(f"cwd={workspace_path}")
|
|
226
|
+
if codex_thread_id:
|
|
227
|
+
parts.append(f"codex={codex_thread_id}")
|
|
228
|
+
if reply_to is not None:
|
|
229
|
+
parts.append(f"reply_to={reply_to}")
|
|
230
|
+
return f"[{' '.join(parts)}] "
|
|
231
|
+
|
|
232
|
+
def _prepare_outgoing_text(
|
|
233
|
+
self,
|
|
234
|
+
text: str,
|
|
235
|
+
*,
|
|
236
|
+
chat_id: int,
|
|
237
|
+
thread_id: Optional[int],
|
|
238
|
+
reply_to: Optional[int] = None,
|
|
239
|
+
topic_key: Optional[str] = None,
|
|
240
|
+
workspace_path: Optional[str] = None,
|
|
241
|
+
codex_thread_id: Optional[str] = None,
|
|
242
|
+
) -> tuple[str, Optional[str]]:
|
|
243
|
+
prefix = self._build_debug_prefix(
|
|
244
|
+
chat_id=chat_id,
|
|
245
|
+
thread_id=thread_id,
|
|
246
|
+
reply_to=reply_to,
|
|
247
|
+
topic_key=topic_key,
|
|
248
|
+
workspace_path=workspace_path,
|
|
249
|
+
codex_thread_id=codex_thread_id,
|
|
250
|
+
)
|
|
251
|
+
if prefix:
|
|
252
|
+
text = f"{prefix}{text}"
|
|
253
|
+
return self._prepare_message(text)
|
|
254
|
+
|
|
255
|
+
def _render_message(self, text: str) -> tuple[str, Optional[str]]:
|
|
256
|
+
parse_mode = self._config.parse_mode
|
|
257
|
+
if not parse_mode:
|
|
258
|
+
return text, None
|
|
259
|
+
if parse_mode == "HTML":
|
|
260
|
+
return _format_telegram_html(text), parse_mode
|
|
261
|
+
if parse_mode in ("Markdown", "MarkdownV2"):
|
|
262
|
+
return _format_telegram_markdown(text, parse_mode), parse_mode
|
|
263
|
+
return text, parse_mode
|
|
264
|
+
|
|
265
|
+
def _prepare_message(self, text: str) -> tuple[str, Optional[str]]:
|
|
266
|
+
rendered, parse_mode = self._render_message(text)
|
|
267
|
+
# Avoid parse_mode when chunking to keep markup intact.
|
|
268
|
+
if parse_mode and len(rendered) <= TELEGRAM_MAX_MESSAGE_LENGTH:
|
|
269
|
+
return rendered, parse_mode
|
|
270
|
+
return text, None
|