codex-autorunner 0.1.2__py3-none-any.whl → 1.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 (276) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/__main__.py +4 -0
  3. codex_autorunner/agents/codex/harness.py +1 -1
  4. codex_autorunner/agents/opencode/client.py +68 -35
  5. codex_autorunner/agents/opencode/constants.py +3 -0
  6. codex_autorunner/agents/opencode/harness.py +6 -1
  7. codex_autorunner/agents/opencode/logging.py +21 -5
  8. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  9. codex_autorunner/agents/opencode/runtime.py +176 -47
  10. codex_autorunner/agents/opencode/supervisor.py +36 -48
  11. codex_autorunner/agents/registry.py +155 -8
  12. codex_autorunner/api.py +25 -0
  13. codex_autorunner/bootstrap.py +22 -37
  14. codex_autorunner/cli.py +5 -1156
  15. codex_autorunner/codex_cli.py +20 -84
  16. codex_autorunner/core/__init__.py +4 -0
  17. codex_autorunner/core/about_car.py +49 -32
  18. codex_autorunner/core/adapter_utils.py +21 -0
  19. codex_autorunner/core/app_server_ids.py +59 -0
  20. codex_autorunner/core/app_server_logging.py +7 -3
  21. codex_autorunner/core/app_server_prompts.py +27 -260
  22. codex_autorunner/core/app_server_threads.py +26 -28
  23. codex_autorunner/core/app_server_utils.py +165 -0
  24. codex_autorunner/core/archive.py +349 -0
  25. codex_autorunner/core/codex_runner.py +12 -2
  26. codex_autorunner/core/config.py +587 -103
  27. codex_autorunner/core/docs.py +10 -2
  28. codex_autorunner/core/drafts.py +136 -0
  29. codex_autorunner/core/engine.py +1531 -866
  30. codex_autorunner/core/exceptions.py +4 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +202 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +88 -0
  35. codex_autorunner/core/flows/reasons.py +52 -0
  36. codex_autorunner/core/flows/reconciler.py +131 -0
  37. codex_autorunner/core/flows/runtime.py +382 -0
  38. codex_autorunner/core/flows/store.py +568 -0
  39. codex_autorunner/core/flows/transition.py +138 -0
  40. codex_autorunner/core/flows/ux_helpers.py +257 -0
  41. codex_autorunner/core/flows/worker_process.py +242 -0
  42. codex_autorunner/core/git_utils.py +62 -0
  43. codex_autorunner/core/hub.py +136 -16
  44. codex_autorunner/core/locks.py +4 -0
  45. codex_autorunner/core/notifications.py +14 -2
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/core/ports/agent_backend.py +150 -0
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/core/ports/run_event.py +91 -0
  50. codex_autorunner/core/prompt.py +15 -7
  51. codex_autorunner/core/redaction.py +29 -0
  52. codex_autorunner/core/review_context.py +5 -8
  53. codex_autorunner/core/run_index.py +6 -0
  54. codex_autorunner/core/runner_process.py +5 -2
  55. codex_autorunner/core/state.py +0 -88
  56. codex_autorunner/core/state_roots.py +57 -0
  57. codex_autorunner/core/supervisor_protocol.py +15 -0
  58. codex_autorunner/core/supervisor_utils.py +67 -0
  59. codex_autorunner/core/text_delta_coalescer.py +54 -0
  60. codex_autorunner/core/ticket_linter_cli.py +201 -0
  61. codex_autorunner/core/ticket_manager_cli.py +432 -0
  62. codex_autorunner/core/update.py +24 -16
  63. codex_autorunner/core/update_paths.py +28 -0
  64. codex_autorunner/core/update_runner.py +2 -0
  65. codex_autorunner/core/usage.py +164 -12
  66. codex_autorunner/core/utils.py +120 -11
  67. codex_autorunner/discovery.py +2 -4
  68. codex_autorunner/flows/review/__init__.py +17 -0
  69. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  70. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  71. codex_autorunner/flows/ticket_flow/definition.py +98 -0
  72. codex_autorunner/integrations/agents/__init__.py +17 -0
  73. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  74. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  75. codex_autorunner/integrations/agents/codex_backend.py +448 -0
  76. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  77. codex_autorunner/integrations/agents/opencode_backend.py +598 -0
  78. codex_autorunner/integrations/agents/runner.py +91 -0
  79. codex_autorunner/integrations/agents/wiring.py +271 -0
  80. codex_autorunner/integrations/app_server/client.py +583 -152
  81. codex_autorunner/integrations/app_server/env.py +2 -107
  82. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  83. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  84. codex_autorunner/integrations/telegram/adapter.py +204 -165
  85. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  86. codex_autorunner/integrations/telegram/config.py +221 -0
  87. codex_autorunner/integrations/telegram/constants.py +17 -2
  88. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  89. codex_autorunner/integrations/telegram/doctor.py +47 -0
  90. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
  91. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  92. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  93. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  94. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
  95. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  96. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  97. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  98. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
  99. codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
  100. codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
  101. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  102. codex_autorunner/integrations/telegram/helpers.py +111 -16
  103. codex_autorunner/integrations/telegram/outbox.py +208 -37
  104. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  105. codex_autorunner/integrations/telegram/service.py +221 -42
  106. codex_autorunner/integrations/telegram/state.py +100 -2
  107. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
  108. codex_autorunner/integrations/telegram/transport.py +39 -4
  109. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  110. codex_autorunner/manifest.py +2 -0
  111. codex_autorunner/plugin_api.py +22 -0
  112. codex_autorunner/routes/__init__.py +37 -67
  113. codex_autorunner/routes/agents.py +2 -137
  114. codex_autorunner/routes/analytics.py +3 -0
  115. codex_autorunner/routes/app_server.py +2 -131
  116. codex_autorunner/routes/base.py +2 -624
  117. codex_autorunner/routes/file_chat.py +7 -0
  118. codex_autorunner/routes/flows.py +7 -0
  119. codex_autorunner/routes/messages.py +7 -0
  120. codex_autorunner/routes/repos.py +2 -196
  121. codex_autorunner/routes/review.py +2 -147
  122. codex_autorunner/routes/sessions.py +2 -175
  123. codex_autorunner/routes/settings.py +2 -168
  124. codex_autorunner/routes/shared.py +2 -275
  125. codex_autorunner/routes/system.py +4 -188
  126. codex_autorunner/routes/usage.py +3 -0
  127. codex_autorunner/routes/voice.py +2 -119
  128. codex_autorunner/routes/workspace.py +3 -0
  129. codex_autorunner/server.py +3 -2
  130. codex_autorunner/static/agentControls.js +41 -11
  131. codex_autorunner/static/agentEvents.js +248 -0
  132. codex_autorunner/static/app.js +35 -24
  133. codex_autorunner/static/archive.js +826 -0
  134. codex_autorunner/static/archiveApi.js +37 -0
  135. codex_autorunner/static/autoRefresh.js +36 -8
  136. codex_autorunner/static/bootstrap.js +1 -0
  137. codex_autorunner/static/bus.js +1 -0
  138. codex_autorunner/static/cache.js +1 -0
  139. codex_autorunner/static/constants.js +20 -4
  140. codex_autorunner/static/dashboard.js +344 -325
  141. codex_autorunner/static/diffRenderer.js +37 -0
  142. codex_autorunner/static/docChatCore.js +324 -0
  143. codex_autorunner/static/docChatStorage.js +65 -0
  144. codex_autorunner/static/docChatVoice.js +65 -0
  145. codex_autorunner/static/docEditor.js +133 -0
  146. codex_autorunner/static/env.js +1 -0
  147. codex_autorunner/static/eventSummarizer.js +166 -0
  148. codex_autorunner/static/fileChat.js +182 -0
  149. codex_autorunner/static/health.js +155 -0
  150. codex_autorunner/static/hub.js +126 -185
  151. codex_autorunner/static/index.html +839 -863
  152. codex_autorunner/static/liveUpdates.js +1 -0
  153. codex_autorunner/static/loader.js +1 -0
  154. codex_autorunner/static/messages.js +873 -0
  155. codex_autorunner/static/mobileCompact.js +2 -1
  156. codex_autorunner/static/preserve.js +17 -0
  157. codex_autorunner/static/settings.js +149 -217
  158. codex_autorunner/static/smartRefresh.js +52 -0
  159. codex_autorunner/static/styles.css +8850 -3876
  160. codex_autorunner/static/tabs.js +175 -11
  161. codex_autorunner/static/terminal.js +32 -0
  162. codex_autorunner/static/terminalManager.js +34 -59
  163. codex_autorunner/static/ticketChatActions.js +333 -0
  164. codex_autorunner/static/ticketChatEvents.js +16 -0
  165. codex_autorunner/static/ticketChatStorage.js +16 -0
  166. codex_autorunner/static/ticketChatStream.js +264 -0
  167. codex_autorunner/static/ticketEditor.js +844 -0
  168. codex_autorunner/static/ticketVoice.js +9 -0
  169. codex_autorunner/static/tickets.js +1988 -0
  170. codex_autorunner/static/utils.js +43 -3
  171. codex_autorunner/static/voice.js +1 -0
  172. codex_autorunner/static/workspace.js +765 -0
  173. codex_autorunner/static/workspaceApi.js +53 -0
  174. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  175. codex_autorunner/surfaces/__init__.py +5 -0
  176. codex_autorunner/surfaces/cli/__init__.py +6 -0
  177. codex_autorunner/surfaces/cli/cli.py +1224 -0
  178. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  179. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  180. codex_autorunner/surfaces/web/__init__.py +1 -0
  181. codex_autorunner/surfaces/web/app.py +2019 -0
  182. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  183. codex_autorunner/surfaces/web/middleware.py +587 -0
  184. codex_autorunner/surfaces/web/pty_session.py +370 -0
  185. codex_autorunner/surfaces/web/review.py +6 -0
  186. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  187. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  188. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  189. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  190. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  191. codex_autorunner/surfaces/web/routes/base.py +615 -0
  192. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  193. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  194. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  195. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  196. codex_autorunner/surfaces/web/routes/review.py +148 -0
  197. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  198. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  199. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  200. codex_autorunner/surfaces/web/routes/system.py +196 -0
  201. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  202. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  203. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  204. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  205. codex_autorunner/surfaces/web/schemas.py +417 -0
  206. codex_autorunner/surfaces/web/static_assets.py +490 -0
  207. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  208. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  209. codex_autorunner/tickets/__init__.py +27 -0
  210. codex_autorunner/tickets/agent_pool.py +399 -0
  211. codex_autorunner/tickets/files.py +89 -0
  212. codex_autorunner/tickets/frontmatter.py +55 -0
  213. codex_autorunner/tickets/lint.py +102 -0
  214. codex_autorunner/tickets/models.py +97 -0
  215. codex_autorunner/tickets/outbox.py +244 -0
  216. codex_autorunner/tickets/replies.py +179 -0
  217. codex_autorunner/tickets/runner.py +881 -0
  218. codex_autorunner/tickets/spec_ingest.py +77 -0
  219. codex_autorunner/web/__init__.py +5 -1
  220. codex_autorunner/web/app.py +2 -1771
  221. codex_autorunner/web/hub_jobs.py +2 -191
  222. codex_autorunner/web/middleware.py +2 -587
  223. codex_autorunner/web/pty_session.py +2 -369
  224. codex_autorunner/web/runner_manager.py +2 -24
  225. codex_autorunner/web/schemas.py +2 -396
  226. codex_autorunner/web/static_assets.py +4 -484
  227. codex_autorunner/web/static_refresh.py +2 -85
  228. codex_autorunner/web/terminal_sessions.py +2 -77
  229. codex_autorunner/workspace/__init__.py +40 -0
  230. codex_autorunner/workspace/paths.py +335 -0
  231. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  232. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  233. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
  234. codex_autorunner/agents/execution/policy.py +0 -292
  235. codex_autorunner/agents/factory.py +0 -52
  236. codex_autorunner/agents/orchestrator.py +0 -358
  237. codex_autorunner/core/doc_chat.py +0 -1446
  238. codex_autorunner/core/snapshot.py +0 -580
  239. codex_autorunner/integrations/github/chatops.py +0 -268
  240. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  241. codex_autorunner/routes/docs.py +0 -381
  242. codex_autorunner/routes/github.py +0 -327
  243. codex_autorunner/routes/runs.py +0 -250
  244. codex_autorunner/spec_ingest.py +0 -812
  245. codex_autorunner/static/docChatActions.js +0 -287
  246. codex_autorunner/static/docChatEvents.js +0 -300
  247. codex_autorunner/static/docChatRender.js +0 -205
  248. codex_autorunner/static/docChatStream.js +0 -361
  249. codex_autorunner/static/docs.js +0 -20
  250. codex_autorunner/static/docsClipboard.js +0 -69
  251. codex_autorunner/static/docsCrud.js +0 -257
  252. codex_autorunner/static/docsDocUpdates.js +0 -62
  253. codex_autorunner/static/docsDrafts.js +0 -16
  254. codex_autorunner/static/docsElements.js +0 -69
  255. codex_autorunner/static/docsInit.js +0 -285
  256. codex_autorunner/static/docsParse.js +0 -160
  257. codex_autorunner/static/docsSnapshot.js +0 -87
  258. codex_autorunner/static/docsSpecIngest.js +0 -263
  259. codex_autorunner/static/docsState.js +0 -127
  260. codex_autorunner/static/docsThreadRegistry.js +0 -44
  261. codex_autorunner/static/docsUi.js +0 -153
  262. codex_autorunner/static/docsVoice.js +0 -56
  263. codex_autorunner/static/github.js +0 -504
  264. codex_autorunner/static/logs.js +0 -678
  265. codex_autorunner/static/review.js +0 -157
  266. codex_autorunner/static/runs.js +0 -418
  267. codex_autorunner/static/snapshot.js +0 -124
  268. codex_autorunner/static/state.js +0 -94
  269. codex_autorunner/static/todoPreview.js +0 -27
  270. codex_autorunner/workspace.py +0 -16
  271. codex_autorunner-0.1.2.dist-info/METADATA +0 -249
  272. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  273. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  274. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  275. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  276. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -1,110 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
