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.
Files changed (147) hide show
  1. codex_autorunner/__init__.py +3 -0
  2. codex_autorunner/bootstrap.py +151 -0
  3. codex_autorunner/cli.py +886 -0
  4. codex_autorunner/codex_cli.py +79 -0
  5. codex_autorunner/codex_runner.py +17 -0
  6. codex_autorunner/core/__init__.py +1 -0
  7. codex_autorunner/core/about_car.py +125 -0
  8. codex_autorunner/core/codex_runner.py +100 -0
  9. codex_autorunner/core/config.py +1465 -0
  10. codex_autorunner/core/doc_chat.py +547 -0
  11. codex_autorunner/core/docs.py +37 -0
  12. codex_autorunner/core/engine.py +720 -0
  13. codex_autorunner/core/git_utils.py +206 -0
  14. codex_autorunner/core/hub.py +756 -0
  15. codex_autorunner/core/injected_context.py +9 -0
  16. codex_autorunner/core/locks.py +57 -0
  17. codex_autorunner/core/logging_utils.py +158 -0
  18. codex_autorunner/core/notifications.py +465 -0
  19. codex_autorunner/core/optional_dependencies.py +41 -0
  20. codex_autorunner/core/prompt.py +107 -0
  21. codex_autorunner/core/prompts.py +275 -0
  22. codex_autorunner/core/request_context.py +21 -0
  23. codex_autorunner/core/runner_controller.py +116 -0
  24. codex_autorunner/core/runner_process.py +29 -0
  25. codex_autorunner/core/snapshot.py +576 -0
  26. codex_autorunner/core/state.py +156 -0
  27. codex_autorunner/core/update.py +567 -0
  28. codex_autorunner/core/update_runner.py +44 -0
  29. codex_autorunner/core/usage.py +1221 -0
  30. codex_autorunner/core/utils.py +108 -0
  31. codex_autorunner/discovery.py +102 -0
  32. codex_autorunner/housekeeping.py +423 -0
  33. codex_autorunner/integrations/__init__.py +1 -0
  34. codex_autorunner/integrations/app_server/__init__.py +6 -0
  35. codex_autorunner/integrations/app_server/client.py +1386 -0
  36. codex_autorunner/integrations/app_server/supervisor.py +206 -0
  37. codex_autorunner/integrations/github/__init__.py +10 -0
  38. codex_autorunner/integrations/github/service.py +889 -0
  39. codex_autorunner/integrations/telegram/__init__.py +1 -0
  40. codex_autorunner/integrations/telegram/adapter.py +1401 -0
  41. codex_autorunner/integrations/telegram/commands_registry.py +104 -0
  42. codex_autorunner/integrations/telegram/config.py +450 -0
  43. codex_autorunner/integrations/telegram/constants.py +154 -0
  44. codex_autorunner/integrations/telegram/dispatch.py +162 -0
  45. codex_autorunner/integrations/telegram/handlers/__init__.py +0 -0
  46. codex_autorunner/integrations/telegram/handlers/approvals.py +241 -0
  47. codex_autorunner/integrations/telegram/handlers/callbacks.py +72 -0
  48. codex_autorunner/integrations/telegram/handlers/commands.py +160 -0
  49. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +5262 -0
  50. codex_autorunner/integrations/telegram/handlers/messages.py +477 -0
  51. codex_autorunner/integrations/telegram/handlers/selections.py +545 -0
  52. codex_autorunner/integrations/telegram/helpers.py +2084 -0
  53. codex_autorunner/integrations/telegram/notifications.py +164 -0
  54. codex_autorunner/integrations/telegram/outbox.py +174 -0
  55. codex_autorunner/integrations/telegram/rendering.py +102 -0
  56. codex_autorunner/integrations/telegram/retry.py +37 -0
  57. codex_autorunner/integrations/telegram/runtime.py +270 -0
  58. codex_autorunner/integrations/telegram/service.py +921 -0
  59. codex_autorunner/integrations/telegram/state.py +1223 -0
  60. codex_autorunner/integrations/telegram/transport.py +318 -0
  61. codex_autorunner/integrations/telegram/types.py +57 -0
  62. codex_autorunner/integrations/telegram/voice.py +413 -0
  63. codex_autorunner/manifest.py +150 -0
  64. codex_autorunner/routes/__init__.py +53 -0
  65. codex_autorunner/routes/base.py +470 -0
  66. codex_autorunner/routes/docs.py +275 -0
  67. codex_autorunner/routes/github.py +197 -0
  68. codex_autorunner/routes/repos.py +121 -0
  69. codex_autorunner/routes/sessions.py +137 -0
  70. codex_autorunner/routes/shared.py +137 -0
  71. codex_autorunner/routes/system.py +175 -0
  72. codex_autorunner/routes/terminal_images.py +107 -0
  73. codex_autorunner/routes/voice.py +128 -0
  74. codex_autorunner/server.py +23 -0
  75. codex_autorunner/spec_ingest.py +113 -0
  76. codex_autorunner/static/app.js +95 -0
  77. codex_autorunner/static/autoRefresh.js +209 -0
  78. codex_autorunner/static/bootstrap.js +105 -0
  79. codex_autorunner/static/bus.js +23 -0
  80. codex_autorunner/static/cache.js +52 -0
  81. codex_autorunner/static/constants.js +48 -0
  82. codex_autorunner/static/dashboard.js +795 -0
  83. codex_autorunner/static/docs.js +1514 -0
  84. codex_autorunner/static/env.js +99 -0
  85. codex_autorunner/static/github.js +168 -0
  86. codex_autorunner/static/hub.js +1511 -0
  87. codex_autorunner/static/index.html +622 -0
  88. codex_autorunner/static/loader.js +28 -0
  89. codex_autorunner/static/logs.js +690 -0
  90. codex_autorunner/static/mobileCompact.js +300 -0
  91. codex_autorunner/static/snapshot.js +116 -0
  92. codex_autorunner/static/state.js +87 -0
  93. codex_autorunner/static/styles.css +4966 -0
  94. codex_autorunner/static/tabs.js +50 -0
  95. codex_autorunner/static/terminal.js +21 -0
  96. codex_autorunner/static/terminalManager.js +3535 -0
  97. codex_autorunner/static/todoPreview.js +25 -0
  98. codex_autorunner/static/types.d.ts +8 -0
  99. codex_autorunner/static/utils.js +597 -0
  100. codex_autorunner/static/vendor/LICENSE.xterm +24 -0
  101. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic-ext.woff2 +0 -0
  102. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic.woff2 +0 -0
  103. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-greek.woff2 +0 -0
  104. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin-ext.woff2 +0 -0
  105. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin.woff2 +0 -0
  106. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-vietnamese.woff2 +0 -0
  107. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic-ext.woff2 +0 -0
  108. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic.woff2 +0 -0
  109. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-greek.woff2 +0 -0
  110. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin-ext.woff2 +0 -0
  111. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin.woff2 +0 -0
  112. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-vietnamese.woff2 +0 -0
  113. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic-ext.woff2 +0 -0
  114. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic.woff2 +0 -0
  115. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-greek.woff2 +0 -0
  116. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin-ext.woff2 +0 -0
  117. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin.woff2 +0 -0
  118. codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-vietnamese.woff2 +0 -0
  119. codex_autorunner/static/vendor/fonts/jetbrains-mono/OFL.txt +93 -0
  120. codex_autorunner/static/vendor/xterm-addon-fit.js +2 -0
  121. codex_autorunner/static/vendor/xterm.css +209 -0
  122. codex_autorunner/static/vendor/xterm.js +2 -0
  123. codex_autorunner/static/voice.js +591 -0
  124. codex_autorunner/voice/__init__.py +39 -0
  125. codex_autorunner/voice/capture.py +349 -0
  126. codex_autorunner/voice/config.py +167 -0
  127. codex_autorunner/voice/provider.py +66 -0
  128. codex_autorunner/voice/providers/__init__.py +7 -0
  129. codex_autorunner/voice/providers/openai_whisper.py +345 -0
  130. codex_autorunner/voice/resolver.py +36 -0
  131. codex_autorunner/voice/service.py +210 -0
  132. codex_autorunner/web/__init__.py +1 -0
  133. codex_autorunner/web/app.py +1037 -0
  134. codex_autorunner/web/hub_jobs.py +181 -0
  135. codex_autorunner/web/middleware.py +552 -0
  136. codex_autorunner/web/pty_session.py +357 -0
  137. codex_autorunner/web/runner_manager.py +25 -0
  138. codex_autorunner/web/schemas.py +253 -0
  139. codex_autorunner/web/static_assets.py +430 -0
  140. codex_autorunner/web/terminal_sessions.py +78 -0
  141. codex_autorunner/workspace.py +16 -0
  142. codex_autorunner-0.1.0.dist-info/METADATA +240 -0
  143. codex_autorunner-0.1.0.dist-info/RECORD +147 -0
  144. codex_autorunner-0.1.0.dist-info/WHEEL +5 -0
  145. codex_autorunner-0.1.0.dist-info/entry_points.txt +3 -0
  146. codex_autorunner-0.1.0.dist-info/licenses/LICENSE +21 -0
  147. 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