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,169 +1,3 @@
1
- """
2
- Session settings routes for autorunner overrides.
3
- """
1
+ """Backward-compatible settings routes."""
4
2
 
5
- from typing import Optional
6
-
7
- from fastapi import APIRouter, HTTPException, Request
8
-
9
- from ..core.state import RunnerState, load_state, save_state, state_lock
10
- from ..web.schemas import SessionSettingsRequest, SessionSettingsResponse
11
-
12
- ALLOWED_APPROVAL_POLICIES = {"never", "unlessTrusted"}
13
- ALLOWED_SANDBOX_MODES = {"dangerFullAccess", "workspaceWrite"}
14
-
15
-
16
- def _normalize_optional_string(value: object, field: str) -> Optional[str]:
17
- if value is None:
18
- return None
19
- if not isinstance(value, str):
20
- raise HTTPException(status_code=400, detail=f"{field} must be a string")
21
- cleaned = value.strip()
22
- return cleaned or None
23
-
24
-
25
- def build_settings_routes() -> APIRouter:
26
- router = APIRouter()
27
-
28
- @router.get("/api/session/settings", response_model=SessionSettingsResponse)
29
- def get_session_settings(request: Request):
30
- state = load_state(request.app.state.engine.state_path)
31
- return {
32
- "autorunner_model_override": state.autorunner_model_override,
33
- "autorunner_effort_override": state.autorunner_effort_override,
34
- "autorunner_approval_policy": state.autorunner_approval_policy,
35
- "autorunner_sandbox_mode": state.autorunner_sandbox_mode,
36
- "autorunner_workspace_write_network": state.autorunner_workspace_write_network,
37
- "runner_stop_after_runs": state.runner_stop_after_runs,
38
- }
39
-
40
- @router.post("/api/session/settings", response_model=SessionSettingsResponse)
41
- def update_session_settings(request: Request, payload: SessionSettingsRequest):
42
- updates = payload.model_dump(exclude_unset=True)
43
- engine = request.app.state.engine
44
- manager = request.app.state.manager
45
- registry = request.app.state.app_server_threads
46
- with state_lock(engine.state_path):
47
- state = load_state(engine.state_path)
48
- model_override = (
49
- _normalize_optional_string(
50
- updates.get("autorunner_model_override"),
51
- "autorunner_model_override",
52
- )
53
- if "autorunner_model_override" in updates
54
- else state.autorunner_model_override
55
- )
56
- effort_override = (
57
- _normalize_optional_string(
58
- updates.get("autorunner_effort_override"),
59
- "autorunner_effort_override",
60
- )
61
- if "autorunner_effort_override" in updates
62
- else state.autorunner_effort_override
63
- )
64
- approval_policy = (
65
- _normalize_optional_string(
66
- updates.get("autorunner_approval_policy"),
67
- "autorunner_approval_policy",
68
- )
69
- if "autorunner_approval_policy" in updates
70
- else state.autorunner_approval_policy
71
- )
72
- if approval_policy and approval_policy not in ALLOWED_APPROVAL_POLICIES:
73
- raise HTTPException(
74
- status_code=400,
75
- detail="approval policy must be never or unlessTrusted",
76
- )
77
- sandbox_mode = (
78
- _normalize_optional_string(
79
- updates.get("autorunner_sandbox_mode"),
80
- "autorunner_sandbox_mode",
81
- )
82
- if "autorunner_sandbox_mode" in updates
83
- else state.autorunner_sandbox_mode
84
- )
85
- if sandbox_mode and sandbox_mode not in ALLOWED_SANDBOX_MODES:
86
- raise HTTPException(
87
- status_code=400,
88
- detail="sandbox mode must be dangerFullAccess or workspaceWrite",
89
- )
90
- workspace_write_network = (
91
- updates.get("autorunner_workspace_write_network")
92
- if "autorunner_workspace_write_network" in updates
93
- else state.autorunner_workspace_write_network
94
- )
95
- if (
96
- "autorunner_workspace_write_network" in updates
97
- and workspace_write_network is not None
98
- and not isinstance(workspace_write_network, bool)
99
- ):
100
- raise HTTPException(
101
- status_code=400,
102
- detail="autorunner_workspace_write_network must be a boolean",
103
- )
104
- runner_stop_after_runs = (
105
- updates.get("runner_stop_after_runs")
106
- if "runner_stop_after_runs" in updates
107
- else state.runner_stop_after_runs
108
- )
109
- if (
110
- "runner_stop_after_runs" in updates
111
- and runner_stop_after_runs is not None
112
- and (
113
- not isinstance(runner_stop_after_runs, int)
114
- or isinstance(runner_stop_after_runs, bool)
115
- or runner_stop_after_runs <= 0
116
- )
117
- ):
118
- raise HTTPException(
119
- status_code=400,
120
- detail="runner_stop_after_runs must be a positive integer",
121
- )
122
-
123
- thread_reset_required = any(
124
- (
125
- model_override != state.autorunner_model_override,
126
- effort_override != state.autorunner_effort_override,
127
- approval_policy != state.autorunner_approval_policy,
128
- sandbox_mode != state.autorunner_sandbox_mode,
129
- workspace_write_network != state.autorunner_workspace_write_network,
130
- runner_stop_after_runs != state.runner_stop_after_runs,
131
- )
132
- )
133
- if thread_reset_required and manager.running:
134
- raise HTTPException(
135
- status_code=409,
136
- detail="Cannot change autorunner settings while a run is active",
137
- )
138
-
139
- new_state = RunnerState(
140
- last_run_id=state.last_run_id,
141
- status=state.status,
142
- last_exit_code=state.last_exit_code,
143
- last_run_started_at=state.last_run_started_at,
144
- last_run_finished_at=state.last_run_finished_at,
145
- autorunner_agent_override=state.autorunner_agent_override,
146
- autorunner_model_override=model_override,
147
- autorunner_effort_override=effort_override,
148
- autorunner_approval_policy=approval_policy,
149
- autorunner_sandbox_mode=sandbox_mode,
150
- autorunner_workspace_write_network=workspace_write_network,
151
- runner_stop_after_runs=runner_stop_after_runs,
152
- runner_pid=state.runner_pid,
153
- sessions=state.sessions,
154
- repo_to_session=state.repo_to_session,
155
- )
156
- save_state(engine.state_path, new_state)
157
- if thread_reset_required:
158
- registry.reset_thread("autorunner")
159
-
160
- return {
161
- "autorunner_model_override": model_override,
162
- "autorunner_effort_override": effort_override,
163
- "autorunner_approval_policy": approval_policy,
164
- "autorunner_sandbox_mode": sandbox_mode,
165
- "autorunner_workspace_write_network": workspace_write_network,
166
- "runner_stop_after_runs": runner_stop_after_runs,
167
- }
168
-
169
- return router
3
+ from ..surfaces.web.routes.settings import * # noqa: F401,F403
@@ -1,276 +1,3 @@
1
- """
2
- Shared utilities for route modules.
3
- """
1
+ """Backward-compatible shared route utilities."""
4
2
 
