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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +176 -47
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +155 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +22 -37
- codex_autorunner/cli.py +5 -1156
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +49 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +26 -28
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +12 -2
- codex_autorunner/core/config.py +587 -103
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +136 -0
- codex_autorunner/core/engine.py +1531 -866
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +202 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +88 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +382 -0
- codex_autorunner/core/flows/store.py +568 -0
- codex_autorunner/core/flows/transition.py +138 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +136 -16
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/core/ports/agent_backend.py +150 -0
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/core/ports/run_event.py +91 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -0
- codex_autorunner/core/update.py +24 -16
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +120 -11
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +98 -0
- codex_autorunner/integrations/agents/__init__.py +17 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +448 -0
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +598 -0
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -0
- codex_autorunner/integrations/app_server/client.py +583 -152
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +204 -165
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +221 -0
- codex_autorunner/integrations/telegram/constants.py +17 -2
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +111 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +221 -42
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
- codex_autorunner/integrations/telegram/transport.py +39 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +37 -67
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +3 -0
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -624
- codex_autorunner/routes/file_chat.py +7 -0
- codex_autorunner/routes/flows.py +7 -0
- codex_autorunner/routes/messages.py +7 -0
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -188
- codex_autorunner/routes/usage.py +3 -0
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +3 -0
- codex_autorunner/server.py +3 -2
- codex_autorunner/static/agentControls.js +41 -11
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +35 -24
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +36 -8
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +344 -325
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +126 -185
- codex_autorunner/static/index.html +839 -863
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +873 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +149 -217
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +8850 -3876
- codex_autorunner/static/tabs.js +175 -11
- codex_autorunner/static/terminal.js +32 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +844 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1988 -0
- codex_autorunner/static/utils.js +43 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +765 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +417 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +27 -0
- codex_autorunner/tickets/agent_pool.py +399 -0
- codex_autorunner/tickets/files.py +89 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +97 -0
- codex_autorunner/tickets/outbox.py +244 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +881 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1771
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -587
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -396
- codex_autorunner/web/static_assets.py +4 -484
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +335 -0
- codex_autorunner-1.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/METADATA +0 -249
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,370 +1,3 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
-
|
|
1
|
+
"""Backward-compatible runner manager exports."""
|
|
2
2
|
|
|
3
|
-
from ..
|
|
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
|