4
- import os
5
- from pathlib import Path
6
- from typing import Mapping, Optional, Sequence
3
+ from ...core.app_server_utils import build_app_server_env
7
4
 
8
- from ...core.logging_utils import log_event
9
- from ...core.utils import resolve_executable, subprocess_env
10
-
11
-
12
- def app_server_env(
13
- command: Sequence[str],
14
- cwd: Path,
15
- *,
16
- base_env: Optional[Mapping[str, str]] = None,
17
- ) -> dict[str, str]:
18
- extra_paths: list[str] = []
19
- if command:
20
- binary = command[0]
21
- resolved = resolve_executable(binary, env=base_env)
22
- candidate: Optional[Path] = Path(resolved) if resolved else None
23
- if candidate is None:
24
- candidate = Path(binary).expanduser()
25
- if not candidate.is_absolute():
26
- candidate = (cwd / candidate).resolve()
27
- if candidate.exists():
28
- extra_paths.append(str(candidate.parent))
29
- return subprocess_env(extra_paths=extra_paths, base_env=base_env)
30
-
31
-
32
- def seed_codex_home(
33
- codex_home: Path,
34
- *,
35
- logger: Optional[logging.Logger] = None,
36
- event_prefix: str = "app_server",
37
- ) -> None:
38
- logger = logger or logging.getLogger(__name__)
39
- auth_path = codex_home / "auth.json"
40
- source_root = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
41
- if source_root.resolve() == codex_home.resolve():
42
- return
43
- source_auth = source_root / "auth.json"
44
- if auth_path.exists():
45
- if auth_path.is_symlink() and auth_path.resolve() == source_auth.resolve():
46
- return
47
- log_event(
48
- logger,
49
- logging.INFO,
50
- f"{event_prefix}.codex_home.seed.skipped",
51
- reason="auth_exists",
52
- source=str(source_root),
53
- target=str(codex_home),
54
- )
55
- return
56
- if not source_root.exists():
57
- log_event(
58
- logger,
59
- logging.WARNING,
60
- f"{event_prefix}.codex_home.seed.skipped",
61
- reason="source_missing",
62
- source=str(source_root),
63
- target=str(codex_home),
64
- )
65
- return
66
- if not source_auth.exists():
67
- log_event(
68
- logger,
69
- logging.WARNING,
70
- f"{event_prefix}.codex_home.seed.skipped",
71
- reason="auth_missing",
72
- source=str(source_root),
73
- target=str(codex_home),
74
- )
75
- return
76
- try:
77
- auth_path.symlink_to(source_auth)
78
- log_event(
79
- logger,
80
- logging.INFO,
81
- f"{event_prefix}.codex_home.seeded",
82
- source=str(source_root),
83
- target=str(codex_home),
84
- )
85
- except OSError as exc:
86
- log_event(
87
- logger,
88
- logging.WARNING,
89
- f"{event_prefix}.codex_home.seed.failed",
90
- exc=exc,
91
- source=str(source_root),
92
- target=str(codex_home),
93
- )
94
-
95
-
96
- def build_app_server_env(
97
- command: Sequence[str],
98
- workspace_root: Path,
99
- state_dir: Path,
100
- *,
101
- logger: Optional[logging.Logger] = None,
102
- event_prefix: str = "app_server",
103
- base_env: Optional[Mapping[str, str]] = None,
104
- ) -> dict[str, str]:
105
- env = app_server_env(command, workspace_root, base_env=base_env)
106
- codex_home = state_dir / "codex_home"
107
- codex_home.mkdir(parents=True, exist_ok=True)
108
- seed_codex_home(codex_home, logger=logger, event_prefix=event_prefix)
109
- env["CODEX_HOME"] = str(codex_home)
110
- return env
5
+ __all__ = ["build_app_server_env"]
@@ -1,17 +1,19 @@
1
1
  import asyncio
