codex-autorunner 0.1.2__py3-none-any.whl → 1.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.
Files changed (276) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/__main__.py +4 -0
  3. codex_autorunner/agents/codex/harness.py +1 -1
  4. codex_autorunner/agents/opencode/client.py +68 -35
  5. codex_autorunner/agents/opencode/constants.py +3 -0
  6. codex_autorunner/agents/opencode/harness.py +6 -1
  7. codex_autorunner/agents/opencode/logging.py +21 -5
  8. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  9. codex_autorunner/agents/opencode/runtime.py +176 -47
  10. codex_autorunner/agents/opencode/supervisor.py +36 -48
  11. codex_autorunner/agents/registry.py +155 -8
  12. codex_autorunner/api.py +25 -0
  13. codex_autorunner/bootstrap.py +22 -37
  14. codex_autorunner/cli.py +5 -1156
  15. codex_autorunner/codex_cli.py +20 -84
  16. codex_autorunner/core/__init__.py +4 -0
  17. codex_autorunner/core/about_car.py +49 -32
  18. codex_autorunner/core/adapter_utils.py +21 -0
  19. codex_autorunner/core/app_server_ids.py +59 -0
  20. codex_autorunner/core/app_server_logging.py +7 -3
  21. codex_autorunner/core/app_server_prompts.py +27 -260
  22. codex_autorunner/core/app_server_threads.py +26 -28
  23. codex_autorunner/core/app_server_utils.py +165 -0
  24. codex_autorunner/core/archive.py +349 -0
  25. codex_autorunner/core/codex_runner.py +12 -2
  26. codex_autorunner/core/config.py +587 -103
  27. codex_autorunner/core/docs.py +10 -2
  28. codex_autorunner/core/drafts.py +136 -0
  29. codex_autorunner/core/engine.py +1531 -866
  30. codex_autorunner/core/exceptions.py +4 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +202 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +88 -0
  35. codex_autorunner/core/flows/reasons.py +52 -0
  36. codex_autorunner/core/flows/reconciler.py +131 -0
  37. codex_autorunner/core/flows/runtime.py +382 -0
  38. codex_autorunner/core/flows/store.py +568 -0
  39. codex_autorunner/core/flows/transition.py +138 -0
  40. codex_autorunner/core/flows/ux_helpers.py +257 -0
  41. codex_autorunner/core/flows/worker_process.py +242 -0
  42. codex_autorunner/core/git_utils.py +62 -0
  43. codex_autorunner/core/hub.py +136 -16
  44. codex_autorunner/core/locks.py +4 -0
  45. codex_autorunner/core/notifications.py +14 -2
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/core/ports/agent_backend.py +150 -0
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/core/ports/run_event.py +91 -0
  50. codex_autorunner/core/prompt.py +15 -7
  51. codex_autorunner/core/redaction.py +29 -0
  52. codex_autorunner/core/review_context.py +5 -8
  53. codex_autorunner/core/run_index.py +6 -0
  54. codex_autorunner/core/runner_process.py +5 -2
  55. codex_autorunner/core/state.py +0 -88
  56. codex_autorunner/core/state_roots.py +57 -0
  57. codex_autorunner/core/supervisor_protocol.py +15 -0
  58. codex_autorunner/core/supervisor_utils.py +67 -0
  59. codex_autorunner/core/text_delta_coalescer.py +54 -0
  60. codex_autorunner/core/ticket_linter_cli.py +201 -0
  61. codex_autorunner/core/ticket_manager_cli.py +432 -0
  62. codex_autorunner/core/update.py +24 -16
  63. codex_autorunner/core/update_paths.py +28 -0
  64. codex_autorunner/core/update_runner.py +2 -0
  65. codex_autorunner/core/usage.py +164 -12
  66. codex_autorunner/core/utils.py +120 -11
  67. codex_autorunner/discovery.py +2 -4
  68. codex_autorunner/flows/review/__init__.py +17 -0
  69. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  70. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  71. codex_autorunner/flows/ticket_flow/definition.py +98 -0
  72. codex_autorunner/integrations/agents/__init__.py +17 -0
  73. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  74. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  75. codex_autorunner/integrations/agents/codex_backend.py +448 -0
  76. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  77. codex_autorunner/integrations/agents/opencode_backend.py +598 -0
  78. codex_autorunner/integrations/agents/runner.py +91 -0
  79. codex_autorunner/integrations/agents/wiring.py +271 -0
  80. codex_autorunner/integrations/app_server/client.py +583 -152
  81. codex_autorunner/integrations/app_server/env.py +2 -107
  82. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  83. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  84. codex_autorunner/integrations/telegram/adapter.py +204 -165
  85. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  86. codex_autorunner/integrations/telegram/config.py +221 -0
  87. codex_autorunner/integrations/telegram/constants.py +17 -2
  88. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  89. codex_autorunner/integrations/telegram/doctor.py +47 -0
  90. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
  91. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  92. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  93. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  94. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
  95. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  96. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  97. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  98. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
  99. codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
  100. codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
  101. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  102. codex_autorunner/integrations/telegram/helpers.py +111 -16
  103. codex_autorunner/integrations/telegram/outbox.py +208 -37
  104. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  105. codex_autorunner/integrations/telegram/service.py +221 -42
  106. codex_autorunner/integrations/telegram/state.py +100 -2
  107. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
  108. codex_autorunner/integrations/telegram/transport.py +39 -4
  109. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  110. codex_autorunner/manifest.py +2 -0
  111. codex_autorunner/plugin_api.py +22 -0
  112. codex_autorunner/routes/__init__.py +37 -67
  113. codex_autorunner/routes/agents.py +2 -137
  114. codex_autorunner/routes/analytics.py +3 -0
  115. codex_autorunner/routes/app_server.py +2 -131
  116. codex_autorunner/routes/base.py +2 -624
  117. codex_autorunner/routes/file_chat.py +7 -0
  118. codex_autorunner/routes/flows.py +7 -0
  119. codex_autorunner/routes/messages.py +7 -0
  120. codex_autorunner/routes/repos.py +2 -196
  121. codex_autorunner/routes/review.py +2 -147
  122. codex_autorunner/routes/sessions.py +2 -175
  123. codex_autorunner/routes/settings.py +2 -168
  124. codex_autorunner/routes/shared.py +2 -275
  125. codex_autorunner/routes/system.py +4 -188
  126. codex_autorunner/routes/usage.py +3 -0
  127. codex_autorunner/routes/voice.py +2 -119
  128. codex_autorunner/routes/workspace.py +3 -0
  129. codex_autorunner/server.py +3 -2
  130. codex_autorunner/static/agentControls.js +41 -11
  131. codex_autorunner/static/agentEvents.js +248 -0
  132. codex_autorunner/static/app.js +35 -24
  133. codex_autorunner/static/archive.js +826 -0
  134. codex_autorunner/static/archiveApi.js +37 -0
  135. codex_autorunner/static/autoRefresh.js +36 -8
  136. codex_autorunner/static/bootstrap.js +1 -0
  137. codex_autorunner/static/bus.js +1 -0
  138. codex_autorunner/static/cache.js +1 -0
  139. codex_autorunner/static/constants.js +20 -4
  140. codex_autorunner/static/dashboard.js +344 -325
  141. codex_autorunner/static/diffRenderer.js +37 -0
  142. codex_autorunner/static/docChatCore.js +324 -0
  143. codex_autorunner/static/docChatStorage.js +65 -0
  144. codex_autorunner/static/docChatVoice.js +65 -0
  145. codex_autorunner/static/docEditor.js +133 -0
  146. codex_autorunner/static/env.js +1 -0
  147. codex_autorunner/static/eventSummarizer.js +166 -0
  148. codex_autorunner/static/fileChat.js +182 -0
  149. codex_autorunner/static/health.js +155 -0
  150. codex_autorunner/static/hub.js +126 -185
  151. codex_autorunner/static/index.html +839 -863
  152. codex_autorunner/static/liveUpdates.js +1 -0
  153. codex_autorunner/static/loader.js +1 -0
  154. codex_autorunner/static/messages.js +873 -0
  155. codex_autorunner/static/mobileCompact.js +2 -1
  156. codex_autorunner/static/preserve.js +17 -0
  157. codex_autorunner/static/settings.js +149 -217
  158. codex_autorunner/static/smartRefresh.js +52 -0
  159. codex_autorunner/static/styles.css +8850 -3876
  160. codex_autorunner/static/tabs.js +175 -11
  161. codex_autorunner/static/terminal.js +32 -0
  162. codex_autorunner/static/terminalManager.js +34 -59
  163. codex_autorunner/static/ticketChatActions.js +333 -0
  164. codex_autorunner/static/ticketChatEvents.js +16 -0
  165. codex_autorunner/static/ticketChatStorage.js +16 -0
  166. codex_autorunner/static/ticketChatStream.js +264 -0
  167. codex_autorunner/static/ticketEditor.js +844 -0
  168. codex_autorunner/static/ticketVoice.js +9 -0
  169. codex_autorunner/static/tickets.js +1988 -0
  170. codex_autorunner/static/utils.js +43 -3
  171. codex_autorunner/static/voice.js +1 -0
  172. codex_autorunner/static/workspace.js +765 -0
  173. codex_autorunner/static/workspaceApi.js +53 -0
  174. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  175. codex_autorunner/surfaces/__init__.py +5 -0
  176. codex_autorunner/surfaces/cli/__init__.py +6 -0
  177. codex_autorunner/surfaces/cli/cli.py +1224 -0
  178. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  179. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  180. codex_autorunner/surfaces/web/__init__.py +1 -0
  181. codex_autorunner/surfaces/web/app.py +2019 -0
  182. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  183. codex_autorunner/surfaces/web/middleware.py +587 -0
  184. codex_autorunner/surfaces/web/pty_session.py +370 -0
  185. codex_autorunner/surfaces/web/review.py +6 -0
  186. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  187. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  188. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  189. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  190. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  191. codex_autorunner/surfaces/web/routes/base.py +615 -0
  192. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  193. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  194. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  195. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  196. codex_autorunner/surfaces/web/routes/review.py +148 -0
  197. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  198. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  199. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  200. codex_autorunner/surfaces/web/routes/system.py +196 -0
  201. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  202. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  203. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  204. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  205. codex_autorunner/surfaces/web/schemas.py +417 -0
  206. codex_autorunner/surfaces/web/static_assets.py +490 -0
  207. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  208. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  209. codex_autorunner/tickets/__init__.py +27 -0
  210. codex_autorunner/tickets/agent_pool.py +399 -0
  211. codex_autorunner/tickets/files.py +89 -0
  212. codex_autorunner/tickets/frontmatter.py +55 -0
  213. codex_autorunner/tickets/lint.py +102 -0
  214. codex_autorunner/tickets/models.py +97 -0
  215. codex_autorunner/tickets/outbox.py +244 -0
  216. codex_autorunner/tickets/replies.py +179 -0
  217. codex_autorunner/tickets/runner.py +881 -0
  218. codex_autorunner/tickets/spec_ingest.py +77 -0
  219. codex_autorunner/web/__init__.py +5 -1
  220. codex_autorunner/web/app.py +2 -1771
  221. codex_autorunner/web/hub_jobs.py +2 -191
  222. codex_autorunner/web/middleware.py +2 -587
  223. codex_autorunner/web/pty_session.py +2 -369
  224. codex_autorunner/web/runner_manager.py +2 -24
  225. codex_autorunner/web/schemas.py +2 -396
  226. codex_autorunner/web/static_assets.py +4 -484
  227. codex_autorunner/web/static_refresh.py +2 -85
  228. codex_autorunner/web/terminal_sessions.py +2 -77
  229. codex_autorunner/workspace/__init__.py +40 -0
  230. codex_autorunner/workspace/paths.py +335 -0
  231. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  232. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  233. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
  234. codex_autorunner/agents/execution/policy.py +0 -292
  235. codex_autorunner/agents/factory.py +0 -52
  236. codex_autorunner/agents/orchestrator.py +0 -358
  237. codex_autorunner/core/doc_chat.py +0 -1446
  238. codex_autorunner/core/snapshot.py +0 -580
  239. codex_autorunner/integrations/github/chatops.py +0 -268
  240. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  241. codex_autorunner/routes/docs.py +0 -381
  242. codex_autorunner/routes/github.py +0 -327
  243. codex_autorunner/routes/runs.py +0 -250
  244. codex_autorunner/spec_ingest.py +0 -812
  245. codex_autorunner/static/docChatActions.js +0 -287
  246. codex_autorunner/static/docChatEvents.js +0 -300
  247. codex_autorunner/static/docChatRender.js +0 -205
  248. codex_autorunner/static/docChatStream.js +0 -361
  249. codex_autorunner/static/docs.js +0 -20
  250. codex_autorunner/static/docsClipboard.js +0 -69
  251. codex_autorunner/static/docsCrud.js +0 -257
  252. codex_autorunner/static/docsDocUpdates.js +0 -62
  253. codex_autorunner/static/docsDrafts.js +0 -16
  254. codex_autorunner/static/docsElements.js +0 -69
  255. codex_autorunner/static/docsInit.js +0 -285
  256. codex_autorunner/static/docsParse.js +0 -160
  257. codex_autorunner/static/docsSnapshot.js +0 -87
  258. codex_autorunner/static/docsSpecIngest.js +0 -263
  259. codex_autorunner/static/docsState.js +0 -127
  260. codex_autorunner/static/docsThreadRegistry.js +0 -44
  261. codex_autorunner/static/docsUi.js +0 -153
  262. codex_autorunner/static/docsVoice.js +0 -56
  263. codex_autorunner/static/github.js +0 -504
  264. codex_autorunner/static/logs.js +0 -678
  265. codex_autorunner/static/review.js +0 -157
  266. codex_autorunner/static/runs.js +0 -418
  267. codex_autorunner/static/snapshot.js +0 -124
  268. codex_autorunner/static/state.js +0 -94
  269. codex_autorunner/static/todoPreview.js +0 -27
  270. codex_autorunner/workspace.py +0 -16
  271. codex_autorunner-0.1.2.dist-info/METADATA +0 -249
  272. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  273. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  274. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  275. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  276. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -7,24 +7,26 @@ import logging
