arkaos 3.70.10 → 3.71.1
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.
- package/VERSION +1 -1
- package/core/personas/__pycache__/obsidian_store.cpython-313.pyc +0 -0
- package/core/terminal/__init__.py +2 -0
- package/core/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/connections.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
- package/core/terminal/connections.py +46 -0
- package/core/terminal/session.py +28 -0
- package/dashboard/app/components/TerminalDock.vue +638 -0
- package/dashboard/app/composables/useTerminalDock.ts +88 -0
- package/dashboard/app/composables/useTerminalSession.ts +68 -29
- package/dashboard/app/composables/useTerminalTabs.ts +121 -46
- package/dashboard/app/layouts/default.vue +6 -0
- package/dashboard/app/pages/terminal.vue +25 -531
- package/dashboard/package.json +3 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +37 -4
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.71.1
|
|
Binary file
|
|
@@ -16,6 +16,7 @@ from core.terminal.session import (
|
|
|
16
16
|
)
|
|
17
17
|
from core.terminal.audit import log_end, log_start
|
|
18
18
|
from core.terminal.token import current_token
|
|
19
|
+
from core.terminal.connections import ConnectionRegistry
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
"TerminalSession",
|
|
@@ -24,4 +25,5 @@ __all__ = [
|
|
|
24
25
|
"log_start",
|
|
25
26
|
"log_end",
|
|
26
27
|
"current_token",
|
|
28
|
+
"ConnectionRegistry",
|
|
27
29
|
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Single active WebSocket connection per terminal session (v3.71.1).
|
|
2
|
+
|
|
3
|
+
asyncio allows only one reader per fd, so two concurrent WebSockets on the
|
|
4
|
+
same session would fight over the PTY master fd and the scrollback replay
|
|
5
|
+
could duplicate output. This registry enforces "latest wins": a new
|
|
6
|
+
connection supersedes the previous one (which the endpoint then closes),
|
|
7
|
+
and release is guarded so a superseded connection's teardown cannot evict
|
|
8
|
+
its replacement.
|
|
9
|
+
|
|
10
|
+
Pure bookkeeping — no FastAPI import, so it's unit-testable standalone.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConnectionRegistry:
|
|
19
|
+
"""Tracks the one live connection per session id."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._active: dict[str, Any] = {}
|
|
23
|
+
|
|
24
|
+
def acquire(self, session_id: str, conn: Any) -> Optional[Any]:
|
|
25
|
+
"""Make ``conn`` the active connection for ``session_id``.
|
|
26
|
+
|
|
27
|
+
Returns the connection it superseded (for the caller to close), or
|
|
28
|
+
``None`` when there was none / it was already active.
|
|
29
|
+
"""
|
|
30
|
+
old = self._active.get(session_id)
|
|
31
|
+
self._active[session_id] = conn
|
|
32
|
+
return old if old is not conn else None
|
|
33
|
+
|
|
34
|
+
def release(self, session_id: str, conn: Any) -> bool:
|
|
35
|
+
"""Drop ``conn`` iff it is still the active connection.
|
|
36
|
+
|
|
37
|
+
Returns whether it was — the caller should only tear down shared
|
|
38
|
+
resources (the fd reader) when this is ``True``.
|
|
39
|
+
"""
|
|
40
|
+
if self._active.get(session_id) is conn:
|
|
41
|
+
del self._active[session_id]
|
|
42
|
+
return True
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
def is_active(self, session_id: str, conn: Any) -> bool:
|
|
46
|
+
return self._active.get(session_id) is conn
|
package/core/terminal/session.py
CHANGED
|
@@ -46,6 +46,13 @@ def _default_cwd() -> str:
|
|
|
46
46
|
return os.path.expanduser("~")
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
# v3.71.0 — in-memory scrollback so a reconnecting client (after the
|
|
50
|
+
# operator navigates away or reloads the dashboard) can replay recent
|
|
51
|
+
# output and find its session as it left it. Bounded, RAM-only, cleared
|
|
52
|
+
# on close, never written to disk and never sent to the audit log.
|
|
53
|
+
DEFAULT_SCROLLBACK_BYTES = 512 * 1024
|
|
54
|
+
|
|
55
|
+
|
|
49
56
|
class TerminalSession:
|
|
50
57
|
"""A single forked PTY + the bookkeeping needed to drive it.
|
|
51
58
|
|
|
@@ -60,6 +67,7 @@ class TerminalSession:
|
|
|
60
67
|
cwd: str,
|
|
61
68
|
cols: int = 120,
|
|
62
69
|
rows: int = 32,
|
|
70
|
+
scrollback_bytes: int = DEFAULT_SCROLLBACK_BYTES,
|
|
63
71
|
) -> None:
|
|
64
72
|
self.session_id = session_id
|
|
65
73
|
self.shell = shell
|
|
@@ -68,6 +76,8 @@ class TerminalSession:
|
|
|
68
76
|
self.last_activity = time.monotonic()
|
|
69
77
|
self.exit_code: Optional[int] = None
|
|
70
78
|
self.title: str = ""
|
|
79
|
+
self.scrollback_max = max(0, int(scrollback_bytes))
|
|
80
|
+
self._scrollback = bytearray()
|
|
71
81
|
self._closed = False
|
|
72
82
|
self.pid, self.master_fd = pty.fork()
|
|
73
83
|
if self.pid == 0:
|
|
@@ -104,8 +114,21 @@ class TerminalSession:
|
|
|
104
114
|
raise
|
|
105
115
|
if data:
|
|
106
116
|
self.last_activity = time.monotonic()
|
|
117
|
+
self._record(data)
|
|
107
118
|
return data
|
|
108
119
|
|
|
120
|
+
def _record(self, data: bytes) -> None:
|
|
121
|
+
"""Append output to the bounded scrollback, evicting the oldest."""
|
|
122
|
+
if self.scrollback_max <= 0:
|
|
123
|
+
return
|
|
124
|
+
self._scrollback += data
|
|
125
|
+
if len(self._scrollback) > self.scrollback_max:
|
|
126
|
+
del self._scrollback[: -self.scrollback_max]
|
|
127
|
+
|
|
128
|
+
def scrollback(self) -> bytes:
|
|
129
|
+
"""Snapshot of recent output, for replay on (re)connect."""
|
|
130
|
+
return bytes(self._scrollback)
|
|
131
|
+
|
|
109
132
|
def write(self, data: bytes) -> int:
|
|
110
133
|
if self._closed or self.master_fd < 0 or not data:
|
|
111
134
|
return 0
|
|
@@ -172,6 +195,7 @@ class TerminalSession:
|
|
|
172
195
|
except OSError:
|
|
173
196
|
pass
|
|
174
197
|
self.master_fd = -1
|
|
198
|
+
self._scrollback.clear()
|
|
175
199
|
self._closed = True
|
|
176
200
|
|
|
177
201
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -207,6 +231,9 @@ class TerminalSessionManager:
|
|
|
207
231
|
self.idle_timeout_s = int(
|
|
208
232
|
os.environ.get("ARKAOS_TERMINAL_IDLE_S", idle_default)
|
|
209
233
|
)
|
|
234
|
+
self.scrollback_bytes = int(
|
|
235
|
+
os.environ.get("ARKAOS_TERMINAL_SCROLLBACK_BYTES", DEFAULT_SCROLLBACK_BYTES)
|
|
236
|
+
)
|
|
210
237
|
self._sessions: dict[str, TerminalSession] = {}
|
|
211
238
|
|
|
212
239
|
def create(
|
|
@@ -230,6 +257,7 @@ class TerminalSessionManager:
|
|
|
230
257
|
cwd=chosen_cwd,
|
|
231
258
|
cols=cols,
|
|
232
259
|
rows=rows,
|
|
260
|
+
scrollback_bytes=self.scrollback_bytes,
|
|
233
261
|
)
|
|
234
262
|
self._sessions[sid] = session
|
|
235
263
|
audit.log_start(sid, chosen_shell, chosen_cwd)
|