5
- import asyncio
6
- import json
7
- import time
8
- from pathlib import Path
9
- from typing import Optional
10
-
11
- from ..codex_cli import apply_codex_options, extract_flag_value, supports_reasoning
12
- from ..core.locks import (
13
- DEFAULT_RUNNER_CMD_HINTS,
14
- assess_lock,
15
- process_is_active,
16
- read_lock_info,
17
- )
18
- from ..core.state import load_state
19
- from ..core.utils import resolve_opencode_binary
20
-
21
- BYPASS_FLAGS = {
22
- "--yolo",
23
- "--dangerously-bypass-approvals-and-sandbox",
24
- }
25
-
26
- SSE_HEADERS = {
27
- "Cache-Control": "no-cache",
28
- "X-Accel-Buffering": "no",
29
- "Connection": "keep-alive",
30
- "Content-Encoding": "identity",
31
- }
32
-
33
-
34
- async def _interruptible_sleep(
35
- seconds: float, shutdown_event: Optional[asyncio.Event]
36
- ) -> bool:
37
- """Sleep that can be interrupted by shutdown_event. Returns True if interrupted."""
38
- if shutdown_event is None:
39
- await asyncio.sleep(seconds)
40
- return False
41
- try:
42
- await asyncio.wait_for(shutdown_event.wait(), timeout=seconds)
43
- return True # Event was set
44
- except asyncio.TimeoutError:
45
- return False # Normal timeout, continue
46
-
47
-
48
- def _extract_bypass_flag(args: list[str]) -> tuple[str, list[str]]:
49
- chosen = None
50
- for arg in args:
51
- if arg in BYPASS_FLAGS:
52
- chosen = arg
53
- break
54
- filtered = [arg for arg in args if arg not in BYPASS_FLAGS]
55
- return chosen or "--yolo", filtered
56
-
57
-
58
- def build_codex_terminal_cmd(
59
- engine,
60
- *,
61
- resume_mode: bool,
62
- model: Optional[str] = None,
63
- reasoning: Optional[str] = None,
64
- ) -> list[str]:
65
- """
66
- Build the subprocess argv for launching the Codex interactive CLI inside a PTY.
67
- """
68
- bypass_flag, terminal_args = _extract_bypass_flag(
69
- list(engine.config.codex_terminal_args)
70
- )
71
- if resume_mode:
72
- cmd = [
73
- engine.config.codex_binary,
74
- bypass_flag,
75
- "resume",
76
- *terminal_args,
77
- ]
78
- return apply_codex_options(
79
- cmd,
80
- model=model,
81
- reasoning=reasoning,
82
- supports_reasoning=supports_reasoning(engine.config.codex_binary),
83
- )
84
-
85
- cmd = [
86
- engine.config.codex_binary,
87
- bypass_flag,
88
- *terminal_args,
89
- ]
90
- return apply_codex_options(
91
- cmd,
92
- model=model,
93
- reasoning=reasoning,
94
- supports_reasoning=supports_reasoning(engine.config.codex_binary),
95
- )
96
-
97
-
98
- def build_opencode_terminal_cmd(binary: str, model: Optional[str] = None) -> list[str]:
99
- resolved = resolve_opencode_binary(binary)
100
- cmd = [resolved or binary]
101
- if model:
102
- cmd.extend(["--model", model])
103
- return cmd
104
-
105
-
106
- def resolve_runner_status(engine, state) -> tuple[str, Optional[int], bool]:
107
- pid = state.runner_pid
108
- alive_pid = pid if pid and process_is_active(pid) else None
109
- if alive_pid is None:
110
- info = read_lock_info(engine.lock_path)
111
- if info.pid and process_is_active(info.pid):
112
- alive_pid = info.pid
113
- running = alive_pid is not None
114
- status = state.status
115
- if status == "running" and not running:
116
- status = "idle"
117
- runner_pid = alive_pid if running else None
118
- return status, runner_pid, running
119
-
120
-
121
- def resolve_lock_payload(engine) -> dict[str, object]:
122
- assessment = assess_lock(
123
- engine.lock_path,
124
- expected_cmd_substrings=DEFAULT_RUNNER_CMD_HINTS,
125
- )
126
- return {
127
- "lock_present": engine.lock_path.exists(),
128
- "lock_pid": assessment.pid,
129
- "lock_freeable": assessment.freeable,
130
- "lock_freeable_reason": assessment.reason,
131
- }
132
-
133
-
134
- async def log_stream(
135
- log_path: Path,
136
- heartbeat_interval: float = 15.0,
137
- shutdown_event: Optional[asyncio.Event] = None,
138
- max_seconds: float = 60.0,
139
- ):
140
- """SSE stream generator for log file tailing."""
141
- if not log_path.exists():
142
- yield "data: log file not found\n\n"
143
- return
144
- last_emit_at = time.monotonic()
145
- start_time = time.monotonic()
146
- with log_path.open("r", encoding="utf-8") as f:
147
- f.seek(0, 2)
148
- while True:
149
- if shutdown_event is not None and shutdown_event.is_set():
150
- return
151
- if time.monotonic() - start_time > max_seconds:
152
- yield "event: timeout\ndata: Stream timeout exceeded\n\n"
153
- return
154
- line = f.readline()
155
- if line:
156
- yield f"data: {line.rstrip()}\n\n"
157
- last_emit_at = time.monotonic()
158
- else:
159
- now = time.monotonic()
160
- if now - last_emit_at >= heartbeat_interval:
161
- yield ": ping\n\n"
162
- last_emit_at = now
163
- if await _interruptible_sleep(0.5, shutdown_event):
164
- return
165
-
166
-
167
- async def jsonl_event_stream(
168
- path: Path,
169
- *,
170
- event_name: str = "message",
171
- heartbeat_interval: float = 15.0,
172
- shutdown_event: Optional[asyncio.Event] = None,
173
- ):
174
- """SSE stream generator for JSONL event files."""
175
- last_emit_at = time.monotonic()
176
- position = 0
177
- while True:
178
- if shutdown_event is not None and shutdown_event.is_set():
179
- return
180
- if not path.exists():
181
- now = time.monotonic()
182
- if now - last_emit_at >= heartbeat_interval:
183
- yield ": ping\n\n"
184
- last_emit_at = now
185
- if await _interruptible_sleep(1.0, shutdown_event):
186
- return
187
- continue
188
- try:
189
- with path.open("r", encoding="utf-8") as handle:
190
- handle.seek(position)
191
- while True:
192
- if shutdown_event is not None and shutdown_event.is_set():
193
- return
194
- line = handle.readline()
195
- if line:
196
- position = handle.tell()
197
- payload = line.strip()
198
- if payload:
199
- yield f"event: {event_name}\ndata: {payload}\n\n"
200
- last_emit_at = time.monotonic()
201
- else:
202
- now = time.monotonic()
203
- if now - last_emit_at >= heartbeat_interval:
204
- yield ": ping\n\n"
205
- last_emit_at = now
206
- if await _interruptible_sleep(0.5, shutdown_event):
207
- return
208
- except OSError:
209
- if await _interruptible_sleep(1.0, shutdown_event):
210
- return
211
-
212
-
213
- async def state_stream(
214
- engine,
215
- manager,
216
- logger=None,
217
- heartbeat_interval: float = 15.0,
218
- shutdown_event: Optional[asyncio.Event] = None,
219
- max_seconds: float = 60.0,
220
- ):
221
- """SSE stream generator for state updates."""
222
- last_payload = None
223
- last_error_log_at = 0.0
224
- last_emit_at = time.monotonic()
225
- start_time = time.monotonic()
226
- terminal_idle_timeout_seconds = engine.config.terminal_idle_timeout_seconds
227
- codex_model = engine.config.codex_model or extract_flag_value(
228
- engine.config.codex_args, "--model"
229
- )
230
- while True:
231
- if shutdown_event is not None and shutdown_event.is_set():
232
- return
233
- if time.monotonic() - start_time > max_seconds:
234
- yield "event: timeout\ndata: Stream timeout exceeded\n\n"
235
- return
236
- emitted = False
237
- try:
238
- state = await asyncio.to_thread(load_state, engine.state_path)
239
- outstanding, done = await asyncio.to_thread(engine.docs.todos)
240
- status, runner_pid, running = resolve_runner_status(engine, state)
241
- lock_payload = resolve_lock_payload(engine)
242
- payload = {
243
- "last_run_id": state.last_run_id,
244
- "status": status,
245
- "last_exit_code": state.last_exit_code,
246
- "last_run_started_at": state.last_run_started_at,
247
- "last_run_finished_at": state.last_run_finished_at,
248
- "outstanding_count": len(outstanding),
249
- "done_count": len(done),
250
- "running": running,
251
- "runner_pid": runner_pid,
252
- **lock_payload,
253
- "terminal_idle_timeout_seconds": terminal_idle_timeout_seconds,
254
- "codex_model": codex_model or "auto",
255
- }
256
- if payload != last_payload:
257
- yield f"data: {json.dumps(payload)}\n\n"
258
- last_payload = payload
259
- last_emit_at = time.monotonic()
260
- emitted = True
261
- except Exception:
262
- # Don't spam logs, but don't swallow silently either.
263
- now = time.time()
264
- if logger is not None and (now - last_error_log_at) > 60:
265
- last_error_log_at = now
266
- try:
267
- logger.warning("state stream error", exc_info=True)
268
- except Exception:
269
- pass
270
- if not emitted:
271
- now = time.monotonic()
272
- if now - last_emit_at >= heartbeat_interval:
273
- yield ": ping\n\n"
274
- last_emit_at = now
275
- if await _interruptible_sleep(1.0, shutdown_event):
276
- return
3
+ from ..surfaces.web.routes.shared import * # noqa: F401,F403
@@ -1,191 +1,7 @@
1
- import asyncio
2
- import logging
3
- from pathlib import Path
4
- from typing import Optional
1
+ """Backward-compatible system routes."""
5
2
 
