codex-autorunner 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -1,370 +1,3 @@
1
- import asyncio
2
- import collections
3
- import fcntl
4
- import logging
5
- import os
6
- import select
7
- import struct
8
- import termios
9
- import time
10
- from typing import Dict, Optional
1
+ """Backward-compatible PTY session exports."""
11
2
 
12
- from ptyprocess import PtyProcess
13
-
14
- logger = logging.getLogger("codex_autorunner.web.pty_session")
15
-
16
- REPLAY_END = object()
17
-
18
- ALT_SCREEN_ENTER_SEQS = (
19
- b"\x1b[?1049h",
20
- b"\x1b[?47h",
21
- b"\x1b[?1047h",
22
- )
23
- ALT_SCREEN_EXIT_SEQS = (
24
- b"\x1b[?1049l",
25
- b"\x1b[?47l",
26
- b"\x1b[?1047l",
27
- )
28
- ALT_SCREEN_SEQS = tuple((seq, True) for seq in ALT_SCREEN_ENTER_SEQS) + tuple(
29
- (seq, False) for seq in ALT_SCREEN_EXIT_SEQS
30
- )
31
- ALT_SCREEN_MAX_LEN = max(len(seq) for seq, _state in ALT_SCREEN_SEQS)
32
- PTY_WRITE_CHUNK_BYTES = 16 * 1024
33
- # Cap per-flush work to keep the event loop responsive.
34
- PTY_WRITE_FLUSH_MAX_BYTES = 256 * 1024
35
- # Hard cap to prevent unbounded buffering when the PTY can't accept input.
36
- PTY_PENDING_MAX_BYTES = 1024 * 1024
37
-
38
-
39
- def default_env(env: Optional[Dict[str, str]] = None) -> Dict[str, str]:
40
- base = os.environ.copy()
41
- if env:
42
- base.update(env)
43
- base.setdefault("TERM", "xterm-256color")
44
- base.setdefault("COLORTERM", "truecolor")
45
- return base
46
-
47
-
48
- class PTYSession:
49
- def __init__(self, cmd: list[str], cwd: str, env: Optional[Dict[str, str]] = None):
50
- # echo=False to avoid double-printing user keystrokes
51
- self.proc = PtyProcess.spawn(cmd, cwd=cwd, env=default_env(env), echo=False)
52
- self.fd = self.proc.fd
53
- self._set_nonblocking()
54
- self.closed = False
55
- self.last_active = time.time()
56
-
57
- def _set_nonblocking(self) -> None:
58
- """Ensure PTY IO doesn't block event loop."""
59
- try:
60
- flags = fcntl.fcntl(self.fd, fcntl.F_GETFL)
61
- if not (flags & os.O_NONBLOCK):
62
- fcntl.fcntl(self.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
63
- except (OSError, IOError) as exc:
64
- logger.debug("Failed to set PTY to non-blocking mode: %s", exc)
65
-
66
- def resize(self, cols: int, rows: int) -> None:
67
- if self.closed:
68
- return
69
- buf = struct.pack("HHHH", rows, cols, 0, 0)
70
- fcntl.ioctl(self.fd, termios.TIOCSWINSZ, buf)
71
- self.last_active = time.time()
72
-
73
- def write(self, data: bytes) -> int:
74
- """Best-effort non-blocking write; returns bytes written.
75
-
76
- For user input, prefer ActiveSession.write_input so the loop never blocks.
77
- """
78
- if self.closed or not data:
79
- return 0
80
- try:
81
- written = os.write(self.fd, data)
82
- except (BlockingIOError, InterruptedError):
83
- return 0
84
- except OSError:
85
- self.terminate()
86
- return 0
87
- if written:
88
- self.last_active = time.time()
89
- return written
90
-
91
- def read(self, max_bytes: int = 4096) -> bytes:
92
- if self.closed:
93
- return b""
94
- readable, _, _ = select.select([self.fd], [], [], 0)
95
- if not readable:
96
- return b""
97
- try:
98
- chunk = os.read(self.fd, max_bytes)
99
- except BlockingIOError:
100
- return b""
101
- except OSError:
102
- self.terminate()
103
- return b""
104
- if chunk:
105
- self.last_active = time.time()
106
- return chunk
107
-
108
- def isalive(self) -> bool:
109
- return not self.closed and self.proc.isalive()
110
-
111
- def exit_code(self) -> Optional[int]:
112
- return self.proc.exitstatus if not self.proc.isalive() else None
113
-
114
- def is_stale(self, max_idle_seconds: int) -> bool:
115
- return (time.time() - self.last_active) > max_idle_seconds
116
-
117
- def terminate(self) -> None:
118
- if self.closed:
119
- return
120
- try:
121
- self.proc.terminate(force=True)
122
- except (OSError, IOError) as exc:
123
- logger.debug("Failed to terminate PTY process: %s", exc)
124
- self.closed = True
125
-
126
-
127
- class ActiveSession:
128
- def __init__(
129
- self, session_id: str, pty: PTYSession, loop: asyncio.AbstractEventLoop
130
- ):
131
- self.id = session_id
132
- self.pty = pty
133
- # Keep a bounded scrollback buffer for reconnects.
134
- # This is sized in bytes (not chunks) so behavior is predictable.
135
- self._buffer_max_bytes = 512 * 1024 # 512KB
136
- self._buffer_bytes = 0
137
- self.buffer: collections.deque[bytes] = collections.deque()
138
- self.subscribers: set[asyncio.Queue[object]] = set()
139
- self.lock = asyncio.Lock()
140
- self.loop = loop
141
- # Buffered input keeps the event loop from blocking on PTY writes.
142
- self._pending_input = bytearray()
143
- self._writer_active = False
144
- # Track recently-seen input IDs (from web UI) to make "send" retries idempotent.
145
- self._seen_input_ids_max = 256
146
- self._seen_input_ids: collections.deque[str] = collections.deque()
147
- self._seen_input_ids_set: set[str] = set()
148
- now = time.time()
149
- self.last_output_at = now
150
- self.last_input_at = now
151
- self._output_since_idle = False
152
- self._idle_notified_at: Optional[float] = None
153
- self._alt_screen_active = False
154
- self._alt_screen_tail = b""
155
- self._setup_reader()
156
-
157
- def mark_input_id_seen(self, input_id: str) -> bool:
158
- """Return True if this is the first time we've seen input_id."""
159
- if input_id in self._seen_input_ids_set:
160
- return False
161
- self._seen_input_ids_set.add(input_id)
162
- self._seen_input_ids.append(input_id)
163
- while len(self._seen_input_ids) > self._seen_input_ids_max:
164
- dropped = self._seen_input_ids.popleft()
165
- self._seen_input_ids_set.discard(dropped)
166
- return True
167
-
168
- def _setup_reader(self):
169
- self.loop.add_reader(self.pty.fd, self._read_callback)
170
-
171
- def write_input(self, data: bytes) -> None:
172
- """Queue terminal input and flush without blocking the event loop."""
173
- if self.pty.closed or not data:
174
- return
175
- if len(self._pending_input) >= PTY_PENDING_MAX_BYTES:
176
- return
177
- remaining = PTY_PENDING_MAX_BYTES - len(self._pending_input)
178
- if len(data) > remaining:
179
- data = data[-remaining:]
180
- self._pending_input.extend(data)
181
- self._flush_pending_input()
182
-
183
- def _enable_writer(self) -> None:
184
- if self._writer_active:
185
- return
186
- try:
187
- self.loop.add_writer(self.pty.fd, self._flush_pending_input)
188
- self._writer_active = True
189
- except (OSError, IOError) as exc:
190
- logger.debug("Failed to enable PTY writer: %s", exc)
191
- self._writer_active = False
192
-
193
- def _disable_writer(self) -> None:
194
- if not self._writer_active:
195
- return
196
- try:
197
- self.loop.remove_writer(self.pty.fd)
198
- except (OSError, IOError) as exc:
199
- logger.debug("Failed to disable PTY writer: %s", exc)
200
- self._writer_active = False
201
-
202
- def _flush_pending_input(self) -> None:
203
- """Drain queued input without blocking the event loop."""
204
- if self.pty.closed:
205
- self._pending_input.clear()
206
- self._disable_writer()
207
- return
208
- if not self._pending_input:
209
- self._disable_writer()
210
- return
211
- bytes_flushed = 0
212
- while self._pending_input and bytes_flushed < PTY_WRITE_FLUSH_MAX_BYTES:
213
- limit = min(len(self._pending_input), PTY_WRITE_CHUNK_BYTES)
214
- chunk = bytes(self._pending_input[:limit])
215
- try:
216
- written = os.write(self.pty.fd, chunk)
217
- except BlockingIOError:
218
- self._enable_writer()
219
- return
220
- except InterruptedError:
221
- continue
222
- except OSError:
223
- self.close()
224
- return
225
- if written <= 0:
226
- break
227
- del self._pending_input[:written]
228
- bytes_flushed += written
229
- self.pty.last_active = time.time()
230
- if self._pending_input:
231
- self._enable_writer()
232
- else:
233
- self._disable_writer()
234
-
235
- def _read_callback(self):
236
- try:
237
- if self.pty.closed:
238
- return
239
- try:
240
- data = os.read(self.pty.fd, 4096)
241
- except BlockingIOError:
242
- return
243
- if data:
244
- self._update_alt_screen_state(data)
245
- now = time.time()
246
- self.pty.last_active = now
247
- self.last_output_at = now
248
- self._output_since_idle = True
249
- self._idle_notified_at = None
250
- self.buffer.append(data)
251
- self._buffer_bytes += len(data)
252
- while self._buffer_bytes > self._buffer_max_bytes and self.buffer:
253
- dropped = self.buffer.popleft()
254
- self._buffer_bytes -= len(dropped)
255
- for queue in list(self.subscribers):
256
- try:
257
- queue.put_nowait(data)
258
- except asyncio.QueueFull:
259
- logger.debug(
260
- "Subscriber queue full, dropping data for session %s",
261
- self.id,
262
- )
263
- else:
264
- self.close()
265
- except OSError:
266
- self.close()
267
-
268
- def add_subscriber(
269
- self, *, include_replay_end: bool = True
270
- ) -> asyncio.Queue[object]:
271
- q: asyncio.Queue[object] = asyncio.Queue()
272
- for chunk in self.buffer:
273
- q.put_nowait(chunk)
274
- if include_replay_end:
275
- q.put_nowait(REPLAY_END)
276
- self.subscribers.add(q)
277
- return q
278
-
279
- def refresh_alt_screen_state(self) -> None:
280
- state = self._alt_screen_active
281
- tail = b""
282
- for chunk in self.buffer:
283
- state, tail = self._scan_alt_screen_chunk(chunk, state, tail)
284
- self._alt_screen_active = state
285
- self._alt_screen_tail = tail
286
-
287
- @property
288
- def alt_screen_active(self) -> bool:
289
- return self._alt_screen_active
290
-
291
- def get_buffer_stats(self) -> tuple[int, int]:
292
- return self._buffer_bytes, len(self.buffer)
293
-
294
- def _scan_alt_screen_chunk(
295
- self, data: bytes, state: bool, tail: bytes
296
- ) -> tuple[bool, bytes]:
297
- if not data:
298
- return state, tail
299
- haystack = tail + data
300
- last_pos = -1
301
- last_state: Optional[bool] = None
302
- for seq, next_state in ALT_SCREEN_SEQS:
303
- pos = haystack.rfind(seq)
304
- if pos > last_pos:
305
- last_pos = pos
306
- last_state = next_state
307
- if last_state is not None:
308
- state = last_state
309
- if ALT_SCREEN_MAX_LEN > 1:
310
- tail = haystack[-(ALT_SCREEN_MAX_LEN - 1) :]
311
- else:
312
- tail = b""
313
- return state, tail
314
-
315
- def _update_alt_screen_state(self, data: bytes) -> None:
316
- self._alt_screen_active, self._alt_screen_tail = self._scan_alt_screen_chunk(
317
- data, self._alt_screen_active, self._alt_screen_tail
318
- )
319
-
320
- def remove_subscriber(self, q: asyncio.Queue[object]):
321
- self.subscribers.discard(q)
322
-
323
- def close(self):
324
- try:
325
- self._disable_writer()
326
- except (OSError, IOError) as exc:
327
- logger.debug("Failed to disable writer during close: %s", exc)
328
- self._pending_input.clear()
329
- if not self.pty.closed:
330
- try:
331
- self.loop.remove_reader(self.pty.fd)
332
- except (OSError, IOError) as exc:
333
- logger.debug("Failed to remove reader during close: %s", exc)
334
- try:
335
- self.pty.terminate()
336
- except (OSError, IOError) as exc:
337
- logger.debug("Failed to terminate PTY during close: %s", exc)
338
- for queue in list(self.subscribers):
339
- try:
340
- queue.put_nowait(None)
341
- except asyncio.QueueFull:
342
- pass
343
- self.subscribers.clear()
344
-
345
- def mark_input_activity(self) -> None:
346
- now = time.time()
347
- self.last_input_at = now
348
- self._output_since_idle = False
349
- self._idle_notified_at = None
350
-
351
- def should_notify_idle(self, idle_seconds: float) -> bool:
352
- if idle_seconds <= 0:
353
- return False
354
- if not self._output_since_idle:
355
- return False
356
- if self._idle_notified_at is not None:
357
- return False
358
- if time.time() - self.last_output_at < idle_seconds:
359
- return False
360
- self._idle_notified_at = time.time()
361
- self._output_since_idle = False
362
- return True
363
-
364
- async def wait_closed(self, timeout: float = 5.0):
365
- """Wait for the underlying PTY process to terminate."""
366
- start = time.time()
367
- while time.time() - start < timeout:
368
- if not self.pty.isalive():
369
- return
370
- await asyncio.sleep(0.1)
3
+ from ..surfaces.web.pty_session import * # noqa: F401,F403
@@ -1,25 +1,3 @@
1
- from __future__ import annotations
1
+ """Backward-compatible runner manager exports."""
2
2
 
3
- from ..core.engine import Engine
4
- from ..core.runner_controller import ProcessRunnerController
5
-
6
-
7
- class RunnerManager:
8
- def __init__(self, engine: Engine):
9
- self._controller = ProcessRunnerController(engine)
10
-
11
- @property
12
- def running(self) -> bool:
13
- return self._controller.running
14
-
15
- def start(self, once: bool = False) -> None:
16
- self._controller.start(once=once)
17
-
18
- def resume(self, once: bool = False) -> None:
19
- self._controller.resume(once=once)
20
-
21
- def stop(self) -> None:
22
- self._controller.stop()
23
-
24
- def kill(self) -> None:
25
- self._controller.kill()
3
+ from ..surfaces.web.runner_manager import * # noqa: F401,F403