clay-server 2.27.0-beta.12 → 2.27.0-beta.13

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/lib/project.js CHANGED
@@ -1183,10 +1183,14 @@ function createProjectContext(opts) {
1183
1183
  },
1184
1184
  warmup: function () {
1185
1185
  sdk.warmup();
1186
+ sdk.startIdleReaper();
1186
1187
  // Migrate existing relay session titles to SDK format (one-time, async)
1187
1188
  sm.migrateSessionTitles(getSDK, cwd);
1188
1189
  },
1189
- destroy: destroy,
1190
+ destroy: function () {
1191
+ sdk.stopIdleReaper();
1192
+ destroy();
1193
+ },
1190
1194
  };
1191
1195
  }
1192
1196
 
package/lib/sdk-bridge.js CHANGED
@@ -138,6 +138,55 @@ function createSDKBridge(opts) {
138
138
  var onProcessingChanged = opts.onProcessingChanged || function () {};
139
139
  var onTurnDone = opts.onTurnDone || null;
140
140
 
141
+ // --- Idle session reaper ---
142
+ // In single-user (in-process) mode, each session's Claude child process stays
143
+ // alive between turns because the messageQueue push-stream is never ended.
144
+ // Without a reaper, processes accumulate indefinitely as users switch between
145
+ // sessions and projects. This reaper ends the messageQueue for sessions that
146
+ // have been idle for IDLE_TIMEOUT_MS, allowing processQueryStream's finally
147
+ // block to clean up the child process. Session state on disk is preserved —
148
+ // the next startQuery() call resumes with a fresh process.
149
+ var IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
150
+ var IDLE_CHECK_INTERVAL_MS = 60 * 1000; // check every 60 seconds
151
+ var _idleReaperTimer = null;
152
+
153
+ function startIdleReaper() {
154
+ if (_idleReaperTimer) return;
155
+ _idleReaperTimer = setInterval(function () {
156
+ var now = Date.now();
157
+ sm.sessions.forEach(function (session) {
158
+ // Skip sessions that are actively processing, have no query, use workers,
159
+ // or are single-turn (Ralph Loop — managed by onQueryComplete).
160
+ if (session.isProcessing) return;
161
+ if (!session.queryInstance) return;
162
+ if (session.worker) return;
163
+ if (session.singleTurn) return;
164
+ if (session.destroying) return;
165
+
166
+ var lastActivity = session.lastActivityAt || 0;
167
+ if (now - lastActivity > IDLE_TIMEOUT_MS) {
168
+ console.log("[sdk-bridge] Reaping idle session " + session.localId +
169
+ " (idle " + Math.round((now - lastActivity) / 60000) + "min)" +
170
+ (session.title ? " title=" + JSON.stringify(session.title) : ""));
171
+ // End the message queue so the for-await loop in processQueryStream
172
+ // exits naturally, triggering the finally block cleanup.
173
+ if (session.messageQueue && typeof session.messageQueue.end === "function") {
174
+ try { session.messageQueue.end(); } catch (e) {}
175
+ }
176
+ }
177
+ });
178
+ }, IDLE_CHECK_INTERVAL_MS);
179
+ // Don't prevent process exit
180
+ if (_idleReaperTimer.unref) _idleReaperTimer.unref();
181
+ }
182
+
183
+ function stopIdleReaper() {
184
+ if (_idleReaperTimer) {
185
+ clearInterval(_idleReaperTimer);
186
+ _idleReaperTimer = null;
187
+ }
188
+ }
189
+
141
190
  // --- Skill discovery helpers ---
142
191
 
143
192
  function discoverSkillDirs() {
@@ -522,6 +571,7 @@ function createSDKBridge(opts) {
522
571
  });
523
572
  }
524
573
  // Reset for next turn in the same query
574
+ session.lastActivityAt = Date.now();
525
575
  var donePreview = session.responsePreview || "";
526
576
  session.responsePreview = "";
527
577
  session.streamedText = false;
@@ -2129,6 +2179,7 @@ function createSDKBridge(opts) {
2129
2179
  session.messageQueue.end();
2130
2180
  }
2131
2181
 
2182
+ session.lastActivityAt = Date.now();
2132
2183
  session.streamPromise = processQueryStream(session).catch(function(err) {
2133
2184
  });
2134
2185
  }
@@ -2150,6 +2201,7 @@ function createSDKBridge(opts) {
2150
2201
  type: "user",
2151
2202
  message: { role: "user", content: content },
2152
2203
  };
2204
+ session.lastActivityAt = Date.now();
2153
2205
  // Route through worker if active, otherwise direct to message queue
2154
2206
  if (session.worker) {
2155
2207
  session.worker.send({ type: "push_message", content: userMsg });
@@ -2566,6 +2618,8 @@ function createSDKBridge(opts) {
2566
2618
  warmup: warmup,
2567
2619
  stopTask: stopTask,
2568
2620
  createMentionSession: createMentionSession,
2621
+ startIdleReaper: startIdleReaper,
2622
+ stopIdleReaper: stopIdleReaper,
2569
2623
  };
2570
2624
  }
2571
2625
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.27.0-beta.12",
3
+ "version": "2.27.0-beta.13",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",