codex-autorunner 1.0.0__py3-none-any.whl → 1.2.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 (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -1,444 +1,7 @@
1
- from __future__ import annotations
1
+ """Backward-compatible static asset exports."""
2
2
 
3
- import hashlib
4
- import logging
5
- import os
6
- import shutil
7
- import time
8
- from contextlib import ExitStack
9
- from pathlib import Path
10
- from typing import Iterable, Optional
11
- from uuid import uuid4
3
+ import sys
12
4
 
13
- from ..core.logging_utils import safe_log
14
- from ..core.static_assets import missing_static_assets, resolve_static_dir
5
+ from ..surfaces.web import static_assets as _static_assets
15
6
 
16
- _ASSET_VERSION_TOKEN = "__CAR_ASSET_VERSION__"
17
-
18
-
19
- def _iter_static_source_files(source_dir: Path) -> Iterable[Path]:
20
- try:
21
- for path in source_dir.rglob("*.ts"):
22
- try:
23
- if path.name.endswith(".d.ts"):
24
- continue
25
- if path.is_dir():
26
- continue
27
- yield path
28
- except OSError:
29
- continue
30
- except Exception:
31
- return
32
-
33
-
34
- def _stale_static_sources(source_dir: Path, static_dir: Path) -> list[str]:
35
- stale: list[str] = []
36
- for source in _iter_static_source_files(source_dir):
37
- try:
38
- rel_path = source.relative_to(source_dir)
39
- except ValueError:
40
- rel_path = Path(source.name)
41
- target = (static_dir / rel_path).with_suffix(".js")
42
- try:
43
- source_mtime = source.stat().st_mtime
44
- except OSError:
45
- continue
46
- try:
47
- target_mtime = target.stat().st_mtime
48
- except OSError:
49
- stale.append(rel_path.as_posix())
50
- continue
51
- if source_mtime > target_mtime:
52
- stale.append(rel_path.as_posix())
53
- return stale
54
-
55
-
56
- def warn_on_stale_static_assets(static_dir: Path, logger: logging.Logger) -> None:
57
- source_dir = Path(__file__).resolve().parent.parent / "static_src"
58
- if not source_dir.exists():
59
- return
60
- stale = _stale_static_sources(source_dir, static_dir)
61
- if not stale:
62
- return
63
- preview = ", ".join(stale[:5])
64
- suffix = f" (+{len(stale) - 5} more)" if len(stale) > 5 else ""
65
- safe_log(
66
- logger,
67
- logging.WARNING,
68
- "Static assets appear stale; run `pnpm run build`. Newer sources: %s%s",
69
- preview,
70
- suffix,
71
- )
72
-
73
-
74
- def _iter_asset_files(static_dir: Path) -> Iterable[Path]:
75
- try:
76
- for path in static_dir.rglob("*"):
77
- try:
78
- if path.is_dir():
79
- continue
80
- yield path
81
- except OSError:
82
- continue
83
- except Exception:
84
- return
85
-
86
-
87
- def _hash_file(path: Path, digest: "hashlib._Hash") -> None:
88
- try:
89
- if path.is_symlink():
90
- digest.update(b"SYMLINK:")
91
- try:
92
- target = path.readlink()
93
- except OSError:
94
- target = Path("dangling")
95
- digest.update(str(target).encode("utf-8", errors="replace"))
96
- return
97
- with path.open("rb") as handle:
98
- while True:
99
- chunk = handle.read(1024 * 1024)
100
- if not chunk:
101
- break
102
- digest.update(chunk)
103
- except OSError:
104
- digest.update(b"UNREADABLE")
105
-
106
-
107
- def asset_version(static_dir: Path) -> str:
108
- digest = hashlib.sha256()
109
- files = sorted(_iter_asset_files(static_dir), key=lambda p: p.as_posix())
110
- if not files:
111
- return "0"
112
- for path in files:
113
- try:
114
- rel_path = path.relative_to(static_dir)
115
- except ValueError:
116
- rel_path = path
117
- digest.update(rel_path.as_posix().encode("utf-8", errors="replace"))
118
- _hash_file(path, digest)
119
- return digest.hexdigest()
120
-
121
-
122
- def render_index_html(static_dir: Path, version: Optional[str]) -> str:
123
- index_path = static_dir / "index.html"
124
- text = index_path.read_text(encoding="utf-8")
125
- if version:
126
- text = text.replace(_ASSET_VERSION_TOKEN, version)
127
- return text
128
-
129
-
130
- def security_headers() -> dict[str, str]:
131
- # CSP: scripts are all local with no inline JS; runtime UI uses inline styles.
132
- return {
133
- "Content-Security-Policy": (
134
- "default-src 'self'; "
135
- "script-src 'self'; "
136
- "style-src 'self' 'unsafe-inline'; "
137
- "img-src 'self' data:; "
138
- "font-src 'self' data:; "
139
- "connect-src 'self' ws: wss:; "
140
- "frame-ancestors 'none'"
141
- ),
142
- "Referrer-Policy": "same-origin",
143
- "X-Content-Type-Options": "nosniff",
144
- "X-Frame-Options": "DENY",
145
- }
146
-
147
-
148
- def index_response_headers() -> dict[str, str]:
149
- headers = {
150
- "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
151
- "Pragma": "no-cache",
152
- "Expires": "0",
153
- }
154
- headers.update(security_headers())
155
- return headers
156
-
157
-
158
- def _cleanup_temp_dir(path: Path, logger: logging.Logger) -> None:
159
- try:
160
- shutil.rmtree(path)
161
- except FileNotFoundError:
162
- return
163
- except Exception as exc:
164
- safe_log(
165
- logger,
166
- logging.WARNING,
167
- "Failed to remove temporary static cache dir %s",
168
- path,
169
- exc=exc,
170
- )
171
-
172
-
173
- def _acquire_cache_lock(lock_path: Path, logger: logging.Logger) -> bool:
174
- try:
175
- fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
176
- except FileExistsError:
177
- return False
178
- except Exception as exc:
179
- safe_log(
180
- logger,
181
- logging.WARNING,
182
- "Failed to create static cache lock %s",
183
- lock_path,
184
- exc=exc,
185
- )
186
- return False
187
- try:
188
- os.write(fd, str(os.getpid()).encode("utf-8"))
189
- except Exception:
190
- pass
191
- finally:
192
- try:
193
- os.close(fd)
194
- except Exception:
195
- pass
196
- return True
197
-
198
-
199
- def _release_cache_lock(lock_path: Path, logger: logging.Logger) -> None:
200
- try:
201
- lock_path.unlink()
202
- except FileNotFoundError:
203
- return
204
- except Exception as exc:
205
- safe_log(
206
- logger,
207
- logging.WARNING,
208
- "Failed to remove static cache lock %s",
209
- lock_path,
210
- exc=exc,
211
- )
212
-
213
-
214
- def _cache_dir_mtime(path: Path) -> float:
215
- index_path = path / "index.html"
216
- try:
217
- return index_path.stat().st_mtime
218
- except OSError:
219
- try:
220
- return path.stat().st_mtime
221
- except OSError:
222
- return 0.0
223
-
224
-
225
- def _list_cache_entries(cache_root: Path) -> list[Path]:
226
- if not cache_root.exists():
227
- return []
228
- entries: list[Path] = []
229
- try:
230
- for entry in cache_root.iterdir():
231
- if entry.name.startswith("."):
232
- continue
233
- try:
234
- if entry.is_dir():
235
- entries.append(entry)
236
- except OSError:
237
- continue
238
- except OSError:
239
- return []
240
- return entries
241
-
242
-
243
- def _select_latest_valid_cache(cache_root: Path) -> Optional[Path]:
244
- candidates = []
245
- for entry in _list_cache_entries(cache_root):
246
- if not missing_static_assets(entry):
247
- candidates.append(entry)
248
- if not candidates:
249
- return None
250
- candidates.sort(key=_cache_dir_mtime, reverse=True)
251
- return candidates[0]
252
-
253
-
254
- def _prune_cache_entries(
255
- cache_root: Path,
256
- *,
257
- keep: set[Path],
258
- max_cache_entries: int,
259
- max_cache_age_days: Optional[int],
260
- logger: logging.Logger,
261
- ) -> None:
262
- if max_cache_entries <= 0 and max_cache_age_days is None:
263
- return
264
- entries = _list_cache_entries(cache_root)
265
- if not entries:
266
- return
267
- now = time.time()
268
- if max_cache_age_days is not None:
269
- cutoff = now - (max_cache_age_days * 86400)
270
- for entry in list(entries):
271
- if entry in keep:
272
- continue
273
- if _cache_dir_mtime(entry) < cutoff:
274
- try:
275
- shutil.rmtree(entry)
276
- entries.remove(entry)
277
- except Exception as exc:
278
- safe_log(
279
- logger,
280
- logging.WARNING,
281
- "Failed to remove stale static cache dir %s",
282
- entry,
283
- exc=exc,
284
- )
285
- if max_cache_entries > 0 and len(entries) > max_cache_entries:
286
- removable = [entry for entry in entries if entry not in keep]
287
- removable.sort(key=_cache_dir_mtime)
288
- for entry in removable[: len(entries) - max_cache_entries]:
289
- try:
290
- shutil.rmtree(entry)
291
- except Exception as exc:
292
- safe_log(
293
- logger,
294
- logging.WARNING,
295
- "Failed to remove old static cache dir %s",
296
- entry,
297
- exc=exc,
298
- )
299
-
300
-
301
- def materialize_static_assets(
302
- cache_root: Path,
303
- *,
304
- max_cache_entries: int,
305
- max_cache_age_days: Optional[int],
306
- logger: logging.Logger,
307
- ) -> tuple[Path, Optional[ExitStack]]:
308
- static_dir, static_context = resolve_static_dir()
309
- existing_cache = _select_latest_valid_cache(cache_root)
310
- missing_source = missing_static_assets(static_dir)
311
- if missing_source:
312
- if static_context is not None:
313
- static_context.close()
314
- if existing_cache is not None:
315
- _prune_cache_entries(
316
- cache_root,
317
- keep={existing_cache},
318
- max_cache_entries=max_cache_entries,
319
- max_cache_age_days=max_cache_age_days,
320
- logger=logger,
321
- )
322
- return existing_cache, None
323
- raise RuntimeError("Static UI assets missing; reinstall package")
324
- fingerprint = asset_version(static_dir)
325
- target_dir = cache_root / fingerprint
326
- if target_dir.exists() and not missing_static_assets(target_dir):
327
- if static_context is not None:
328
- static_context.close()
329
- _prune_cache_entries(
330
- cache_root,
331
- keep={target_dir},
332
- max_cache_entries=max_cache_entries,
333
- max_cache_age_days=max_cache_age_days,
334
- logger=logger,
335
- )
336
- return target_dir, None
337
- try:
338
- cache_root.mkdir(parents=True, exist_ok=True)
339
- except Exception as exc:
340
- safe_log(
341
- logger,
342
- logging.WARNING,
343
- "Failed to create static cache root %s",
344
- cache_root,
345
- exc=exc,
346
- )
347
- if static_context is not None:
348
- static_context.close()
349
- if existing_cache is not None:
350
- return existing_cache, None
351
- raise RuntimeError("Static UI assets missing; reinstall package") from exc
352
- lock_path = cache_root / f".lock-{fingerprint}"
353
- lock_acquired = _acquire_cache_lock(lock_path, logger)
354
- if not lock_acquired:
355
- deadline = time.monotonic() + 5.0
356
- while time.monotonic() < deadline:
357
- if target_dir.exists() and not missing_static_assets(target_dir):
358
- if static_context is not None:
359
- static_context.close()
360
- _prune_cache_entries(
361
- cache_root,
362
- keep={target_dir},
363
- max_cache_entries=max_cache_entries,
364
- max_cache_age_days=max_cache_age_days,
365
- logger=logger,
366
- )
367
- return target_dir, None
368
- time.sleep(0.2)
369
- temp_dir = cache_root / f".tmp-{fingerprint}-{uuid4().hex}"
370
- try:
371
- shutil.copytree(static_dir, temp_dir, symlinks=True)
372
- missing = missing_static_assets(temp_dir)
373
- if missing:
374
- safe_log(
375
- logger,
376
- logging.WARNING,
377
- "Static UI assets missing in cache copy %s: %s",
378
- temp_dir,
379
- ", ".join(missing),
380
- )
381
- raise RuntimeError("Static UI assets missing; reinstall package")
382
- if target_dir.exists():
383
- existing_missing = missing_static_assets(target_dir)
384
- if not existing_missing:
385
- _cleanup_temp_dir(temp_dir, logger)
386
- if static_context is not None:
387
- static_context.close()
388
- _prune_cache_entries(
389
- cache_root,
390
- keep={target_dir},
391
- max_cache_entries=max_cache_entries,
392
- max_cache_age_days=max_cache_age_days,
393
- logger=logger,
394
- )
395
- return target_dir, None
396
- try:
397
- shutil.rmtree(target_dir)
398
- except Exception as exc:
399
- safe_log(
400
- logger,
401
- logging.WARNING,
402
- "Failed to replace stale static cache dir %s",
403
- target_dir,
404
- exc=exc,
405
- )
406
- raise RuntimeError(
407
- "Static UI assets missing; reinstall package"
408
- ) from exc
409
- temp_dir.replace(target_dir)
410
- except Exception as exc:
411
- _cleanup_temp_dir(temp_dir, logger)
412
- if static_context is not None:
413
- static_context.close()
414
- if existing_cache is not None:
415
- return existing_cache, None
416
- raise RuntimeError("Static UI assets missing; reinstall package") from exc
417
- finally:
418
- if lock_acquired:
419
- _release_cache_lock(lock_path, logger)
420
- if static_context is not None:
421
- static_context.close()
422
- _prune_cache_entries(
423
- cache_root,
424
- keep={target_dir},
425
- max_cache_entries=max_cache_entries,
426
- max_cache_age_days=max_cache_age_days,
427
- logger=logger,
428
- )
429
- return target_dir, None
430
-
431
-
432
- def require_static_assets(static_dir: Path, logger: logging.Logger) -> None:
433
- missing = missing_static_assets(static_dir)
434
- if not missing:
435
- warn_on_stale_static_assets(static_dir, logger)
436
- return
437
- safe_log(
438
- logger,
439
- logging.ERROR,
440
- "Static UI assets missing in %s: %s",
441
- static_dir,
442
- ", ".join(missing),
443
- )
444
- raise RuntimeError("Static UI assets missing; reinstall package")
7
+ sys.modules[__name__] = _static_assets
@@ -1,86 +1,3 @@
1
- from __future__ import annotations
1
+ """Backward-compatible static refresh exports."""
2
2
 
3
- import logging
4
- from pathlib import Path
5
- from typing import Optional
6
-
7
- from ..core.logging_utils import safe_log
8
- from .static_assets import (
9
- asset_version,
10
- materialize_static_assets,
11
- missing_static_assets,
12
- require_static_assets,
13
- )
14
-
15
-
16
- def _update_static_files(static_files: object, static_dir: Path) -> None:
17
- try:
18
- static_files.directory = static_dir
19
- static_files.all_directories = static_files.get_directories( # type: ignore[attr-defined]
20
- static_dir,
21
- static_files.packages, # type: ignore[attr-defined]
22
- )
23
- static_files.config_checked = False
24
- except Exception:
25
- return
26
-
27
-
28
- def refresh_static_assets(app: object) -> bool:
29
- lock = getattr(getattr(app, "state", None), "static_assets_lock", None)
30
- if lock is None or not lock.acquire(blocking=False):
31
- return False
32
- try:
33
- state = getattr(app, "state", None)
34
- if state is None:
35
- return False
36
- current_dir = getattr(state, "static_dir", None)
37
- if isinstance(current_dir, Path) and not missing_static_assets(current_dir):
38
- return True
39
- config = getattr(state, "config", None)
40
- logger = getattr(state, "logger", None)
41
- static_candidates = []
42
- if config is not None:
43
- static_candidates.append(config.static_assets)
44
- hub_static = getattr(state, "hub_static_assets", None)
45
- if hub_static is not None and (
46
- not static_candidates
47
- or hub_static.cache_root != static_candidates[0].cache_root
48
- ):
49
- static_candidates.append(hub_static)
50
- for static_cfg in static_candidates:
51
- try:
52
- static_dir, static_context = materialize_static_assets(
53
- static_cfg.cache_root,
54
- max_cache_entries=static_cfg.max_cache_entries,
55
- max_cache_age_days=static_cfg.max_cache_age_days,
56
- logger=logger,
57
- )
58
- require_static_assets(static_dir, logger)
59
- except Exception as exc:
60
- if logger is not None:
61
- safe_log(
62
- logger,
63
- logging.WARNING,
64
- "Static assets refresh failed for cache root %s",
65
- static_cfg.cache_root,
66
- exc=exc,
67
- )
68
- continue
69
- old_context: Optional[object] = getattr(
70
- state, "static_assets_context", None
71
- )
72
- if old_context is not None:
73
- try:
74
- old_context.close()
75
- except Exception:
76
- pass
77
- state.static_dir = static_dir
78
- state.static_assets_context = static_context
79
- state.asset_version = asset_version(static_dir)
80
- static_files = getattr(state, "static_files", None)
81
- if static_files is not None:
82
- _update_static_files(static_files, static_dir)
83
- return True
84
- return False
85
- finally:
86
- lock.release()
3
+ from ..surfaces.web.static_refresh import * # noqa: F401,F403
@@ -1,78 +1,3 @@
1
- from __future__ import annotations
1
+ """Backward-compatible terminal session exports."""
2
2
 
3
- import time
4
- from datetime import datetime, timezone
5
- from pathlib import Path
6
- from typing import Optional
7
-
8
- from ..core.state import persist_session_registry
9
- from .pty_session import ActiveSession
10
-
11
-
12
- def parse_last_seen_at(value: Optional[str]) -> Optional[float]:
13
- if not value:
14
- return None
15
- try:
16
- parsed = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
17
- except ValueError:
18
- return None
19
- return parsed.replace(tzinfo=timezone.utc).timestamp()
20
-
21
-
22
- def session_last_touch(session: ActiveSession, record) -> float:
23
- last_seen = parse_last_seen_at(getattr(record, "last_seen_at", None))
24
- if last_seen is None:
25
- return session.pty.last_active
26
- return max(last_seen, session.pty.last_active)
27
-
28
-
29
- def parse_tui_idle_seconds(config) -> Optional[float]:
30
- notifications_cfg = (
31
- config.notifications if isinstance(config.notifications, dict) else {}
32
- )
33
- idle_seconds = notifications_cfg.get("tui_idle_seconds")
34
- if idle_seconds is None:
35
- return None
36
- try:
37
- idle_seconds = float(idle_seconds)
38
- except (TypeError, ValueError):
39
- return None
40
- if idle_seconds <= 0:
41
- return None
42
- return idle_seconds
43
-
44
-
45
- def prune_terminal_registry(
46
- state_path: Path,
47
- terminal_sessions: dict[str, ActiveSession],
48
- session_registry: dict,
49
- repo_to_session: dict[str, str],
50
- max_idle_seconds: Optional[int],
51
- ) -> bool:
52
- now = time.time()
53
- removed_any = False
54
- for session_id, session in list(terminal_sessions.items()):
55
- if not session.pty.isalive():
56
- session.close()
57
- terminal_sessions.pop(session_id, None)
58
- session_registry.pop(session_id, None)
59
- removed_any = True
60
- continue
61
- if max_idle_seconds is not None and max_idle_seconds > 0:
62
- last_touch = session_last_touch(session, session_registry.get(session_id))
63
- if now - last_touch > max_idle_seconds:
64
- session.close()
65
- terminal_sessions.pop(session_id, None)
66
- session_registry.pop(session_id, None)
67
- removed_any = True
68
- for session_id in list(session_registry.keys()):
69
- if session_id not in terminal_sessions:
70
- session_registry.pop(session_id, None)
71
- removed_any = True
72
- for repo_path, session_id in list(repo_to_session.items()):
73
- if session_id not in session_registry:
74
- repo_to_session.pop(repo_path, None)
75
- removed_any = True
76
- if removed_any:
77
- persist_session_registry(state_path, session_registry, repo_to_session)
78
- return removed_any
3
+ from ..surfaces.web.terminal_sessions import * # noqa: F401,F403