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 +5 -1
- package/lib/sdk-bridge.js +54 -0
- package/package.json +1 -1
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:
|
|
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