2
2
  import json
3
+ import logging
3
4
  import threading
4
5
  import time
5
6
  from dataclasses import dataclass, field
6
7
  from typing import Any, AsyncIterator, Dict, Optional
7
8
 
8
- from ..integrations.app_server.client import (
9
- _extract_thread_id,
10
- _extract_thread_id_for_turn,
11
- _extract_turn_id,
9
+ from ...core.app_server_ids import (
10
+ extract_thread_id,
11
+ extract_thread_id_for_turn,
12
+ extract_turn_id,
12
13
  )
13
14
 
14
15
  TurnKey = tuple[str, str]
16
+ LOGGER = logging.getLogger("codex_autorunner.app_server")
15
17
 
16
18
 
17
19
  def format_sse(event: str, data: object) -> str:
@@ -143,11 +145,11 @@ class AppServerEventBuffer:
143
145
  ) -> tuple[Optional[str], Optional[str]]:
144
146
  params_raw = message.get("params")
145
147
  params: Dict[str, Any] = params_raw if isinstance(params_raw, dict) else {}
146
- turn_id = _extract_turn_id(params) or _extract_turn_id(message)
148
+ turn_id = extract_turn_id(params) or extract_turn_id(message)
147
149
  thread_id = (
148
- _extract_thread_id_for_turn(params)
149
- or _extract_thread_id(params)
150
- or _extract_thread_id(message)
150
+ extract_thread_id_for_turn(params)
151
+ or extract_thread_id(params)
152
+ or extract_thread_id(message)
151
153
  )
