sessix-server 0.5.0 → 0.5.3
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/dist/index.js +185 -35
- package/dist/server.js +185 -35
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -308,6 +308,8 @@ var import_node_os9 = require("os");
|
|
|
308
308
|
var import_node_path9 = require("path");
|
|
309
309
|
var import_node_child_process12 = require("child_process");
|
|
310
310
|
var import_node_util3 = require("util");
|
|
311
|
+
var import_node_v8 = require("v8");
|
|
312
|
+
var import_node_vm = require("vm");
|
|
311
313
|
|
|
312
314
|
// src/providers/ProcessProvider.ts
|
|
313
315
|
var import_child_process = require("child_process");
|
|
@@ -450,26 +452,33 @@ function getSessionFilePath(projectPath, sessionId) {
|
|
|
450
452
|
}
|
|
451
453
|
async function getSessionModel(projectPath, sessionId) {
|
|
452
454
|
const filePath = getSessionFilePath(projectPath, sessionId);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
455
|
+
let fileHandle;
|
|
456
|
+
try {
|
|
457
|
+
fileHandle = await (0, import_promises.open)(filePath, "r");
|
|
458
|
+
const rl = (0, import_readline.createInterface)({
|
|
459
|
+
input: fileHandle.createReadStream({ encoding: "utf-8" }),
|
|
460
|
+
crlfDelay: Infinity
|
|
461
|
+
});
|
|
462
|
+
let lastModel;
|
|
463
|
+
for await (const line of rl) {
|
|
464
|
+
if (!line.trim()) continue;
|
|
465
|
+
try {
|
|
466
|
+
const obj = JSON.parse(line);
|
|
467
|
+
if (obj.type !== "assistant" || !obj.message) continue;
|
|
468
|
+
const model = obj.message.model;
|
|
469
|
+
if (typeof model === "string" && model && model !== "unknown") {
|
|
470
|
+
lastModel = model;
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
468
473
|
}
|
|
469
|
-
} catch {
|
|
470
474
|
}
|
|
475
|
+
return lastModel;
|
|
476
|
+
} catch (err) {
|
|
477
|
+
if (err.code === "ENOENT") return void 0;
|
|
478
|
+
throw err;
|
|
479
|
+
} finally {
|
|
480
|
+
await fileHandle?.close();
|
|
471
481
|
}
|
|
472
|
-
return void 0;
|
|
473
482
|
}
|
|
474
483
|
async function getProjects() {
|
|
475
484
|
try {
|
|
@@ -598,17 +607,23 @@ async function getHistoricalSessions(projectPath) {
|
|
|
598
607
|
}
|
|
599
608
|
}
|
|
600
609
|
async function getSessionHistory(projectPath, sessionId) {
|
|
610
|
+
let fileHandle;
|
|
601
611
|
try {
|
|
602
612
|
const encodedPath = encodeDirName(projectPath);
|
|
603
613
|
const filePath = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
|
|
604
|
-
|
|
605
|
-
|
|
614
|
+
try {
|
|
615
|
+
fileHandle = await (0, import_promises.open)(filePath, "r");
|
|
616
|
+
} catch (err) {
|
|
617
|
+
if (err.code === "ENOENT") return { ok: true, value: [] };
|
|
606
618
|
throw err;
|
|
619
|
+
}
|
|
620
|
+
const rl = (0, import_readline.createInterface)({
|
|
621
|
+
input: fileHandle.createReadStream({ encoding: "utf-8" }),
|
|
622
|
+
crlfDelay: Infinity
|
|
607
623
|
});
|
|
608
|
-
if (raw === null) return { ok: true, value: [] };
|
|
609
|
-
const lines = raw.split("\n").filter((l) => l.trim());
|
|
610
624
|
const events = [];
|
|
611
|
-
for (const line of
|
|
625
|
+
for await (const line of rl) {
|
|
626
|
+
if (!line.trim()) continue;
|
|
612
627
|
try {
|
|
613
628
|
const obj = JSON.parse(line);
|
|
614
629
|
const type = obj.type;
|
|
@@ -681,6 +696,8 @@ async function getSessionHistory(projectPath, sessionId) {
|
|
|
681
696
|
ok: false,
|
|
682
697
|
error: err instanceof Error ? err : new Error(String(err))
|
|
683
698
|
};
|
|
699
|
+
} finally {
|
|
700
|
+
await fileHandle?.close();
|
|
684
701
|
}
|
|
685
702
|
}
|
|
686
703
|
async function extractLastTimestamp(filePath) {
|
|
@@ -1025,6 +1042,27 @@ var ProcessProvider = class {
|
|
|
1025
1042
|
}
|
|
1026
1043
|
return swept;
|
|
1027
1044
|
}
|
|
1045
|
+
/**
|
|
1046
|
+
* 枚举可淘汰的老会话
|
|
1047
|
+
*
|
|
1048
|
+
* 进程已退出(已被空闲 GC kill)且空闲超过 maxIdleMs 的会话——其 entry 与各 Map
|
|
1049
|
+
* 仍长期占内存。调用方对返回 id 执行 killSession 彻底清除;淘汰后手机端发消息
|
|
1050
|
+
* 会自动走 resume 路径(--resume + JSONL),不影响继续对话。
|
|
1051
|
+
*
|
|
1052
|
+
* @returns 可淘汰的 sessionId 列表(仅枚举,不删除)
|
|
1053
|
+
*/
|
|
1054
|
+
listEvictableSessions(maxIdleMs) {
|
|
1055
|
+
if (maxIdleMs <= 0) return [];
|
|
1056
|
+
const now = Date.now();
|
|
1057
|
+
const evictable = [];
|
|
1058
|
+
for (const [sessionId, entry] of this.activeSessions) {
|
|
1059
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null) continue;
|
|
1060
|
+
if (entry.session.status === "running" || entry.session.status === "waiting_question" || entry.session.status === "waiting_approval") continue;
|
|
1061
|
+
if (now - entry.session.lastActiveAt < maxIdleMs) continue;
|
|
1062
|
+
evictable.push(sessionId);
|
|
1063
|
+
}
|
|
1064
|
+
return evictable;
|
|
1065
|
+
}
|
|
1028
1066
|
// ============================================
|
|
1029
1067
|
// 私有方法
|
|
1030
1068
|
// ============================================
|
|
@@ -2041,10 +2079,18 @@ var SessionManager = class {
|
|
|
2041
2079
|
sessionAgentType = /* @__PURE__ */ new Map();
|
|
2042
2080
|
/** 事件回调列表(事件会被转发到 WsBridge) */
|
|
2043
2081
|
eventCallbacks = [];
|
|
2082
|
+
/** 会话被移除(kill / 淘汰)时的回调列表(用于释放外部模块的会话级状态,如 NotificationService) */
|
|
2083
|
+
sessionRemovedCallbacks = [];
|
|
2044
2084
|
/** 每个会话的事件流取消订阅函数 */
|
|
2045
2085
|
unsubscribeMap = /* @__PURE__ */ new Map();
|
|
2046
2086
|
/** 每个会话的事件缓冲区(用于新订阅者重放)*/
|
|
2047
2087
|
sessionEventBuffers = /* @__PURE__ */ new Map();
|
|
2088
|
+
/**
|
|
2089
|
+
* 每个会话最近一次 AskUserQuestion tool_use 的真实 id(从 claude_event 流捕获)。
|
|
2090
|
+
* PreToolUse hook payload 不含 tool_use_id,但内联卡片需要它来匹配状态,
|
|
2091
|
+
* 故在转发流事件时记录,askQuestion 时兜底回填。
|
|
2092
|
+
*/
|
|
2093
|
+
lastAskQuestionToolUseId = /* @__PURE__ */ new Map();
|
|
2048
2094
|
/** AskUserQuestion 问题映射:requestId → resolve 回调 + 原始问题内容 */
|
|
2049
2095
|
pendingQuestions = /* @__PURE__ */ new Map();
|
|
2050
2096
|
/**
|
|
@@ -2153,6 +2199,7 @@ var SessionManager = class {
|
|
|
2153
2199
|
this.bufferTruncated.delete(sessionId);
|
|
2154
2200
|
this.sessionProjectPaths.delete(sessionId);
|
|
2155
2201
|
this.sessionStats.delete(sessionId);
|
|
2202
|
+
this.lastAskQuestionToolUseId.delete(sessionId);
|
|
2156
2203
|
const pending = this.pendingAssistantEvents.get(sessionId);
|
|
2157
2204
|
if (pending) {
|
|
2158
2205
|
clearTimeout(pending.timer);
|
|
@@ -2161,6 +2208,13 @@ var SessionManager = class {
|
|
|
2161
2208
|
const provider = this.getProviderForSession(sessionId);
|
|
2162
2209
|
await provider.killSession(sessionId);
|
|
2163
2210
|
this.sessionAgentType.delete(sessionId);
|
|
2211
|
+
for (const cb of this.sessionRemovedCallbacks) {
|
|
2212
|
+
try {
|
|
2213
|
+
cb(sessionId);
|
|
2214
|
+
} catch (err) {
|
|
2215
|
+
console.error("[SessionManager] sessionRemoved callback failed:", err);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2164
2218
|
console.log(`[SessionManager] Session killed: ${sessionId}`);
|
|
2165
2219
|
}
|
|
2166
2220
|
/**
|
|
@@ -2351,6 +2405,21 @@ var SessionManager = class {
|
|
|
2351
2405
|
}
|
|
2352
2406
|
};
|
|
2353
2407
|
}
|
|
2408
|
+
/**
|
|
2409
|
+
* 注册"会话被移除"回调(会话 kill 或淘汰时触发,传入 sessionId)。
|
|
2410
|
+
* 用于让外部模块释放会话级状态,如 NotificationService.releaseSession。
|
|
2411
|
+
*
|
|
2412
|
+
* @returns 取消注册的函数
|
|
2413
|
+
*/
|
|
2414
|
+
onSessionRemoved(callback) {
|
|
2415
|
+
this.sessionRemovedCallbacks.push(callback);
|
|
2416
|
+
return () => {
|
|
2417
|
+
const index = this.sessionRemovedCallbacks.indexOf(callback);
|
|
2418
|
+
if (index !== -1) {
|
|
2419
|
+
this.sessionRemovedCallbacks.splice(index, 1);
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2354
2423
|
/**
|
|
2355
2424
|
* 清理所有资源
|
|
2356
2425
|
*/
|
|
@@ -2374,6 +2443,7 @@ var SessionManager = class {
|
|
|
2374
2443
|
this.pendingQuestions.clear();
|
|
2375
2444
|
this.lastBroadcastStatus.clear();
|
|
2376
2445
|
this.eventCallbacks.length = 0;
|
|
2446
|
+
this.sessionRemovedCallbacks.length = 0;
|
|
2377
2447
|
console.log("[SessionManager] Destroyed");
|
|
2378
2448
|
}
|
|
2379
2449
|
// ============================================
|
|
@@ -2422,6 +2492,13 @@ var SessionManager = class {
|
|
|
2422
2492
|
console.log(`[SessionManager] \u{1F9E0} thinking block detected in ${sessionId}: msgId=${event.message.id}, blocks=${thinkingBlocks.length}, len=${thinkingBlocks.map((b) => (b.thinking || "").length).join(",")}`);
|
|
2423
2493
|
}
|
|
2424
2494
|
}
|
|
2495
|
+
if (event.type === "assistant" && Array.isArray(event.message?.content)) {
|
|
2496
|
+
for (const block of event.message.content) {
|
|
2497
|
+
if (block.type === "tool_use" && (block.name === "AskUserQuestion" || block.name === "AskFollowupQuestion") && typeof block.id === "string") {
|
|
2498
|
+
this.lastAskQuestionToolUseId.set(sessionId, block.id);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2425
2502
|
switch (event.type) {
|
|
2426
2503
|
case "assistant":
|
|
2427
2504
|
this.bufferAssistantEvent(sessionId, event);
|
|
@@ -2542,10 +2619,11 @@ var SessionManager = class {
|
|
|
2542
2619
|
* 返回的 Promise 在 handleQuestionResponse 时 resolve。
|
|
2543
2620
|
*/
|
|
2544
2621
|
askQuestion(sessionId, toolUseId, questions, requestId) {
|
|
2622
|
+
const resolvedToolUseId = toolUseId || this.lastAskQuestionToolUseId.get(sessionId) || "";
|
|
2545
2623
|
const request = {
|
|
2546
2624
|
id: requestId,
|
|
2547
2625
|
sessionId,
|
|
2548
|
-
toolUseId,
|
|
2626
|
+
toolUseId: resolvedToolUseId,
|
|
2549
2627
|
question: questions[0]?.question ?? "",
|
|
2550
2628
|
options: questions[0]?.options?.map((o) => o.label),
|
|
2551
2629
|
questions,
|
|
@@ -2557,7 +2635,7 @@ var SessionManager = class {
|
|
|
2557
2635
|
return new Promise((resolve) => {
|
|
2558
2636
|
this.pendingQuestions.set(requestId, {
|
|
2559
2637
|
sessionId,
|
|
2560
|
-
toolUseId,
|
|
2638
|
+
toolUseId: resolvedToolUseId,
|
|
2561
2639
|
question: request.question,
|
|
2562
2640
|
options: request.options,
|
|
2563
2641
|
questions,
|
|
@@ -4079,7 +4157,8 @@ var HookInstaller = class {
|
|
|
4079
4157
|
const isLatestVersion = approvalScriptContent.includes("permissionDecision") && approvalScriptContent.includes("Sessix Approval Hook v2");
|
|
4080
4158
|
const settings = await this.readClaudeSettings();
|
|
4081
4159
|
const configExists = this.hasHookConfig(settings);
|
|
4082
|
-
|
|
4160
|
+
const hasLegacyHook = this.hasHookEntry(settings?.hooks?.PreToolUse, LEGACY_HOOK_COMMANDS[0]) || this.hasHookEntry(settings?.hooks?.PermissionRequest, LEGACY_HOOK_COMMANDS[1]);
|
|
4161
|
+
return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists && !hasLegacyHook;
|
|
4083
4162
|
}
|
|
4084
4163
|
// ============================================
|
|
4085
4164
|
// 内部方法
|
|
@@ -4091,8 +4170,14 @@ var HookInstaller = class {
|
|
|
4091
4170
|
let settings = await this.readClaudeSettings();
|
|
4092
4171
|
let changed = false;
|
|
4093
4172
|
for (const cmd of LEGACY_HOOK_COMMANDS) {
|
|
4094
|
-
this.
|
|
4095
|
-
|
|
4173
|
+
if (this.hasHookEntry(settings?.hooks?.PreToolUse, cmd)) {
|
|
4174
|
+
this.removeHookCommand(settings, "PreToolUse", cmd);
|
|
4175
|
+
changed = true;
|
|
4176
|
+
}
|
|
4177
|
+
if (this.hasHookEntry(settings?.hooks?.PermissionRequest, cmd)) {
|
|
4178
|
+
this.removeHookCommand(settings, "PermissionRequest", cmd);
|
|
4179
|
+
changed = true;
|
|
4180
|
+
}
|
|
4096
4181
|
}
|
|
4097
4182
|
if (!settings.hooks) {
|
|
4098
4183
|
settings.hooks = {};
|
|
@@ -4489,12 +4574,40 @@ var NotificationService = class _NotificationService {
|
|
|
4489
4574
|
this.latestAssistantText.clear();
|
|
4490
4575
|
for (const timer of this.activityPushTimers.values()) clearTimeout(timer);
|
|
4491
4576
|
this.activityPushTimers.clear();
|
|
4577
|
+
for (const timer of this.idleEndTimers.values()) clearTimeout(timer);
|
|
4578
|
+
this.idleEndTimers.clear();
|
|
4579
|
+
for (const timer of this.laHeartbeatTimers.values()) clearInterval(timer);
|
|
4580
|
+
this.laHeartbeatTimers.clear();
|
|
4492
4581
|
this.recentActivityState.clear();
|
|
4493
4582
|
this.lastActivityPushAt.clear();
|
|
4494
4583
|
this.pendingPriority.clear();
|
|
4495
4584
|
this.activityCounters.clear();
|
|
4496
4585
|
this.lastPushedFingerprint.clear();
|
|
4497
4586
|
}
|
|
4587
|
+
/**
|
|
4588
|
+
* 释放单个会话的全部内存状态(会话被 kill 或淘汰时调用)。
|
|
4589
|
+
* 由 SessionManager.onSessionRemoved 钩子触发,覆盖用户主动 kill 和自动淘汰两条路径。
|
|
4590
|
+
* 幂等:重复调用或对未知会话调用都安全。
|
|
4591
|
+
*/
|
|
4592
|
+
releaseSession(sessionId) {
|
|
4593
|
+
this.clearActivityPushTimer(sessionId);
|
|
4594
|
+
this.cancelIdleEndTimer(sessionId);
|
|
4595
|
+
this.stopLaHeartbeat(sessionId);
|
|
4596
|
+
this.clearSessionActivityState(sessionId);
|
|
4597
|
+
this.yoloModeState.delete(sessionId);
|
|
4598
|
+
this.lastActivityPushAt.delete(sessionId);
|
|
4599
|
+
this.lastPushedFingerprint.delete(sessionId);
|
|
4600
|
+
this.pendingPriority.delete(sessionId);
|
|
4601
|
+
}
|
|
4602
|
+
/**
|
|
4603
|
+
* 清空单会话可重建的重状态(recentActivity / 计数器 / 最新文本)。
|
|
4604
|
+
* 会话走到 idle 时调用即可释放内存——resume 后这些状态会随新事件自动重建。
|
|
4605
|
+
*/
|
|
4606
|
+
clearSessionActivityState(sessionId) {
|
|
4607
|
+
this.recentActivityState.delete(sessionId);
|
|
4608
|
+
this.activityCounters.delete(sessionId);
|
|
4609
|
+
this.latestAssistantText.delete(sessionId);
|
|
4610
|
+
}
|
|
4498
4611
|
// ============================================
|
|
4499
4612
|
// 内部方法
|
|
4500
4613
|
// ============================================
|
|
@@ -4532,6 +4645,7 @@ var NotificationService = class _NotificationService {
|
|
|
4532
4645
|
badge: this.getGlobalPendingCount(),
|
|
4533
4646
|
data: { type: "task_complete", sessionId: event.sessionId }
|
|
4534
4647
|
});
|
|
4648
|
+
this.clearSessionActivityState(event.sessionId);
|
|
4535
4649
|
}
|
|
4536
4650
|
} else if (event.status === "running" || event.status === "waiting_approval" || event.status === "waiting_question") {
|
|
4537
4651
|
this.cancelIdleEndTimer(event.sessionId);
|
|
@@ -4820,9 +4934,8 @@ var NotificationService = class _NotificationService {
|
|
|
4820
4934
|
});
|
|
4821
4935
|
}
|
|
4822
4936
|
this.stopLaHeartbeat(sessionId);
|
|
4823
|
-
this.
|
|
4937
|
+
this.clearSessionActivityState(sessionId);
|
|
4824
4938
|
this.lastActivityPushAt.delete(sessionId);
|
|
4825
|
-
this.activityCounters.delete(sessionId);
|
|
4826
4939
|
this.lastPushedFingerprint.delete(sessionId);
|
|
4827
4940
|
console.log(`[NotificationService] \u{1F3C1} LA end (${reason}) session=${sessionId.slice(0, 8)}\u2026`);
|
|
4828
4941
|
}
|
|
@@ -5923,7 +6036,6 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
5923
6036
|
"DerivedData",
|
|
5924
6037
|
"Pods",
|
|
5925
6038
|
".build",
|
|
5926
|
-
"build",
|
|
5927
6039
|
"dist",
|
|
5928
6040
|
"__pycache__",
|
|
5929
6041
|
".next",
|
|
@@ -7091,10 +7203,11 @@ function isValidTask(value) {
|
|
|
7091
7203
|
// src/utils/cliCapabilities.ts
|
|
7092
7204
|
var import_node_child_process11 = require("child_process");
|
|
7093
7205
|
var DEFAULT_MODELS = [
|
|
7094
|
-
{ value: "opus", label: "Opus 4.
|
|
7095
|
-
{ value: "claude-opus-4-
|
|
7096
|
-
{ value: "
|
|
7097
|
-
{ value: "
|
|
7206
|
+
{ value: "opus", label: "Opus 4.8", sublabel: "Most capable for ambitious work", maxEffort: "max", defaultEffort: "xhigh" },
|
|
7207
|
+
{ value: "claude-opus-4-7", label: "Opus 4.7", sublabel: "Previous generation flagship", maxEffort: "max", defaultEffort: "xhigh" },
|
|
7208
|
+
{ value: "claude-opus-4-6", label: "Opus 4.6", sublabel: "Earlier generation flagship", maxEffort: "max", defaultEffort: "xhigh" },
|
|
7209
|
+
{ value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks", maxEffort: "high", defaultEffort: "high" },
|
|
7210
|
+
{ value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers", maxEffort: "medium", defaultEffort: "medium" }
|
|
7098
7211
|
];
|
|
7099
7212
|
var DEFAULT_CAPABILITIES = {
|
|
7100
7213
|
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
@@ -7262,6 +7375,7 @@ async function start(opts = {}) {
|
|
|
7262
7375
|
const notificationService = new NotificationService(sessionManager, expoChannel);
|
|
7263
7376
|
notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
|
|
7264
7377
|
notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
|
|
7378
|
+
sessionManager.onSessionRemoved((sessionId) => notificationService.releaseSession(sessionId));
|
|
7265
7379
|
const activityPushOpts = opts.activityPush ?? await loadApnsConfigFromFile();
|
|
7266
7380
|
if (activityPushOpts) {
|
|
7267
7381
|
try {
|
|
@@ -7950,8 +8064,30 @@ async function start(opts = {}) {
|
|
|
7950
8064
|
const idleTimeoutMs = Number(process.env.SESSIX_IDLE_TIMEOUT_MS ?? 30 * 60 * 1e3);
|
|
7951
8065
|
const idleSweepIntervalMs = Number(process.env.SESSIX_IDLE_SWEEP_INTERVAL_MS ?? 5 * 60 * 1e3);
|
|
7952
8066
|
const maxActiveProcesses = Number(process.env.SESSIX_MAX_ACTIVE_PROCESSES ?? 15);
|
|
8067
|
+
const sessionEvictMs = Number(process.env.SESSIX_SESSION_EVICT_MS ?? 2 * 60 * 60 * 1e3);
|
|
8068
|
+
let gcFn;
|
|
8069
|
+
const maybeGc = () => {
|
|
8070
|
+
if (gcFn === void 0) {
|
|
8071
|
+
gcFn = globalThis.gc ?? null;
|
|
8072
|
+
if (!gcFn) {
|
|
8073
|
+
try {
|
|
8074
|
+
(0, import_node_v8.setFlagsFromString)("--expose-gc");
|
|
8075
|
+
const fn = (0, import_node_vm.runInNewContext)("gc");
|
|
8076
|
+
gcFn = typeof fn === "function" ? fn : null;
|
|
8077
|
+
} catch {
|
|
8078
|
+
gcFn = null;
|
|
8079
|
+
}
|
|
8080
|
+
}
|
|
8081
|
+
}
|
|
8082
|
+
if (gcFn) {
|
|
8083
|
+
try {
|
|
8084
|
+
gcFn();
|
|
8085
|
+
} catch {
|
|
8086
|
+
}
|
|
8087
|
+
}
|
|
8088
|
+
};
|
|
7953
8089
|
let idleSweepTimer = null;
|
|
7954
|
-
if (idleSweepIntervalMs > 0 && (idleTimeoutMs > 0 || maxActiveProcesses > 0)) {
|
|
8090
|
+
if (idleSweepIntervalMs > 0 && (idleTimeoutMs > 0 || maxActiveProcesses > 0 || sessionEvictMs > 0)) {
|
|
7955
8091
|
idleSweepTimer = setInterval(async () => {
|
|
7956
8092
|
try {
|
|
7957
8093
|
let totalSwept = 0;
|
|
@@ -7970,7 +8106,18 @@ async function start(opts = {}) {
|
|
|
7970
8106
|
swept.forEach(broadcastShrink);
|
|
7971
8107
|
totalSwept += swept.length;
|
|
7972
8108
|
}
|
|
8109
|
+
if (sessionEvictMs > 0 && typeof provider.listEvictableSessions === "function") {
|
|
8110
|
+
const evictable = provider.listEvictableSessions(sessionEvictMs);
|
|
8111
|
+
for (const id of evictable) {
|
|
8112
|
+
await sessionManager.killSession(id);
|
|
8113
|
+
}
|
|
8114
|
+
if (evictable.length > 0) {
|
|
8115
|
+
console.log(`[Server] Idle GC: evicted ${evictable.length} stale session(s)`);
|
|
8116
|
+
totalSwept += evictable.length;
|
|
8117
|
+
}
|
|
8118
|
+
}
|
|
7973
8119
|
}
|
|
8120
|
+
const hasRunning = sessionManager.getActiveSessions().some((s) => s.status === "running");
|
|
7974
8121
|
if (totalSwept > 0) {
|
|
7975
8122
|
console.log(`[Server] Idle GC: swept ${totalSwept} idle session(s)`);
|
|
7976
8123
|
wsBridge.broadcast({
|
|
@@ -7978,6 +8125,9 @@ async function start(opts = {}) {
|
|
|
7978
8125
|
sessions: sessionManager.getActiveSessions()
|
|
7979
8126
|
});
|
|
7980
8127
|
}
|
|
8128
|
+
if (totalSwept > 0 || !hasRunning) {
|
|
8129
|
+
maybeGc();
|
|
8130
|
+
}
|
|
7981
8131
|
} catch (err) {
|
|
7982
8132
|
console.error("[Server] Idle GC failed:", err);
|
|
7983
8133
|
}
|
package/dist/server.js
CHANGED
|
@@ -313,6 +313,8 @@ var import_node_os9 = require("os");
|
|
|
313
313
|
var import_node_path9 = require("path");
|
|
314
314
|
var import_node_child_process12 = require("child_process");
|
|
315
315
|
var import_node_util3 = require("util");
|
|
316
|
+
var import_node_v8 = require("v8");
|
|
317
|
+
var import_node_vm = require("vm");
|
|
316
318
|
|
|
317
319
|
// src/providers/ProcessProvider.ts
|
|
318
320
|
var import_child_process = require("child_process");
|
|
@@ -455,26 +457,33 @@ function getSessionFilePath(projectPath, sessionId) {
|
|
|
455
457
|
}
|
|
456
458
|
async function getSessionModel(projectPath, sessionId) {
|
|
457
459
|
const filePath = getSessionFilePath(projectPath, sessionId);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
460
|
+
let fileHandle;
|
|
461
|
+
try {
|
|
462
|
+
fileHandle = await (0, import_promises.open)(filePath, "r");
|
|
463
|
+
const rl = (0, import_readline.createInterface)({
|
|
464
|
+
input: fileHandle.createReadStream({ encoding: "utf-8" }),
|
|
465
|
+
crlfDelay: Infinity
|
|
466
|
+
});
|
|
467
|
+
let lastModel;
|
|
468
|
+
for await (const line of rl) {
|
|
469
|
+
if (!line.trim()) continue;
|
|
470
|
+
try {
|
|
471
|
+
const obj = JSON.parse(line);
|
|
472
|
+
if (obj.type !== "assistant" || !obj.message) continue;
|
|
473
|
+
const model = obj.message.model;
|
|
474
|
+
if (typeof model === "string" && model && model !== "unknown") {
|
|
475
|
+
lastModel = model;
|
|
476
|
+
}
|
|
477
|
+
} catch {
|
|
473
478
|
}
|
|
474
|
-
} catch {
|
|
475
479
|
}
|
|
480
|
+
return lastModel;
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (err.code === "ENOENT") return void 0;
|
|
483
|
+
throw err;
|
|
484
|
+
} finally {
|
|
485
|
+
await fileHandle?.close();
|
|
476
486
|
}
|
|
477
|
-
return void 0;
|
|
478
487
|
}
|
|
479
488
|
async function getProjects() {
|
|
480
489
|
try {
|
|
@@ -603,17 +612,23 @@ async function getHistoricalSessions(projectPath) {
|
|
|
603
612
|
}
|
|
604
613
|
}
|
|
605
614
|
async function getSessionHistory(projectPath, sessionId) {
|
|
615
|
+
let fileHandle;
|
|
606
616
|
try {
|
|
607
617
|
const encodedPath = encodeDirName(projectPath);
|
|
608
618
|
const filePath = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
|
|
609
|
-
|
|
610
|
-
|
|
619
|
+
try {
|
|
620
|
+
fileHandle = await (0, import_promises.open)(filePath, "r");
|
|
621
|
+
} catch (err) {
|
|
622
|
+
if (err.code === "ENOENT") return { ok: true, value: [] };
|
|
611
623
|
throw err;
|
|
624
|
+
}
|
|
625
|
+
const rl = (0, import_readline.createInterface)({
|
|
626
|
+
input: fileHandle.createReadStream({ encoding: "utf-8" }),
|
|
627
|
+
crlfDelay: Infinity
|
|
612
628
|
});
|
|
613
|
-
if (raw === null) return { ok: true, value: [] };
|
|
614
|
-
const lines = raw.split("\n").filter((l) => l.trim());
|
|
615
629
|
const events = [];
|
|
616
|
-
for (const line of
|
|
630
|
+
for await (const line of rl) {
|
|
631
|
+
if (!line.trim()) continue;
|
|
617
632
|
try {
|
|
618
633
|
const obj = JSON.parse(line);
|
|
619
634
|
const type = obj.type;
|
|
@@ -686,6 +701,8 @@ async function getSessionHistory(projectPath, sessionId) {
|
|
|
686
701
|
ok: false,
|
|
687
702
|
error: err instanceof Error ? err : new Error(String(err))
|
|
688
703
|
};
|
|
704
|
+
} finally {
|
|
705
|
+
await fileHandle?.close();
|
|
689
706
|
}
|
|
690
707
|
}
|
|
691
708
|
async function extractLastTimestamp(filePath) {
|
|
@@ -1030,6 +1047,27 @@ var ProcessProvider = class {
|
|
|
1030
1047
|
}
|
|
1031
1048
|
return swept;
|
|
1032
1049
|
}
|
|
1050
|
+
/**
|
|
1051
|
+
* 枚举可淘汰的老会话
|
|
1052
|
+
*
|
|
1053
|
+
* 进程已退出(已被空闲 GC kill)且空闲超过 maxIdleMs 的会话——其 entry 与各 Map
|
|
1054
|
+
* 仍长期占内存。调用方对返回 id 执行 killSession 彻底清除;淘汰后手机端发消息
|
|
1055
|
+
* 会自动走 resume 路径(--resume + JSONL),不影响继续对话。
|
|
1056
|
+
*
|
|
1057
|
+
* @returns 可淘汰的 sessionId 列表(仅枚举,不删除)
|
|
1058
|
+
*/
|
|
1059
|
+
listEvictableSessions(maxIdleMs) {
|
|
1060
|
+
if (maxIdleMs <= 0) return [];
|
|
1061
|
+
const now = Date.now();
|
|
1062
|
+
const evictable = [];
|
|
1063
|
+
for (const [sessionId, entry] of this.activeSessions) {
|
|
1064
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null) continue;
|
|
1065
|
+
if (entry.session.status === "running" || entry.session.status === "waiting_question" || entry.session.status === "waiting_approval") continue;
|
|
1066
|
+
if (now - entry.session.lastActiveAt < maxIdleMs) continue;
|
|
1067
|
+
evictable.push(sessionId);
|
|
1068
|
+
}
|
|
1069
|
+
return evictable;
|
|
1070
|
+
}
|
|
1033
1071
|
// ============================================
|
|
1034
1072
|
// 私有方法
|
|
1035
1073
|
// ============================================
|
|
@@ -2046,10 +2084,18 @@ var SessionManager = class {
|
|
|
2046
2084
|
sessionAgentType = /* @__PURE__ */ new Map();
|
|
2047
2085
|
/** 事件回调列表(事件会被转发到 WsBridge) */
|
|
2048
2086
|
eventCallbacks = [];
|
|
2087
|
+
/** 会话被移除(kill / 淘汰)时的回调列表(用于释放外部模块的会话级状态,如 NotificationService) */
|
|
2088
|
+
sessionRemovedCallbacks = [];
|
|
2049
2089
|
/** 每个会话的事件流取消订阅函数 */
|
|
2050
2090
|
unsubscribeMap = /* @__PURE__ */ new Map();
|
|
2051
2091
|
/** 每个会话的事件缓冲区(用于新订阅者重放)*/
|
|
2052
2092
|
sessionEventBuffers = /* @__PURE__ */ new Map();
|
|
2093
|
+
/**
|
|
2094
|
+
* 每个会话最近一次 AskUserQuestion tool_use 的真实 id(从 claude_event 流捕获)。
|
|
2095
|
+
* PreToolUse hook payload 不含 tool_use_id,但内联卡片需要它来匹配状态,
|
|
2096
|
+
* 故在转发流事件时记录,askQuestion 时兜底回填。
|
|
2097
|
+
*/
|
|
2098
|
+
lastAskQuestionToolUseId = /* @__PURE__ */ new Map();
|
|
2053
2099
|
/** AskUserQuestion 问题映射:requestId → resolve 回调 + 原始问题内容 */
|
|
2054
2100
|
pendingQuestions = /* @__PURE__ */ new Map();
|
|
2055
2101
|
/**
|
|
@@ -2158,6 +2204,7 @@ var SessionManager = class {
|
|
|
2158
2204
|
this.bufferTruncated.delete(sessionId);
|
|
2159
2205
|
this.sessionProjectPaths.delete(sessionId);
|
|
2160
2206
|
this.sessionStats.delete(sessionId);
|
|
2207
|
+
this.lastAskQuestionToolUseId.delete(sessionId);
|
|
2161
2208
|
const pending = this.pendingAssistantEvents.get(sessionId);
|
|
2162
2209
|
if (pending) {
|
|
2163
2210
|
clearTimeout(pending.timer);
|
|
@@ -2166,6 +2213,13 @@ var SessionManager = class {
|
|
|
2166
2213
|
const provider = this.getProviderForSession(sessionId);
|
|
2167
2214
|
await provider.killSession(sessionId);
|
|
2168
2215
|
this.sessionAgentType.delete(sessionId);
|
|
2216
|
+
for (const cb of this.sessionRemovedCallbacks) {
|
|
2217
|
+
try {
|
|
2218
|
+
cb(sessionId);
|
|
2219
|
+
} catch (err) {
|
|
2220
|
+
console.error("[SessionManager] sessionRemoved callback failed:", err);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2169
2223
|
console.log(`[SessionManager] Session killed: ${sessionId}`);
|
|
2170
2224
|
}
|
|
2171
2225
|
/**
|
|
@@ -2356,6 +2410,21 @@ var SessionManager = class {
|
|
|
2356
2410
|
}
|
|
2357
2411
|
};
|
|
2358
2412
|
}
|
|
2413
|
+
/**
|
|
2414
|
+
* 注册"会话被移除"回调(会话 kill 或淘汰时触发,传入 sessionId)。
|
|
2415
|
+
* 用于让外部模块释放会话级状态,如 NotificationService.releaseSession。
|
|
2416
|
+
*
|
|
2417
|
+
* @returns 取消注册的函数
|
|
2418
|
+
*/
|
|
2419
|
+
onSessionRemoved(callback) {
|
|
2420
|
+
this.sessionRemovedCallbacks.push(callback);
|
|
2421
|
+
return () => {
|
|
2422
|
+
const index = this.sessionRemovedCallbacks.indexOf(callback);
|
|
2423
|
+
if (index !== -1) {
|
|
2424
|
+
this.sessionRemovedCallbacks.splice(index, 1);
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2359
2428
|
/**
|
|
2360
2429
|
* 清理所有资源
|
|
2361
2430
|
*/
|
|
@@ -2379,6 +2448,7 @@ var SessionManager = class {
|
|
|
2379
2448
|
this.pendingQuestions.clear();
|
|
2380
2449
|
this.lastBroadcastStatus.clear();
|
|
2381
2450
|
this.eventCallbacks.length = 0;
|
|
2451
|
+
this.sessionRemovedCallbacks.length = 0;
|
|
2382
2452
|
console.log("[SessionManager] Destroyed");
|
|
2383
2453
|
}
|
|
2384
2454
|
// ============================================
|
|
@@ -2427,6 +2497,13 @@ var SessionManager = class {
|
|
|
2427
2497
|
console.log(`[SessionManager] \u{1F9E0} thinking block detected in ${sessionId}: msgId=${event.message.id}, blocks=${thinkingBlocks.length}, len=${thinkingBlocks.map((b) => (b.thinking || "").length).join(",")}`);
|
|
2428
2498
|
}
|
|
2429
2499
|
}
|
|
2500
|
+
if (event.type === "assistant" && Array.isArray(event.message?.content)) {
|
|
2501
|
+
for (const block of event.message.content) {
|
|
2502
|
+
if (block.type === "tool_use" && (block.name === "AskUserQuestion" || block.name === "AskFollowupQuestion") && typeof block.id === "string") {
|
|
2503
|
+
this.lastAskQuestionToolUseId.set(sessionId, block.id);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2430
2507
|
switch (event.type) {
|
|
2431
2508
|
case "assistant":
|
|
2432
2509
|
this.bufferAssistantEvent(sessionId, event);
|
|
@@ -2547,10 +2624,11 @@ var SessionManager = class {
|
|
|
2547
2624
|
* 返回的 Promise 在 handleQuestionResponse 时 resolve。
|
|
2548
2625
|
*/
|
|
2549
2626
|
askQuestion(sessionId, toolUseId, questions, requestId) {
|
|
2627
|
+
const resolvedToolUseId = toolUseId || this.lastAskQuestionToolUseId.get(sessionId) || "";
|
|
2550
2628
|
const request = {
|
|
2551
2629
|
id: requestId,
|
|
2552
2630
|
sessionId,
|
|
2553
|
-
toolUseId,
|
|
2631
|
+
toolUseId: resolvedToolUseId,
|
|
2554
2632
|
question: questions[0]?.question ?? "",
|
|
2555
2633
|
options: questions[0]?.options?.map((o) => o.label),
|
|
2556
2634
|
questions,
|
|
@@ -2562,7 +2640,7 @@ var SessionManager = class {
|
|
|
2562
2640
|
return new Promise((resolve) => {
|
|
2563
2641
|
this.pendingQuestions.set(requestId, {
|
|
2564
2642
|
sessionId,
|
|
2565
|
-
toolUseId,
|
|
2643
|
+
toolUseId: resolvedToolUseId,
|
|
2566
2644
|
question: request.question,
|
|
2567
2645
|
options: request.options,
|
|
2568
2646
|
questions,
|
|
@@ -4084,7 +4162,8 @@ var HookInstaller = class {
|
|
|
4084
4162
|
const isLatestVersion = approvalScriptContent.includes("permissionDecision") && approvalScriptContent.includes("Sessix Approval Hook v2");
|
|
4085
4163
|
const settings = await this.readClaudeSettings();
|
|
4086
4164
|
const configExists = this.hasHookConfig(settings);
|
|
4087
|
-
|
|
4165
|
+
const hasLegacyHook = this.hasHookEntry(settings?.hooks?.PreToolUse, LEGACY_HOOK_COMMANDS[0]) || this.hasHookEntry(settings?.hooks?.PermissionRequest, LEGACY_HOOK_COMMANDS[1]);
|
|
4166
|
+
return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists && !hasLegacyHook;
|
|
4088
4167
|
}
|
|
4089
4168
|
// ============================================
|
|
4090
4169
|
// 内部方法
|
|
@@ -4096,8 +4175,14 @@ var HookInstaller = class {
|
|
|
4096
4175
|
let settings = await this.readClaudeSettings();
|
|
4097
4176
|
let changed = false;
|
|
4098
4177
|
for (const cmd of LEGACY_HOOK_COMMANDS) {
|
|
4099
|
-
this.
|
|
4100
|
-
|
|
4178
|
+
if (this.hasHookEntry(settings?.hooks?.PreToolUse, cmd)) {
|
|
4179
|
+
this.removeHookCommand(settings, "PreToolUse", cmd);
|
|
4180
|
+
changed = true;
|
|
4181
|
+
}
|
|
4182
|
+
if (this.hasHookEntry(settings?.hooks?.PermissionRequest, cmd)) {
|
|
4183
|
+
this.removeHookCommand(settings, "PermissionRequest", cmd);
|
|
4184
|
+
changed = true;
|
|
4185
|
+
}
|
|
4101
4186
|
}
|
|
4102
4187
|
if (!settings.hooks) {
|
|
4103
4188
|
settings.hooks = {};
|
|
@@ -4494,12 +4579,40 @@ var NotificationService = class _NotificationService {
|
|
|
4494
4579
|
this.latestAssistantText.clear();
|
|
4495
4580
|
for (const timer of this.activityPushTimers.values()) clearTimeout(timer);
|
|
4496
4581
|
this.activityPushTimers.clear();
|
|
4582
|
+
for (const timer of this.idleEndTimers.values()) clearTimeout(timer);
|
|
4583
|
+
this.idleEndTimers.clear();
|
|
4584
|
+
for (const timer of this.laHeartbeatTimers.values()) clearInterval(timer);
|
|
4585
|
+
this.laHeartbeatTimers.clear();
|
|
4497
4586
|
this.recentActivityState.clear();
|
|
4498
4587
|
this.lastActivityPushAt.clear();
|
|
4499
4588
|
this.pendingPriority.clear();
|
|
4500
4589
|
this.activityCounters.clear();
|
|
4501
4590
|
this.lastPushedFingerprint.clear();
|
|
4502
4591
|
}
|
|
4592
|
+
/**
|
|
4593
|
+
* 释放单个会话的全部内存状态(会话被 kill 或淘汰时调用)。
|
|
4594
|
+
* 由 SessionManager.onSessionRemoved 钩子触发,覆盖用户主动 kill 和自动淘汰两条路径。
|
|
4595
|
+
* 幂等:重复调用或对未知会话调用都安全。
|
|
4596
|
+
*/
|
|
4597
|
+
releaseSession(sessionId) {
|
|
4598
|
+
this.clearActivityPushTimer(sessionId);
|
|
4599
|
+
this.cancelIdleEndTimer(sessionId);
|
|
4600
|
+
this.stopLaHeartbeat(sessionId);
|
|
4601
|
+
this.clearSessionActivityState(sessionId);
|
|
4602
|
+
this.yoloModeState.delete(sessionId);
|
|
4603
|
+
this.lastActivityPushAt.delete(sessionId);
|
|
4604
|
+
this.lastPushedFingerprint.delete(sessionId);
|
|
4605
|
+
this.pendingPriority.delete(sessionId);
|
|
4606
|
+
}
|
|
4607
|
+
/**
|
|
4608
|
+
* 清空单会话可重建的重状态(recentActivity / 计数器 / 最新文本)。
|
|
4609
|
+
* 会话走到 idle 时调用即可释放内存——resume 后这些状态会随新事件自动重建。
|
|
4610
|
+
*/
|
|
4611
|
+
clearSessionActivityState(sessionId) {
|
|
4612
|
+
this.recentActivityState.delete(sessionId);
|
|
4613
|
+
this.activityCounters.delete(sessionId);
|
|
4614
|
+
this.latestAssistantText.delete(sessionId);
|
|
4615
|
+
}
|
|
4503
4616
|
// ============================================
|
|
4504
4617
|
// 内部方法
|
|
4505
4618
|
// ============================================
|
|
@@ -4537,6 +4650,7 @@ var NotificationService = class _NotificationService {
|
|
|
4537
4650
|
badge: this.getGlobalPendingCount(),
|
|
4538
4651
|
data: { type: "task_complete", sessionId: event.sessionId }
|
|
4539
4652
|
});
|
|
4653
|
+
this.clearSessionActivityState(event.sessionId);
|
|
4540
4654
|
}
|
|
4541
4655
|
} else if (event.status === "running" || event.status === "waiting_approval" || event.status === "waiting_question") {
|
|
4542
4656
|
this.cancelIdleEndTimer(event.sessionId);
|
|
@@ -4825,9 +4939,8 @@ var NotificationService = class _NotificationService {
|
|
|
4825
4939
|
});
|
|
4826
4940
|
}
|
|
4827
4941
|
this.stopLaHeartbeat(sessionId);
|
|
4828
|
-
this.
|
|
4942
|
+
this.clearSessionActivityState(sessionId);
|
|
4829
4943
|
this.lastActivityPushAt.delete(sessionId);
|
|
4830
|
-
this.activityCounters.delete(sessionId);
|
|
4831
4944
|
this.lastPushedFingerprint.delete(sessionId);
|
|
4832
4945
|
console.log(`[NotificationService] \u{1F3C1} LA end (${reason}) session=${sessionId.slice(0, 8)}\u2026`);
|
|
4833
4946
|
}
|
|
@@ -5928,7 +6041,6 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
5928
6041
|
"DerivedData",
|
|
5929
6042
|
"Pods",
|
|
5930
6043
|
".build",
|
|
5931
|
-
"build",
|
|
5932
6044
|
"dist",
|
|
5933
6045
|
"__pycache__",
|
|
5934
6046
|
".next",
|
|
@@ -7096,10 +7208,11 @@ function isValidTask(value) {
|
|
|
7096
7208
|
// src/utils/cliCapabilities.ts
|
|
7097
7209
|
var import_node_child_process11 = require("child_process");
|
|
7098
7210
|
var DEFAULT_MODELS = [
|
|
7099
|
-
{ value: "opus", label: "Opus 4.
|
|
7100
|
-
{ value: "claude-opus-4-
|
|
7101
|
-
{ value: "
|
|
7102
|
-
{ value: "
|
|
7211
|
+
{ value: "opus", label: "Opus 4.8", sublabel: "Most capable for ambitious work", maxEffort: "max", defaultEffort: "xhigh" },
|
|
7212
|
+
{ value: "claude-opus-4-7", label: "Opus 4.7", sublabel: "Previous generation flagship", maxEffort: "max", defaultEffort: "xhigh" },
|
|
7213
|
+
{ value: "claude-opus-4-6", label: "Opus 4.6", sublabel: "Earlier generation flagship", maxEffort: "max", defaultEffort: "xhigh" },
|
|
7214
|
+
{ value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks", maxEffort: "high", defaultEffort: "high" },
|
|
7215
|
+
{ value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers", maxEffort: "medium", defaultEffort: "medium" }
|
|
7103
7216
|
];
|
|
7104
7217
|
var DEFAULT_CAPABILITIES = {
|
|
7105
7218
|
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
@@ -7267,6 +7380,7 @@ async function start(opts = {}) {
|
|
|
7267
7380
|
const notificationService = new NotificationService(sessionManager, expoChannel);
|
|
7268
7381
|
notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
|
|
7269
7382
|
notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
|
|
7383
|
+
sessionManager.onSessionRemoved((sessionId) => notificationService.releaseSession(sessionId));
|
|
7270
7384
|
const activityPushOpts = opts.activityPush ?? await loadApnsConfigFromFile();
|
|
7271
7385
|
if (activityPushOpts) {
|
|
7272
7386
|
try {
|
|
@@ -7955,8 +8069,30 @@ async function start(opts = {}) {
|
|
|
7955
8069
|
const idleTimeoutMs = Number(process.env.SESSIX_IDLE_TIMEOUT_MS ?? 30 * 60 * 1e3);
|
|
7956
8070
|
const idleSweepIntervalMs = Number(process.env.SESSIX_IDLE_SWEEP_INTERVAL_MS ?? 5 * 60 * 1e3);
|
|
7957
8071
|
const maxActiveProcesses = Number(process.env.SESSIX_MAX_ACTIVE_PROCESSES ?? 15);
|
|
8072
|
+
const sessionEvictMs = Number(process.env.SESSIX_SESSION_EVICT_MS ?? 2 * 60 * 60 * 1e3);
|
|
8073
|
+
let gcFn;
|
|
8074
|
+
const maybeGc = () => {
|
|
8075
|
+
if (gcFn === void 0) {
|
|
8076
|
+
gcFn = globalThis.gc ?? null;
|
|
8077
|
+
if (!gcFn) {
|
|
8078
|
+
try {
|
|
8079
|
+
(0, import_node_v8.setFlagsFromString)("--expose-gc");
|
|
8080
|
+
const fn = (0, import_node_vm.runInNewContext)("gc");
|
|
8081
|
+
gcFn = typeof fn === "function" ? fn : null;
|
|
8082
|
+
} catch {
|
|
8083
|
+
gcFn = null;
|
|
8084
|
+
}
|
|
8085
|
+
}
|
|
8086
|
+
}
|
|
8087
|
+
if (gcFn) {
|
|
8088
|
+
try {
|
|
8089
|
+
gcFn();
|
|
8090
|
+
} catch {
|
|
8091
|
+
}
|
|
8092
|
+
}
|
|
8093
|
+
};
|
|
7958
8094
|
let idleSweepTimer = null;
|
|
7959
|
-
if (idleSweepIntervalMs > 0 && (idleTimeoutMs > 0 || maxActiveProcesses > 0)) {
|
|
8095
|
+
if (idleSweepIntervalMs > 0 && (idleTimeoutMs > 0 || maxActiveProcesses > 0 || sessionEvictMs > 0)) {
|
|
7960
8096
|
idleSweepTimer = setInterval(async () => {
|
|
7961
8097
|
try {
|
|
7962
8098
|
let totalSwept = 0;
|
|
@@ -7975,7 +8111,18 @@ async function start(opts = {}) {
|
|
|
7975
8111
|
swept.forEach(broadcastShrink);
|
|
7976
8112
|
totalSwept += swept.length;
|
|
7977
8113
|
}
|
|
8114
|
+
if (sessionEvictMs > 0 && typeof provider.listEvictableSessions === "function") {
|
|
8115
|
+
const evictable = provider.listEvictableSessions(sessionEvictMs);
|
|
8116
|
+
for (const id of evictable) {
|
|
8117
|
+
await sessionManager.killSession(id);
|
|
8118
|
+
}
|
|
8119
|
+
if (evictable.length > 0) {
|
|
8120
|
+
console.log(`[Server] Idle GC: evicted ${evictable.length} stale session(s)`);
|
|
8121
|
+
totalSwept += evictable.length;
|
|
8122
|
+
}
|
|
8123
|
+
}
|
|
7978
8124
|
}
|
|
8125
|
+
const hasRunning = sessionManager.getActiveSessions().some((s) => s.status === "running");
|
|
7979
8126
|
if (totalSwept > 0) {
|
|
7980
8127
|
console.log(`[Server] Idle GC: swept ${totalSwept} idle session(s)`);
|
|
7981
8128
|
wsBridge.broadcast({
|
|
@@ -7983,6 +8130,9 @@ async function start(opts = {}) {
|
|
|
7983
8130
|
sessions: sessionManager.getActiveSessions()
|
|
7984
8131
|
});
|
|
7985
8132
|
}
|
|
8133
|
+
if (totalSwept > 0 || !hasRunning) {
|
|
8134
|
+
maybeGc();
|
|
8135
|
+
}
|
|
7986
8136
|
} catch (err) {
|
|
7987
8137
|
console.error("[Server] Idle GC failed:", err);
|
|
7988
8138
|
}
|