6
- from fastapi import APIRouter, HTTPException, Request
7
- from fastapi.responses import JSONResponse
3
+ import sys
8
4
 
9
- from ..core import update as update_core
10
- from ..core.config import HubConfig
11
- from ..core.update import (
12
- UpdateInProgressError,
13
- _normalize_update_ref,
14
- _normalize_update_target,
15
- _read_update_status,
16
- _spawn_update_process,
17
- _system_update_check,
18
- )
19
- from ..web.schemas import (
20
- SystemHealthResponse,
21
- SystemUpdateCheckResponse,
22
- SystemUpdateRequest,
23
- SystemUpdateResponse,
24
- SystemUpdateStatusResponse,
25
- )
26
- from ..web.static_assets import missing_static_assets
27
- from ..web.static_refresh import refresh_static_assets
5
+ from ..surfaces.web.routes import system as _system
28
6
 
29
- _pid_is_running = update_core._pid_is_running
30
- _system_update_worker = update_core._system_update_worker
31
- _update_lock_active = update_core._update_lock_active
32
- _update_lock_path = update_core._update_lock_path
33
- _update_status_path = update_core._update_status_path
34
- shutil = update_core.shutil
35
- subprocess = update_core.subprocess
36
-
37
-
38
- def build_system_routes() -> APIRouter:
39
- router = APIRouter()
40
-
41
- @router.get("/health", response_model=SystemHealthResponse)
42
- async def system_health(request: Request):
43
- try:
44
- config = request.app.state.config
45
- except AttributeError:
46
- config = None
47
- mode = "hub" if isinstance(config, HubConfig) else "repo"
48
- base_path = getattr(request.app.state, "base_path", "")
49
- asset_version = getattr(request.app.state, "asset_version", None)
50
- static_dir = getattr(getattr(request.app, "state", None), "static_dir", None)
51
- if not isinstance(static_dir, Path):
52
- return JSONResponse(
53
- {
54
- "status": "error",
55
- "detail": "Static UI assets missing; reinstall package",
56
- "mode": mode,
57
- "base_path": base_path,
58
- },
59
- status_code=500,
60
- )
61
- missing = await asyncio.to_thread(missing_static_assets, static_dir)
62
- if missing:
63
- if refresh_static_assets(request.app):
64
- static_dir = getattr(
65
- getattr(request.app, "state", None), "static_dir", None
66
- )
67
- if isinstance(static_dir, Path):
68
- missing = await asyncio.to_thread(missing_static_assets, static_dir)
69
- else:
70
- missing = ["index.html"]
71
- if not missing:
72
- return {
73
- "status": "ok",
74
- "mode": mode,
75
- "base_path": base_path,
76
- "asset_version": asset_version,
77
- }
78
- return JSONResponse(
79
- {
80
- "status": "error",
81
- "detail": "Static UI assets missing; reinstall package",
82
- "missing": missing,
83
- "mode": mode,
84
- "base_path": base_path,
85
- },
86
- status_code=500,
87
- )
88
- return {
89
- "status": "ok",
90
- "mode": mode,
91
- "base_path": base_path,
92
- "asset_version": asset_version,
93
- }
94
-
95
- @router.get("/system/update/check", response_model=SystemUpdateCheckResponse)
96
- async def system_update_check(request: Request):
97
- """
98
- Check if an update is available by comparing local git state vs remote.
99
- If local git state is unavailable, report that an update may be available.
100
- """
101
- try:
102
- config = request.app.state.config
103
- except AttributeError:
104
- config = None
105
-
106
- repo_url = "https://github.com/Git-on-my-level/codex-autorunner.git"
107
- repo_ref = "main"
108
- if config and isinstance(config, HubConfig):
109
- configured_url = getattr(config, "update_repo_url", None)
110
- if configured_url:
111
- repo_url = configured_url
112
- configured_ref = getattr(config, "update_repo_ref", None)
113
- if configured_ref:
114
- repo_ref = configured_ref
115
-
116
- try:
117
- return await asyncio.to_thread(
118
- _system_update_check, repo_url=repo_url, repo_ref=repo_ref
119
- )
120
- except Exception as e:
121
- logger = getattr(getattr(request.app, "state", None), "logger", None)
122
- if logger:
123
- logger.error("Update check error: %s", e, exc_info=True)
124
- raise HTTPException(status_code=500, detail=str(e)) from e
125
-
126
- @router.post("/system/update", response_model=SystemUpdateResponse)
127
- async def system_update(
128
- request: Request, payload: Optional[SystemUpdateRequest] = None
129
- ):
130
- """
131
- Pull latest code and refresh the running service.
132
- This will restart the server if successful.
133
- """
134
- try:
135
- config = request.app.state.config
136
- except AttributeError:
137
- config = None
138
-
139
- # Determine URL
140
- repo_url = "https://github.com/Git-on-my-level/codex-autorunner.git"
141
- repo_ref = "main"
142
- if config and isinstance(config, HubConfig):
143
- configured_url = getattr(config, "update_repo_url", None)
144
- if configured_url:
145
- repo_url = configured_url
146
- configured_ref = getattr(config, "update_repo_ref", None)
147
- if configured_ref:
148
- repo_ref = configured_ref
149
-
150
- home_dot_car = Path.home() / ".codex-autorunner"
151
- update_dir = home_dot_car / "update_cache"
152
-
153
- try:
154
- target_raw = payload.target if payload else None
155
- if target_raw is None:
156
- target_raw = request.query_params.get("target")
157
- update_target = _normalize_update_target(target_raw)
158
- logger = getattr(getattr(request.app, "state", None), "logger", None)
159
- if logger is None:
160
- logger = logging.getLogger("codex_autorunner.system_update")
161
- await asyncio.to_thread(
162
- _spawn_update_process,
163
- repo_url=repo_url,
164
- repo_ref=_normalize_update_ref(repo_ref),
165
- update_dir=update_dir,
166
- logger=logger,
167
- update_target=update_target,
168
- )
169
- return {
170
- "status": "ok",
171
- "message": f"Update started ({update_target}). Service will restart shortly.",
172
- "target": update_target,
173
- }
174
- except UpdateInProgressError as exc:
175
- raise HTTPException(status_code=409, detail=str(exc)) from exc
176
- except ValueError as exc:
177
- raise HTTPException(status_code=400, detail=str(exc)) from exc
178
- except Exception as e:
179
- logger = getattr(getattr(request.app, "state", None), "logger", None)
180
- if logger:
181
- logger.error("Update error: %s", e, exc_info=True)
182
- raise HTTPException(status_code=500, detail=str(e)) from e
183
-
184
- @router.get("/system/update/status", response_model=SystemUpdateStatusResponse)
185
- async def system_update_status():
186
- status = await asyncio.to_thread(_read_update_status)
187
- if status is None:
188
- return {"status": "unknown", "message": "No update status recorded."}
189
- return status
190
-
191
- return router
7
+ sys.modules[__name__] = _system
@@ -0,0 +1,3 @@
1
+ """Backward-compatible usage routes."""
2
+
3
+ from ..surfaces.web.routes.usage import * # noqa: F401,F403