7
7
  import os
8
8
  import socket
9
9
  import time
10
+ from datetime import datetime
10
11
  from pathlib import Path
11
- from typing import TYPE_CHECKING, Any, Coroutine, Optional, Sequence
12
+ from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional, Sequence
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from .progress_stream import TurnProgressTracker
15
16
  from .state import TelegramTopicRecord
16
17
 
17
18
  from ...agents.opencode.supervisor import OpenCodeSupervisor
19
+ from ...core.flows.models import FlowRunRecord
18
20
  from ...core.locks import process_alive
19
21
  from ...core.logging_utils import log_event
20
22
  from ...core.request_context import reset_conversation_id, set_conversation_id
21
23
  from ...core.state import now_iso
24
+ from ...core.state_roots import resolve_global_state_root
22
25
  from ...core.text_delta_coalescer import TextDeltaCoalescer
23
- from ...core.utils import (
24
- build_opencode_supervisor,
25
- )
26
+ from ...core.utils import build_opencode_supervisor
26
27
  from ...housekeeping import HousekeepingConfig, run_housekeeping_for_roots
27
28
  from ...manifest import load_manifest
29
+ from ...tickets.replies import dispatch_reply, ensure_reply_dirs, resolve_reply_paths
28
30
  from ...voice import VoiceConfig, VoiceService