152
154
  if not thread_id and turn_id:
153
155
  thread_id = self._turn_index.get(turn_id)
@@ -184,9 +186,14 @@ class AppServerEventBuffer:
184
186
  try:
185
187
  lines = formatter.format_event(message)
186
188
  except Exception:
189
+ LOGGER.warning("Failed to format app server event log line.", exc_info=True)
187
190
  return
188
191
  for line in lines:
189
192
  try:
190
193
  emit(line)
191
194
  except Exception:
195
+ LOGGER.warning(
196
+ "Failed to emit app server event log line.",
197
+ exc_info=True,
198
+ )
192
199
  continue
@@ -2,12 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ import os
5
6
  import time
6
7
  from dataclasses import dataclass
7
8
  from pathlib import Path
8
9
  from typing import Callable, Dict, Optional, Sequence
9
10
 
10
11
  from ...core.logging_utils import log_event
12
+ from ...core.supervisor_utils import evict_lru_handle_locked, pop_idle_handles_locked
11
13
  from ...workspace import canonical_workspace_root, workspace_id_for_path
12
14
  from .client import ApprovalHandler, CodexAppServerClient, NotificationHandler
13
15
 
@@ -34,8 +36,17 @@ class WorkspaceAppServerSupervisor:
34
36
  approval_handler: Optional[ApprovalHandler] = None,
