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
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Optional
9
+
10
+ from .config import load_repo_config
11
+ from .lifecycle_events import LifecycleEvent, LifecycleEventType
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class DispatchInterceptRule:
18
+ pattern: str
19
+ reply: str
20
+ description: str = ""
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class InterceptionResult:
25
+ action: str
26
+ reason: str
27
+ reply: Optional[str] = None
28
+ escalated: bool = False
29
+
30
+
31
+ def default_dispatch_rules() -> list[DispatchInterceptRule]:
32
+ return [
33
+ DispatchInterceptRule(
34
+ pattern=r"(?i)^(proceed|continue|resume|ok|yes|go ahead)([\s!.]*)?$",
35
+ reply="Continuing with the current task.",
36
+ description="Simple continuation confirmation",
37
+ ),
38
+ DispatchInterceptRule(
39
+ pattern=r"(?i)^skip.*(?:pre.?commit|commit.*hook|hook)([\s!.]*)?$",
40
+ reply="Skipping pre-commit hooks and proceeding.",
41
+ description="Skip pre-commit hooks",
42
+ ),
43
+ ]
44
+
45
+
46
+ def _match_dispatch(
47
+ body: str, rules: list[DispatchInterceptRule]
48
+ ) -> Optional[DispatchInterceptRule]:
49
+ body_stripped = body.strip()
50
+ for rule in rules:
51
+ try:
52
+ if re.match(rule.pattern, body_stripped):
53
+ return rule
54
+ except re.error:
55
+ logger.warning("Invalid dispatch pattern: %s", rule.pattern)
56
+ continue
57
+ return None
58
+
59
+
60
+ async def _can_auto_resume_run(repo_root: Path, run_id: str) -> bool:
61
+ from .flows.models import FlowRunStatus
62
+ from .flows.store import FlowStore
63
+
64
+ db_path = repo_root / ".codex-autorunner" / "flows.db"
65
+ if not db_path.exists():
66
+ return False
67
+ try:
68
+ config = load_repo_config(repo_root)
69
+ with FlowStore(db_path, durable=config.durable_writes) as store:
70
+ run = store.get_flow_run(run_id)
71
+ if not run:
72
+ return False
73
+ return run.status == FlowRunStatus.PAUSED
74
+ except Exception as exc:
75
+ logger.warning("Failed to check run status for %s: %s", run_id, exc)
76
+ return False
77
+
78
+
79
+ async def _write_reply(repo_root: Path, run_id: str, reply_body: str) -> bool:
80
+ from ..tickets.replies import ensure_reply_dirs, resolve_reply_paths
81
+
82
+ try:
83
+ config = load_repo_config(repo_root)
84
+ runs_dir = Path(config.raw.get("runs_dir") or ".codex-autorunner/runs")
85
+ workspace_root = repo_root
86
+
87
+ reply_paths = resolve_reply_paths(
88
+ workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_id
89
+ )
90
+ ensure_reply_dirs(reply_paths)
91
+
92
+ reply_history_dir = reply_paths.reply_history_dir
93
+ if not reply_history_dir:
94
+ logger.warning("Reply history dir not found for run %s", run_id)
95
+ return False
96
+
97
+ next_seq = 1
98
+ for child in reply_history_dir.iterdir():
99
+ if child.is_dir() and child.name.isdigit():
100
+ next_seq = max(next_seq, int(child.name) + 1)
101
+
102
+ seq_dir = reply_history_dir / f"{next_seq:04d}"
103
+ seq_dir.mkdir(parents=True, exist_ok=True)
104
+
105
+ reply_content = f"""---
106
+ title: Auto-resolved by PMA
107
+ ---
108
+
109
+ {reply_body}
110
+ """
111
+ reply_path = seq_dir / "USER_REPLY.md"
112
+ reply_path.write_text(reply_content, encoding="utf-8")
113
+
114
+ return True
115
+
116
+ except Exception:
117
+ logger.exception("Failed to write reply for run %s", run_id)
118
+ return False
119
+
120
+
121
+ class PmaDispatchInterceptor:
122
+ def __init__(
123
+ self,
124
+ hub_root: Path,
125
+ supervisor: Any,
126
+ rules: Optional[list[DispatchInterceptRule]] = None,
127
+ on_intercept: Optional[Callable[[str, InterceptionResult], None]] = None,
128
+ ):
129
+ self._hub_root = hub_root
130
+ self._supervisor = supervisor
131
+ self._rules = rules or default_dispatch_rules()
132
+ self._on_intercept = on_intercept
133
+ self._processing = set[str]()
134
+ self._logger = logging.getLogger(f"{__name__}.PmaDispatchInterceptor")
135
+
136
+ async def process_dispatch_event(
137
+ self, event: LifecycleEvent, repo_root: Path
138
+ ) -> Optional[InterceptionResult]:
139
+ if event.event_type != LifecycleEventType.DISPATCH_CREATED:
140
+ return None
141
+
142
+ event_id = event.event_id
143
+ if event_id in self._processing:
144
+ return None
145
+
146
+ run_id = event.run_id
147
+ repo_id = event.data.get("repo_id") or event.repo_id
148
+
149
+ try:
150
+ self._processing.add(event_id)
151
+
152
+ self._logger.info(
153
+ "Processing dispatch event %s for run %s in repo %s",
154
+ event_id,
155
+ run_id,
156
+ repo_id,
157
+ )
158
+
159
+ from .pma_context import _latest_dispatch
160
+
161
+ latest = _latest_dispatch(repo_root, run_id, {}, max_text_chars=10000)
162
+ if not latest or not latest.get("dispatch"):
163
+ return InterceptionResult(
164
+ action="ignore",
165
+ reason="No dispatch found or dispatch is invalid",
166
+ )
167
+
168
+ dispatch_info = latest["dispatch"]
169
+ mode = dispatch_info.get("mode")
170
+ body = dispatch_info.get("body", "")
171
+
172
+ if mode != "pause":
173
+ return InterceptionResult(
174
+ action="ignore",
175
+ reason=f"Dispatch mode is '{mode}', only 'pause' is intercepted",
176
+ )
177
+
178
+ matched_rule = _match_dispatch(body, self._rules)
179
+ if matched_rule:
180
+ self._logger.info(
181
+ "Dispatch matched auto-resolve rule: %s",
182
+ matched_rule.description,
183
+ )
184
+
185
+ can_resume = await _can_auto_resume_run(repo_root, run_id)
186
+ if not can_resume:
187
+ return InterceptionResult(
188
+ action="escalate",
189
+ reason="Run cannot be auto-resumed",
190
+ escalated=True,
191
+ )
192
+
193
+ success = await _write_reply(repo_root, run_id, matched_rule.reply)
194
+ if success:
195
+ result = InterceptionResult(
196
+ action="auto_resolved",
197
+ reason=matched_rule.description,
198
+ reply=matched_rule.reply,
199
+ )
200
+ if self._on_intercept:
201
+ self._on_intercept(event_id, result)
202
+ return result
203
+ else:
204
+ return InterceptionResult(
205
+ action="escalate",
206
+ reason="Failed to write reply",
207
+ escalated=True,
208
+ )
209
+
210
+ return InterceptionResult(
211
+ action="escalate",
212
+ reason="Dispatch does not match any auto-resolve rule",
213
+ escalated=True,
214
+ )
215
+
216
+ except Exception as exc:
217
+ self._logger.exception(
218
+ "Error processing dispatch event %s for run %s", event_id, run_id
219
+ )
220
+ return InterceptionResult(
221
+ action="error",
222
+ reason=f"Processing error: {exc}",
223
+ )
224
+ finally:
225
+ self._processing.discard(event_id)
226
+
227
+
228
+ async def run_dispatch_interceptor(
229
+ hub_root: Path,
230
+ supervisor: Any,
231
+ interval_seconds: float = 5.0,
232
+ on_intercept: Optional[Callable[[str, InterceptionResult], None]] = None,
233
+ ) -> asyncio.Task[None]:
234
+ interceptor = PmaDispatchInterceptor(
235
+ hub_root=hub_root,
236
+ supervisor=supervisor,
237
+ on_intercept=on_intercept,
238
+ )
239
+
240
+ async def loop() -> None:
241
+ while True:
242
+ try:
243
+ lifecycle_store = supervisor.lifecycle_store
244
+ unprocessed = lifecycle_store.get_unprocessed(limit=50)
245
+
246
+ for event in unprocessed:
247
+ if event.event_type != LifecycleEventType.DISPATCH_CREATED:
248
+ continue
249
+
250
+ repo_id = event.repo_id
251
+ snapshots = supervisor.list_repos()
252
+ repo_snapshot = None
253
+ for snap in snapshots:
254
+ if snap.id == repo_id:
255
+ repo_snapshot = snap
256
+ break
257
+
258
+ if not repo_snapshot or not repo_snapshot.exists_on_disk:
259
+ lifecycle_store.mark_processed(event.event_id)
260
+ continue
261
+
262
+ result = await interceptor.process_dispatch_event(
263
+ event, repo_snapshot.path
264
+ )
265
+
266
+ if result and result.action != "ignore":
267
+ lifecycle_store.mark_processed(event.event_id)
268
+
269
+ await asyncio.sleep(interval_seconds)
270
+ except asyncio.CancelledError:
271
+ break
272
+ except Exception:
273
+ logger.exception("Dispatch interceptor loop error")
274
+
275
+ return asyncio.create_task(loop())
276
+
277
+
278
+ __all__ = [
279
+ "DispatchInterceptRule",
280
+ "InterceptionResult",
281
+ "default_dispatch_rules",
282
+ "PmaDispatchInterceptor",
283
+ "run_dispatch_interceptor",
284
+ ]