codex-autorunner 0.1.2__py3-none-any.whl → 1.0.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 (189) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/opencode/client.py +68 -35
  3. codex_autorunner/agents/opencode/logging.py +21 -5
  4. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  5. codex_autorunner/agents/opencode/runtime.py +118 -30
  6. codex_autorunner/agents/opencode/supervisor.py +36 -48
  7. codex_autorunner/agents/registry.py +136 -8
  8. codex_autorunner/api.py +25 -0
  9. codex_autorunner/bootstrap.py +16 -35
  10. codex_autorunner/cli.py +157 -139
  11. codex_autorunner/core/about_car.py +44 -32
  12. codex_autorunner/core/adapter_utils.py +21 -0
  13. codex_autorunner/core/app_server_logging.py +7 -3
  14. codex_autorunner/core/app_server_prompts.py +27 -260
  15. codex_autorunner/core/app_server_threads.py +15 -26
  16. codex_autorunner/core/codex_runner.py +6 -0
  17. codex_autorunner/core/config.py +390 -100
  18. codex_autorunner/core/docs.py +10 -2
  19. codex_autorunner/core/drafts.py +82 -0
  20. codex_autorunner/core/engine.py +278 -262
  21. codex_autorunner/core/flows/__init__.py +25 -0
  22. codex_autorunner/core/flows/controller.py +178 -0
  23. codex_autorunner/core/flows/definition.py +82 -0
  24. codex_autorunner/core/flows/models.py +75 -0
  25. codex_autorunner/core/flows/runtime.py +351 -0
  26. codex_autorunner/core/flows/store.py +485 -0
  27. codex_autorunner/core/flows/transition.py +133 -0
  28. codex_autorunner/core/flows/worker_process.py +242 -0
  29. codex_autorunner/core/hub.py +15 -9
  30. codex_autorunner/core/locks.py +4 -0
  31. codex_autorunner/core/prompt.py +15 -7
  32. codex_autorunner/core/redaction.py +29 -0
  33. codex_autorunner/core/review_context.py +5 -8
  34. codex_autorunner/core/run_index.py +6 -0
  35. codex_autorunner/core/runner_process.py +5 -2
  36. codex_autorunner/core/state.py +0 -88
  37. codex_autorunner/core/static_assets.py +55 -0
  38. codex_autorunner/core/supervisor_utils.py +67 -0
  39. codex_autorunner/core/update.py +20 -11
  40. codex_autorunner/core/update_runner.py +2 -0
  41. codex_autorunner/core/utils.py +29 -2
  42. codex_autorunner/discovery.py +2 -4
  43. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  44. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  45. codex_autorunner/integrations/agents/__init__.py +27 -0
  46. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  47. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  48. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  49. codex_autorunner/integrations/agents/run_event.py +71 -0
  50. codex_autorunner/integrations/app_server/client.py +576 -92
  51. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  52. codex_autorunner/integrations/telegram/adapter.py +141 -167
  53. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  54. codex_autorunner/integrations/telegram/config.py +175 -0
  55. codex_autorunner/integrations/telegram/constants.py +16 -1
  56. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  57. codex_autorunner/integrations/telegram/doctor.py +47 -0
  58. codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
  59. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  63. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  64. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  65. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  66. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
  67. codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
  68. codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
  69. codex_autorunner/integrations/telegram/helpers.py +88 -16
  70. codex_autorunner/integrations/telegram/outbox.py +208 -37
  71. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  72. codex_autorunner/integrations/telegram/service.py +214 -40
  73. codex_autorunner/integrations/telegram/state.py +100 -2
  74. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  75. codex_autorunner/integrations/telegram/transport.py +36 -3
  76. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  77. codex_autorunner/manifest.py +2 -0
  78. codex_autorunner/plugin_api.py +22 -0
  79. codex_autorunner/routes/__init__.py +23 -14
  80. codex_autorunner/routes/analytics.py +239 -0
  81. codex_autorunner/routes/base.py +81 -109
  82. codex_autorunner/routes/file_chat.py +836 -0
  83. codex_autorunner/routes/flows.py +980 -0
  84. codex_autorunner/routes/messages.py +459 -0
  85. codex_autorunner/routes/system.py +6 -1
  86. codex_autorunner/routes/usage.py +87 -0
  87. codex_autorunner/routes/workspace.py +271 -0
  88. codex_autorunner/server.py +2 -1
  89. codex_autorunner/static/agentControls.js +1 -0
  90. codex_autorunner/static/agentEvents.js +248 -0
  91. codex_autorunner/static/app.js +25 -22
  92. codex_autorunner/static/autoRefresh.js +29 -1
  93. codex_autorunner/static/bootstrap.js +1 -0
  94. codex_autorunner/static/bus.js +1 -0
  95. codex_autorunner/static/cache.js +1 -0
  96. codex_autorunner/static/constants.js +20 -4
  97. codex_autorunner/static/dashboard.js +162 -196
  98. codex_autorunner/static/diffRenderer.js +37 -0
  99. codex_autorunner/static/docChatCore.js +324 -0
  100. codex_autorunner/static/docChatStorage.js +65 -0
  101. codex_autorunner/static/docChatVoice.js +65 -0
  102. codex_autorunner/static/docEditor.js +133 -0
  103. codex_autorunner/static/env.js +1 -0
  104. codex_autorunner/static/eventSummarizer.js +166 -0
  105. codex_autorunner/static/fileChat.js +182 -0
  106. codex_autorunner/static/health.js +155 -0
  107. codex_autorunner/static/hub.js +41 -118
  108. codex_autorunner/static/index.html +787 -858
  109. codex_autorunner/static/liveUpdates.js +1 -0
  110. codex_autorunner/static/loader.js +1 -0
  111. codex_autorunner/static/messages.js +470 -0
  112. codex_autorunner/static/mobileCompact.js +2 -1
  113. codex_autorunner/static/settings.js +24 -211
  114. codex_autorunner/static/styles.css +7567 -3865
  115. codex_autorunner/static/tabs.js +28 -5
  116. codex_autorunner/static/terminal.js +14 -0
  117. codex_autorunner/static/terminalManager.js +34 -59
  118. codex_autorunner/static/ticketChatActions.js +333 -0
  119. codex_autorunner/static/ticketChatEvents.js +16 -0
  120. codex_autorunner/static/ticketChatStorage.js +16 -0
  121. codex_autorunner/static/ticketChatStream.js +264 -0
  122. codex_autorunner/static/ticketEditor.js +750 -0
  123. codex_autorunner/static/ticketVoice.js +9 -0
  124. codex_autorunner/static/tickets.js +1315 -0
  125. codex_autorunner/static/utils.js +32 -3
  126. codex_autorunner/static/voice.js +1 -0
  127. codex_autorunner/static/workspace.js +672 -0
  128. codex_autorunner/static/workspaceApi.js +53 -0
  129. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  130. codex_autorunner/tickets/__init__.py +20 -0
  131. codex_autorunner/tickets/agent_pool.py +377 -0
  132. codex_autorunner/tickets/files.py +85 -0
  133. codex_autorunner/tickets/frontmatter.py +55 -0
  134. codex_autorunner/tickets/lint.py +102 -0
  135. codex_autorunner/tickets/models.py +95 -0
  136. codex_autorunner/tickets/outbox.py +232 -0
  137. codex_autorunner/tickets/replies.py +179 -0
  138. codex_autorunner/tickets/runner.py +823 -0
  139. codex_autorunner/tickets/spec_ingest.py +77 -0
  140. codex_autorunner/web/app.py +269 -91
  141. codex_autorunner/web/middleware.py +3 -4
  142. codex_autorunner/web/schemas.py +89 -109
  143. codex_autorunner/web/static_assets.py +1 -44
  144. codex_autorunner/workspace/__init__.py +40 -0
  145. codex_autorunner/workspace/paths.py +319 -0
  146. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
  147. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  148. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  149. codex_autorunner/agents/execution/policy.py +0 -292
  150. codex_autorunner/agents/factory.py +0 -52
  151. codex_autorunner/agents/orchestrator.py +0 -358
  152. codex_autorunner/core/doc_chat.py +0 -1446
  153. codex_autorunner/core/snapshot.py +0 -580
  154. codex_autorunner/integrations/github/chatops.py +0 -268
  155. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  156. codex_autorunner/routes/docs.py +0 -381
  157. codex_autorunner/routes/github.py +0 -327
  158. codex_autorunner/routes/runs.py +0 -250
  159. codex_autorunner/spec_ingest.py +0 -812
  160. codex_autorunner/static/docChatActions.js +0 -287
  161. codex_autorunner/static/docChatEvents.js +0 -300
  162. codex_autorunner/static/docChatRender.js +0 -205
  163. codex_autorunner/static/docChatStream.js +0 -361
  164. codex_autorunner/static/docs.js +0 -20
  165. codex_autorunner/static/docsClipboard.js +0 -69
  166. codex_autorunner/static/docsCrud.js +0 -257
  167. codex_autorunner/static/docsDocUpdates.js +0 -62
  168. codex_autorunner/static/docsDrafts.js +0 -16
  169. codex_autorunner/static/docsElements.js +0 -69
  170. codex_autorunner/static/docsInit.js +0 -285
  171. codex_autorunner/static/docsParse.js +0 -160
  172. codex_autorunner/static/docsSnapshot.js +0 -87
  173. codex_autorunner/static/docsSpecIngest.js +0 -263
  174. codex_autorunner/static/docsState.js +0 -127
  175. codex_autorunner/static/docsThreadRegistry.js +0 -44
  176. codex_autorunner/static/docsUi.js +0 -153
  177. codex_autorunner/static/docsVoice.js +0 -56
  178. codex_autorunner/static/github.js +0 -504
  179. codex_autorunner/static/logs.js +0 -678
  180. codex_autorunner/static/review.js +0 -157
  181. codex_autorunner/static/runs.js +0 -418
  182. codex_autorunner/static/snapshot.js +0 -124
  183. codex_autorunner/static/state.js +0 -94
  184. codex_autorunner/static/todoPreview.js +0 -27
  185. codex_autorunner/workspace.py +0 -16
  186. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  187. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  188. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  189. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -36,10 +36,17 @@ def build_command_specs(handlers: Any) -> dict[str, CommandSpec]:
36
36
  "run a code review",
37
37
  handlers._handle_review,
38
38
  ),
39
- "pr": CommandSpec(
40
- "pr",
41
- "run the GitHub PR flow",
42
- handlers._handle_pr,
39
+ "flow": CommandSpec(
40
+ "flow",
41
+ "start/resume ticket flow runs",
42
+ lambda message, args, _runtime: handlers._handle_flow(message, args),
43
+ allow_during_turn=True,
44
+ ),
45
+ "reply": CommandSpec(
46
+ "reply",
47
+ "reply to a paused ticket flow dispatch",
48
+ lambda message, args, _runtime: handlers._handle_reply(message, args),
49
+ allow_during_turn=True,
43
50
  ),
44
51
  "agent": CommandSpec(
45
52
  "agent",
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  from typing import Any, Optional, Sequence
10
10
 
11
11
  from ....core.logging_utils import log_event
12
- from ....integrations.github.service import parse_github_url
12
+ from ....core.utils import canonicalize_path
13
13
  from ..adapter import (
14
14
  TelegramDocument,
15
15
  TelegramMessage,
@@ -19,6 +19,7 @@ from ..adapter import (
19
19
  )
20
20
  from ..config import TelegramMediaCandidate
21
21
  from ..constants import TELEGRAM_MAX_MESSAGE_LENGTH
22
+ from ..trigger_mode import should_trigger_run
22
23
  from .questions import handle_custom_text_input
23
24
 
24
25
  COALESCE_LONG_MESSAGE_WINDOW_SECONDS = 6.0
@@ -37,6 +38,14 @@ IMAGE_EXTS = set(IMAGE_CONTENT_TYPES.values())
37
38
  MAX_BATCH_ITEMS = 10
38
39
 
39
40
 
41
+ def _is_ticket_reply(message: TelegramMessage, bot_username: Optional[str]) -> bool:
42
+ if message.reply_to_is_bot and message.reply_to_message_id is not None:
43
+ if bot_username and message.reply_to_username:
44
+ return message.reply_to_username.lower() == bot_username.lower()
45
+ return True
46
+ return False
47
+
48
+
40
49
  @dataclass
41
50
  class _CoalescedBuffer:
42
51
  message: TelegramMessage
@@ -246,14 +255,6 @@ async def handle_message_inner(
246
255
  await _clear_placeholder()
247
256
  return
248
257
 
249
- if text:
250
- parsed = parse_github_url(text.strip())
251
- if parsed and parsed[1] == "issue":
252
- slug, kind, number = parsed
253
- await handlers._handle_github_issue_url(message, key, slug, number)
254
- await _clear_placeholder()
255
- return
256
-
257
258
  if text and is_interrupt_alias(text):
258
259
  await handlers._handle_interrupt(message, runtime)
259
260
  await _clear_placeholder()
@@ -347,6 +348,66 @@ async def handle_message_inner(
347
348
  )
348
349
  return
349
350
 
351
+ record = await handlers._router.get_topic(key)
352
+ paused = None
353
+ workspace_root: Optional[Path] = None
354
+ if record and record.workspace_path:
355
+ workspace_root = canonicalize_path(Path(record.workspace_path))
356
+ preferred_run_id = handlers._ticket_flow_pause_targets.get(
357
+ str(workspace_root), None
358
+ )
359
+ paused = handlers._get_paused_ticket_flow(
360
+ workspace_root, preferred_run_id=preferred_run_id
361
+ )
362
+ if (
363
+ paused
364
+ and text
365
+ and not _is_ticket_reply(message, handlers._bot_username)
366
+ and not command
367
+ ):
368
+ await handlers._send_message(
369
+ message.chat_id,
370
+ "Ticket flow is paused. Reply to the latest dispatch message (tap Reply) or use /flow resume.",
371
+ thread_id=message.thread_id,
372
+ reply_to=message.message_id,
373
+ )
374
+ await _clear_placeholder()
375
+ return
376
+ if paused and text and _is_ticket_reply(message, handlers._bot_username):
377
+ run_id, run_record = paused
378
+ success, result = await handlers._write_user_reply_from_telegram(
379
+ workspace_root or Path("."), run_id, run_record, message, text
380
+ )
381
+ await handlers._send_message(
382
+ message.chat_id,
383
+ result,
384
+ thread_id=message.thread_id,
385
+ reply_to=message.message_id,
386
+ )
387
+ if success and getattr(handlers._config, "ticket_flow_auto_resume", False):
388
+ await handlers._ticket_flow_bridge.auto_resume_run(
389
+ workspace_root or Path("."), run_id
390
+ )
391
+ await _clear_placeholder()
392
+ return
393
+
394
+ if handlers._config.trigger_mode == "mentions" and not should_trigger_run(
395
+ message,
396
+ text=text,
397
+ bot_username=handlers._bot_username,
398
+ ):
399
+ log_event(
400
+ handlers._logger,
401
+ logging.INFO,
402
+ "telegram.trigger.ignored",
403
+ chat_id=message.chat_id,
404
+ thread_id=message.thread_id,
405
+ message_id=message.message_id,
406
+ reason="mentions_only",
407
+ )
408
+ await _clear_placeholder()
409
+ return
410
+
350
411
  if has_media:
351
412
 
352
413
  async def work() -> None:
@@ -752,6 +813,56 @@ async def handle_media_message(
752
813
  )
753
814
  return
754
815
 
816
+ workspace_root = canonicalize_path(Path(record.workspace_path))
817
+ preferred_run_id = handlers._ticket_flow_pause_targets.get(
818
+ str(workspace_root), None
819
+ )
820
+ paused = handlers._get_paused_ticket_flow(
821
+ workspace_root, preferred_run_id=preferred_run_id
822
+ )
823
+ if paused and caption_text and _is_ticket_reply(message, handlers._bot_username):
824
+ run_id, run_record = paused
825
+ files = []
826
+ if message.photos:
827
+ photos = sorted(
828
+ message.photos,
829
+ key=lambda p: (p.file_size or 0, p.width * p.height),
830
+ reverse=True,
831
+ )
832
+ if photos:
833
+ best = photos[0]
834
+ try:
835
+ file_info = await handlers._bot.get_file(best.file_id)
836
+ data = await handlers._bot.download_file(file_info.file_path)
837
+ filename = f"photo_{best.file_id}.jpg"
838
+ files.append((filename, data))
839
+ except Exception as exc:
840
+ handlers._logger.debug("Failed to download photo: %s", exc)
841
+ pass
842
+ elif message.document:
843
+ try:
844
+ file_info = await handlers._bot.get_file(message.document.file_id)
845
+ data = await handlers._bot.download_file(file_info.file_path)
846
+ filename = (
847
+ message.document.file_name or f"document_{message.document.file_id}"
848
+ )
849
+ files.append((filename, data))
850
+ except Exception as exc:
851
+ handlers._logger.debug("Failed to download document: %s", exc)
852
+ pass
853
+ success, result = await handlers._write_user_reply_from_telegram(
854
+ workspace_root, run_id, run_record, message, caption_text, files
855
+ )
856
+ await handlers._send_message(
857
+ message.chat_id,
858
+ result,
859
+ thread_id=message.thread_id,
860
+ reply_to=message.message_id,
861
+ )
862
+ if success and getattr(handlers._config, "ticket_flow_auto_resume", False):
863
+ await handlers._ticket_flow_bridge.auto_resume_run(workspace_root, run_id)
864
+ return
865
+
755
866
  image_candidate = select_image_candidate(message)
756
867
  if image_candidate:
757
868
  if not handlers._config.media.images:
@@ -42,6 +42,85 @@ class ModelOption:
42
42
  default_effort: Optional[str] = None
43
43
 
44
44
 
45
+ @dataclass(frozen=True)
46
+ class CodexFeatureRow:
47
+ key: str
48
+ stage: str
49
+ enabled: bool
50
+
51
+
52
+ def derive_codex_features_command(app_server_command: Sequence[str]) -> list[str]:
53
+ """
54
+ Build a Codex CLI invocation for `features list` that mirrors the configured app-server command.
55
+
56
+ We strip a trailing \"app-server\" subcommand (plus keep any preceding flags/binary),
57
+ so custom binaries or wrapper scripts stay aligned with the running app server.
58
+ """
59
+ base = list(app_server_command or [])
60
+ if base and base[-1] == "app-server":
61
+ base = base[:-1]
62
+ if not base:
63
+ base = ["codex"]
64
+ return [*base, "features", "list"]
65
+
66
+
67
+ def parse_codex_features_list(stdout: str) -> list[CodexFeatureRow]:
68
+ rows: list[CodexFeatureRow] = []
69
+ if not isinstance(stdout, str):
70
+ return rows
71
+ for line in stdout.splitlines():
72
+ line = line.strip()
73
+ if not line:
74
+ continue
75
+ parts = line.split("\t")
76
+ if len(parts) != 3:
77
+ continue
78
+ key, stage, enabled_raw = parts
79
+ key = key.strip()
80
+ stage = stage.strip()
81
+ enabled_raw = enabled_raw.strip().lower()
82
+ if not key or not stage:
83
+ continue
84
+ if enabled_raw in ("true", "1", "yes", "y", "on"):
85
+ enabled = True
86
+ elif enabled_raw in ("false", "0", "no", "n", "off"):
87
+ enabled = False
88
+ else:
89
+ continue
90
+ rows.append(CodexFeatureRow(key=key, stage=stage, enabled=enabled))
91
+ return rows
92
+
93
+
94
+ def format_codex_features(
95
+ rows: Sequence[CodexFeatureRow], *, stage_filter: Optional[str]
96
+ ) -> str:
97
+ filtered = [
98
+ row
99
+ for row in rows
100
+ if stage_filter is None or row.stage.lower() == stage_filter.lower()
101
+ ]
102
+ if not filtered:
103
+ label = (
104
+ "feature flags" if stage_filter is None else f"{stage_filter} feature flags"
105
+ )
106
+ return f"No {label} found."
107
+ header = (
108
+ "Codex feature flags (all):"
109
+ if stage_filter is None
110
+ else f"Codex feature flags ({stage_filter}):"
111
+ )
112
+ lines = [header]
113
+ for row in sorted(filtered, key=lambda r: r.key):
114
+ lines.append(f"- {row.key}: {row.enabled}")
115
+ lines.append("")
116
+ lines.append("Usage:")
117
+ lines.append("/experimental enable <flag>")
118
+ lines.append("/experimental disable <flag>")
119
+ if stage_filter is not None:
120
+ lines.append("/experimental all")
121
+ return "\n".join(lines)
122
+
123
+
45
124
  def _extract_thread_id(payload: Any) -> Optional[str]:
46
125
  if not isinstance(payload, dict):
47
126
  return None
@@ -837,14 +916,7 @@ def _format_help_text(command_specs: dict[str, CommandSpec]) -> str:
837
916
  lines.append("/review commit <sha> (or /review commit to pick)")
838
917
  lines.append("/review custom <instructions> (or /review custom to prompt)")
839
918
  lines.append("/review detached ...")
840
- if "pr" in command_specs:
841
- lines.append("")
842
- lines.append("PR Flow:")
843
- lines.append(
844
- "/pr start <issueRef> [--draft|--ready] [--base <branch>] [--until minor|clean] [--max-cycles N]"
845
- )
846
- lines.append("/pr fix <prRef> [--until minor|clean] [--max-cycles N]")
847
- lines.append("/pr status | /pr stop | /pr resume | /pr collect")
919
+
848
920
  lines.append("")
849
921
  lines.append("Other:")
850
922
  lines.append("Note: /resume is supported for the codex and opencode agents.")
@@ -1244,7 +1316,7 @@ FIRST_USER_PREVIEW_IGNORE_PATTERNS = (
1244
1316
  ),
1245
1317
  )
1246
1318
 
1247
- USER_MESSAGE_BEGIN_STRIP_RE = re.compile(
1319
+ DISPATCH_BEGIN_STRIP_RE = re.compile(
1248
1320
  r"(?s)^\s*(?:<prior context>\s*)?##\s*My request for Codex:\s*",
1249
1321
  re.IGNORECASE,
1250
1322
  )
@@ -1261,17 +1333,17 @@ def _is_ignored_first_user_preview(text: Optional[str]) -> bool:
1261
1333
  )
1262
1334
 
1263
1335
 
1264
- def _strip_user_message_begin(text: Optional[str]) -> Optional[str]:
1336
+ def _strip_dispatch_begin(text: Optional[str]) -> Optional[str]:
1265
1337
  if not isinstance(text, str):
1266
1338
  return text
1267
- stripped = USER_MESSAGE_BEGIN_STRIP_RE.sub("", text)
1339
+ stripped = DISPATCH_BEGIN_STRIP_RE.sub("", text)
1268
1340
  return stripped if stripped != text else text
1269
1341
 
1270
1342
 
1271
1343
  def _sanitize_user_preview(text: Optional[str]) -> Optional[str]:
1272
1344
  if not isinstance(text, str):
1273
1345
  return text
1274
- stripped = _strip_user_message_begin(text)
1346
+ stripped = _strip_dispatch_begin(text)
1275
1347
  if _is_ignored_first_user_preview(stripped):
1276
1348
  return None
1277
1349
  return stripped
@@ -1293,7 +1365,7 @@ def _github_preview_matcher(text: Optional[str]) -> Optional[str]:
1293
1365
  return None
1294
1366
 
1295
1367
 
1296
- COMPACT_SEED_PREFIX = "Context handoff from previous thread:"
1368
+ COMPACT_SEED_PREFIX = "Context from previous thread:"
1297
1369
  COMPACT_SEED_SUFFIX = "Continue from this context. Ask for missing info if needed."
1298
1370
 
1299
1371
 
@@ -1572,7 +1644,7 @@ def _extract_rollout_first_user_preview(path: Path) -> Optional[str]:
1572
1644
  continue
1573
1645
  for role, text in _iter_role_texts(payload):
1574
1646
  if role == "user" and text:
1575
- stripped = _strip_user_message_begin(text)
1647
+ stripped = _strip_dispatch_begin(text)
1576
1648
  if stripped and not _is_ignored_first_user_preview(stripped):
1577
1649
  return stripped
1578
1650
  return None
@@ -1632,7 +1704,7 @@ def _extract_turns_first_user_preview(turns: Any) -> Optional[str]:
1632
1704
  for item in iterable:
1633
1705
  for role, text in _iter_role_texts(item):
1634
1706
  if role == "user" and text:
1635
- stripped = _strip_user_message_begin(text)
1707
+ stripped = _strip_dispatch_begin(text)
1636
1708
  if stripped and not _is_ignored_first_user_preview(stripped):
1637
1709
  return stripped
1638
1710
  return None
@@ -1770,7 +1842,7 @@ def _extract_first_user_preview(entry: Any) -> Optional[str]:
1770
1842
  "initialMessage",
1771
1843
  )
1772
1844
  user_preview = _coerce_preview_field(entry, user_preview_keys)
1773
- user_preview = _strip_user_message_begin(user_preview)
1845
+ user_preview = _strip_dispatch_begin(user_preview)
1774
1846
  if _is_ignored_first_user_preview(user_preview):
1775
1847
  user_preview = None
1776
1848
  turns = entry.get("turns")