35
37
  notification_handler: Optional[NotificationHandler] = None,
36
38
  logger: Optional[logging.Logger] = None,
37
- auto_restart: bool = True,
39
+ auto_restart: Optional[bool] = None,
38
40
  request_timeout: Optional[float] = None,
41
+ turn_stall_timeout_seconds: Optional[float] = None,
42
+ turn_stall_poll_interval_seconds: Optional[float] = None,
43
+ turn_stall_recovery_min_interval_seconds: Optional[float] = None,
44
+ max_message_bytes: Optional[int] = None,
45
+ oversize_preview_bytes: Optional[int] = None,
46
+ max_oversize_drain_bytes: Optional[int] = None,
47
+ restart_backoff_initial_seconds: Optional[float] = None,
48
+ restart_backoff_max_seconds: Optional[float] = None,
49
+ restart_backoff_jitter_ratio: Optional[float] = None,
39
50
  default_approval_decision: str = "cancel",
40
51
  max_handles: Optional[int] = None,
41
52
  idle_ttl_seconds: Optional[float] = None,
@@ -46,8 +57,27 @@ class WorkspaceAppServerSupervisor:
46
57
  self._approval_handler = approval_handler
47
58
  self._notification_handler = notification_handler
48
59
  self._logger = logger or logging.getLogger(__name__)
