codex-autorunner 1.1.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.
- 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 +114 -1
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +236 -1
- codex_autorunner/core/context_awareness.py +38 -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 +496 -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/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 +26 -1
- 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/chatUploads.js +137 -0
- 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 +255 -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 +9125 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- 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/file_chat.py +317 -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 +70 -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.0.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/RECORD +125 -94
- 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.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import dataclasses
|
|
4
5
|
import json
|
|
5
6
|
import logging
|
|
6
7
|
import math
|
|
@@ -27,16 +28,23 @@ from .....agents.opencode.runtime import (
|
|
|
27
28
|
split_model_id,
|
|
28
29
|
)
|
|
29
30
|
from .....agents.opencode.supervisor import OpenCodeSupervisorError
|
|
30
|
-
from .....core.
|
|
31
|
+
from .....core.app_server_threads import (
|
|
32
|
+
PMA_KEY,
|
|
33
|
+
PMA_OPENCODE_KEY,
|
|
34
|
+
AppServerThreadRegistry,
|
|
35
|
+
)
|
|
31
36
|
from .....core.config import load_repo_config
|
|
37
|
+
from .....core.context_awareness import CAR_AWARENESS_BLOCK
|
|
32
38
|
from .....core.injected_context import wrap_injected_context
|
|
33
39
|
from .....core.logging_utils import log_event
|
|
40
|
+
from .....core.pma_context import build_hub_snapshot, format_pma_prompt, load_pma_prompt
|
|
34
41
|
from .....core.state import now_iso
|
|
35
42
|
from .....core.utils import canonicalize_path
|
|
36
43
|
from .....integrations.github.service import GitHubService
|
|
37
44
|
from ....app_server.client import (
|
|
38
45
|
CodexAppServerClient,
|
|
39
46
|
CodexAppServerDisconnected,
|
|
47
|
+
CodexAppServerResponseError,
|
|
40
48
|
_normalize_sandbox_policy,
|
|
41
49
|
)
|
|
42
50
|
from ...adapter import (
|
|
@@ -75,6 +83,7 @@ from ...helpers import (
|
|
|
75
83
|
find_github_links,
|
|
76
84
|
is_interrupt_status,
|
|
77
85
|
)
|
|
86
|
+
from ...state import topic_key as build_topic_key
|
|
78
87
|
|
|
79
88
|
if TYPE_CHECKING:
|
|
80
89
|
from ...state import TelegramTopicRecord
|
|
@@ -547,18 +556,11 @@ class ExecutionCommands(SharedHelpers):
|
|
|
547
556
|
return f"{prompt_text}{separator}{injection}", True
|
|
548
557
|
|
|
549
558
|
def _maybe_inject_car_context(self, prompt_text: str) -> tuple[str, bool]:
|
|
550
|
-
if
|
|
551
|
-
return prompt_text, False
|
|
552
|
-
lowered = prompt_text.lower()
|
|
553
|
-
if "about_car.md" in lowered:
|
|
554
|
-
return prompt_text, False
|
|
555
|
-
if CAR_CONTEXT_HINT in prompt_text:
|
|
559
|
+
if CAR_AWARENESS_BLOCK in prompt_text:
|
|
556
560
|
return prompt_text, False
|
|
557
|
-
if not
|
|
558
|
-
return
|
|
559
|
-
|
|
560
|
-
injection = wrap_injected_context(CAR_CONTEXT_HINT)
|
|
561
|
-
return f"{prompt_text}{separator}{injection}", True
|
|
561
|
+
if not prompt_text or not prompt_text.strip():
|
|
562
|
+
return CAR_AWARENESS_BLOCK, True
|
|
563
|
+
return f"{CAR_AWARENESS_BLOCK}\n\n{prompt_text}", True
|
|
562
564
|
|
|
563
565
|
def _maybe_inject_outbox_context(
|
|
564
566
|
self,
|
|
@@ -1046,6 +1048,8 @@ class ExecutionCommands(SharedHelpers):
|
|
|
1046
1048
|
missing_thread_message: Optional[str],
|
|
1047
1049
|
transcript_message_id: Optional[int],
|
|
1048
1050
|
transcript_text: Optional[str],
|
|
1051
|
+
pma_thread_registry: Optional[AppServerThreadRegistry] = None,
|
|
1052
|
+
pma_thread_key: Optional[str] = None,
|
|
1049
1053
|
) -> _TurnRunResult | _TurnRunFailure:
|
|
1050
1054
|
supervisor = getattr(self, "_opencode_supervisor", None)
|
|
1051
1055
|
if supervisor is None:
|
|
@@ -1109,6 +1113,7 @@ class ExecutionCommands(SharedHelpers):
|
|
|
1109
1113
|
transcript_text,
|
|
1110
1114
|
)
|
|
1111
1115
|
|
|
1116
|
+
pma_mode = bool(pma_thread_registry and pma_thread_key)
|
|
1112
1117
|
try:
|
|
1113
1118
|
if not thread_id:
|
|
1114
1119
|
if not allow_new_thread:
|
|
@@ -1164,27 +1169,34 @@ class ExecutionCommands(SharedHelpers):
|
|
|
1164
1169
|
rollout_path=record.rollout_path,
|
|
1165
1170
|
)
|
|
1166
1171
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1172
|
+
if pma_mode:
|
|
1173
|
+
pma_thread_registry.set_thread_id(pma_thread_key, thread_id)
|
|
1174
|
+
else:
|
|
1175
|
+
record = await self._router.update_topic(
|
|
1176
|
+
message.chat_id, message.thread_id, apply
|
|
1177
|
+
)
|
|
1170
1178
|
else:
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1179
|
+
if not pma_mode:
|
|
1180
|
+
record = await self._router.set_active_thread(
|
|
1181
|
+
message.chat_id, message.thread_id, thread_id
|
|
1182
|
+
)
|
|
1174
1183
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
thread_id,
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1184
|
+
if not pma_mode:
|
|
1185
|
+
user_preview = _preview_from_text(
|
|
1186
|
+
prompt_text, RESUME_PREVIEW_USER_LIMIT
|
|
1187
|
+
)
|
|
1188
|
+
await self._router.update_topic(
|
|
1189
|
+
message.chat_id,
|
|
1190
|
+
message.thread_id,
|
|
1191
|
+
lambda record: _set_thread_summary(
|
|
1192
|
+
record,
|
|
1193
|
+
thread_id,
|
|
1194
|
+
user_preview=user_preview,
|
|
1195
|
+
last_used_at=now_iso(),
|
|
1196
|
+
workspace_path=record.workspace_path,
|
|
1197
|
+
rollout_path=record.rollout_path,
|
|
1198
|
+
),
|
|
1199
|
+
)
|
|
1188
1200
|
|
|
1189
1201
|
pending_seed = None
|
|
1190
1202
|
pending_seed_thread_id = record.pending_compact_seed_thread_id
|
|
@@ -1924,11 +1936,43 @@ class ExecutionCommands(SharedHelpers):
|
|
|
1924
1936
|
missing_thread_message: Optional[str],
|
|
1925
1937
|
transcript_message_id: Optional[int],
|
|
1926
1938
|
transcript_text: Optional[str],
|
|
1939
|
+
pma_thread_registry: Optional[AppServerThreadRegistry] = None,
|
|
1940
|
+
pma_thread_key: Optional[str] = None,
|
|
1927
1941
|
) -> _TurnRunResult | _TurnRunFailure:
|
|
1928
1942
|
turn_handle = None
|
|
1929
1943
|
turn_key: Optional[TurnKey] = None
|
|
1930
1944
|
turn_started_at: Optional[float] = None
|
|
1931
1945
|
|
|
1946
|
+
def _is_missing_thread_error(exc: Exception) -> bool:
|
|
1947
|
+
if not isinstance(exc, CodexAppServerResponseError):
|
|
1948
|
+
return False
|
|
1949
|
+
message = str(exc).lower()
|
|
1950
|
+
return "thread not found" in message
|
|
1951
|
+
|
|
1952
|
+
async def _start_new_thread(agent: str) -> Optional[str]:
|
|
1953
|
+
nonlocal record
|
|
1954
|
+
workspace_path = record.workspace_path
|
|
1955
|
+
if not workspace_path:
|
|
1956
|
+
return None
|
|
1957
|
+
thread = await client.thread_start(workspace_path, agent=agent)
|
|
1958
|
+
if not await self._require_thread_workspace(
|
|
1959
|
+
message, workspace_path, thread, action="thread_start"
|
|
1960
|
+
):
|
|
1961
|
+
return None
|
|
1962
|
+
new_thread_id = _extract_thread_id(thread)
|
|
1963
|
+
if not new_thread_id:
|
|
1964
|
+
return None
|
|
1965
|
+
if pma_mode and pma_thread_registry and pma_thread_key:
|
|
1966
|
+
pma_thread_registry.set_thread_id(pma_thread_key, new_thread_id)
|
|
1967
|
+
elif not pma_mode:
|
|
1968
|
+
record = await self._apply_thread_result(
|
|
1969
|
+
message.chat_id,
|
|
1970
|
+
message.thread_id,
|
|
1971
|
+
thread,
|
|
1972
|
+
active_thread_id=new_thread_id,
|
|
1973
|
+
)
|
|
1974
|
+
return new_thread_id
|
|
1975
|
+
|
|
1932
1976
|
try:
|
|
1933
1977
|
client = await self._client_for_workspace(record.workspace_path)
|
|
1934
1978
|
except AppServerUnavailableError as exc:
|
|
@@ -1966,6 +2010,7 @@ class ExecutionCommands(SharedHelpers):
|
|
|
1966
2010
|
failure_message, None, transcript_message_id, transcript_text
|
|
1967
2011
|
)
|
|
1968
2012
|
|
|
2013
|
+
pma_mode = bool(pma_thread_registry and pma_thread_key)
|
|
1969
2014
|
try:
|
|
1970
2015
|
if not thread_id:
|
|
1971
2016
|
if not allow_new_thread:
|
|
@@ -1986,26 +2031,8 @@ class ExecutionCommands(SharedHelpers):
|
|
|
1986
2031
|
transcript_message_id,
|
|
1987
2032
|
transcript_text,
|
|
1988
2033
|
)
|
|
1989
|
-
workspace_path = record.workspace_path
|
|
1990
|
-
if not workspace_path:
|
|
1991
|
-
return _TurnRunFailure(
|
|
1992
|
-
"Workspace missing.",
|
|
1993
|
-
None,
|
|
1994
|
-
transcript_message_id,
|
|
1995
|
-
transcript_text,
|
|
1996
|
-
)
|
|
1997
2034
|
agent = self._effective_agent(record)
|
|
1998
|
-
|
|
1999
|
-
if not await self._require_thread_workspace(
|
|
2000
|
-
message, workspace_path, thread, action="thread_start"
|
|
2001
|
-
):
|
|
2002
|
-
return _TurnRunFailure(
|
|
2003
|
-
"Thread workspace mismatch.",
|
|
2004
|
-
None,
|
|
2005
|
-
transcript_message_id,
|
|
2006
|
-
transcript_text,
|
|
2007
|
-
)
|
|
2008
|
-
thread_id = _extract_thread_id(thread)
|
|
2035
|
+
thread_id = await _start_new_thread(agent)
|
|
2009
2036
|
if not thread_id:
|
|
2010
2037
|
failure_message = "Failed to start a new thread."
|
|
2011
2038
|
if send_failure_response:
|
|
@@ -2021,18 +2048,13 @@ class ExecutionCommands(SharedHelpers):
|
|
|
2021
2048
|
transcript_message_id,
|
|
2022
2049
|
transcript_text,
|
|
2023
2050
|
)
|
|
2024
|
-
record = await self._apply_thread_result(
|
|
2025
|
-
message.chat_id,
|
|
2026
|
-
message.thread_id,
|
|
2027
|
-
thread,
|
|
2028
|
-
active_thread_id=thread_id,
|
|
2029
|
-
)
|
|
2030
2051
|
else:
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2052
|
+
if not pma_mode:
|
|
2053
|
+
record = await self._router.set_active_thread(
|
|
2054
|
+
message.chat_id, message.thread_id, thread_id
|
|
2055
|
+
)
|
|
2034
2056
|
|
|
2035
|
-
if thread_id:
|
|
2057
|
+
if thread_id and not pma_mode:
|
|
2036
2058
|
user_preview = _preview_from_text(
|
|
2037
2059
|
prompt_text, RESUME_PREVIEW_USER_LIMIT
|
|
2038
2060
|
)
|
|
@@ -2052,7 +2074,9 @@ class ExecutionCommands(SharedHelpers):
|
|
|
2052
2074
|
pending_seed = None
|
|
2053
2075
|
pending_seed_thread_id = record.pending_compact_seed_thread_id
|
|
2054
2076
|
if record.pending_compact_seed:
|
|
2055
|
-
if
|
|
2077
|
+
if pma_mode:
|
|
2078
|
+
pending_seed = None
|
|
2079
|
+
elif pending_seed_thread_id is None:
|
|
2056
2080
|
pending_seed = record.pending_compact_seed
|
|
2057
2081
|
elif thread_id and pending_seed_thread_id == thread_id:
|
|
2058
2082
|
pending_seed = record.pending_compact_seed
|
|
@@ -2147,14 +2171,69 @@ class ExecutionCommands(SharedHelpers):
|
|
|
2147
2171
|
PLACEHOLDER_TEXT,
|
|
2148
2172
|
)
|
|
2149
2173
|
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2174
|
+
try:
|
|
2175
|
+
turn_handle = await client.turn_start(
|
|
2176
|
+
thread_id,
|
|
2177
|
+
prompt_text,
|
|
2178
|
+
input_items=input_items,
|
|
2179
|
+
approval_policy=approval_policy,
|
|
2180
|
+
sandbox_policy=sandbox_policy,
|
|
2181
|
+
**turn_kwargs,
|
|
2182
|
+
)
|
|
2183
|
+
except Exception as exc:
|
|
2184
|
+
if (
|
|
2185
|
+
pma_mode
|
|
2186
|
+
and _is_missing_thread_error(exc)
|
|
2187
|
+
and pma_thread_registry
|
|
2188
|
+
and pma_thread_key
|
|
2189
|
+
):
|
|
2190
|
+
log_event(
|
|
2191
|
+
self._logger,
|
|
2192
|
+
logging.WARNING,
|
|
2193
|
+
"telegram.pma.thread.reset",
|
|
2194
|
+
topic_key=key,
|
|
2195
|
+
chat_id=message.chat_id,
|
|
2196
|
+
thread_id=message.thread_id,
|
|
2197
|
+
codex_thread_id=thread_id,
|
|
2198
|
+
reason="thread_not_found",
|
|
2199
|
+
)
|
|
2200
|
+
pma_thread_registry.reset_thread(pma_thread_key)
|
|
2201
|
+
if not allow_new_thread:
|
|
2202
|
+
failure_message = (
|
|
2203
|
+
"PMA thread no longer exists. Send a new message to "
|
|
2204
|
+
"start a PMA thread, then retry /compact."
|
|
2205
|
+
)
|
|
2206
|
+
if send_failure_response:
|
|
2207
|
+
await self._send_message(
|
|
2208
|
+
message.chat_id,
|
|
2209
|
+
failure_message,
|
|
2210
|
+
thread_id=message.thread_id,
|
|
2211
|
+
reply_to=message.message_id,
|
|
2212
|
+
)
|
|
2213
|
+
if placeholder_id is not None:
|
|
2214
|
+
await self._delete_message(
|
|
2215
|
+
message.chat_id, placeholder_id
|
|
2216
|
+
)
|
|
2217
|
+
return _TurnRunFailure(
|
|
2218
|
+
failure_message,
|
|
2219
|
+
placeholder_id,
|
|
2220
|
+
transcript_message_id,
|
|
2221
|
+
transcript_text,
|
|
2222
|
+
)
|
|
2223
|
+
agent = self._effective_agent(record)
|
|
2224
|
+
thread_id = await _start_new_thread(agent)
|
|
2225
|
+
if thread_id is None:
|
|
2226
|
+
raise
|
|
2227
|
+
turn_handle = await client.turn_start(
|
|
2228
|
+
thread_id,
|
|
2229
|
+
prompt_text,
|
|
2230
|
+
input_items=input_items,
|
|
2231
|
+
approval_policy=approval_policy,
|
|
2232
|
+
sandbox_policy=sandbox_policy,
|
|
2233
|
+
**turn_kwargs,
|
|
2234
|
+
)
|
|
2235
|
+
else:
|
|
2236
|
+
raise
|
|
2158
2237
|
if pending_seed:
|
|
2159
2238
|
await self._router.update_topic(
|
|
2160
2239
|
message.chat_id,
|
|
@@ -2388,6 +2467,41 @@ class ExecutionCommands(SharedHelpers):
|
|
|
2388
2467
|
)
|
|
2389
2468
|
return prompt_text
|
|
2390
2469
|
|
|
2470
|
+
def _pma_registry_key(
|
|
2471
|
+
self, record: "TelegramTopicRecord", message: Optional[TelegramMessage] = None
|
|
2472
|
+
) -> str:
|
|
2473
|
+
"""
|
|
2474
|
+
Return PMA thread registry key.
|
|
2475
|
+
|
|
2476
|
+
Thread scoping decision:
|
|
2477
|
+
- When require_topics is false (default): use global keys (pma/pma.opencode).
|
|
2478
|
+
All Telegram topics share one PMA conversation per agent.
|
|
2479
|
+
- When require_topics is true: use per-topic keys (pma.{topic_key}/pma.opencode.{topic_key}).
|
|
2480
|
+
Each Telegram topic gets its own isolated PMA conversation.
|
|
2481
|
+
|
|
2482
|
+
This allows hubs with multiple topics to maintain separate PMA contexts
|
|
2483
|
+
when require_topics is enabled, while keeping a single shared context
|
|
2484
|
+
in the common case (require_topics disabled).
|
|
2485
|
+
"""
|
|
2486
|
+
agent = self._effective_agent(record)
|
|
2487
|
+
base_key = PMA_OPENCODE_KEY if agent == "opencode" else PMA_KEY
|
|
2488
|
+
|
|
2489
|
+
# PMA thread scoping: per-topic when require_topics is true
|
|
2490
|
+
require_topics = getattr(self._config, "require_topics", False)
|
|
2491
|
+
if require_topics and message is not None:
|
|
2492
|
+
topic_key = build_topic_key(message.chat_id, message.thread_id)
|
|
2493
|
+
return f"{base_key}.{topic_key}"
|
|
2494
|
+
return base_key
|
|
2495
|
+
|
|
2496
|
+
async def _prepare_pma_prompt(self, message_text: str) -> Optional[str]:
|
|
2497
|
+
hub_root = getattr(self, "_hub_root", None)
|
|
2498
|
+
if hub_root is None:
|
|
2499
|
+
return None
|
|
2500
|
+
supervisor = getattr(self, "_hub_supervisor", None)
|
|
2501
|
+
snapshot = await build_hub_snapshot(supervisor, hub_root=Path(hub_root))
|
|
2502
|
+
base_prompt = load_pma_prompt(hub_root)
|
|
2503
|
+
return format_pma_prompt(base_prompt, snapshot, message_text, hub_root=hub_root)
|
|
2504
|
+
|
|
2391
2505
|
async def _prepare_turn_context(
|
|
2392
2506
|
self,
|
|
2393
2507
|
message: TelegramMessage,
|
|
@@ -2493,6 +2607,26 @@ class ExecutionCommands(SharedHelpers):
|
|
|
2493
2607
|
) -> _TurnRunResult | _TurnRunFailure:
|
|
2494
2608
|
key = await self._resolve_topic_key(message.chat_id, message.thread_id)
|
|
2495
2609
|
record = record or await self._router.get_topic(key)
|
|
2610
|
+
pma_enabled = bool(record and getattr(record, "pma_enabled", False))
|
|
2611
|
+
if pma_enabled:
|
|
2612
|
+
hub_root = getattr(self, "_hub_root", None)
|
|
2613
|
+
if hub_root is None:
|
|
2614
|
+
failure_message = "PMA unavailable; hub root not configured."
|
|
2615
|
+
if send_failure_response:
|
|
2616
|
+
await self._send_message(
|
|
2617
|
+
message.chat_id,
|
|
2618
|
+
failure_message,
|
|
2619
|
+
thread_id=message.thread_id,
|
|
2620
|
+
reply_to=message.message_id,
|
|
2621
|
+
)
|
|
2622
|
+
return _TurnRunFailure(
|
|
2623
|
+
failure_message, None, transcript_message_id, transcript_text
|
|
2624
|
+
)
|
|
2625
|
+
if record is None:
|
|
2626
|
+
from ...state import TelegramTopicRecord
|
|
2627
|
+
|
|
2628
|
+
record = TelegramTopicRecord(pma_enabled=True)
|
|
2629
|
+
record = dataclasses.replace(record, workspace_path=str(hub_root))
|
|
2496
2630
|
if record is None or not record.workspace_path:
|
|
2497
2631
|
failure_message = "Topic not bound. Use /bind <repo_id> or /bind <path>."
|
|
2498
2632
|
if send_failure_response:
|
|
@@ -2506,7 +2640,7 @@ class ExecutionCommands(SharedHelpers):
|
|
|
2506
2640
|
failure_message, None, transcript_message_id, transcript_text
|
|
2507
2641
|
)
|
|
2508
2642
|
|
|
2509
|
-
if record.active_thread_id:
|
|
2643
|
+
if record.active_thread_id and not pma_enabled:
|
|
2510
2644
|
conflict_key = await self._find_thread_conflict(
|
|
2511
2645
|
record.active_thread_id,
|
|
2512
2646
|
key=key,
|
|
@@ -2536,16 +2670,40 @@ class ExecutionCommands(SharedHelpers):
|
|
|
2536
2670
|
)
|
|
2537
2671
|
record = verified
|
|
2538
2672
|
|
|
2539
|
-
|
|
2673
|
+
pma_thread_registry = (
|
|
2674
|
+
getattr(self, "_hub_thread_registry", None) if pma_enabled else None
|
|
2675
|
+
)
|
|
2676
|
+
pma_thread_key = (
|
|
2677
|
+
self._pma_registry_key(record, message) if pma_enabled else None
|
|
2678
|
+
)
|
|
2679
|
+
thread_id = None if pma_enabled else record.active_thread_id
|
|
2680
|
+
if pma_enabled and pma_thread_registry and pma_thread_key:
|
|
2681
|
+
thread_id = pma_thread_registry.get_thread_id(pma_thread_key)
|
|
2540
2682
|
prompt_text = (
|
|
2541
2683
|
text_override if text_override is not None else (message.text or "")
|
|
2542
2684
|
)
|
|
2543
2685
|
prompt_text = self._prepare_turn_prompt(
|
|
2544
2686
|
prompt_text, transcript_text=transcript_text
|
|
2545
2687
|
)
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2688
|
+
if pma_enabled:
|
|
2689
|
+
pma_prompt = await self._prepare_pma_prompt(prompt_text)
|
|
2690
|
+
if pma_prompt is None:
|
|
2691
|
+
failure_message = "PMA unavailable; hub snapshot failed."
|
|
2692
|
+
if send_failure_response:
|
|
2693
|
+
await self._send_message(
|
|
2694
|
+
message.chat_id,
|
|
2695
|
+
failure_message,
|
|
2696
|
+
thread_id=message.thread_id,
|
|
2697
|
+
reply_to=message.message_id,
|
|
2698
|
+
)
|
|
2699
|
+
return _TurnRunFailure(
|
|
2700
|
+
failure_message, None, transcript_message_id, transcript_text
|
|
2701
|
+
)
|
|
2702
|
+
prompt_text = pma_prompt
|
|
2703
|
+
else:
|
|
2704
|
+
prompt_text, key = await self._prepare_turn_context(
|
|
2705
|
+
message, prompt_text, record
|
|
2706
|
+
)
|
|
2549
2707
|
|
|
2550
2708
|
turn_semaphore = self._ensure_turn_semaphore()
|
|
2551
2709
|
queued = turn_semaphore.locked()
|
|
@@ -2574,6 +2732,8 @@ class ExecutionCommands(SharedHelpers):
|
|
|
2574
2732
|
missing_thread_message=missing_thread_message,
|
|
2575
2733
|
transcript_message_id=transcript_message_id,
|
|
2576
2734
|
transcript_text=transcript_text,
|
|
2735
|
+
pma_thread_registry=pma_thread_registry,
|
|
2736
|
+
pma_thread_key=pma_thread_key,
|
|
2577
2737
|
)
|
|
2578
2738
|
|
|
2579
2739
|
return await self._execute_codex_turn(
|
|
@@ -2592,4 +2752,6 @@ class ExecutionCommands(SharedHelpers):
|
|
|
2592
2752
|
missing_thread_message=missing_thread_message,
|
|
2593
2753
|
transcript_message_id=transcript_message_id,
|
|
2594
2754
|
transcript_text=transcript_text,
|
|
2755
|
+
pma_thread_registry=pma_thread_registry,
|
|
2756
|
+
pma_thread_key=pma_thread_key,
|
|
2595
2757
|
)
|