codex-autorunner 1.1.0__py3-none-any.whl → 1.2.1__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.
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +17 -7
- codex_autorunner/bootstrap.py +219 -1
- codex_autorunner/core/__init__.py +17 -1
- codex_autorunner/core/about_car.py +124 -11
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +238 -3
- codex_autorunner/core/context_awareness.py +39 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +71 -1
- codex_autorunner/core/flows/reconciler.py +4 -1
- codex_autorunner/core/flows/runtime.py +22 -0
- codex_autorunner/core/flows/store.py +61 -9
- codex_autorunner/core/flows/transition.py +23 -16
- codex_autorunner/core/flows/ux_helpers.py +18 -3
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/hub.py +198 -41
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +683 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/agent_backend.py +2 -5
- codex_autorunner/core/ports/run_event.py +1 -4
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +5 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/ticket_linter_cli.py +17 -0
- codex_autorunner/core/ticket_manager_cli.py +154 -92
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/utils.py +34 -6
- codex_autorunner/flows/review/service.py +23 -25
- codex_autorunner/flows/ticket_flow/definition.py +43 -1
- codex_autorunner/integrations/agents/__init__.py +2 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
- codex_autorunner/integrations/agents/codex_backend.py +19 -8
- codex_autorunner/integrations/agents/runner.py +3 -8
- codex_autorunner/integrations/agents/wiring.py +8 -0
- codex_autorunner/integrations/telegram/adapter.py +1 -1
- codex_autorunner/integrations/telegram/config.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
- codex_autorunner/integrations/telegram/handlers/messages.py +34 -3
- codex_autorunner/integrations/telegram/helpers.py +1 -3
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +30 -0
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
- codex_autorunner/integrations/telegram/transport.py +10 -3
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/server.py +2 -2
- codex_autorunner/static/agentControls.js +21 -5
- codex_autorunner/static/app.js +115 -11
- codex_autorunner/static/archive.js +274 -81
- codex_autorunner/static/archiveApi.js +21 -0
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/constants.js +1 -1
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +46 -81
- codex_autorunner/static/index.html +303 -24
- codex_autorunner/static/messages.js +82 -4
- codex_autorunner/static/notifications.js +288 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/settings.js +3 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9141 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminalManager.js +22 -3
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +41 -13
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +69 -19
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +28 -0
- codex_autorunner/static/workspace.js +258 -44
- codex_autorunner/static/workspaceFileBrowser.js +6 -4
- codex_autorunner/surfaces/cli/cli.py +1465 -155
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/web/app.py +253 -49
- codex_autorunner/surfaces/web/routes/__init__.py +4 -0
- codex_autorunner/surfaces/web/routes/analytics.py +29 -22
- codex_autorunner/surfaces/web/routes/archive.py +197 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +297 -36
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +219 -29
- codex_autorunner/surfaces/web/routes/messages.py +70 -39
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +1 -1
- codex_autorunner/surfaces/web/routes/shared.py +0 -3
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/runner_manager.py +2 -2
- codex_autorunner/surfaces/web/schemas.py +81 -18
- codex_autorunner/tickets/agent_pool.py +27 -0
- codex_autorunner/tickets/files.py +33 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +3 -0
- codex_autorunner/tickets/outbox.py +41 -5
- codex_autorunner/tickets/runner.py +350 -69
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +132 -101
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -3302
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -870,7 +870,7 @@ class TelegramCommandHandlers(
|
|
|
870
870
|
key = await self._resolve_topic_key(message.chat_id, message.thread_id)
|
|
871
871
|
self._model_options.pop(key, None)
|
|
872
872
|
self._model_pending.pop(key, None)
|
|
873
|
-
record = await self._router.
|
|
873
|
+
record = await self._router.ensure_topic(message.chat_id, message.thread_id)
|
|
874
874
|
agent = self._effective_agent(record)
|
|
875
875
|
supports_effort = self._agent_supports_effort(agent)
|
|
876
876
|
list_params = {
|
|
@@ -878,10 +878,17 @@ class TelegramCommandHandlers(
|
|
|
878
878
|
"limit": DEFAULT_MODEL_LIST_LIMIT,
|
|
879
879
|
"agent": agent,
|
|
880
880
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
881
|
+
workspace_path, error = self._resolve_workspace_path(record, allow_pma=True)
|
|
882
|
+
if workspace_path is None:
|
|
883
|
+
await self._send_message(
|
|
884
|
+
message.chat_id,
|
|
885
|
+
error or "Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
886
|
+
thread_id=message.thread_id,
|
|
887
|
+
reply_to=message.message_id,
|
|
884
888
|
)
|
|
889
|
+
return
|
|
890
|
+
try:
|
|
891
|
+
client = await self._client_for_workspace(workspace_path)
|
|
885
892
|
except AppServerUnavailableError as exc:
|
|
886
893
|
log_event(
|
|
887
894
|
self._logger,
|
|
@@ -901,16 +908,20 @@ class TelegramCommandHandlers(
|
|
|
901
908
|
if client is None:
|
|
902
909
|
await self._send_message(
|
|
903
910
|
message.chat_id,
|
|
904
|
-
"Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
911
|
+
error or "Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
905
912
|
thread_id=message.thread_id,
|
|
906
913
|
reply_to=message.message_id,
|
|
907
914
|
)
|
|
908
915
|
return
|
|
909
916
|
argv = self._parse_command_args(args)
|
|
910
917
|
if not argv:
|
|
918
|
+
record_for_models = self._record_with_workspace_path(record, workspace_path)
|
|
911
919
|
try:
|
|
912
920
|
result = await self._fetch_model_list(
|
|
913
|
-
|
|
921
|
+
record_for_models,
|
|
922
|
+
agent=agent,
|
|
923
|
+
client=client,
|
|
924
|
+
list_params=list_params,
|
|
914
925
|
)
|
|
915
926
|
except OpenCodeSupervisorError as exc:
|
|
916
927
|
log_event(
|
|
@@ -993,9 +1004,13 @@ class TelegramCommandHandlers(
|
|
|
993
1004
|
)
|
|
994
1005
|
return
|
|
995
1006
|
if argv[0].lower() in ("list", "ls"):
|
|
1007
|
+
record_for_models = self._record_with_workspace_path(record, workspace_path)
|
|
996
1008
|
try:
|
|
997
1009
|
result = await self._fetch_model_list(
|
|
998
|
-
|
|
1010
|
+
record_for_models,
|
|
1011
|
+
agent=agent,
|
|
1012
|
+
client=client,
|
|
1013
|
+
list_params=list_params,
|
|
999
1014
|
)
|
|
1000
1015
|
except OpenCodeSupervisorError as exc:
|
|
1001
1016
|
log_event(
|
|
@@ -1108,6 +1123,101 @@ class TelegramCommandHandlers(
|
|
|
1108
1123
|
reply_to=message.message_id,
|
|
1109
1124
|
)
|
|
1110
1125
|
|
|
1126
|
+
async def _handle_pma(
|
|
1127
|
+
self, message: TelegramMessage, args: str, _runtime: Any
|
|
1128
|
+
) -> None:
|
|
1129
|
+
if not self._hub_root:
|
|
1130
|
+
await self._send_message(
|
|
1131
|
+
message.chat_id,
|
|
1132
|
+
"PMA unavailable; hub root not configured.",
|
|
1133
|
+
thread_id=message.thread_id,
|
|
1134
|
+
reply_to=message.message_id,
|
|
1135
|
+
)
|
|
1136
|
+
return
|
|
1137
|
+
|
|
1138
|
+
supervisor = getattr(self, "_hub_supervisor", None)
|
|
1139
|
+
if supervisor and hasattr(supervisor, "hub_config"):
|
|
1140
|
+
pma_config = supervisor.hub_config.pma
|
|
1141
|
+
if not pma_config.enabled:
|
|
1142
|
+
await self._send_message(
|
|
1143
|
+
message.chat_id,
|
|
1144
|
+
"PMA is disabled in hub config. Set pma.enabled: true to enable.",
|
|
1145
|
+
thread_id=message.thread_id,
|
|
1146
|
+
reply_to=message.message_id,
|
|
1147
|
+
)
|
|
1148
|
+
return
|
|
1149
|
+
argv = self._parse_command_args(args)
|
|
1150
|
+
action = argv[0].lower() if argv else ""
|
|
1151
|
+
key = await self._resolve_topic_key(message.chat_id, message.thread_id)
|
|
1152
|
+
record = await self._router.get_topic(key)
|
|
1153
|
+
current = bool(record and record.pma_enabled)
|
|
1154
|
+
if action in ("status", "show"):
|
|
1155
|
+
enabled = current
|
|
1156
|
+
elif action in ("on", "enable", "true"):
|
|
1157
|
+
enabled = True
|
|
1158
|
+
elif action in ("off", "disable", "false"):
|
|
1159
|
+
enabled = False
|
|
1160
|
+
elif action:
|
|
1161
|
+
await self._send_message(
|
|
1162
|
+
message.chat_id,
|
|
1163
|
+
"Usage: /pma [on|off|status]",
|
|
1164
|
+
thread_id=message.thread_id,
|
|
1165
|
+
reply_to=message.message_id,
|
|
1166
|
+
)
|
|
1167
|
+
return
|
|
1168
|
+
else:
|
|
1169
|
+
enabled = not current
|
|
1170
|
+
|
|
1171
|
+
if record is None:
|
|
1172
|
+
await self._router.ensure_topic(message.chat_id, message.thread_id)
|
|
1173
|
+
|
|
1174
|
+
def apply_pma(record: TelegramTopicRecord) -> None:
|
|
1175
|
+
record.pma_enabled = enabled
|
|
1176
|
+
if enabled:
|
|
1177
|
+
# Save previous binding before entering PMA mode.
|
|
1178
|
+
record.pma_prev_repo_id = record.repo_id
|
|
1179
|
+
record.pma_prev_workspace_path = record.workspace_path
|
|
1180
|
+
record.pma_prev_workspace_id = record.workspace_id
|
|
1181
|
+
record.pma_prev_active_thread_id = record.active_thread_id
|
|
1182
|
+
# Mutual exclusion: PMA mode implies Hub context, so unbind specific repo.
|
|
1183
|
+
record.workspace_path = None
|
|
1184
|
+
record.repo_id = None
|
|
1185
|
+
record.workspace_id = None
|
|
1186
|
+
record.active_thread_id = None
|
|
1187
|
+
else:
|
|
1188
|
+
# Restore previous binding when exiting PMA mode.
|
|
1189
|
+
if record.pma_prev_repo_id or record.pma_prev_workspace_path:
|
|
1190
|
+
record.repo_id = record.pma_prev_repo_id
|
|
1191
|
+
record.workspace_path = record.pma_prev_workspace_path
|
|
1192
|
+
record.workspace_id = record.pma_prev_workspace_id
|
|
1193
|
+
record.active_thread_id = record.pma_prev_active_thread_id
|
|
1194
|
+
# Clear saved previous binding after restore.
|
|
1195
|
+
record.pma_prev_repo_id = None
|
|
1196
|
+
record.pma_prev_workspace_path = None
|
|
1197
|
+
record.pma_prev_workspace_id = None
|
|
1198
|
+
record.pma_prev_active_thread_id = None
|
|
1199
|
+
|
|
1200
|
+
await self._router.update_topic(
|
|
1201
|
+
message.chat_id,
|
|
1202
|
+
message.thread_id,
|
|
1203
|
+
apply_pma,
|
|
1204
|
+
)
|
|
1205
|
+
status = "enabled" if enabled else "disabled"
|
|
1206
|
+
if enabled:
|
|
1207
|
+
hint = "Use /pma off to exit. Previous repo binding saved."
|
|
1208
|
+
else:
|
|
1209
|
+
previous = (record and record.pma_prev_workspace_path) or None
|
|
1210
|
+
if previous:
|
|
1211
|
+
hint = f"Back to repo mode. Restored {previous}."
|
|
1212
|
+
else:
|
|
1213
|
+
hint = "Back to repo mode."
|
|
1214
|
+
await self._send_message(
|
|
1215
|
+
message.chat_id,
|
|
1216
|
+
f"PMA mode {status}. {hint}",
|
|
1217
|
+
thread_id=message.thread_id,
|
|
1218
|
+
reply_to=message.message_id,
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1111
1221
|
async def _opencode_review_arguments(target: dict[str, Any]) -> str:
|
|
1112
1222
|
target_type = target.get("type")
|
|
1113
1223
|
if target_type == "uncommittedChanges":
|
|
@@ -1834,10 +1944,14 @@ class TelegramCommandHandlers(
|
|
|
1834
1944
|
async def _apply_compact_summary(
|
|
1835
1945
|
self, message: TelegramMessage, record: "TelegramTopicRecord", summary_text: str
|
|
1836
1946
|
) -> tuple[bool, str | None]:
|
|
1837
|
-
|
|
1838
|
-
|
|
1947
|
+
workspace_path, error = self._resolve_workspace_path(record, allow_pma=True)
|
|
1948
|
+
if not workspace_path:
|
|
1949
|
+
return (
|
|
1950
|
+
False,
|
|
1951
|
+
error or "Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
1952
|
+
)
|
|
1839
1953
|
try:
|
|
1840
|
-
client = await self._client_for_workspace(
|
|
1954
|
+
client = await self._client_for_workspace(workspace_path)
|
|
1841
1955
|
except AppServerUnavailableError as exc:
|
|
1842
1956
|
log_event(
|
|
1843
1957
|
self._logger,
|
|
@@ -1849,7 +1963,10 @@ class TelegramCommandHandlers(
|
|
|
1849
1963
|
)
|
|
1850
1964
|
return False, "App server unavailable; try again or check logs."
|
|
1851
1965
|
if client is None:
|
|
1852
|
-
return (
|
|
1966
|
+
return (
|
|
1967
|
+
False,
|
|
1968
|
+
error or "Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
1969
|
+
)
|
|
1853
1970
|
log_event(
|
|
1854
1971
|
self._logger,
|
|
1855
1972
|
logging.INFO,
|
|
@@ -1857,11 +1974,11 @@ class TelegramCommandHandlers(
|
|
|
1857
1974
|
chat_id=message.chat_id,
|
|
1858
1975
|
thread_id=message.thread_id,
|
|
1859
1976
|
summary_len=len(summary_text),
|
|
1860
|
-
workspace_path=
|
|
1977
|
+
workspace_path=workspace_path,
|
|
1861
1978
|
)
|
|
1862
1979
|
try:
|
|
1863
1980
|
agent = self._effective_agent(record)
|
|
1864
|
-
thread = await client.thread_start(
|
|
1981
|
+
thread = await client.thread_start(workspace_path, agent=agent)
|
|
1865
1982
|
except Exception as exc:
|
|
1866
1983
|
log_event(
|
|
1867
1984
|
self._logger,
|
|
@@ -1873,7 +1990,7 @@ class TelegramCommandHandlers(
|
|
|
1873
1990
|
)
|
|
1874
1991
|
return False, "Failed to start a new thread."
|
|
1875
1992
|
if not await self._require_thread_workspace(
|
|
1876
|
-
message,
|
|
1993
|
+
message, workspace_path, thread, action="thread_start"
|
|
1877
1994
|
):
|
|
1878
1995
|
return False, "Failed to start a new thread."
|
|
1879
1996
|
new_thread_id = _extract_thread_id(thread)
|
|
@@ -1911,7 +2028,7 @@ class TelegramCommandHandlers(
|
|
|
1911
2028
|
) -> None:
|
|
1912
2029
|
argv = self._parse_command_args(args)
|
|
1913
2030
|
if argv and argv[0].lower() in ("soft", "summary", "summarize"):
|
|
1914
|
-
record = await self._require_bound_record(message)
|
|
2031
|
+
record = await self._require_bound_record(message, allow_pma=True)
|
|
1915
2032
|
if not record:
|
|
1916
2033
|
return
|
|
1917
2034
|
await self._handle_normal_message(
|
|
@@ -1919,33 +2036,51 @@ class TelegramCommandHandlers(
|
|
|
1919
2036
|
)
|
|
1920
2037
|
return
|
|
1921
2038
|
auto_apply = bool(argv and argv[0].lower() == "apply")
|
|
1922
|
-
record = await self._require_bound_record(message)
|
|
2039
|
+
record = await self._require_bound_record(message, allow_pma=True)
|
|
1923
2040
|
if not record:
|
|
1924
2041
|
return
|
|
1925
2042
|
key = await self._resolve_topic_key(message.chat_id, message.thread_id)
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2043
|
+
pma_enabled = bool(record.pma_enabled)
|
|
2044
|
+
if pma_enabled:
|
|
2045
|
+
registry = getattr(self, "_hub_thread_registry", None)
|
|
2046
|
+
pma_key = self._pma_registry_key(record, message)
|
|
2047
|
+
pma_thread_id = (
|
|
2048
|
+
registry.get_thread_id(pma_key)
|
|
2049
|
+
if registry is not None and pma_key
|
|
2050
|
+
else None
|
|
2051
|
+
)
|
|
2052
|
+
if not pma_thread_id:
|
|
2053
|
+
await self._send_message(
|
|
2054
|
+
message.chat_id,
|
|
2055
|
+
"No active PMA thread to compact. Send a message or use /new to start one.",
|
|
2056
|
+
thread_id=message.thread_id,
|
|
2057
|
+
reply_to=message.message_id,
|
|
2058
|
+
)
|
|
2059
|
+
return
|
|
2060
|
+
else:
|
|
2061
|
+
if not record.active_thread_id:
|
|
2062
|
+
await self._send_message(
|
|
2063
|
+
message.chat_id,
|
|
2064
|
+
"No active thread to compact. Use /new to start one.",
|
|
2065
|
+
thread_id=message.thread_id,
|
|
2066
|
+
reply_to=message.message_id,
|
|
2067
|
+
)
|
|
2068
|
+
return
|
|
2069
|
+
conflict_key = await self._find_thread_conflict(
|
|
2070
|
+
record.active_thread_id, key=key
|
|
1943
2071
|
)
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2072
|
+
if conflict_key:
|
|
2073
|
+
await self._router.set_active_thread(
|
|
2074
|
+
message.chat_id, message.thread_id, None
|
|
2075
|
+
)
|
|
2076
|
+
await self._handle_thread_conflict(
|
|
2077
|
+
message, record.active_thread_id, conflict_key
|
|
2078
|
+
)
|
|
2079
|
+
return
|
|
2080
|
+
verified = await self._verify_active_thread(message, record)
|
|
2081
|
+
if not verified:
|
|
2082
|
+
return
|
|
2083
|
+
record = verified
|
|
1949
2084
|
outcome = await self._run_turn_and_collect_result(
|
|
1950
2085
|
message,
|
|
1951
2086
|
runtime,
|
|
@@ -2067,9 +2202,13 @@ Compact canceled.""",
|
|
|
2067
2202
|
)
|
|
2068
2203
|
self._compact_pending.pop(key, None)
|
|
2069
2204
|
record = await self._router.get_topic(key)
|
|
2070
|
-
if record is None
|
|
2205
|
+
if record is None:
|
|
2071
2206
|
await self._answer_callback(callback, "Selection expired")
|
|
2072
2207
|
return
|
|
2208
|
+
workspace_path, error = self._resolve_workspace_path(record, allow_pma=True)
|
|
2209
|
+
if workspace_path is None:
|
|
2210
|
+
await self._answer_callback(callback, error or "Selection expired")
|
|
2211
|
+
return
|
|
2073
2212
|
if callback.chat_id is None:
|
|
2074
2213
|
return
|
|
2075
2214
|
await self._answer_callback(callback, "Applying summary...")
|
|
@@ -2634,14 +2773,23 @@ Summary applied.""",
|
|
|
2634
2773
|
async def _handle_logout(
|
|
2635
2774
|
self, message: TelegramMessage, _args: str, _runtime: Any
|
|
2636
2775
|
) -> None:
|
|
2637
|
-
record = await self._require_bound_record(message)
|
|
2776
|
+
record = await self._require_bound_record(message, allow_pma=True)
|
|
2638
2777
|
if not record:
|
|
2639
2778
|
return
|
|
2640
|
-
|
|
2779
|
+
workspace_path, error = self._resolve_workspace_path(record, allow_pma=True)
|
|
2780
|
+
if workspace_path is None:
|
|
2781
|
+
await self._send_message(
|
|
2782
|
+
message.chat_id,
|
|
2783
|
+
error or "Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
2784
|
+
thread_id=message.thread_id,
|
|
2785
|
+
reply_to=message.message_id,
|
|
2786
|
+
)
|
|
2787
|
+
return
|
|
2788
|
+
client = await self._client_for_workspace(workspace_path)
|
|
2641
2789
|
if client is None:
|
|
2642
2790
|
await self._send_message(
|
|
2643
2791
|
message.chat_id,
|
|
2644
|
-
"Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
2792
|
+
error or "Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
2645
2793
|
thread_id=message.thread_id,
|
|
2646
2794
|
reply_to=message.message_id,
|
|
2647
2795
|
)
|
|
@@ -2687,14 +2835,23 @@ Summary applied.""",
|
|
|
2687
2835
|
reply_to=message.message_id,
|
|
2688
2836
|
)
|
|
2689
2837
|
return
|
|
2690
|
-
record = await self._require_bound_record(message)
|
|
2838
|
+
record = await self._require_bound_record(message, allow_pma=True)
|
|
2691
2839
|
if not record:
|
|
2692
2840
|
return
|
|
2693
|
-
|
|
2841
|
+
workspace_path, error = self._resolve_workspace_path(record, allow_pma=True)
|
|
2842
|
+
if workspace_path is None:
|
|
2843
|
+
await self._send_message(
|
|
2844
|
+
message.chat_id,
|
|
2845
|
+
error or "Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
2846
|
+
thread_id=message.thread_id,
|
|
2847
|
+
reply_to=message.message_id,
|
|
2848
|
+
)
|
|
2849
|
+
return
|
|
2850
|
+
client = await self._client_for_workspace(workspace_path)
|
|
2694
2851
|
if client is None:
|
|
2695
2852
|
await self._send_message(
|
|
2696
2853
|
message.chat_id,
|
|
2697
|
-
"Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
2854
|
+
error or "Topic not bound. Use /bind <repo_id> or /bind <path>.",
|
|
2698
2855
|
thread_id=message.thread_id,
|
|
2699
2856
|
reply_to=message.message_id,
|
|
2700
2857
|
)
|
|
@@ -16,6 +16,12 @@ class CommandSpec:
|
|
|
16
16
|
|
|
17
17
|
def build_command_specs(handlers: Any) -> dict[str, CommandSpec]:
|
|
18
18
|
return {
|
|
19
|
+
"repos": CommandSpec(
|
|
20
|
+
"repos",
|
|
21
|
+
"list available repositories in the hub",
|
|
22
|
+
handlers._handle_repos,
|
|
23
|
+
allow_during_turn=True,
|
|
24
|
+
),
|
|
19
25
|
"bind": CommandSpec(
|
|
20
26
|
"bind",
|
|
21
27
|
"bind this topic to a workspace",
|
|
@@ -23,9 +29,14 @@ def build_command_specs(handlers: Any) -> dict[str, CommandSpec]:
|
|
|
23
29
|
),
|
|
24
30
|
"new": CommandSpec(
|
|
25
31
|
"new",
|
|
26
|
-
"start a new session",
|
|
32
|
+
"start a new PMA session",
|
|
27
33
|
lambda message, _args, _runtime: handlers._handle_new(message),
|
|
28
34
|
),
|
|
35
|
+
"reset": CommandSpec(
|
|
36
|
+
"reset",
|
|
37
|
+
"reset PMA thread state (clear volatile state)",
|
|
38
|
+
lambda message, _args, _runtime: handlers._handle_reset(message),
|
|
39
|
+
),
|
|
29
40
|
"resume": CommandSpec(
|
|
30
41
|
"resume",
|
|
31
42
|
"list or resume a previous session",
|
|
@@ -42,12 +53,6 @@ def build_command_specs(handlers: Any) -> dict[str, CommandSpec]:
|
|
|
42
53
|
lambda message, args, _runtime: handlers._handle_flow(message, args),
|
|
43
54
|
allow_during_turn=True,
|
|
44
55
|
),
|
|
45
|
-
"flow_status": CommandSpec(
|
|
46
|
-
"flow_status",
|
|
47
|
-
"show ticket flow status (alias for /flow status)",
|
|
48
|
-
lambda message, args, _runtime: handlers._handle_flow_status(message, args),
|
|
49
|
-
allow_during_turn=True,
|
|
50
|
-
),
|
|
51
56
|
"reply": CommandSpec(
|
|
52
57
|
"reply",
|
|
53
58
|
"reply to a paused ticket flow dispatch (prefer /flow reply)",
|
|
@@ -69,6 +74,12 @@ def build_command_specs(handlers: Any) -> dict[str, CommandSpec]:
|
|
|
69
74
|
"set approval and sandbox policy",
|
|
70
75
|
handlers._handle_approvals,
|
|
71
76
|
),
|
|
77
|
+
"pma": CommandSpec(
|
|
78
|
+
"pma",
|
|
79
|
+
"toggle PMA mode for this topic",
|
|
80
|
+
handlers._handle_pma,
|
|
81
|
+
allow_during_turn=True,
|
|
82
|
+
),
|
|
72
83
|
"status": CommandSpec(
|
|
73
84
|
"status",
|
|
74
85
|
"show current binding and thread status",
|
|
@@ -75,6 +75,21 @@ def _message_text_candidate(message: TelegramMessage) -> tuple[str, str, Any]:
|
|
|
75
75
|
return raw_text, text_candidate, entities
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def _record_with_media_workspace(
|
|
79
|
+
handlers: Any, record: Any
|
|
80
|
+
) -> tuple[Any, Optional[str]]:
|
|
81
|
+
"""Ensure media handlers have a workspace path, including PMA topics."""
|
|
82
|
+
if record is None:
|
|
83
|
+
return None, None
|
|
84
|
+
pma_enabled = bool(getattr(record, "pma_enabled", False))
|
|
85
|
+
if not pma_enabled:
|
|
86
|
+
return record, None
|
|
87
|
+
hub_root = getattr(handlers, "_hub_root", None)
|
|
88
|
+
if hub_root is None:
|
|
89
|
+
return None, "PMA unavailable; hub root not configured."
|
|
90
|
+
return dataclasses.replace(record, workspace_path=str(hub_root)), None
|
|
91
|
+
|
|
92
|
+
|
|
78
93
|
async def _clear_pending_options(
|
|
79
94
|
handlers: Any, key: str, message: TelegramMessage
|
|
80
95
|
) -> None:
|
|
@@ -352,7 +367,8 @@ async def handle_message_inner(
|
|
|
352
367
|
record = await handlers._router.get_topic(key)
|
|
353
368
|
paused = None
|
|
354
369
|
workspace_root: Optional[Path] = None
|
|
355
|
-
|
|
370
|
+
pma_enabled = bool(record and getattr(record, "pma_enabled", False))
|
|
371
|
+
if not pma_enabled and record and record.workspace_path:
|
|
356
372
|
workspace_root = canonicalize_path(Path(record.workspace_path))
|
|
357
373
|
preferred_run_id = handlers._ticket_flow_pause_targets.get(
|
|
358
374
|
str(workspace_root), None
|
|
@@ -801,6 +817,15 @@ async def handle_media_message(
|
|
|
801
817
|
return
|
|
802
818
|
key = await handlers._resolve_topic_key(message.chat_id, message.thread_id)
|
|
803
819
|
record = await handlers._router.get_topic(key)
|
|
820
|
+
record, pma_error = _record_with_media_workspace(handlers, record)
|
|
821
|
+
if pma_error:
|
|
822
|
+
await handlers._send_message(
|
|
823
|
+
message.chat_id,
|
|
824
|
+
pma_error,
|
|
825
|
+
thread_id=message.thread_id,
|
|
826
|
+
reply_to=message.message_id,
|
|
827
|
+
)
|
|
828
|
+
return
|
|
804
829
|
if record is None or not record.workspace_path:
|
|
805
830
|
await handlers._send_message(
|
|
806
831
|
message.chat_id,
|
|
@@ -834,7 +859,10 @@ async def handle_media_message(
|
|
|
834
859
|
best = photos[0]
|
|
835
860
|
try:
|
|
836
861
|
file_info = await handlers._bot.get_file(best.file_id)
|
|
837
|
-
data = await handlers._bot.download_file(
|
|
862
|
+
data = await handlers._bot.download_file(
|
|
863
|
+
file_info.file_path,
|
|
864
|
+
max_size_bytes=handlers._config.media.max_image_bytes,
|
|
865
|
+
)
|
|
838
866
|
filename = f"photo_{best.file_id}.jpg"
|
|
839
867
|
files.append((filename, data))
|
|
840
868
|
except Exception as exc:
|
|
@@ -843,7 +871,10 @@ async def handle_media_message(
|
|
|
843
871
|
elif message.document:
|
|
844
872
|
try:
|
|
845
873
|
file_info = await handlers._bot.get_file(message.document.file_id)
|
|
846
|
-
data = await handlers._bot.download_file(
|
|
874
|
+
data = await handlers._bot.download_file(
|
|
875
|
+
file_info.file_path,
|
|
876
|
+
max_size_bytes=handlers._config.media.max_file_bytes,
|
|
877
|
+
)
|
|
847
878
|
filename = (
|
|
848
879
|
message.document.file_name or f"document_{message.document.file_id}"
|
|
849
880
|
)
|
|
@@ -887,12 +887,12 @@ def _format_help_text(command_specs: dict[str, CommandSpec]) -> str:
|
|
|
887
887
|
"resume",
|
|
888
888
|
"review",
|
|
889
889
|
"flow",
|
|
890
|
-
"flow_status",
|
|
891
890
|
"reply",
|
|
892
891
|
"pr",
|
|
893
892
|
"agent",
|
|
894
893
|
"model",
|
|
895
894
|
"approvals",
|
|
895
|
+
"pma",
|
|
896
896
|
"status",
|
|
897
897
|
"diff",
|
|
898
898
|
"mention",
|
|
@@ -935,8 +935,6 @@ def _format_help_text(command_specs: dict[str, CommandSpec]) -> str:
|
|
|
935
935
|
lines.append("/flow restart")
|
|
936
936
|
lines.append("/flow archive [run_id] [--force]")
|
|
937
937
|
lines.append("/flow reply <message>")
|
|
938
|
-
if "flow_status" in command_specs:
|
|
939
|
-
lines.append("/flow_status [run_id]")
|
|
940
938
|
if "reply" in command_specs:
|
|
941
939
|
lines.append("/reply <message> (legacy)")
|
|
942
940
|
|
|
@@ -265,8 +265,11 @@ class TelegramRuntimeHelpers:
|
|
|
265
265
|
text = f"{prefix}{text}"
|
|
266
266
|
return self._prepare_message(text)
|
|
267
267
|
|
|
268
|
-
def _render_message(
|
|
269
|
-
parse_mode =
|
|
268
|
+
def _render_message(
|
|
269
|
+
self, text: str, *, parse_mode: Optional[str] = None
|
|
270
|
+
) -> tuple[str, Optional[str]]:
|
|
271
|
+
# Allow callers to override parse_mode (needed for ad-hoc Markdown/HTML sends)
|
|
272
|
+
parse_mode = self._config.parse_mode if parse_mode is None else parse_mode
|
|
270
273
|
if not parse_mode:
|
|
271
274
|
return text, None
|
|
272
275
|
if parse_mode == "HTML":
|
|
@@ -275,8 +278,10 @@ class TelegramRuntimeHelpers:
|
|
|
275
278
|
return _format_telegram_markdown(text, parse_mode), parse_mode
|
|
276
279
|
return text, parse_mode
|
|
277
280
|
|
|
278
|
-
def _prepare_message(
|
|
279
|
-
|
|
281
|
+
def _prepare_message(
|
|
282
|
+
self, text: str, *, parse_mode: Optional[str] = None
|
|
283
|
+
) -> tuple[str, Optional[str]]:
|
|
284
|
+
rendered, parse_mode = self._render_message(text, parse_mode=parse_mode)
|
|
280
285
|
# Avoid parse_mode when chunking to keep markup intact.
|
|
281
286
|
if parse_mode and len(rendered) <= TELEGRAM_MAX_MESSAGE_LENGTH:
|
|
282
287
|
return rendered, parse_mode
|
|
@@ -16,7 +16,12 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from .state import TelegramTopicRecord
|
|
17
17
|
|
|
18
18
|
from ...agents.opencode.supervisor import OpenCodeSupervisor
|
|
19
|
+
from ...core.app_server_threads import (
|
|
20
|
+
AppServerThreadRegistry,
|
|
21
|
+
default_app_server_threads_path,
|
|
22
|
+
)
|
|
19
23
|
from ...core.flows.models import FlowRunRecord
|
|
24
|
+
from ...core.hub import HubSupervisor
|
|
20
25
|
from ...core.locks import process_alive
|
|
21
26
|
from ...core.logging_utils import log_event
|
|
22
27
|
from ...core.request_context import reset_conversation_id, set_conversation_id
|
|
@@ -173,6 +178,31 @@ class TelegramBotService(
|
|
|
173
178
|
self._logger = logger or logging.getLogger(__name__)
|
|
174
179
|
self._hub_root = hub_root
|
|
175
180
|
self._manifest_path = manifest_path
|
|
181
|
+
self._hub_supervisor = None
|
|
182
|
+
self._hub_thread_registry = None
|
|
183
|
+
if self._hub_root:
|
|
184
|
+
try:
|
|
185
|
+
self._hub_supervisor = HubSupervisor.from_path(self._hub_root)
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
log_event(
|
|
188
|
+
self._logger,
|
|
189
|
+
logging.WARNING,
|
|
190
|
+
"telegram.pma.hub_supervisor.unavailable",
|
|
191
|
+
hub_root=str(self._hub_root),
|
|
192
|
+
exc=exc,
|
|
193
|
+
)
|
|
194
|
+
try:
|
|
195
|
+
self._hub_thread_registry = AppServerThreadRegistry(
|
|
196
|
+
default_app_server_threads_path(self._hub_root)
|
|
197
|
+
)
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
log_event(
|
|
200
|
+
self._logger,
|
|
201
|
+
logging.WARNING,
|
|
202
|
+
"telegram.pma.thread_registry.unavailable",
|
|
203
|
+
hub_root=str(self._hub_root),
|
|
204
|
+
exc=exc,
|
|
205
|
+
)
|
|
176
206
|
self._update_repo_url = update_repo_url
|
|
177
207
|
self._update_repo_ref = update_repo_ref
|
|
178
208
|
self._update_skip_checks = update_skip_checks
|
|
@@ -190,6 +190,11 @@ class TelegramTopicRecord:
|
|
|
190
190
|
repo_id: Optional[str] = None
|
|
191
191
|
workspace_path: Optional[str] = None
|
|
192
192
|
workspace_id: Optional[str] = None
|
|
193
|
+
pma_enabled: bool = False
|
|
194
|
+
pma_prev_repo_id: Optional[str] = None
|
|
195
|
+
pma_prev_workspace_path: Optional[str] = None
|
|
196
|
+
pma_prev_workspace_id: Optional[str] = None
|
|
197
|
+
pma_prev_active_thread_id: Optional[str] = None
|
|
193
198
|
active_thread_id: Optional[str] = None
|
|
194
199
|
thread_ids: list[str] = dataclasses.field(default_factory=list)
|
|
195
200
|
thread_summaries: dict[str, ThreadSummary] = dataclasses.field(default_factory=dict)
|
|
@@ -220,6 +225,29 @@ class TelegramTopicRecord:
|
|
|
220
225
|
workspace_id = payload.get("workspace_id") or payload.get("workspaceId")
|
|
221
226
|
if not isinstance(workspace_id, str):
|
|
222
227
|
workspace_id = None
|
|
228
|
+
pma_enabled = payload.get("pma_enabled") or payload.get("pmaEnabled")
|
|
229
|
+
if not isinstance(pma_enabled, bool):
|
|
230
|
+
pma_enabled = False
|
|
231
|
+
pma_prev_repo_id = payload.get("pma_prev_repo_id") or payload.get(
|
|
232
|
+
"pmaPrevRepoId"
|
|
233
|
+
)
|
|
234
|
+
if not isinstance(pma_prev_repo_id, str):
|
|
235
|
+
pma_prev_repo_id = None
|
|
236
|
+
pma_prev_workspace_path = payload.get("pma_prev_workspace_path") or payload.get(
|
|
237
|
+
"pmaPrevWorkspacePath"
|
|
238
|
+
)
|
|
239
|
+
if not isinstance(pma_prev_workspace_path, str):
|
|
240
|
+
pma_prev_workspace_path = None
|
|
241
|
+
pma_prev_workspace_id = payload.get("pma_prev_workspace_id") or payload.get(
|
|
242
|
+
"pmaPrevWorkspaceId"
|
|
243
|
+
)
|
|
244
|
+
if not isinstance(pma_prev_workspace_id, str):
|
|
245
|
+
pma_prev_workspace_id = None
|
|
246
|
+
pma_prev_active_thread_id = payload.get(
|
|
247
|
+
"pma_prev_active_thread_id"
|
|
248
|
+
) or payload.get("pmaPrevActiveThreadId")
|
|
249
|
+
if not isinstance(pma_prev_active_thread_id, str):
|
|
250
|
+
pma_prev_active_thread_id = None
|
|
223
251
|
active_thread_id = payload.get("active_thread_id") or payload.get(
|
|
224
252
|
"activeThreadId"
|
|
225
253
|
)
|
|
@@ -301,6 +329,11 @@ class TelegramTopicRecord:
|
|
|
301
329
|
repo_id=repo_id,
|
|
302
330
|
workspace_path=workspace_path,
|
|
303
331
|
workspace_id=workspace_id,
|
|
332
|
+
pma_enabled=pma_enabled,
|
|
333
|
+
pma_prev_repo_id=pma_prev_repo_id,
|
|
334
|
+
pma_prev_workspace_path=pma_prev_workspace_path,
|
|
335
|
+
pma_prev_workspace_id=pma_prev_workspace_id,
|
|
336
|
+
pma_prev_active_thread_id=pma_prev_active_thread_id,
|
|
304
337
|
active_thread_id=active_thread_id,
|
|
305
338
|
thread_ids=thread_ids,
|
|
306
339
|
thread_summaries=thread_summaries,
|
|
@@ -324,6 +357,11 @@ class TelegramTopicRecord:
|
|
|
324
357
|
"repo_id": self.repo_id,
|
|
325
358
|
"workspace_path": self.workspace_path,
|
|
326
359
|
"workspace_id": self.workspace_id,
|
|
360
|
+
"pma_enabled": self.pma_enabled,
|
|
361
|
+
"pma_prev_repo_id": self.pma_prev_repo_id,
|
|
362
|
+
"pma_prev_workspace_path": self.pma_prev_workspace_path,
|
|
363
|
+
"pma_prev_workspace_id": self.pma_prev_workspace_id,
|
|
364
|
+
"pma_prev_active_thread_id": self.pma_prev_active_thread_id,
|
|
327
365
|
"active_thread_id": self.active_thread_id,
|
|
328
366
|
"thread_ids": list(self.thread_ids),
|
|
329
367
|
"thread_summaries": {
|