49
- self._auto_restart = auto_restart
60
+ disable_restart_env = os.environ.get(
61
+ "CODEX_DISABLE_APP_SERVER_AUTORESTART_FOR_TESTS"
62
+ )
63
+ if disable_restart_env:
64
+ self._auto_restart = False
65
+ elif auto_restart is None:
66
+ self._auto_restart = True
67
+ else:
68
+ self._auto_restart = auto_restart
50
69
  self._request_timeout = request_timeout
70
+ self._turn_stall_timeout_seconds = turn_stall_timeout_seconds
71
+ self._turn_stall_poll_interval_seconds = turn_stall_poll_interval_seconds
72
+ self._turn_stall_recovery_min_interval_seconds = (
73
+ turn_stall_recovery_min_interval_seconds
74
+ )
75
+ self._max_message_bytes = max_message_bytes
76
+ self._oversize_preview_bytes = oversize_preview_bytes
77
+ self._max_oversize_drain_bytes = max_oversize_drain_bytes
78
+ self._restart_backoff_initial_seconds = restart_backoff_initial_seconds
79
+ self._restart_backoff_max_seconds = restart_backoff_max_seconds
80
+ self._restart_backoff_jitter_ratio = restart_backoff_jitter_ratio
51
81
  self._default_approval_decision = default_approval_decision
52
82
  self._max_handles = max_handles
53
83
  self._idle_ttl_seconds = idle_ttl_seconds
@@ -78,7 +108,8 @@ class WorkspaceAppServerSupervisor:
78
108
  last_used_at=handle.last_used_at,
79
109
  )
80
110
  await handle.client.close()
