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
@@ -29,6 +29,13 @@ FILES_HINT_TEMPLATE = (
29
29
  "Check delivery with /files outbox.\n"
30
30
  "Max file size: {max_bytes} bytes."
31
31
  )
32
+ PMA_FILES_HINT_TEMPLATE = (
33
+ "PMA inbox: {inbox}\n"
34
+ "PMA outbox (pending): {outbox}\n"
35
+ "Place files in outbox pending to send after this turn finishes.\n"
36
+ "Check delivery with /files outbox.\n"
37
+ "Max file size: {max_bytes} bytes."
38
+ )
32
39
 
33
40
 
34
41
  _GENERIC_TELEGRAM_ERRORS = {
@@ -97,6 +104,63 @@ class MediaBatchResult:
97
104
 
98
105
 
99
106
  class FilesCommands(SharedHelpers):
107
+ def _files_usage(self, *, pma: bool) -> str:
108
+ header = "Usage:"
109
+ lines = [
110
+ header,
111
+ "/files",
112
+ "/files inbox",
113
+ "/files outbox",
114
+ "/files all",
115
+ "/files send <filename>",
116
+ "/files clear inbox|outbox|all",
117
+ ]
118
+ if pma:
119
+ lines.append("Note: PMA files live in .codex-autorunner/pma/inbox|outbox.")
120
+ return "\n".join(lines)
121
+
122
+ async def _send_pma_outbox_file(
123
+ self,
124
+ path: Path,
125
+ *,
126
+ chat_id: int,
127
+ thread_id: Optional[int],
128
+ reply_to: Optional[int],
129
+ ) -> bool:
130
+ try:
131
+ data = path.read_bytes()
132
+ except Exception as exc:
133
+ log_event(
134
+ self._logger,
135
+ logging.WARNING,
136
+ "telegram.files.pma_outbox.read_failed",
137
+ chat_id=chat_id,
138
+ thread_id=thread_id,
139
+ path=str(path),
140
+ exc=exc,
141
+ )
142
+ return False
143
+ try:
144
+ await self._bot.send_document(
145
+ chat_id,
146
+ data,
147
+ filename=path.name,
148
+ message_thread_id=thread_id,
149
+ reply_to_message_id=reply_to,
150
+ )
151
+ except Exception as exc:
152
+ log_event(
153
+ self._logger,
154
+ logging.WARNING,
155
+ "telegram.files.pma_outbox.send_failed",
156
+ chat_id=chat_id,
157
+ thread_id=thread_id,
158
+ path=str(path),
159
+ exc=exc,
160
+ )
161
+ return False
162
+ return True
163
+
100
164
  def _format_telegram_download_error(self, exc: Exception) -> Optional[str]:
101
165
  for current in _iter_exception_chain(exc):
102
166
  if isinstance(current, Exception):
@@ -245,7 +309,11 @@ class FilesCommands(SharedHelpers):
245
309
  return
246
310
  try:
247
311
  image_path = self._save_image_file(
248
- record.workspace_path, data, file_path, candidate
312
+ record.workspace_path,
313
+ data,
314
+ file_path,
315
+ candidate,
316
+ pma_enabled=bool(getattr(record, "pma_enabled", False)),
249
317
  )
250
318
  except asyncio.CancelledError:
251
319
  raise
@@ -442,6 +510,7 @@ class FilesCommands(SharedHelpers):
442
510
  data,
443
511
  candidate=candidate,
444
512
  file_path=file_path,
513
+ pma_enabled=bool(getattr(record, "pma_enabled", False)),
445
514
  )
446
515
  except asyncio.CancelledError:
447
516
  raise
@@ -470,6 +539,7 @@ class FilesCommands(SharedHelpers):
470
539
  file_size=file_size or len(data),
471
540
  topic_key=key,
472
541
  workspace_path=record.workspace_path,
542
+ pma_enabled=bool(getattr(record, "pma_enabled", False)),
473
543
  )
474
544
  log_event(
475
545
  self._logger,
@@ -543,6 +613,15 @@ class FilesCommands(SharedHelpers):
543
613
  first_msg.chat_id, first_msg.thread_id
544
614
  )
545
615
  record = await self._router.get_topic(topic_key)
