codex-autorunner 0.1.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/__init__.py +3 -0
- codex_autorunner/bootstrap.py +151 -0
- codex_autorunner/cli.py +886 -0
- codex_autorunner/codex_cli.py +79 -0
- codex_autorunner/codex_runner.py +17 -0
- codex_autorunner/core/__init__.py +1 -0
- codex_autorunner/core/about_car.py +125 -0
- codex_autorunner/core/codex_runner.py +100 -0
- codex_autorunner/core/config.py +1465 -0
- codex_autorunner/core/doc_chat.py +547 -0
- codex_autorunner/core/docs.py +37 -0
- codex_autorunner/core/engine.py +720 -0
- codex_autorunner/core/git_utils.py +206 -0
- codex_autorunner/core/hub.py +756 -0
- codex_autorunner/core/injected_context.py +9 -0
- codex_autorunner/core/locks.py +57 -0
- codex_autorunner/core/logging_utils.py +158 -0
- codex_autorunner/core/notifications.py +465 -0
- codex_autorunner/core/optional_dependencies.py +41 -0
- codex_autorunner/core/prompt.py +107 -0
- codex_autorunner/core/prompts.py +275 -0
- codex_autorunner/core/request_context.py +21 -0
- codex_autorunner/core/runner_controller.py +116 -0
- codex_autorunner/core/runner_process.py +29 -0
- codex_autorunner/core/snapshot.py +576 -0
- codex_autorunner/core/state.py +156 -0
- codex_autorunner/core/update.py +567 -0
- codex_autorunner/core/update_runner.py +44 -0
- codex_autorunner/core/usage.py +1221 -0
- codex_autorunner/core/utils.py +108 -0
- codex_autorunner/discovery.py +102 -0
- codex_autorunner/housekeeping.py +423 -0
- codex_autorunner/integrations/__init__.py +1 -0
- codex_autorunner/integrations/app_server/__init__.py +6 -0
- codex_autorunner/integrations/app_server/client.py +1386 -0
- codex_autorunner/integrations/app_server/supervisor.py +206 -0
- codex_autorunner/integrations/github/__init__.py +10 -0
- codex_autorunner/integrations/github/service.py +889 -0
- codex_autorunner/integrations/telegram/__init__.py +1 -0
- codex_autorunner/integrations/telegram/adapter.py +1401 -0
- codex_autorunner/integrations/telegram/commands_registry.py +104 -0
- codex_autorunner/integrations/telegram/config.py +450 -0
- codex_autorunner/integrations/telegram/constants.py +154 -0
- codex_autorunner/integrations/telegram/dispatch.py +162 -0
- codex_autorunner/integrations/telegram/handlers/__init__.py +0 -0
- codex_autorunner/integrations/telegram/handlers/approvals.py +241 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +72 -0
- codex_autorunner/integrations/telegram/handlers/commands.py +160 -0
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +5262 -0
- codex_autorunner/integrations/telegram/handlers/messages.py +477 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +545 -0
- codex_autorunner/integrations/telegram/helpers.py +2084 -0
- codex_autorunner/integrations/telegram/notifications.py +164 -0
- codex_autorunner/integrations/telegram/outbox.py +174 -0
- codex_autorunner/integrations/telegram/rendering.py +102 -0
- codex_autorunner/integrations/telegram/retry.py +37 -0
- codex_autorunner/integrations/telegram/runtime.py +270 -0
- codex_autorunner/integrations/telegram/service.py +921 -0
- codex_autorunner/integrations/telegram/state.py +1223 -0
- codex_autorunner/integrations/telegram/transport.py +318 -0
- codex_autorunner/integrations/telegram/types.py +57 -0
- codex_autorunner/integrations/telegram/voice.py +413 -0
- codex_autorunner/manifest.py +150 -0
- codex_autorunner/routes/__init__.py +53 -0
- codex_autorunner/routes/base.py +470 -0
- codex_autorunner/routes/docs.py +275 -0
- codex_autorunner/routes/github.py +197 -0
- codex_autorunner/routes/repos.py +121 -0
- codex_autorunner/routes/sessions.py +137 -0
- codex_autorunner/routes/shared.py +137 -0
- codex_autorunner/routes/system.py +175 -0
- codex_autorunner/routes/terminal_images.py +107 -0
- codex_autorunner/routes/voice.py +128 -0
- codex_autorunner/server.py +23 -0
- codex_autorunner/spec_ingest.py +113 -0
- codex_autorunner/static/app.js +95 -0
- codex_autorunner/static/autoRefresh.js +209 -0
- codex_autorunner/static/bootstrap.js +105 -0
- codex_autorunner/static/bus.js +23 -0
- codex_autorunner/static/cache.js +52 -0
- codex_autorunner/static/constants.js +48 -0
- codex_autorunner/static/dashboard.js +795 -0
- codex_autorunner/static/docs.js +1514 -0
- codex_autorunner/static/env.js +99 -0
- codex_autorunner/static/github.js +168 -0
- codex_autorunner/static/hub.js +1511 -0
- codex_autorunner/static/index.html +622 -0
- codex_autorunner/static/loader.js +28 -0
- codex_autorunner/static/logs.js +690 -0
- codex_autorunner/static/mobileCompact.js +300 -0
- codex_autorunner/static/snapshot.js +116 -0
- codex_autorunner/static/state.js +87 -0
- codex_autorunner/static/styles.css +4966 -0
- codex_autorunner/static/tabs.js +50 -0
- codex_autorunner/static/terminal.js +21 -0
- codex_autorunner/static/terminalManager.js +3535 -0
- codex_autorunner/static/todoPreview.js +25 -0
- codex_autorunner/static/types.d.ts +8 -0
- codex_autorunner/static/utils.js +597 -0
- codex_autorunner/static/vendor/LICENSE.xterm +24 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-cyrillic.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-greek.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-latin.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-400-vietnamese.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-cyrillic.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-greek.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-latin.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-500-vietnamese.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-cyrillic.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-greek.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin-ext.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-latin.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/JetBrainsMono-600-vietnamese.woff2 +0 -0
- codex_autorunner/static/vendor/fonts/jetbrains-mono/OFL.txt +93 -0
- codex_autorunner/static/vendor/xterm-addon-fit.js +2 -0
- codex_autorunner/static/vendor/xterm.css +209 -0
- codex_autorunner/static/vendor/xterm.js +2 -0
- codex_autorunner/static/voice.js +591 -0
- codex_autorunner/voice/__init__.py +39 -0
- codex_autorunner/voice/capture.py +349 -0
- codex_autorunner/voice/config.py +167 -0
- codex_autorunner/voice/provider.py +66 -0
- codex_autorunner/voice/providers/__init__.py +7 -0
- codex_autorunner/voice/providers/openai_whisper.py +345 -0
- codex_autorunner/voice/resolver.py +36 -0
- codex_autorunner/voice/service.py +210 -0
- codex_autorunner/web/__init__.py +1 -0
- codex_autorunner/web/app.py +1037 -0
- codex_autorunner/web/hub_jobs.py +181 -0
- codex_autorunner/web/middleware.py +552 -0
- codex_autorunner/web/pty_session.py +357 -0
- codex_autorunner/web/runner_manager.py +25 -0
- codex_autorunner/web/schemas.py +253 -0
- codex_autorunner/web/static_assets.py +430 -0
- codex_autorunner/web/terminal_sessions.py +78 -0
- codex_autorunner/workspace.py +16 -0
- codex_autorunner-0.1.0.dist-info/METADATA +240 -0
- codex_autorunner-0.1.0.dist-info/RECORD +147 -0
- codex_autorunner-0.1.0.dist-info/WHEEL +5 -0
- codex_autorunner-0.1.0.dist-info/entry_points.txt +3 -0
- codex_autorunner-0.1.0.dist-info/licenses/LICENSE +21 -0
- codex_autorunner-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
DEFAULT_PAGE_SIZE = 10
|
|
4
|
+
TELEGRAM_MAX_MESSAGE_LENGTH = 4096
|
|
5
|
+
TELEGRAM_CALLBACK_DATA_LIMIT = 64
|
|
6
|
+
THREAD_LIST_PAGE_LIMIT = 100
|
|
7
|
+
THREAD_LIST_MAX_PAGES = 5
|
|
8
|
+
DEFAULT_MODEL_LIST_LIMIT = 25
|
|
9
|
+
DEFAULT_MCP_LIST_LIMIT = 50
|
|
10
|
+
DEFAULT_SKILLS_LIST_LIMIT = 50
|
|
11
|
+
MAX_TOPIC_THREAD_HISTORY = 50
|
|
12
|
+
RESUME_BUTTON_PREVIEW_LIMIT = 60
|
|
13
|
+
RESUME_PREVIEW_USER_LIMIT = 1000
|
|
14
|
+
RESUME_PREVIEW_ASSISTANT_LIMIT = 1000
|
|
15
|
+
RESUME_PREVIEW_SCAN_LINES = 200
|
|
16
|
+
RESUME_MISSING_IDS_LOG_LIMIT = 10
|
|
17
|
+
RESUME_REFRESH_LIMIT = 10
|
|
18
|
+
TOKEN_USAGE_CACHE_LIMIT = 256
|
|
19
|
+
TOKEN_USAGE_TURN_CACHE_LIMIT = 512
|
|
20
|
+
DEFAULT_INTERRUPT_TIMEOUT_SECONDS = 30.0
|
|
21
|
+
DEFAULT_WORKSPACE_STATE_ROOT = "~/.codex-autorunner/workspaces"
|
|
22
|
+
APP_SERVER_START_BACKOFF_INITIAL_SECONDS = 1.0
|
|
23
|
+
APP_SERVER_START_BACKOFF_MAX_SECONDS = 30.0
|
|
24
|
+
CACHE_CLEANUP_INTERVAL_SECONDS = 300.0
|
|
25
|
+
COALESCE_BUFFER_TTL_SECONDS = 60.0
|
|
26
|
+
MODEL_PENDING_TTL_SECONDS = 1800.0
|
|
27
|
+
PENDING_APPROVAL_TTL_SECONDS = 600.0
|
|
28
|
+
REASONING_BUFFER_TTL_SECONDS = 900.0
|
|
29
|
+
SELECTION_STATE_TTL_SECONDS = 1800.0
|
|
30
|
+
TURN_PREVIEW_TTL_SECONDS = 900.0
|
|
31
|
+
OVERSIZE_WARNING_TTL_SECONDS = 3600.0
|
|
32
|
+
UPDATE_ID_PERSIST_INTERVAL_SECONDS = 60.0
|
|
33
|
+
OUTBOX_RETRY_INTERVAL_SECONDS = 10.0
|
|
34
|
+
OUTBOX_IMMEDIATE_RETRY_DELAYS = (0.5, 2.0, 5.0)
|
|
35
|
+
OUTBOX_MAX_ATTEMPTS = 8
|
|
36
|
+
VOICE_RETRY_INTERVAL_SECONDS = 5.0
|
|
37
|
+
VOICE_RETRY_INITIAL_SECONDS = 2.0
|
|
38
|
+
VOICE_RETRY_MAX_SECONDS = 300.0
|
|
39
|
+
VOICE_RETRY_JITTER_RATIO = 0.2
|
|
40
|
+
VOICE_MAX_ATTEMPTS = 20
|
|
41
|
+
VOICE_RETRY_AFTER_BUFFER_SECONDS = 1.0
|
|
42
|
+
WHISPER_TRANSCRIPT_DISCLAIMER = (
|
|
43
|
+
"Note: transcribed from user voice. If confusing or possibly inaccurate and you "
|
|
44
|
+
"cannot infer the intention please clarify before proceeding."
|
|
45
|
+
)
|
|
46
|
+
DEFAULT_UPDATE_REPO_URL = "https://github.com/Git-on-my-level/codex-autorunner.git"
|
|
47
|
+
DEFAULT_UPDATE_REPO_REF = "main"
|
|
48
|
+
RESUME_PICKER_PROMPT = (
|
|
49
|
+
"Select a thread to resume (buttons below or reply with number/id)."
|
|
50
|
+
)
|
|
51
|
+
BIND_PICKER_PROMPT = "Select a repo to bind (buttons below or reply with number/id)."
|
|
52
|
+
MODEL_PICKER_PROMPT = "Select a model (buttons below)."
|
|
53
|
+
EFFORT_PICKER_PROMPT = "Select a reasoning effort for {model}."
|
|
54
|
+
UPDATE_PICKER_PROMPT = "Select update target (buttons below)."
|
|
55
|
+
REVIEW_COMMIT_PICKER_PROMPT = (
|
|
56
|
+
"Select a commit to review (buttons below or reply with number)."
|
|
57
|
+
)
|
|
58
|
+
REVIEW_COMMIT_BUTTON_LABEL_LIMIT = 80
|
|
59
|
+
UPDATE_TARGET_OPTIONS = (
|
|
60
|
+
("both", "Both (web + Telegram)"),
|
|
61
|
+
("web", "Web only"),
|
|
62
|
+
("telegram", "Telegram only"),
|
|
63
|
+
)
|
|
64
|
+
TRACE_MESSAGE_TOKENS = (
|
|
65
|
+
"failed",
|
|
66
|
+
"error",
|
|
67
|
+
"denied",
|
|
68
|
+
"unknown",
|
|
69
|
+
"not bound",
|
|
70
|
+
"not found",
|
|
71
|
+
"invalid",
|
|
72
|
+
"unsupported",
|
|
73
|
+
"disabled",
|
|
74
|
+
"missing",
|
|
75
|
+
"mismatch",
|
|
76
|
+
"different workspace",
|
|
77
|
+
"no previous",
|
|
78
|
+
"no resumable",
|
|
79
|
+
"no workspace-tagged",
|
|
80
|
+
"not applicable",
|
|
81
|
+
"selection expired",
|
|
82
|
+
"timed out",
|
|
83
|
+
"timeout",
|
|
84
|
+
"aborted",
|
|
85
|
+
"canceled",
|
|
86
|
+
"cancelled",
|
|
87
|
+
)
|
|
88
|
+
PLACEHOLDER_TEXT = "Working..."
|
|
89
|
+
STREAM_PREVIEW_PREFIX = ""
|
|
90
|
+
THINKING_PREVIEW_MAX_LEN = 80
|
|
91
|
+
THINKING_PREVIEW_MIN_EDIT_INTERVAL_SECONDS = 1.0
|
|
92
|
+
COMMAND_DISABLED_TEMPLATE = "'/{name}' is disabled while a task is in progress."
|
|
93
|
+
MAX_MENTION_BYTES = 200_000
|
|
94
|
+
VALID_REASONING_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh"}
|
|
95
|
+
CONTEXT_BASELINE_TOKENS = 12000
|
|
96
|
+
APPROVAL_POLICY_VALUES = {"untrusted", "on-failure", "on-request", "never"}
|
|
97
|
+
APPROVAL_PRESETS = {
|
|
98
|
+
"read-only": ("on-request", "readOnly"),
|
|
99
|
+
"auto": ("on-request", "workspaceWrite"),
|
|
100
|
+
"full-access": ("never", "dangerFullAccess"),
|
|
101
|
+
}
|
|
102
|
+
SHELL_OUTPUT_TRUNCATION_SUFFIX = "\n...(truncated)"
|
|
103
|
+
SHELL_MESSAGE_BUFFER_CHARS = 200
|
|
104
|
+
COMPACT_SUMMARY_PROMPT = (
|
|
105
|
+
"Summarize the conversation so far into a concise context block I can paste into "
|
|
106
|
+
"a new thread. Include goals, constraints, decisions, and current state."
|
|
107
|
+
)
|
|
108
|
+
INIT_PROMPT = "\n".join(
|
|
109
|
+
[
|
|
110
|
+
"Generate a file named AGENTS.md that serves as a contributor guide for this repository.",
|
|
111
|
+
"Your goal is to produce a clear, concise, and well-structured document with descriptive headings and actionable explanations for each section.",
|
|
112
|
+
"Follow the outline below, but adapt as needed - add sections if relevant, and omit those that do not apply to this project.",
|
|
113
|
+
"",
|
|
114
|
+
"Document Requirements",
|
|
115
|
+
"",
|
|
116
|
+
'- Title the document "Repository Guidelines".',
|
|
117
|
+
"- Use Markdown headings (#, ##, etc.) for structure.",
|
|
118
|
+
"- Keep the document concise. 200-400 words is optimal.",
|
|
119
|
+
"- Keep explanations short, direct, and specific to this repository.",
|
|
120
|
+
"- Provide examples where helpful (commands, directory paths, naming patterns).",
|
|
121
|
+
"- Maintain a professional, instructional tone.",
|
|
122
|
+
"",
|
|
123
|
+
"Recommended Sections",
|
|
124
|
+
"",
|
|
125
|
+
"Project Structure & Module Organization",
|
|
126
|
+
"",
|
|
127
|
+
"- Outline the project structure, including where the source code, tests, and assets are located.",
|
|
128
|
+
"",
|
|
129
|
+
"Build, Test, and Development Commands",
|
|
130
|
+
"",
|
|
131
|
+
"- List key commands for building, testing, and running locally (e.g., npm test, make build).",
|
|
132
|
+
"- Briefly explain what each command does.",
|
|
133
|
+
"",
|
|
134
|
+
"Coding Style & Naming Conventions",
|
|
135
|
+
"",
|
|
136
|
+
"- Specify indentation rules, language-specific style preferences, and naming patterns.",
|
|
137
|
+
"- Include any formatting or linting tools used.",
|
|
138
|
+
"",
|
|
139
|
+
"Testing Guidelines",
|
|
140
|
+
"",
|
|
141
|
+
"- Identify testing frameworks and coverage requirements.",
|
|
142
|
+
"- State test naming conventions and how to run tests.",
|
|
143
|
+
"",
|
|
144
|
+
"Commit & Pull Request Guidelines",
|
|
145
|
+
"",
|
|
146
|
+
"- Summarize commit message conventions found in the project's Git history.",
|
|
147
|
+
"- Outline pull request requirements (descriptions, linked issues, screenshots, etc.).",
|
|
148
|
+
"",
|
|
149
|
+
"(Optional) Add other sections if relevant, such as Security & Configuration Tips, Architecture Overview, or Agent-Specific Instructions.",
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
TurnKey = tuple[str, str]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
6
|
+
|
|
7
|
+
from ...core.logging_utils import log_event
|
|
8
|
+
from .adapter import TelegramUpdate, allowlist_allows
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class DispatchContext:
|
|
13
|
+
chat_id: Optional[int]
|
|
14
|
+
user_id: Optional[int]
|
|
15
|
+
thread_id: Optional[int]
|
|
16
|
+
message_id: Optional[int]
|
|
17
|
+
is_topic: Optional[bool]
|
|
18
|
+
is_edited: Optional[bool]
|
|
19
|
+
topic_key: Optional[str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
DispatchRoute = Callable[[Any, TelegramUpdate, DispatchContext], Awaitable[None]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_context(handlers: Any, update: TelegramUpdate) -> DispatchContext:
|
|
26
|
+
chat_id = None
|
|
27
|
+
user_id = None
|
|
28
|
+
thread_id = None
|
|
29
|
+
message_id = None
|
|
30
|
+
is_topic = None
|
|
31
|
+
is_edited = None
|
|
32
|
+
key = None
|
|
33
|
+
if update.message:
|
|
34
|
+
chat_id = update.message.chat_id
|
|
35
|
+
user_id = update.message.from_user_id
|
|
36
|
+
thread_id = update.message.thread_id
|
|
37
|
+
message_id = update.message.message_id
|
|
38
|
+
is_topic = update.message.is_topic_message
|
|
39
|
+
is_edited = update.message.is_edited
|
|
40
|
+
key = handlers._resolve_topic_key(chat_id, thread_id)
|
|
41
|
+
elif update.callback:
|
|
42
|
+
chat_id = update.callback.chat_id
|
|
43
|
+
user_id = update.callback.from_user_id
|
|
44
|
+
thread_id = update.callback.thread_id
|
|
45
|
+
message_id = update.callback.message_id
|
|
46
|
+
if chat_id is not None:
|
|
47
|
+
key = handlers._resolve_topic_key(chat_id, thread_id)
|
|
48
|
+
return DispatchContext(
|
|
49
|
+
chat_id=chat_id,
|
|
50
|
+
user_id=user_id,
|
|
51
|
+
thread_id=thread_id,
|
|
52
|
+
message_id=message_id,
|
|
53
|
+
is_topic=is_topic,
|
|
54
|
+
is_edited=is_edited,
|
|
55
|
+
topic_key=key,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _log_denied(handlers: Any, update: TelegramUpdate) -> None:
|
|
60
|
+
chat_id = None
|
|
61
|
+
user_id = None
|
|
62
|
+
thread_id = None
|
|
63
|
+
if update.message:
|
|
64
|
+
chat_id = update.message.chat_id
|
|
65
|
+
user_id = update.message.from_user_id
|
|
66
|
+
thread_id = update.message.thread_id
|
|
67
|
+
elif update.callback:
|
|
68
|
+
chat_id = update.callback.chat_id
|
|
69
|
+
user_id = update.callback.from_user_id
|
|
70
|
+
thread_id = update.callback.thread_id
|
|
71
|
+
log_event(
|
|
72
|
+
handlers._logger,
|
|
73
|
+
logging.INFO,
|
|
74
|
+
"telegram.allowlist.denied",
|
|
75
|
+
chat_id=chat_id,
|
|
76
|
+
user_id=user_id,
|
|
77
|
+
thread_id=thread_id,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _dispatch_callback(
|
|
82
|
+
handlers: Any, update: TelegramUpdate, context: DispatchContext
|
|
83
|
+
) -> None:
|
|
84
|
+
callback = update.callback
|
|
85
|
+
if callback is None:
|
|
86
|
+
return
|
|
87
|
+
if context.topic_key:
|
|
88
|
+
handlers._enqueue_topic_work(
|
|
89
|
+
context.topic_key,
|
|
90
|
+
lambda: handlers._handle_callback(callback),
|
|
91
|
+
force_queue=True,
|
|
92
|
+
)
|
|
93
|
+
return
|
|
94
|
+
await handlers._handle_callback(callback)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def _dispatch_message(
|
|
98
|
+
handlers: Any, update: TelegramUpdate, context: DispatchContext
|
|
99
|
+
) -> None:
|
|
100
|
+
message = update.message
|
|
101
|
+
if message is None:
|
|
102
|
+
return
|
|
103
|
+
if context.topic_key:
|
|
104
|
+
if handlers._should_bypass_topic_queue(message):
|
|
105
|
+
await handlers._handle_message(message)
|
|
106
|
+
return
|
|
107
|
+
handlers._enqueue_topic_work(
|
|
108
|
+
context.topic_key,
|
|
109
|
+
lambda: handlers._handle_message(message),
|
|
110
|
+
force_queue=True,
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
await handlers._handle_message(message)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
_ROUTES: tuple[tuple[str, DispatchRoute], ...] = (
|
|
117
|
+
("callback", _dispatch_callback),
|
|
118
|
+
("message", _dispatch_message),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def dispatch_update(handlers: Any, update: TelegramUpdate) -> None:
|
|
123
|
+
context = _build_context(handlers, update)
|
|
124
|
+
log_event(
|
|
125
|
+
handlers._logger,
|
|
126
|
+
logging.INFO,
|
|
127
|
+
"telegram.update.received",
|
|
128
|
+
update_id=update.update_id,
|
|
129
|
+
chat_id=context.chat_id,
|
|
130
|
+
user_id=context.user_id,
|
|
131
|
+
thread_id=context.thread_id,
|
|
132
|
+
message_id=context.message_id,
|
|
133
|
+
is_topic=context.is_topic,
|
|
134
|
+
is_edited=context.is_edited,
|
|
135
|
+
has_message=bool(update.message),
|
|
136
|
+
has_callback=bool(update.callback),
|
|
137
|
+
)
|
|
138
|
+
if (
|
|
139
|
+
update.update_id is not None
|
|
140
|
+
and context.topic_key
|
|
141
|
+
and not handlers._should_process_update(context.topic_key, update.update_id)
|
|
142
|
+
):
|
|
143
|
+
log_event(
|
|
144
|
+
handlers._logger,
|
|
145
|
+
logging.INFO,
|
|
146
|
+
"telegram.update.duplicate",
|
|
147
|
+
update_id=update.update_id,
|
|
148
|
+
chat_id=context.chat_id,
|
|
149
|
+
thread_id=context.thread_id,
|
|
150
|
+
message_id=context.message_id,
|
|
151
|
+
)
|
|
152
|
+
return
|
|
153
|
+
if not allowlist_allows(update, handlers._allowlist):
|
|
154
|
+
_log_denied(handlers, update)
|
|
155
|
+
return
|
|
156
|
+
for name, route in _ROUTES:
|
|
157
|
+
if name == "callback" and update.callback:
|
|
158
|
+
await route(handlers, update, context)
|
|
159
|
+
return
|
|
160
|
+
if name == "message" and update.message:
|
|
161
|
+
await route(handlers, update, context)
|
|
162
|
+
return
|
|
File without changes
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ....core.logging_utils import log_event
|
|
8
|
+
from ....core.state import now_iso
|
|
9
|
+
from ...app_server.client import ApprovalDecision
|
|
10
|
+
from ..adapter import ApprovalCallback, TelegramCallbackQuery, build_approval_keyboard
|
|
11
|
+
from ..config import DEFAULT_APPROVAL_TIMEOUT_SECONDS
|
|
12
|
+
from ..helpers import (
|
|
13
|
+
_approval_age_seconds,
|
|
14
|
+
_coerce_id,
|
|
15
|
+
_extract_turn_thread_id,
|
|
16
|
+
_format_approval_decision,
|
|
17
|
+
_format_approval_prompt,
|
|
18
|
+
)
|
|
19
|
+
from ..state import PendingApprovalRecord
|
|
20
|
+
from ..types import PendingApproval
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TelegramApprovalHandlers:
|
|
24
|
+
async def _restore_pending_approvals(self) -> None:
|
|
25
|
+
state = self._store.load()
|
|
26
|
+
if not state.pending_approvals:
|
|
27
|
+
return
|
|
28
|
+
grouped: dict[tuple[int, int | None], list[PendingApprovalRecord]] = {}
|
|
29
|
+
for record in state.pending_approvals.values():
|
|
30
|
+
key = (record.chat_id, record.thread_id)
|
|
31
|
+
grouped.setdefault(key, []).append(record)
|
|
32
|
+
for (chat_id, thread_id), records in grouped.items():
|
|
33
|
+
items = []
|
|
34
|
+
for record in records:
|
|
35
|
+
age = _approval_age_seconds(record.created_at)
|
|
36
|
+
age_label = f"{age}s" if isinstance(age, int) else "unknown age"
|
|
37
|
+
items.append(f"{record.request_id} ({age_label})")
|
|
38
|
+
self._store.clear_pending_approval(record.request_id)
|
|
39
|
+
message = (
|
|
40
|
+
"Cleared stale approval requests from a previous session. "
|
|
41
|
+
"Re-run the request or use /interrupt if the turn is still active.\n"
|
|
42
|
+
f"Requests: {', '.join(items)}"
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
await self._send_message(
|
|
46
|
+
chat_id,
|
|
47
|
+
message,
|
|
48
|
+
thread_id=thread_id,
|
|
49
|
+
)
|
|
50
|
+
except Exception:
|
|
51
|
+
log_event(
|
|
52
|
+
self._logger,
|
|
53
|
+
logging.WARNING,
|
|
54
|
+
"telegram.approval.restore_failed",
|
|
55
|
+
chat_id=chat_id,
|
|
56
|
+
thread_id=thread_id,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
async def _handle_approval_request(
|
|
60
|
+
self, message: dict[str, Any]
|
|
61
|
+
) -> ApprovalDecision:
|
|
62
|
+
req_id = message.get("id")
|
|
63
|
+
params = (
|
|
64
|
+
message.get("params") if isinstance(message.get("params"), dict) else {}
|
|
65
|
+
)
|
|
66
|
+
turn_id = _coerce_id(params.get("turnId")) if isinstance(params, dict) else None
|
|
67
|
+
if not req_id or not turn_id:
|
|
68
|
+
return "cancel"
|
|
69
|
+
codex_thread_id = _extract_turn_thread_id(params)
|
|
70
|
+
ctx = self._resolve_turn_context(turn_id, thread_id=codex_thread_id)
|
|
71
|
+
if ctx is None:
|
|
72
|
+
return "cancel"
|
|
73
|
+
request_id = str(req_id)
|
|
74
|
+
prompt = _format_approval_prompt(message)
|
|
75
|
+
created_at = now_iso()
|
|
76
|
+
approval_record = PendingApprovalRecord(
|
|
77
|
+
request_id=request_id,
|
|
78
|
+
turn_id=str(turn_id),
|
|
79
|
+
chat_id=ctx.chat_id,
|
|
80
|
+
thread_id=ctx.thread_id,
|
|
81
|
+
message_id=None,
|
|
82
|
+
prompt=prompt,
|
|
83
|
+
created_at=created_at,
|
|
84
|
+
topic_key=ctx.topic_key,
|
|
85
|
+
)
|
|
86
|
+
self._store.upsert_pending_approval(approval_record)
|
|
87
|
+
log_event(
|
|
88
|
+
self._logger,
|
|
89
|
+
logging.INFO,
|
|
90
|
+
"telegram.approval.requested",
|
|
91
|
+
request_id=request_id,
|
|
92
|
+
turn_id=turn_id,
|
|
93
|
+
chat_id=ctx.chat_id,
|
|
94
|
+
thread_id=ctx.thread_id,
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
keyboard = build_approval_keyboard(request_id, include_session=False)
|
|
98
|
+
except ValueError:
|
|
99
|
+
log_event(
|
|
100
|
+
self._logger,
|
|
101
|
+
logging.WARNING,
|
|
102
|
+
"telegram.approval.callback_too_long",
|
|
103
|
+
request_id=request_id,
|
|
104
|
+
)
|
|
105
|
+
self._store.clear_pending_approval(request_id)
|
|
106
|
+
return "cancel"
|
|
107
|
+
payload_text, parse_mode = self._prepare_outgoing_text(
|
|
108
|
+
prompt,
|
|
109
|
+
chat_id=ctx.chat_id,
|
|
110
|
+
thread_id=ctx.thread_id,
|
|
111
|
+
reply_to=ctx.reply_to_message_id,
|
|
112
|
+
topic_key=ctx.topic_key,
|
|
113
|
+
codex_thread_id=codex_thread_id,
|
|
114
|
+
)
|
|
115
|
+
try:
|
|
116
|
+
response = await self._bot.send_message(
|
|
117
|
+
ctx.chat_id,
|
|
118
|
+
payload_text,
|
|
119
|
+
message_thread_id=ctx.thread_id,
|
|
120
|
+
reply_to_message_id=ctx.reply_to_message_id,
|
|
121
|
+
reply_markup=keyboard,
|
|
122
|
+
parse_mode=parse_mode,
|
|
123
|
+
)
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
log_event(
|
|
126
|
+
self._logger,
|
|
127
|
+
logging.WARNING,
|
|
128
|
+
"telegram.approval.send_failed",
|
|
129
|
+
request_id=request_id,
|
|
130
|
+
turn_id=turn_id,
|
|
131
|
+
chat_id=ctx.chat_id,
|
|
132
|
+
thread_id=ctx.thread_id,
|
|
133
|
+
exc=exc,
|
|
134
|
+
)
|
|
135
|
+
self._store.clear_pending_approval(request_id)
|
|
136
|
+
try:
|
|
137
|
+
await self._send_message(
|
|
138
|
+
ctx.chat_id,
|
|
139
|
+
"Approval prompt failed to send; canceling approval. "
|
|
140
|
+
"Please retry or use /interrupt.",
|
|
141
|
+
thread_id=ctx.thread_id,
|
|
142
|
+
reply_to=ctx.reply_to_message_id,
|
|
143
|
+
)
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
return "cancel"
|
|
147
|
+
message_id = response.get("message_id") if isinstance(response, dict) else None
|
|
148
|
+
if isinstance(message_id, int):
|
|
149
|
+
approval_record.message_id = message_id
|
|
150
|
+
self._store.upsert_pending_approval(approval_record)
|
|
151
|
+
loop = asyncio.get_running_loop()
|
|
152
|
+
future: asyncio.Future[ApprovalDecision] = loop.create_future()
|
|
153
|
+
pending = PendingApproval(
|
|
154
|
+
request_id=request_id,
|
|
155
|
+
turn_id=str(turn_id),
|
|
156
|
+
codex_thread_id=codex_thread_id,
|
|
157
|
+
chat_id=ctx.chat_id,
|
|
158
|
+
thread_id=ctx.thread_id,
|
|
159
|
+
topic_key=ctx.topic_key,
|
|
160
|
+
message_id=message_id if isinstance(message_id, int) else None,
|
|
161
|
+
created_at=created_at,
|
|
162
|
+
future=future,
|
|
163
|
+
)
|
|
164
|
+
self._pending_approvals[request_id] = pending
|
|
165
|
+
self._touch_cache_timestamp("pending_approvals", request_id)
|
|
166
|
+
runtime = self._router.runtime_for(ctx.topic_key)
|
|
167
|
+
runtime.pending_request_id = request_id
|
|
168
|
+
try:
|
|
169
|
+
return await asyncio.wait_for(
|
|
170
|
+
future, timeout=DEFAULT_APPROVAL_TIMEOUT_SECONDS
|
|
171
|
+
)
|
|
172
|
+
except asyncio.TimeoutError:
|
|
173
|
+
self._pending_approvals.pop(request_id, None)
|
|
174
|
+
self._store.clear_pending_approval(request_id)
|
|
175
|
+
runtime.pending_request_id = None
|
|
176
|
+
log_event(
|
|
177
|
+
self._logger,
|
|
178
|
+
logging.WARNING,
|
|
179
|
+
"telegram.approval.timeout",
|
|
180
|
+
request_id=request_id,
|
|
181
|
+
turn_id=turn_id,
|
|
182
|
+
chat_id=ctx.chat_id,
|
|
183
|
+
thread_id=ctx.thread_id,
|
|
184
|
+
timeout_seconds=DEFAULT_APPROVAL_TIMEOUT_SECONDS,
|
|
185
|
+
)
|
|
186
|
+
if pending.message_id is not None:
|
|
187
|
+
await self._edit_message_text(
|
|
188
|
+
pending.chat_id,
|
|
189
|
+
pending.message_id,
|
|
190
|
+
"Approval timed out.",
|
|
191
|
+
reply_markup={"inline_keyboard": []},
|
|
192
|
+
)
|
|
193
|
+
return "cancel"
|
|
194
|
+
except asyncio.CancelledError:
|
|
195
|
+
self._pending_approvals.pop(request_id, None)
|
|
196
|
+
self._store.clear_pending_approval(request_id)
|
|
197
|
+
runtime.pending_request_id = None
|
|
198
|
+
raise
|
|
199
|
+
|
|
200
|
+
async def _handle_approval_callback(
|
|
201
|
+
self, callback: TelegramCallbackQuery, parsed: ApprovalCallback
|
|
202
|
+
) -> None:
|
|
203
|
+
self._store.clear_pending_approval(parsed.request_id)
|
|
204
|
+
pending = self._pending_approvals.pop(parsed.request_id, None)
|
|
205
|
+
if pending is None:
|
|
206
|
+
await self._answer_callback(callback, "Approval already handled")
|
|
207
|
+
return
|
|
208
|
+
if not pending.future.done():
|
|
209
|
+
pending.future.set_result(parsed.decision)
|
|
210
|
+
ctx = self._resolve_turn_context(
|
|
211
|
+
pending.turn_id, thread_id=pending.codex_thread_id
|
|
212
|
+
)
|
|
213
|
+
if ctx:
|
|
214
|
+
runtime_key = ctx.topic_key
|
|
215
|
+
elif pending.topic_key:
|
|
216
|
+
runtime_key = pending.topic_key
|
|
217
|
+
else:
|
|
218
|
+
runtime_key = self._resolve_topic_key(pending.chat_id, pending.thread_id)
|
|
219
|
+
runtime = self._router.runtime_for(runtime_key)
|
|
220
|
+
runtime.pending_request_id = None
|
|
221
|
+
log_event(
|
|
222
|
+
self._logger,
|
|
223
|
+
logging.INFO,
|
|
224
|
+
"telegram.approval.decision",
|
|
225
|
+
request_id=parsed.request_id,
|
|
226
|
+
decision=parsed.decision,
|
|
227
|
+
chat_id=callback.chat_id,
|
|
228
|
+
thread_id=callback.thread_id,
|
|
229
|
+
message_id=callback.message_id,
|
|
230
|
+
)
|
|
231
|
+
await self._answer_callback(callback, f"Decision: {parsed.decision}")
|
|
232
|
+
if pending.message_id is not None:
|
|
233
|
+
try:
|
|
234
|
+
await self._edit_message_text(
|
|
235
|
+
pending.chat_id,
|
|
236
|
+
pending.message_id,
|
|
237
|
+
_format_approval_decision(parsed.decision),
|
|
238
|
+
reply_markup={"inline_keyboard": []},
|
|
239
|
+
)
|
|
240
|
+
except Exception:
|
|
241
|
+
return
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Sequence
|
|
4
|
+
|
|
5
|
+
from ..adapter import (
|
|
6
|
+
ApprovalCallback,
|
|
7
|
+
BindCallback,
|
|
8
|
+
CancelCallback,
|
|
9
|
+
CompactCallback,
|
|
10
|
+
EffortCallback,
|
|
11
|
+
ModelCallback,
|
|
12
|
+
PageCallback,
|
|
13
|
+
ResumeCallback,
|
|
14
|
+
ReviewCommitCallback,
|
|
15
|
+
TelegramCallbackQuery,
|
|
16
|
+
UpdateCallback,
|
|
17
|
+
UpdateConfirmCallback,
|
|
18
|
+
parse_callback_data,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _selection_contains(items: Sequence[tuple[str, str]], value: str) -> bool:
|
|
23
|
+
return any(item_id == value for item_id, _ in items)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def handle_callback(handlers: Any, callback: TelegramCallbackQuery) -> None:
|
|
27
|
+
parsed = parse_callback_data(callback.data)
|
|
28
|
+
if parsed is None:
|
|
29
|
+
return
|
|
30
|
+
key = None
|
|
31
|
+
if callback.chat_id is not None:
|
|
32
|
+
key = handlers._resolve_topic_key(callback.chat_id, callback.thread_id)
|
|
33
|
+
if isinstance(parsed, ApprovalCallback):
|
|
34
|
+
await handlers._handle_approval_callback(callback, parsed)
|
|
35
|
+
elif isinstance(parsed, ResumeCallback):
|
|
36
|
+
if key:
|
|
37
|
+
state = handlers._resume_options.get(key)
|
|
38
|
+
if not state or not _selection_contains(state.items, parsed.thread_id):
|
|
39
|
+
await handlers._answer_callback(callback, "Selection expired")
|
|
40
|
+
return
|
|
41
|
+
await handlers._resume_thread_by_id(key, parsed.thread_id, callback)
|
|
42
|
+
elif isinstance(parsed, BindCallback):
|
|
43
|
+
if key:
|
|
44
|
+
state = handlers._bind_options.get(key)
|
|
45
|
+
if not state or not _selection_contains(state.items, parsed.repo_id):
|
|
46
|
+
await handlers._answer_callback(callback, "Selection expired")
|
|
47
|
+
return
|
|
48
|
+
await handlers._bind_topic_by_repo_id(key, parsed.repo_id, callback)
|
|
49
|
+
elif isinstance(parsed, ModelCallback):
|
|
50
|
+
if key:
|
|
51
|
+
await handlers._handle_model_callback(key, callback, parsed)
|
|
52
|
+
elif isinstance(parsed, EffortCallback):
|
|
53
|
+
if key:
|
|
54
|
+
await handlers._handle_effort_callback(key, callback, parsed)
|
|
55
|
+
elif isinstance(parsed, UpdateCallback):
|
|
56
|
+
if key:
|
|
57
|
+
await handlers._handle_update_callback(key, callback, parsed)
|
|
58
|
+
elif isinstance(parsed, UpdateConfirmCallback):
|
|
59
|
+
if key:
|
|
60
|
+
await handlers._handle_update_confirm_callback(key, callback, parsed)
|
|
61
|
+
elif isinstance(parsed, ReviewCommitCallback):
|
|
62
|
+
if key:
|
|
63
|
+
await handlers._handle_review_commit_callback(key, callback, parsed)
|
|
64
|
+
elif isinstance(parsed, CancelCallback):
|
|
65
|
+
if key:
|
|
66
|
+
await handlers._handle_selection_cancel(key, parsed, callback)
|
|
67
|
+
elif isinstance(parsed, CompactCallback):
|
|
68
|
+
if key:
|
|
69
|
+
await handlers._handle_compact_callback(key, callback, parsed)
|
|
70
|
+
elif isinstance(parsed, PageCallback):
|
|
71
|
+
if key:
|
|
72
|
+
await handlers._handle_selection_page(key, parsed, callback)
|