arkaos 3.71.0 → 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 CHANGED
@@ -1 +1 @@
1
- 3.71.0
1
+ 3.71.1
@@ -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
  ]
@@ -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
@@ -8,7 +8,8 @@
8
8
  "preview": "nuxt preview",
9
9
  "postinstall": "nuxt prepare",
10
10
  "lint": "eslint .",
11
- "typecheck": "nuxt typecheck"
11
+ "typecheck": "nuxt typecheck",
12
+ "test:e2e": "playwright test"
12
13
  },
13
14
  "dependencies": {
14
15
  "@iconify-json/lucide": "^1.2.100",
@@ -34,6 +35,7 @@
34
35
  },
35
36
  "devDependencies": {
36
37
  "@nuxt/eslint": "^1.15.2",
38
+ "@playwright/test": "^1.60.0",
37
39
  "eslint": "^10.1.0",
38
40
  "typescript": "^6.0.2",
39
41
  "vue-tsc": "^3.2.6"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.71.0",
3
+ "version": "3.71.1",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "3.71.0"
3
+ version = "3.71.1"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -2282,6 +2282,12 @@ def _terminal_origin_ok(origin: str) -> bool:
2282
2282
  )
2283
2283
 
2284
2284
 
2285
+ # v3.71.1 — enforce a single live WebSocket per session (latest wins), so a
2286
+ # reload or a second tab can't fight over the one PTY fd reader.
2287
+ from core.terminal.connections import ConnectionRegistry as _TerminalConnRegistry
2288
+ _terminal_conns = _TerminalConnRegistry()
2289
+
2290
+
2285
2291
  @app.get("/api/terminal/token")
2286
2292
  def terminal_token_endpoint():
2287
2293
  """Return the per-process bearer token used in WS handshakes.
@@ -2366,6 +2372,16 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
2366
2372
 
2367
2373
  await ws.accept()
2368
2374
 
2375
+ # v3.71.1 — single live connection per session: a newer connection
2376
+ # (a reload, or a second tab) supersedes the previous one, which we
2377
+ # close so it stops pumping the shared PTY fd.
2378
+ superseded = _terminal_conns.acquire(session_id, ws)
2379
+ if superseded is not None:
2380
+ try:
2381
+ await superseded.close(code=4409, reason="superseded by a newer connection")
2382
+ except Exception:
2383
+ pass
2384
+
2369
2385
  # v3.71.0 — replay recent scrollback so a client reconnecting after
2370
2386
  # the operator navigated away / reloaded restores its session as it
2371
2387
  # left it. Sent before the live reader is attached, so the historical
@@ -2437,10 +2453,13 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
2437
2453
  pass
2438
2454
  finally:
2439
2455
  pump_task.cancel()
2440
- try:
2441
- loop.remove_reader(session.master_fd)
2442
- except (ValueError, OSError):
2443
- pass
2456
+ # Only the still-active connection owns the fd reader — a superseded
2457
+ # connection tearing down must not remove its replacement's reader.
2458
+ if _terminal_conns.release(session_id, ws):
2459
+ try:
2460
+ loop.remove_reader(session.master_fd)
2461
+ except (ValueError, OSError):
2462
+ pass
2444
2463
 
2445
2464
 
2446
2465
  @app.on_event("startup")