81
- except Exception:
111
+ except Exception as exc:
112
+ self._logger.debug("Failed to close handle: %s", exc)
82
113
  continue
83
114
 
84
115
  async def prune_idle(self) -> int:
@@ -100,7 +131,8 @@ class WorkspaceAppServerSupervisor:
100
131
  )
101
132
  await handle.client.close()
102
133
  closed += 1
103
- except Exception:
134
+ except Exception as exc:
135
+ self._logger.debug("Failed to prune handle: %s", exc)
104
136
  continue
105
137
  return closed
106
138
 
@@ -129,6 +161,15 @@ class WorkspaceAppServerSupervisor:
129
161
  default_approval_decision=self._default_approval_decision,
130
162
  auto_restart=self._auto_restart,
131
163
  request_timeout=self._request_timeout,
164
+ turn_stall_timeout_seconds=self._turn_stall_timeout_seconds,
165
+ turn_stall_poll_interval_seconds=self._turn_stall_poll_interval_seconds,
166
+ turn_stall_recovery_min_interval_seconds=self._turn_stall_recovery_min_interval_seconds,
167
+ max_message_bytes=self._max_message_bytes,
168
+ oversize_preview_bytes=self._oversize_preview_bytes,
169
+ max_oversize_drain_bytes=self._max_oversize_drain_bytes,
170
+ restart_backoff_initial_seconds=self._restart_backoff_initial_seconds,
171
+ restart_backoff_max_seconds=self._restart_backoff_max_seconds,
172
+ restart_backoff_jitter_ratio=self._restart_backoff_jitter_ratio,
132
173
  notification_handler=self._notification_handler,
133
174
  logger=self._logger,
134
175
  )
@@ -157,7 +198,8 @@ class WorkspaceAppServerSupervisor:
157
198
  last_used_at=handle.last_used_at,
158
199
  )
159
200
  await handle.client.close()
160
- except Exception:
201
+ except Exception as exc:
202
+ self._logger.debug("Failed to close handle: %s", exc)
161
203
  continue
162
204
  return handle
163
205
 
@@ -173,35 +215,19 @@ class WorkspaceAppServerSupervisor:
173
215
  return self._pop_idle_handles_locked()
174
216
 
175
217
  def _pop_idle_handles_locked(self) -> list[AppServerHandle]:
176
- if not self._idle_ttl_seconds or self._idle_ttl_seconds <= 0:
177
- return []
178
- cutoff = time.monotonic() - self._idle_ttl_seconds
179
- stale: list[AppServerHandle] = []
180
- for handle in list(self._handles.values()):
181
- if handle.last_used_at and handle.last_used_at < cutoff:
182
- self._handles.pop(handle.workspace_id, None)
183
- stale.append(handle)
184
- return stale
218
+ return pop_idle_handles_locked(
219
+ self._handles,
220
+ self._idle_ttl_seconds,
221
+ self._logger,
222
+ "app_server",
223
+ last_used_at_getter=lambda h: h.last_used_at,
224
+ )
185
225
 
186
226
  def _evict_lru_handle_locked(self) -> Optional[AppServerHandle]:
187
- if not self._max_handles or self._max_handles <= 0:
188
- return None
189
- if len(self._handles) < self._max_handles:
190
- return None
191
- lru_handle = min(
192
- self._handles.values(),
193
- key=lambda handle: handle.last_used_at or 0.0,
194
- )
195
- log_event(
227
+ return evict_lru_handle_locked(
228
+ self._handles,
229
+ self._max_handles,
196
230
  self._logger,
197
- logging.INFO,
198
- "app_server.handle.evicted",
199
- reason="max_handles",
200
- workspace_id=lru_handle.workspace_id,
201
- workspace_root=str(lru_handle.workspace_root),
202
- max_handles=self._max_handles,
203
- handle_count=len(self._handles),
204
- last_used_at=lru_handle.last_used_at,
231
+ "app_server",
232
+ last_used_at_getter=lambda h: h.last_used_at or 0.0,
205
233
  )
206
- self._handles.pop(lru_handle.workspace_id, None)
207
- return lru_handle