clay-server 2.40.0-beta.3 → 2.40.0

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.
@@ -3,6 +3,13 @@ var { createTerminal } = require("./terminal");
3
3
  var MAX_TERMINALS = 10;
4
4
  var SCROLLBACK_MAX = 50 * 1024; // 50 KB per terminal
5
5
 
6
+ // Idle reclaim: background `claude` TUI PTYs that nobody is viewing and that
7
+ // have produced no output / received no input for this long are killed to
8
+ // free system resources. Safe because TUI sessions resume on demand from
9
+ // their on-disk transcript (lazy resume), so the conversation isn't lost.
10
+ var TUI_IDLE_REAP_MS = 3 * 60 * 1000; // 3 minutes
11
+ var TUI_REAP_INTERVAL_MS = 60 * 1000; // sweep cadence
12
+
6
13
  /**
7
14
  * Create a terminal manager for a project.
8
15
  * Manages persistent PTY sessions with scrollback buffering.
@@ -49,6 +56,7 @@ function createTerminalManager(opts) {
49
56
  rows: rows || 24,
50
57
  title: (opts && opts.title) || ("Terminal " + id),
51
58
  kind: (opts && opts.kind) || "shell",
59
+ lastActivityAt: Date.now(),
52
60
  exited: false,
53
61
  exitCode: null,
54
62
  subscribers: new Set(),
@@ -60,6 +68,7 @@ function createTerminalManager(opts) {
60
68
  pty.onData(function (data) {
61
69
  // Buffer scrollback with timestamps
62
70
  var ts = Date.now();
71
+ session.lastActivityAt = ts; // output counts as activity (don't reap a working claude)
63
72
  session.scrollback.push({ ts: ts, data: data });
64
73
  session.scrollbackSize += data.length;
65
74
  session.totalBytesWritten += data.length;
@@ -149,6 +158,7 @@ function createTerminalManager(opts) {
149
158
  function write(id, data) {
150
159
  var session = terminals.get(id);
151
160
  if (session && session.pty) {
161
+ session.lastActivityAt = Date.now(); // user input counts as activity
152
162
  session.pty.write(data);
153
163
  }
154
164
  }
@@ -225,7 +235,40 @@ function createTerminalManager(opts) {
225
235
  };
226
236
  }
227
237
 
238
+ // Periodic sweep: reclaim idle background TUI PTYs. A terminal is reaped
239
+ // only when it's a `tui-session`, has no live subscribers (nobody's viewing
240
+ // it), and has seen no input/output for TUI_IDLE_REAP_MS. The session is
241
+ // flagged reclaimed so its onExitHook keeps the Clay session record
242
+ // (lazy-resume re-spawns claude on demand) instead of deleting it.
243
+ function reapIdleTuiTerminals() {
244
+ var now = Date.now();
245
+ var toReap = [];
246
+ for (var session of terminals.values()) {
247
+ if (session.kind !== "tui-session" || session.exited || !session.pty) continue;
248
+ if (session.subscribers.size > 0) continue;
249
+ if (now - (session.lastActivityAt || 0) < TUI_IDLE_REAP_MS) continue;
250
+ toReap.push(session);
251
+ }
252
+ for (var i = 0; i < toReap.length; i++) {
253
+ toReap[i].reclaimed = true;
254
+ close(toReap[i].id); // pty.kill -> onExit -> onExitHook(session)
255
+ }
256
+ }
257
+
258
+ // Flag a terminal as reclaimed (PTY closed but the owning Clay session
259
+ // should be kept, not deleted). Used by idle reap and by an explicit
260
+ // user "Close" before close() so the onExitHook can tell the difference
261
+ // from a real /exit.
262
+ function markReclaimed(id) {
263
+ var session = terminals.get(id);
264
+ if (session) session.reclaimed = true;
265
+ }
266
+
267
+ var reapTimer = setInterval(reapIdleTuiTerminals, TUI_REAP_INTERVAL_MS);
268
+ if (reapTimer && typeof reapTimer.unref === "function") reapTimer.unref();
269
+
228
270
  function destroyAll() {
271
+ if (reapTimer) { clearInterval(reapTimer); reapTimer = null; }
229
272
  for (var session of terminals.values()) {
230
273
  // Shutdown teardown: pty.kill triggers pty.onExit asynchronously, and
231
274
  // that handler would normally invoke onExitHook (which for TUI
@@ -260,6 +303,7 @@ function createTerminalManager(opts) {
260
303
  getScrollback: getScrollback,
261
304
  destroyAll: destroyAll,
262
305
  has: has,
306
+ markReclaimed: markReclaimed,
263
307
  };
264
308
  }
265
309
 
package/lib/ws-schema.js CHANGED
@@ -17,6 +17,8 @@ var schema = {
17
17
  // Session management
18
18
  // -----------------------------------------------------------------------
19
19
  "switch_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Switch the active session by local ID" },
20
+ "resume_tui_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Spawn the claude --resume PTY for a TUI session shown read-only (lazy resume)" },
21
+ "suspend_tui_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Close a live TUI session's PTY now but keep it resumable (explicit Close)" },
20
22
  "new_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Create a new blank session" },
21
23
  "delete_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a session by ID" },
22
24
  "rename_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Rename a session" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.40.0-beta.3",
3
+ "version": "2.40.0",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",