29
31
  from ..app_server.supervisor import WorkspaceAppServerSupervisor
30
32
  from .adapter import (
@@ -45,21 +47,8 @@ from .config import (
45
47
  TelegramMediaCandidate,
46
48
  )
47
49
  from .constants import (
48
- CACHE_CLEANUP_INTERVAL_SECONDS,
49
- COALESCE_BUFFER_TTL_SECONDS,
50
50
  DEFAULT_INTERRUPT_TIMEOUT_SECONDS,
51
- DEFAULT_WORKSPACE_STATE_ROOT,
52
- MEDIA_BATCH_BUFFER_TTL_SECONDS,
53
- MODEL_PENDING_TTL_SECONDS,
54
- OVERSIZE_WARNING_TTL_SECONDS,
55
- PENDING_APPROVAL_TTL_SECONDS,
56
- PENDING_QUESTION_TTL_SECONDS,
57
- PROGRESS_STREAM_TTL_SECONDS,
58
51
  QUEUED_PLACEHOLDER_TEXT,
59
- REASONING_BUFFER_TTL_SECONDS,
60
- SELECTION_STATE_TTL_SECONDS,
61
- TURN_PREVIEW_TTL_SECONDS,
62
- UPDATE_ID_PERSIST_INTERVAL_SECONDS,
63
52
  TurnKey,
64
53
  )
65
54
  from .dispatch import dispatch_update
@@ -88,6 +77,7 @@ from .state import (
88
77
  parse_topic_key,
89
78
  topic_key,
90
79
  )
80
+ from .ticket_flow_bridge import TelegramTicketFlowBridge
91
81
  from .transport import TelegramMessageTransport
92
82
  from .types import (
93
83
  CompactState,
@@ -100,17 +90,19 @@ from .types import (
100
90
  )
101
91
  from .voice import TelegramVoiceManager
102
92
 
93
+ TICKET_FLOW_WATCH_INTERVAL_SECONDS = 20
94
+
103
95
 
104
96
  def _build_opencode_supervisor(
105
97
  config: TelegramBotConfig,
106
98
  *,
107
99
  logger: logging.Logger,
108
100
  ) -> Optional[OpenCodeSupervisor]:
109
- raw_command = os.environ.get("CAR_OPENCODE_COMMAND")
101
+ opencode_command = config.opencode_command or None
110
102
  opencode_binary = config.agent_binaries.get("opencode")
111
103
 
112
104
  supervisor = build_opencode_supervisor(
113
- opencode_command=[raw_command] if raw_command else None,
105
+ opencode_command=opencode_command,
114
106
  opencode_binary=opencode_binary,
115
107
  workspace_root=config.root,
116
108
  logger=logger,
@@ -133,6 +125,26 @@ def _build_opencode_supervisor(
133
125
  return supervisor
134
126
 
135
127
 
128
+ def _next_reply_seq_sync(reply_history_dir: Any) -> int:
129
+ from pathlib import Path
130
+
131
+ path = Path(reply_history_dir)
132
+ if not path.exists() or not path.is_dir():
133
+ return 1
134
+ existing: list[int] = []
135
+ _SEQ_RE = __import__("re").compile(r"^[0-9]{4}$")
136
+ for child in path.iterdir():
137
+ try:
138
+ if not child.is_dir():
139
+ continue
140
+ if not _SEQ_RE.fullmatch(child.name):
141
+ continue
142
+ existing.append(int(child.name))
143
+ except OSError:
144
+ continue
145
+ return (max(existing) + 1) if existing else 1
146
+
147
+
136
148
  class TelegramBotService(
137
149
  TelegramRuntimeHelpers,
138
150
  TelegramMessageTransport,
@@ -154,6 +166,8 @@ class TelegramBotService(
154
166
  housekeeping_config: Optional[HousekeepingConfig] = None,
155
167
  update_repo_url: Optional[str] = None,
156
168
  update_repo_ref: Optional[str] = None,
169
+ update_skip_checks: bool = False,
170
+ app_server_auto_restart: Optional[bool] = None,
157
171
  ) -> None:
158
172
  self._config = config
159
173
  self._logger = logger or logging.getLogger(__name__)
@@ -161,12 +175,14 @@ class TelegramBotService(
161
175
  self._manifest_path = manifest_path
162
176
  self._update_repo_url = update_repo_url
163
177
  self._update_repo_ref = update_repo_ref
178
+ self._update_skip_checks = update_skip_checks
179
+ self._app_server_auto_restart = app_server_auto_restart
164
180
  self._allowlist = config.allowlist()
165
181
  self._store = TelegramStateStore(
166
182
  config.state_file, default_approval_mode=config.defaults.approval_mode
167
183
  )
168
184
  self._router = TopicRouter(self._store)
169
- self._app_server_state_root = Path(DEFAULT_WORKSPACE_STATE_ROOT).expanduser()
185
+ self._app_server_state_root = resolve_global_state_root() / "workspaces"
170
186
  self._app_server_supervisor = WorkspaceAppServerSupervisor(
171
187
  config.app_server_command,
172
188
  state_root=self._app_server_state_root,
@@ -174,6 +190,7 @@ class TelegramBotService(
174
190
  approval_handler=self._handle_approval_request,
175
191
  notification_handler=self._handle_app_server_notification,
176
192
  logger=self._logger,
193
+ auto_restart=self._app_server_auto_restart,
177
194
  max_handles=config.app_server_max_handles,
178
195
  idle_ttl_seconds=config.app_server_idle_ttl_seconds,
179
196
  )
@@ -224,8 +241,22 @@ class TelegramBotService(
224
241
  self._oversize_warnings: set[TurnKey] = set()
225
242
  self._pending_approvals: dict[str, PendingApproval] = {}
226
243
  self._pending_questions: dict[str, PendingQuestion] = {}
244
+ self._ticket_flow_pause_targets: dict[str, str] = {}
245
+ self._ticket_flow_bridge = TelegramTicketFlowBridge(
246
+ logger=self._logger,
247
+ store=self._store,
248
+ pause_targets=self._ticket_flow_pause_targets,
249
+ send_message_with_outbox=self._send_message_with_outbox,
250
+ send_document=self._send_document,
251
+ pause_config=self._config.pause_dispatch_notifications,
252
+ default_notification_chat_id=self._config.default_notification_chat_id,
253
+ hub_root=hub_root,
254
+ manifest_path=manifest_path,
255
+ config_root=self._config.root,
256
+ )
227
257
  self._resume_options: dict[str, SelectionState] = {}
228
258
  self._bind_options: dict[str, SelectionState] = {}
259
+ self._flow_run_options: dict[str, SelectionState] = {}
229
260
  self._update_options: dict[str, SelectionState] = {}
230
261
  self._update_confirm_options: dict[str, bool] = {}
231
262
  self._review_commit_options: dict[str, ReviewCommitSelectionState] = {}
@@ -249,6 +280,7 @@ class TelegramBotService(
249
280
  )
250
281
  self._outbox_task: Optional[asyncio.Task[None]] = None
251
282
  self._cache_cleanup_task: Optional[asyncio.Task[None]] = None
283
+ self._ticket_flow_watch_task: Optional[asyncio.Task[None]] = None
252
284
  self._cache_timestamps: dict[str, dict[object, float]] = {}
253
285
  self._last_update_ids: dict[str, int] = {}
254
286
  self._last_update_persisted_at: dict[str, float] = {}
@@ -540,6 +572,11 @@ class TelegramBotService(
540
572
  self._voice_task = asyncio.create_task(self._voice_manager.run_loop())
541
573
  self._housekeeping_task = asyncio.create_task(self._housekeeping_loop())
542
574
  self._cache_cleanup_task = asyncio.create_task(self._cache_cleanup_loop())
575
+ self._ticket_flow_watch_task = asyncio.create_task(
576
+ self._ticket_flow_bridge.watch_ticket_flow_pauses(
577
+ TICKET_FLOW_WATCH_INTERVAL_SECONDS
578
+ )
579
+ )
543
580
  self._spawn_task(self._prewarm_workspace_clients())
544
581
  log_event(
545
582
  self._logger,
@@ -557,6 +594,9 @@ class TelegramBotService(
557
594
  media_images=self._config.media.images,
558
595
  media_voice=self._config.media.voice,
559
596
  app_server_turn_timeout_seconds=self._config.app_server_turn_timeout_seconds,
597
+ agent_turn_timeout_seconds=dict(
598
+ self._config.agent_turn_timeout_seconds
599
+ ),
560
600
  poller_offset=self._poller.offset,
561
601
  )
562
602
  try:
@@ -622,6 +662,12 @@ class TelegramBotService(
622
662
  await self._cache_cleanup_task
623
663
  except asyncio.CancelledError:
624
664
  pass
665
+ if self._ticket_flow_watch_task is not None:
666
+ self._ticket_flow_watch_task.cancel()
667
+ try:
668
+ await self._ticket_flow_watch_task
669
+ except asyncio.CancelledError:
670
+ pass
625
671
  if self._spawned_tasks:
626
672
  for task in list(self._spawned_tasks):
627
673
  task.cancel()
@@ -847,6 +893,8 @@ class TelegramBotService(
847
893
  self._resume_options.pop(key, None)
848
894
  elif cache_name == "bind_options":
849
895
  self._bind_options.pop(key, None)
896
+ elif cache_name == "flow_run_options":
897
+ self._flow_run_options.pop(key, None)
850
898
  elif cache_name == "agent_options":
851
899
  self._agent_options.pop(key, None)
852
900
  elif cache_name == "update_options":
@@ -871,73 +919,202 @@ class TelegramBotService(
871
919
  self._pending_questions.pop(key, None)
872
920
 
873
921
  async def _cache_cleanup_loop(self) -> None:
874
- interval = max(CACHE_CLEANUP_INTERVAL_SECONDS, 1.0)
922
+ interval = max(self._config.cache.cleanup_interval_seconds, 1.0)
875
923
  while True:
876
924
  await asyncio.sleep(interval)
877
925
  self._evict_expired_cache_entries(
878
- "reasoning_buffers", REASONING_BUFFER_TTL_SECONDS
926
+ "reasoning_buffers", self._config.cache.reasoning_buffer_ttl_seconds
927
+ )
928
+ self._evict_expired_cache_entries(
929
+ "turn_preview", self._config.cache.turn_preview_ttl_seconds
930
+ )
931
+ self._evict_expired_cache_entries(
932
+ "progress_trackers", self._config.cache.progress_stream_ttl_seconds
879
933
  )
880
- self._evict_expired_cache_entries("turn_preview", TURN_PREVIEW_TTL_SECONDS)
881
934
  self._evict_expired_cache_entries(
882
- "progress_trackers", PROGRESS_STREAM_TTL_SECONDS
935
+ "oversize_warnings", self._config.cache.oversize_warning_ttl_seconds
883
936
  )
884
937
  self._evict_expired_cache_entries(
885
- "oversize_warnings", OVERSIZE_WARNING_TTL_SECONDS
938
+ "coalesced_buffers", self._config.cache.coalesce_buffer_ttl_seconds
886
939
  )
887
940
  self._evict_expired_cache_entries(
888
- "coalesced_buffers", COALESCE_BUFFER_TTL_SECONDS
941
+ "media_batch_buffers",
942
+ self._config.cache.media_batch_buffer_ttl_seconds,
889
943
  )
890
944
  self._evict_expired_cache_entries(
891
- "media_batch_buffers", MEDIA_BATCH_BUFFER_TTL_SECONDS
945
+ "resume_options", self._config.cache.selection_state_ttl_seconds
892
946
  )
893
947
  self._evict_expired_cache_entries(
894
- "resume_options", SELECTION_STATE_TTL_SECONDS
948
+ "bind_options", self._config.cache.selection_state_ttl_seconds
895
949
  )
896
950
  self._evict_expired_cache_entries(
897
- "bind_options", SELECTION_STATE_TTL_SECONDS
951
+ "flow_run_options", self._config.cache.selection_state_ttl_seconds
898
952
  )
899
953
  self._evict_expired_cache_entries(
900
- "agent_options", SELECTION_STATE_TTL_SECONDS
954
+ "agent_options", self._config.cache.selection_state_ttl_seconds
901
955
  )
902
956
  self._evict_expired_cache_entries(
903
- "update_options", SELECTION_STATE_TTL_SECONDS
957
+ "update_options", self._config.cache.selection_state_ttl_seconds
904
958
  )
905
959
  self._evict_expired_cache_entries(
906
- "update_confirm_options", SELECTION_STATE_TTL_SECONDS
960
+ "update_confirm_options",
961
+ self._config.cache.selection_state_ttl_seconds,
907
962
  )
908
963
  self._evict_expired_cache_entries(
909
- "review_commit_options", SELECTION_STATE_TTL_SECONDS
964
+ "review_commit_options",
965
+ self._config.cache.selection_state_ttl_seconds,
910
966
  )
911
967
  self._evict_expired_cache_entries(
912
- "review_commit_subjects", SELECTION_STATE_TTL_SECONDS
968
+ "review_commit_subjects",
969
+ self._config.cache.selection_state_ttl_seconds,
913
970
  )
914
971
  self._evict_expired_cache_entries(
915
- "pending_review_custom", SELECTION_STATE_TTL_SECONDS
972
+ "pending_review_custom",
973
+ self._config.cache.selection_state_ttl_seconds,
916
974
  )
917
975
  self._evict_expired_cache_entries(
918
- "compact_pending", SELECTION_STATE_TTL_SECONDS
976
+ "compact_pending", self._config.cache.selection_state_ttl_seconds
919
977
  )
920
978
  self._evict_expired_cache_entries(
921
- "model_options", SELECTION_STATE_TTL_SECONDS
979
+ "model_options", self._config.cache.selection_state_ttl_seconds
922
980
  )
923
981
  self._evict_expired_cache_entries(
924
- "model_pending", MODEL_PENDING_TTL_SECONDS
982
+ "model_pending", self._config.cache.model_pending_ttl_seconds
925
983
  )
926
984
  self._evict_expired_cache_entries(
927
- "pending_approvals", PENDING_APPROVAL_TTL_SECONDS
985
+ "pending_approvals", self._config.cache.pending_approval_ttl_seconds
928
986
  )
929
987
  self._evict_expired_cache_entries(
930
- "pending_questions", PENDING_QUESTION_TTL_SECONDS
988
+ "pending_questions", self._config.cache.pending_question_ttl_seconds
931
989
  )
932
990
  now = time.monotonic()
933
991
  expired_placeholders = []
934
992
  for key, timestamp in self._queued_placeholder_timestamps.items():
935
- if (now - timestamp) > PENDING_APPROVAL_TTL_SECONDS:
993
+ if (now - timestamp) > self._config.cache.pending_approval_ttl_seconds:
936
994
  expired_placeholders.append(key)
937
995
  for key in expired_placeholders:
938
996
  self._queued_placeholder_map.pop(key, None)
939
997
  self._queued_placeholder_timestamps.pop(key, None)
940
998
 
999
+ @staticmethod
1000
+ def _parse_last_active(record: "TelegramTopicRecord") -> float:
1001
+ raw = getattr(record, "last_active_at", None)
1002
+ if isinstance(raw, str):
1003
+ try:
1004
+ return datetime.strptime(raw, "%Y-%m-%dT%H:%M:%SZ").timestamp()
1005
+ except ValueError:
1006
+ return float("-inf")
1007
+ return float("-inf")
1008
+
1009
+ def _select_ticket_flow_topic(
1010
+ self, entries: list[tuple[str, "TelegramTopicRecord"]]
1011
+ ) -> Optional[tuple[str, "TelegramTopicRecord"]]:
1012
+ return self._ticket_flow_bridge._select_ticket_flow_topic(entries)
1013
+
1014
+ @staticmethod
1015
+ def _set_ticket_dispatch_marker(
1016
+ value: Optional[str],
1017
+ ) -> "Callable[[TelegramTopicRecord], None]":
1018
+ def apply(topic: "TelegramTopicRecord") -> None:
1019
+ topic.last_ticket_dispatch_seq = value
1020
+
1021
+ return apply
1022
+
1023
+ async def _ticket_flow_watch_loop(self) -> None:
1024
+ await self._ticket_flow_bridge.watch_ticket_flow_pauses(
1025
+ TICKET_FLOW_WATCH_INTERVAL_SECONDS
1026
+ )
1027
+
1028
+ async def _watch_ticket_flow_pauses(self) -> None:
1029
+ await self._ticket_flow_bridge._scan_and_notify_pauses()
1030
+
1031
+ async def _notify_ticket_flow_pause(
1032
+ self,
1033
+ workspace_root: Path,
1034
+ entries: list[tuple[str, "TelegramTopicRecord"]],
1035
+ ) -> None:
1036
+ await self._ticket_flow_bridge._notify_ticket_flow_pause(
1037
+ workspace_root, entries
1038
+ )
1039
+
1040
+ def _load_ticket_flow_pause(
1041
+ self, workspace_root: Path
1042
+ ) -> Optional[tuple[str, str, str, Optional[Path]]]:
1043
+ return self._ticket_flow_bridge._load_ticket_flow_pause(workspace_root)
1044
+
1045
+ def _latest_dispatch_seq(self, history_dir: Path) -> Optional[str]:
1046
+ return self._ticket_flow_bridge._latest_dispatch_seq(history_dir)
1047
+
1048
+ def _format_ticket_flow_pause_reason(self, record: "FlowRunRecord") -> str:
1049
+ return self._ticket_flow_bridge._format_ticket_flow_pause_reason(record)
1050
+
1051
+ def _get_paused_ticket_flow(
1052
+ self, workspace_root: Path, preferred_run_id: Optional[str] = None
1053
+ ) -> Optional[tuple[str, FlowRunRecord]]:
1054
+ return self._ticket_flow_bridge.get_paused_ticket_flow(
1055
+ workspace_root, preferred_run_id=preferred_run_id
1056
+ )
1057
+
1058
+ async def _write_user_reply_from_telegram(
1059
+ self,
1060
+ workspace_root: Path,
1061
+ run_id: str,
1062
+ run_record: FlowRunRecord,
1063
+ message: TelegramMessage,
1064
+ text: str,
1065
+ files: Optional[list[tuple[str, bytes]]] = None,
1066
+ ) -> tuple[bool, str]:
1067
+ try:
1068
+ input_data = dict(run_record.input_data or {})
1069
+ runs_dir_raw = input_data.get("runs_dir")
1070
+ runs_dir = (
1071
+ Path(runs_dir_raw)
1072
+ if isinstance(runs_dir_raw, str) and runs_dir_raw
1073
+ else Path(".codex-autorunner/runs")
1074
+ )
1075
+ reply_paths = resolve_reply_paths(
1076
+ workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_id
1077
+ )
1078
+ ensure_reply_dirs(reply_paths)
1079
+
1080
+ cleaned_text = text.strip()
1081
+ raw = cleaned_text
1082
+ if raw and not raw.endswith("\n"):
1083
+ raw += "\n"
1084
+
1085
+ await asyncio.to_thread(
1086
+ reply_paths.user_reply_path.write_text, raw, encoding="utf-8"
1087
+ )
1088
+
1089
+ if files:
1090
+ for filename, data in files:
1091
+ dest = reply_paths.reply_dir / filename
1092
+ dest.parent.mkdir(parents=True, exist_ok=True)
1093
+ await asyncio.to_thread(dest.write_bytes, data)
1094
+
1095
+ seq = await asyncio.to_thread(
1096
+ lambda: _next_reply_seq_sync(reply_paths.reply_history_dir)
1097
+ )
1098
+ dispatch, errors = await asyncio.to_thread(
1099
+ dispatch_reply, reply_paths, next_seq=seq
1100
+ )
1101
+ if errors:
1102
+ return False, "\n".join(errors)
1103
+ if dispatch is None:
1104
+ return False, "Failed to archive reply"
1105
+ return (
1106
+ True,
1107
+ f"Reply archived (seq {dispatch.seq}). Use /flow resume to continue.",
1108
+ )
1109
+ except Exception as exc:
1110
+ self._logger.warning(
1111
+ "Failed to write USER_REPLY.md from Telegram",
1112
+ exc=exc,
1113
+ workspace_root=str(workspace_root),
1114
+ run_id=run_id,
1115
+ )
1116
+ return False, f"Failed to write reply: {exc}"
1117
+
941
1118
  async def _interrupt_timeout_check(
942
1119
  self, key: str, turn_id: str, message_id: int
943
1120
  ) -> None:
@@ -1205,7 +1382,9 @@ class TelegramBotService(
1205
1382
  async def _maybe_persist_update_id(self, key: str, update_id: int) -> None:
1206
1383
  now = time.monotonic()
1207
1384
  last_persisted = self._last_update_persisted_at.get(key, 0.0)
1208
- if (now - last_persisted) < UPDATE_ID_PERSIST_INTERVAL_SECONDS:
1385
+ if (
1386
+ now - last_persisted
1387
+ ) < self._config.cache.update_id_persist_interval_seconds:
1209
1388
  return
1210
1389
 
1211
1390
  def apply(record: "TelegramTopicRecord") -> None:
@@ -205,6 +205,7 @@ class TelegramTopicRecord:
205
205
  rollout_path: Optional[str] = None
206
206
  approval_mode: str = APPROVAL_MODE_YOLO
207
207
  last_active_at: Optional[str] = None
208
+ last_ticket_dispatch_seq: Optional[str] = None
208
209
 
209
210
  @classmethod
210
211
  def from_dict(
@@ -291,6 +292,11 @@ class TelegramTopicRecord:
291
292
  last_active_at = payload.get("last_active_at") or payload.get("lastActiveAt")
292
293
  if not isinstance(last_active_at, str):
293
294
  last_active_at = None
295
+ last_ticket_dispatch_seq = payload.get(
296
+ "last_ticket_dispatch_seq"
297
+ ) or payload.get("lastTicketDispatchSeq")
298
+ if not isinstance(last_ticket_dispatch_seq, str):
299
+ last_ticket_dispatch_seq = None
294
300
  return cls(
295
301
  repo_id=repo_id,
296
302
  workspace_path=workspace_path,
@@ -310,6 +316,7 @@ class TelegramTopicRecord:
310
316
  rollout_path=rollout_path,
311
317
  approval_mode=approval_mode,
312
318
  last_active_at=last_active_at,
319
+ last_ticket_dispatch_seq=last_ticket_dispatch_seq,
313
320
  )
314
321
 
315
322
  def to_dict(self) -> dict[str, Any]:
@@ -335,6 +342,7 @@ class TelegramTopicRecord:
335
342
  "rollout_path": self.rollout_path,
336
343
  "approval_mode": self.approval_mode,
337
344
  "last_active_at": self.last_active_at,
345
+ "last_ticket_dispatch_seq": self.last_ticket_dispatch_seq,
338
346
  }
339
347
 
340
348
 
@@ -444,6 +452,10 @@ class OutboxRecord:
444
452
  attempts: int = 0
445
453
  last_error: Optional[str] = None
446
454
  last_attempt_at: Optional[str] = None
455
+ next_attempt_at: Optional[str] = None
456
+ operation: Optional[str] = None
457
+ message_id: Optional[int] = None
458
+ outbox_key: Optional[str] = None
447
459
 
448
460
  @classmethod
449
461
  def from_dict(cls, payload: dict[str, Any]) -> Optional["OutboxRecord"]:
@@ -459,6 +471,10 @@ class OutboxRecord:
459
471
  attempts = payload.get("attempts", 0)
460
472
  last_error = payload.get("last_error")
461
473
  last_attempt_at = payload.get("last_attempt_at")
474
+ next_attempt_at = payload.get("next_attempt_at")
475
+ operation = payload.get("operation")
476
+ message_id = payload.get("message_id")
477
+ outbox_key = payload.get("outbox_key")
462
478
  if not isinstance(record_id, str) or not record_id:
463
479
  return None
464
480
  if not isinstance(chat_id, int):
@@ -481,6 +497,14 @@ class OutboxRecord:
481
497
  last_error = None
482
498
  if not isinstance(last_attempt_at, str):
483
499
  last_attempt_at = None
500
+ if not isinstance(next_attempt_at, str):
501
+ next_attempt_at = None
502
+ if not isinstance(operation, str):
503
+ operation = None
504
+ if message_id is not None and not isinstance(message_id, int):
505
+ message_id = None
506
+ if not isinstance(outbox_key, str):
507
+ outbox_key = None
484
508
  return cls(
485
509
  record_id=record_id,
486
510
  chat_id=chat_id,
@@ -492,6 +516,10 @@ class OutboxRecord:
492
516
  attempts=attempts,
493
517
  last_error=last_error,
494
518
  last_attempt_at=last_attempt_at,
519
+ next_attempt_at=next_attempt_at,
520
+ operation=operation,
521
+ message_id=message_id,
522
+ outbox_key=outbox_key,
495
523
  )
496
524
 
497
525
  def to_dict(self) -> dict[str, Any]:
@@ -506,6 +534,10 @@ class OutboxRecord:
506
534
  "attempts": self.attempts,
507
535
  "last_error": self.last_error,
508
536
  "last_attempt_at": self.last_attempt_at,
537
+ "next_attempt_at": self.next_attempt_at,
538
+ "operation": self.operation,
539
+ "message_id": self.message_id,
540
+ "outbox_key": self.outbox_key,
509
541
  }
510
542
 
511
543
 
@@ -674,6 +706,27 @@ class TelegramStateStore:
674
706
  async def get_topic(self, key: str) -> Optional[TelegramTopicRecord]:
675
707
  return await self._run(self._get_topic_sync, key)
676
708
 
709
+ async def list_topics(self) -> dict[str, TelegramTopicRecord]:
710
+ """Return all stored topics keyed by topic_key."""
711
+ return await self._run(self._list_topics_sync)
712
+
713
+ def _list_topics_sync(self) -> dict[str, TelegramTopicRecord]:
714
+ conn = self._ensure_connection()
715
+ cursor = conn.execute("SELECT topic_key, payload_json FROM telegram_topics")
716
+ topics: dict[str, TelegramTopicRecord] = {}
717
+ for key, payload_json in cursor.fetchall():
718
+ try:
719
+ payload = (
720
+ json.loads(payload_json) if isinstance(payload_json, str) else {}
721
+ )
722
+ except Exception:
723
+ payload = {}
724
+ record = TelegramTopicRecord.from_dict(
725
+ payload, default_approval_mode=self._default_approval_mode
726
+ )
727
+ topics[str(key)] = record
728
+ return topics
729
+
677
730
  async def get_topic_scope(self, key: str) -> Optional[str]:
678
731
  return await self._run(self._get_topic_scope_sync, key)
679
732
 
@@ -807,6 +860,11 @@ class TelegramStateStore:
807
860
  loop = asyncio.get_running_loop()
808
861
  return await loop.run_in_executor(self._executor, func, *args)
809
862
 
863
+ def _ensure_connection(self) -> sqlite3.Connection:
864
+ # Backwards-compatible helper used by older call sites.
865
+ # _connection_sync() remains the single source of truth for opening the DB.
866
+ return self._connection_sync()
867
+
810
868
  def _connection_sync(self) -> sqlite3.Connection:
811
869
  if self._connection is None:
812
870
  conn = connect_sqlite(self._path)
@@ -927,16 +985,41 @@ class TelegramStateStore:
927
985
  thread_id INTEGER,
928
986
  created_at TEXT NOT NULL,
929
987
  updated_at TEXT NOT NULL,
988
+ next_attempt_at TEXT,
989
+ operation TEXT,
990
+ message_id INTEGER,
991
+ outbox_key TEXT,
930
992
  payload_json TEXT NOT NULL
931
993
  )
932
994
  """
933
995
  )
996
+ # Ensure legacy DBs gain the newer columns before creating indexes that
997
+ # reference them. The ALTERs are idempotent and cheap.
998
+ for col, col_type in [
999
+ ("next_attempt_at", "TEXT"),
1000
+ ("operation", "TEXT"),
1001
+ ("message_id", "INTEGER"),
1002
+ ("outbox_key", "TEXT"),
1003
+ ]:
1004
+ try:
1005
+ conn.execute(
1006
+ f"ALTER TABLE telegram_outbox ADD COLUMN {col} {col_type}"
1007
+ )
1008
+ except sqlite3.OperationalError:
1009
+ pass
934
1010
  conn.execute(
935
1011
  """
936
1012
  CREATE INDEX IF NOT EXISTS idx_tg_outbox_created
937
1013
  ON telegram_outbox(created_at)
938
1014
  """
939
1015
  )
1016
+ conn.execute(
1017
+ """
1018
+ CREATE INDEX IF NOT EXISTS idx_tg_outbox_key
1019
+ ON telegram_outbox(outbox_key)
1020
+ WHERE outbox_key IS NOT NULL
1021
+ """
1022
+ )
940
1023
  conn.execute(
941
1024
  """
942
1025
  CREATE TABLE IF NOT EXISTS telegram_pending_voice (
@@ -1019,7 +1102,10 @@ class TelegramStateStore:
1019
1102
  def _load_legacy_state_json(self, path: Path) -> Optional[TelegramState]:
1020
1103
  try:
1021
1104
  payload = json.loads(path.read_text(encoding="utf-8"))
1022
- except (OSError, json.JSONDecodeError):
1105
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError):
1106
+ # The path may already be a SQLite file (e.g., state_file still ends
1107
+ # with .json after the migration to SQLite). In that case, ignore the
1108
+ # legacy load attempt and treat the DB as the source of truth.
1023
1109
  return None
1024
1110
  if not isinstance(payload, dict):
1025
1111
  return None
@@ -1748,14 +1834,22 @@ class TelegramStateStore:
1748
1834
  thread_id,
1749
1835
  created_at,
1750
1836
  updated_at,
1837
+ next_attempt_at,
1838
+ operation,
1839
+ message_id,
1840
+ outbox_key,
1751
1841
  payload_json
1752
1842
  )
1753
- VALUES (?, ?, ?, ?, ?, ?)
1843
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1754
1844
  ON CONFLICT(record_id) DO UPDATE SET
1755
1845
  chat_id=excluded.chat_id,
1756
1846
  thread_id=excluded.thread_id,
1757
1847
  created_at=excluded.created_at,
1758
1848
  updated_at=excluded.updated_at,
1849
+ next_attempt_at=excluded.next_attempt_at,
1850
+ operation=excluded.operation,
1851
+ message_id=excluded.message_id,
1852
+ outbox_key=excluded.outbox_key,
1759
1853
  payload_json=excluded.payload_json
1760
1854
  """,
1761
1855
  (
@@ -1764,6 +1858,10 @@ class TelegramStateStore:
1764
1858
  record.thread_id,
1765
1859
  record.created_at,
1766
1860
  updated_at,
1861
+ record.next_attempt_at,
1862
+ record.operation,
1863
+ record.message_id,
1864
+ record.outbox_key,
1767
1865
  payload_json,
1768
1866
  ),
1769
1867
  )