616
+ record, pma_error = message_handlers._record_with_media_workspace(self, record)
617
+ if pma_error:
618
+ await self._send_message(
619
+ first_msg.chat_id,
620
+ pma_error,
621
+ thread_id=first_msg.thread_id,
622
+ reply_to=first_msg.message_id,
623
+ )
624
+ return None
546
625
  if record is None or not record.workspace_path:
547
626
  await self._send_message(
548
627
  first_msg.chat_id,
@@ -669,7 +748,11 @@ class FilesCommands(SharedHelpers):
669
748
  return True
670
749
  try:
671
750
  image_path = self._save_image_file(
672
- context.record.workspace_path, data, file_path, candidate
751
+ context.record.workspace_path,
752
+ data,
753
+ file_path,
754
+ candidate,
755
+ pma_enabled=bool(getattr(context.record, "pma_enabled", False)),
673
756
  )
674
757
  saved_image_paths.append(image_path)
675
758
  except asyncio.CancelledError:
@@ -759,6 +842,7 @@ class FilesCommands(SharedHelpers):
759
842
  data,
760
843
  candidate=candidate,
761
844
  file_path=file_path,
845
+ pma_enabled=bool(getattr(context.record, "pma_enabled", False)),
762
846
  )
763
847
  original_name = (
764
848
  candidate.file_name
@@ -815,23 +899,10 @@ class FilesCommands(SharedHelpers):
815
899
  prompt_parts.append(
816
900
  f"\nFailed to process {result.stats.failed_count} item(s)."
817
901
  )
818
- inbox_dir = self._files_inbox_dir(
819
- context.record.workspace_path, context.topic_key
820
- )
821
- outbox_dir = self._files_outbox_pending_dir(
822
- context.record.workspace_path, context.topic_key
823
- )
824
- topic_dir = self._files_topic_dir(
825
- context.record.workspace_path, context.topic_key
826
- )
827
- hint = wrap_injected_context(
828
- FILES_HINT_TEMPLATE.format(
829
- inbox=str(inbox_dir),
830
- outbox=str(outbox_dir),
831
- topic_key=context.topic_key,
832
- topic_dir=str(topic_dir),
833
- max_bytes=self._config.media.max_file_bytes,
834
- )
902
+ hint = self._build_files_hint(
903
+ workspace_path=context.record.workspace_path,
904
+ topic_key=context.topic_key,
905
+ pma_enabled=bool(getattr(context.record, "pma_enabled", False)),
835
906
  )
836
907
  prompt_parts.append(hint)
837
908
  combined_prompt = "\n\n".join(prompt_parts)
@@ -905,7 +976,11 @@ class FilesCommands(SharedHelpers):
905
976
  data = await self._bot.download_file(file_path)
906
977
  return data, file_path, file_size
907
978
 
908
- def _image_storage_dir(self, workspace_path: str) -> Path:
979
+ def _image_storage_dir(self, workspace_path: str, *, pma_enabled: bool) -> Path:
980
+ if pma_enabled:
981
+ pma_inbox = self._pma_inbox_dir()
982
+ if pma_inbox is not None:
983
+ return pma_inbox
909
984
  return (
910
985
  Path(workspace_path) / ".codex-autorunner" / "uploads" / "telegram-images"
911
986
  )
@@ -935,8 +1010,10 @@ class FilesCommands(SharedHelpers):
935
1010
  data: bytes,
936
1011
  file_path: Optional[str],
937
1012
  candidate: TelegramMediaCandidate,
1013
+ *,
1014
+ pma_enabled: bool,
938
1015
  ) -> Path:
939
- images_dir = self._image_storage_dir(workspace_path)
1016
+ images_dir = self._image_storage_dir(workspace_path, pma_enabled=pma_enabled)
940
1017
  images_dir.mkdir(parents=True, exist_ok=True)
941
1018
  ext = self._choose_image_extension(
942
1019
  file_path=file_path,
@@ -952,6 +1029,24 @@ class FilesCommands(SharedHelpers):
952
1029
  def _files_root_dir(self, workspace_path: str) -> Path:
953
1030
  return Path(workspace_path) / ".codex-autorunner" / "uploads" / "telegram-files"
954
1031
 
1032
+ def _pma_root_dir(self) -> Optional[Path]:
1033
+ hub_root = getattr(self, "_hub_root", None)
1034
+ if hub_root is None:
1035
+ return None
1036
+ return Path(hub_root) / ".codex-autorunner" / "pma"
1037
+
1038
+ def _pma_inbox_dir(self) -> Optional[Path]:
1039
+ pma_root = self._pma_root_dir()
1040
+ if pma_root is None:
1041
+ return None
1042
+ return pma_root / "inbox"
1043
+
1044
+ def _pma_outbox_dir(self) -> Optional[Path]:
1045
+ pma_root = self._pma_root_dir()
1046
+ if pma_root is None:
1047
+ return None
1048
+ return pma_root / "outbox"
1049
+
955
1050
  def _sanitize_topic_dir_name(self, key: str) -> str:
956
1051
  cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", key).strip("._-")
957
1052
  if not cleaned:
@@ -1013,8 +1108,13 @@ class FilesCommands(SharedHelpers):
1013
1108
  *,
1014
1109
  candidate: TelegramMediaCandidate,
1015
1110
  file_path: Optional[str],
1111
+ pma_enabled: bool,
1016
1112
  ) -> Path:
1017
1113
  inbox_dir = self._files_inbox_dir(workspace_path, topic_key)
1114
+ if pma_enabled:
1115
+ pma_inbox = self._pma_inbox_dir()
1116
+ if pma_inbox is not None:
1117
+ inbox_dir = pma_inbox
1018
1118
  inbox_dir.mkdir(parents=True, exist_ok=True)
1019
1119
  stem = self._sanitize_filename_component(
1020
1120
  self._choose_file_stem(candidate.file_name, file_path),
@@ -1041,6 +1141,7 @@ class FilesCommands(SharedHelpers):
1041
1141
  file_size: int,
1042
1142
  topic_key: str,
1043
1143
  workspace_path: str,
1144
+ pma_enabled: bool,
1044
1145
  ) -> str:
1045
1146
  header = caption_text.strip() or "File received."
1046
1147
  original_name = (
@@ -1048,17 +1149,10 @@ class FilesCommands(SharedHelpers):
1048
1149
  or (Path(source_path).name if source_path else None)
1049
1150
  or "unknown"
1050
1151
  )
1051
- inbox_dir = self._files_inbox_dir(workspace_path, topic_key)
1052
- outbox_dir = self._files_outbox_pending_dir(workspace_path, topic_key)
1053
- topic_dir = self._files_topic_dir(workspace_path, topic_key)
1054
- hint = wrap_injected_context(
1055
- FILES_HINT_TEMPLATE.format(
1056
- inbox=str(inbox_dir),
1057
- outbox=str(outbox_dir),
1058
- topic_key=topic_key,
1059
- topic_dir=str(topic_dir),
1060
- max_bytes=self._config.media.max_file_bytes,
1061
- )
1152
+ hint = self._build_files_hint(
1153
+ workspace_path=workspace_path,
1154
+ topic_key=topic_key,
1155
+ pma_enabled=pma_enabled,
1062
1156
  )
1063
1157
  parts = [
1064
1158
  header,
@@ -1074,6 +1168,37 @@ class FilesCommands(SharedHelpers):
1074
1168
  parts.append(hint)
1075
1169
  return "\n".join(parts)
1076
1170
 
1171
+ def _build_files_hint(
1172
+ self,
1173
+ *,
1174
+ workspace_path: str,
1175
+ topic_key: str,
1176
+ pma_enabled: bool,
1177
+ ) -> str:
1178
+ if pma_enabled:
1179
+ pma_inbox = self._pma_inbox_dir()
1180
+ pma_outbox = self._pma_outbox_dir()
1181
+ if pma_inbox is not None and pma_outbox is not None:
1182
+ return wrap_injected_context(
1183
+ PMA_FILES_HINT_TEMPLATE.format(
1184
+ inbox=str(pma_inbox),
1185
+ outbox=str(pma_outbox),
1186
+ max_bytes=self._config.media.max_file_bytes,
1187
+ )
1188
+ )
1189
+ inbox_dir = self._files_inbox_dir(workspace_path, topic_key)
1190
+ outbox_dir = self._files_outbox_pending_dir(workspace_path, topic_key)
1191
+ topic_dir = self._files_topic_dir(workspace_path, topic_key)
1192
+ return wrap_injected_context(
1193
+ FILES_HINT_TEMPLATE.format(
1194
+ inbox=str(inbox_dir),
1195
+ outbox=str(outbox_dir),
1196
+ topic_key=topic_key,
1197
+ topic_dir=str(topic_dir),
1198
+ max_bytes=self._config.media.max_file_bytes,
1199
+ )
1200
+ )
1201
+
1077
1202
  def _format_bytes(self, size: int) -> str:
1078
1203
  if size < 1024:
1079
1204
  return f"{size} B"
@@ -1192,7 +1317,12 @@ class FilesCommands(SharedHelpers):
1192
1317
  key = topic_key
1193
1318
  else:
1194
1319
  key = await self._resolve_topic_key(chat_id, thread_id)
1320
+ pma_enabled = bool(getattr(record, "pma_enabled", False))
1195
1321
  pending_dir = self._files_outbox_pending_dir(record.workspace_path, key)
1322
+ if pma_enabled:
1323
+ pma_outbox = self._pma_outbox_dir()
1324
+ if pma_outbox is not None:
1325
+ pending_dir = pma_outbox
1196
1326
  if not pending_dir.exists():
1197
1327
  return
1198
1328
  files = self._list_files(pending_dir)
@@ -1215,13 +1345,34 @@ class FilesCommands(SharedHelpers):
1215
1345
  reply_to=reply_to,
1216
1346
  )
1217
1347
  continue
1218
- await self._send_outbox_file(
1219
- path,
1220
- sent_dir=sent_dir,
1221
- chat_id=chat_id,
1222
- thread_id=thread_id,
1223
- reply_to=reply_to,
1224
- )
1348
+ if pma_enabled:
1349
+ success = await self._send_pma_outbox_file(
1350
+ path,
1351
+ chat_id=chat_id,
1352
+ thread_id=thread_id,
1353
+ reply_to=reply_to,
1354
+ )
1355
+ if success:
1356
+ try:
1357
+ path.unlink()
1358
+ except OSError as exc:
1359
+ log_event(
1360
+ self._logger,
1361
+ logging.WARNING,
1362
+ "telegram.files.pma_outbox.delete_failed",
1363
+ chat_id=chat_id,
1364
+ thread_id=thread_id,
1365
+ path=str(path),
1366
+ exc=exc,
1367
+ )
1368
+ else:
1369
+ await self._send_outbox_file(
1370
+ path,
1371
+ sent_dir=sent_dir,
1372
+ chat_id=chat_id,
1373
+ thread_id=thread_id,
1374
+ reply_to=reply_to,
1375
+ )
1225
1376
 
1226
1377
  def _format_file_listing(self, title: str, files: list[Path]) -> str:
1227
1378
  if not files:
@@ -1266,26 +1417,72 @@ class FilesCommands(SharedHelpers):
1266
1417
  reply_to=message.message_id,
1267
1418
  )
1268
1419
  return
1269
- record = await self._require_bound_record(message)
1270
- if not record:
1271
- return
1272
1420
  key = await self._resolve_topic_key(message.chat_id, message.thread_id)
1273
- inbox_dir = self._files_inbox_dir(record.workspace_path, key)
1274
- pending_dir = self._files_outbox_pending_dir(record.workspace_path, key)
1275
- sent_dir = self._files_outbox_sent_dir(record.workspace_path, key)
1421
+ record = await self._router.ensure_topic(message.chat_id, message.thread_id)
1422
+ record, pma_error = message_handlers._record_with_media_workspace(self, record)
1423
+ if pma_error:
1424
+ await self._send_message(
1425
+ message.chat_id,
1426
+ pma_error,
1427
+ thread_id=message.thread_id,
1428
+ reply_to=message.message_id,
1429
+ )
1430
+ return
1431
+ if record is None or not record.workspace_path:
1432
+ await self._send_message(
1433
+ message.chat_id,
1434
+ self._with_conversation_id(
1435
+ "Topic not bound. Use /bind <repo_id> or /bind <path>.",
1436
+ chat_id=message.chat_id,
1437
+ thread_id=message.thread_id,
1438
+ ),
1439
+ thread_id=message.thread_id,
1440
+ reply_to=message.message_id,
1441
+ )
1442
+ return
1443
+ pma_enabled = bool(getattr(record, "pma_enabled", False))
1444
+ if pma_enabled:
1445
+ hub_root = getattr(self, "_hub_root", None)
1446
+ if hub_root is None:
1447
+ await self._send_message(
1448
+ message.chat_id,
1449
+ "PMA unavailable; hub root not configured.",
1450
+ thread_id=message.thread_id,
1451
+ reply_to=message.message_id,
1452
+ )
1453
+ return
1454
+ pma_root = Path(hub_root) / ".codex-autorunner" / "pma"
1455
+ inbox_dir = pma_root / "inbox"
1456
+ outbox_dir = pma_root / "outbox"
1457
+ pending_dir = outbox_dir
1458
+ sent_dir = outbox_dir
1459
+ else:
1460
+ inbox_dir = self._files_inbox_dir(record.workspace_path, key)
1461
+ pending_dir = self._files_outbox_pending_dir(record.workspace_path, key)
1462
+ sent_dir = self._files_outbox_sent_dir(record.workspace_path, key)
1276
1463
  argv = self._parse_command_args(args)
1277
1464
  if not argv:
1278
1465
  inbox_items = self._list_files(inbox_dir)
1279
1466
  pending_items = self._list_files(pending_dir)
1280
- sent_items = self._list_files(sent_dir)
1281
- text = "\n".join(
1282
- [
1283
- f"Inbox: {len(inbox_items)} item(s)",
1284
- f"Outbox pending: {len(pending_items)} item(s)",
1285
- f"Outbox sent: {len(sent_items)} item(s)",
1286
- "Usage: /files inbox|outbox|clear inbox|outbox|all|send <filename>",
1287
- ]
1288
- )
1467
+ sent_items = [] if pma_enabled else self._list_files(sent_dir)
1468
+ usage = self._files_usage(pma=pma_enabled)
1469
+ if pma_enabled:
1470
+ text = "\n".join(
1471
+ [
1472
+ f"Inbox: {len(inbox_items)} file(s)",
1473
+ f"Outbox: {len(pending_items)} file(s)",
1474
+ usage,
1475
+ ]
1476
+ )
1477
+ else:
1478
+ text = "\n".join(
1479
+ [
1480
+ f"Inbox: {len(inbox_items)} item(s)",
1481
+ f"Outbox pending: {len(pending_items)} item(s)",
1482
+ f"Outbox sent: {len(sent_items)} item(s)",
1483
+ usage,
1484
+ ]
1485
+ )
1289
1486
  await self._send_message(
1290
1487
  message.chat_id,
1291
1488
  text,
@@ -1306,14 +1503,46 @@ class FilesCommands(SharedHelpers):
1306
1503
  return
1307
1504
  if subcommand == "outbox":
1308
1505
  pending_items = self._list_files(pending_dir)
1309
- sent_items = self._list_files(sent_dir)
1310
- text = "\n".join(
1311
- [
1312
- self._format_file_listing("Outbox pending", pending_items),
1313
- "",
1314
- self._format_file_listing("Outbox sent", sent_items),
1315
- ]
1506
+ if pma_enabled:
1507
+ text = self._format_file_listing("Outbox", pending_items)
1508
+ else:
1509
+ sent_items = self._list_files(sent_dir)
1510
+ text = "\n".join(
1511
+ [
1512
+ self._format_file_listing("Outbox pending", pending_items),
1513
+ "",
1514
+ self._format_file_listing("Outbox sent", sent_items),
1515
+ ]
1516
+ )
1517
+ await self._send_message(
1518
+ message.chat_id,
1519
+ text,
1520
+ thread_id=message.thread_id,
1521
+ reply_to=message.message_id,
1316
1522
  )
1523
+ return
1524
+ if subcommand == "all":
1525
+ inbox_items = self._list_files(inbox_dir)
1526
+ pending_items = self._list_files(pending_dir)
1527
+ if pma_enabled:
1528
+ text = "\n".join(
1529
+ [
1530
+ self._format_file_listing("Inbox", inbox_items),
1531
+ "",
1532
+ self._format_file_listing("Outbox", pending_items),
1533
+ ]
1534
+ )
1535
+ else:
1536
+ sent_items = self._list_files(sent_dir)
1537
+ text = "\n".join(
1538
+ [
1539
+ self._format_file_listing("Inbox", inbox_items),
1540
+ "",
1541
+ self._format_file_listing("Outbox pending", pending_items),
1542
+ "",
1543
+ self._format_file_listing("Outbox sent", sent_items),
1544
+ ]
1545
+ )
1317
1546
  await self._send_message(
1318
1547
  message.chat_id,
1319
1548
  text,
@@ -1325,7 +1554,7 @@ class FilesCommands(SharedHelpers):
1325
1554
  if len(argv) < 2:
1326
1555
  await self._send_message(
1327
1556
  message.chat_id,
1328
- "Usage: /files clear inbox|outbox|all",
1557
+ self._files_usage(pma=pma_enabled),
1329
1558
  thread_id=message.thread_id,
1330
1559
  reply_to=message.message_id,
1331
1560
  )
@@ -1336,15 +1565,17 @@ class FilesCommands(SharedHelpers):
1336
1565
  deleted = self._delete_files_in_dir(inbox_dir)
1337
1566
  elif target == "outbox":
1338
1567
  deleted = self._delete_files_in_dir(pending_dir)
1339
- deleted += self._delete_files_in_dir(sent_dir)
1568
+ if not pma_enabled:
1569
+ deleted += self._delete_files_in_dir(sent_dir)
1340
1570
  elif target == "all":
1341
1571
  deleted = self._delete_files_in_dir(inbox_dir)
1342
1572
  deleted += self._delete_files_in_dir(pending_dir)
1343
- deleted += self._delete_files_in_dir(sent_dir)
1573
+ if not pma_enabled:
1574
+ deleted += self._delete_files_in_dir(sent_dir)
1344
1575
  else:
1345
1576
  await self._send_message(
1346
1577
  message.chat_id,
1347
- "Usage: /files clear inbox|outbox|all",
1578
+ self._files_usage(pma=pma_enabled),
1348
1579
  thread_id=message.thread_id,
1349
1580
  reply_to=message.message_id,
1350
1581
  )
@@ -1360,7 +1591,7 @@ class FilesCommands(SharedHelpers):
1360
1591
  if len(argv) < 2:
1361
1592
  await self._send_message(
1362
1593
  message.chat_id,
1363
- "Usage: /files send <filename>",
1594
+ self._files_usage(pma=pma_enabled),
1364
1595
  thread_id=message.thread_id,
1365
1596
  reply_to=message.message_id,
1366
1597
  )
@@ -1370,7 +1601,7 @@ class FilesCommands(SharedHelpers):
1370
1601
  if not _path_within(pending_dir, candidate) or not candidate.is_file():
1371
1602
  await self._send_message(
1372
1603
  message.chat_id,
1373
- f"Outbox pending file not found: {name}",
1604
+ f"Outbox file not found: {name}",
1374
1605
  thread_id=message.thread_id,
1375
1606
  reply_to=message.message_id,
1376
1607
  )
@@ -1385,13 +1616,21 @@ class FilesCommands(SharedHelpers):
1385
1616
  reply_to=message.message_id,
1386
1617
  )
1387
1618
  return
1388
- success = await self._send_outbox_file(
1389
- candidate,
1390
- sent_dir=sent_dir,
1391
- chat_id=message.chat_id,
1392
- thread_id=message.thread_id,
1393
- reply_to=message.message_id,
1394
- )
1619
+ if pma_enabled:
1620
+ success = await self._send_pma_outbox_file(
1621
+ candidate,
1622
+ chat_id=message.chat_id,
1623
+ thread_id=message.thread_id,
1624
+ reply_to=message.message_id,
1625
+ )
1626
+ else:
1627
+ success = await self._send_outbox_file(
1628
+ candidate,
1629
+ sent_dir=sent_dir,
1630
+ chat_id=message.chat_id,
1631
+ thread_id=message.thread_id,
1632
+ reply_to=message.message_id,
1633
+ )
1395
1634
  result = "Sent." if success else "Failed to send."
1396
1635
  await self._send_message(
1397
1636
  message.chat_id,
@@ -1402,7 +1641,7 @@ class FilesCommands(SharedHelpers):
1402
1641
  return
1403
1642
  await self._send_message(
1404
1643
  message.chat_id,
1405
- "Usage: /files inbox|outbox|clear inbox|outbox|all|send <filename>",
1644
+ self._files_usage(pma=pma_enabled),
1406
1645
  thread_id=message.thread_id,
1407
1646
  reply_to=message.message_id,
1408
1647
  )