sessix-server 0.5.0 → 0.5.6
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 +342 -81
- package/dist/server.js +337 -76
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -309,10 +309,12 @@ function t(key, params) {
|
|
|
309
309
|
// src/server.ts
|
|
310
310
|
var import_uuid8 = require("uuid");
|
|
311
311
|
var import_promises7 = require("fs/promises");
|
|
312
|
-
var
|
|
313
|
-
var
|
|
312
|
+
var import_node_os11 = require("os");
|
|
313
|
+
var import_node_path11 = 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");
|
|
@@ -449,32 +451,54 @@ var import_promises = require("fs/promises");
|
|
|
449
451
|
var import_readline = require("readline");
|
|
450
452
|
var import_path = require("path");
|
|
451
453
|
var import_os = require("os");
|
|
454
|
+
|
|
455
|
+
// src/utils/modelValidation.ts
|
|
456
|
+
function isUsableModel(model) {
|
|
457
|
+
if (typeof model !== "string") return false;
|
|
458
|
+
const trimmed = model.trim();
|
|
459
|
+
if (!trimmed) return false;
|
|
460
|
+
if (trimmed === "unknown") return false;
|
|
461
|
+
if (trimmed.startsWith("<")) return false;
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
function sanitizeModel(model) {
|
|
465
|
+
return isUsableModel(model) ? model.trim() : void 0;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/session/ProjectReader.ts
|
|
452
469
|
var CLAUDE_PROJECTS_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
|
|
453
470
|
function getSessionFilePath(projectPath, sessionId) {
|
|
454
471
|
return (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
|
|
455
472
|
}
|
|
456
473
|
async function getSessionModel(projectPath, sessionId) {
|
|
457
474
|
const filePath = getSessionFilePath(projectPath, sessionId);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
475
|
+
let fileHandle;
|
|
476
|
+
try {
|
|
477
|
+
fileHandle = await (0, import_promises.open)(filePath, "r");
|
|
478
|
+
const rl = (0, import_readline.createInterface)({
|
|
479
|
+
input: fileHandle.createReadStream({ encoding: "utf-8" }),
|
|
480
|
+
crlfDelay: Infinity
|
|
481
|
+
});
|
|
482
|
+
let lastModel;
|
|
483
|
+
for await (const line of rl) {
|
|
484
|
+
if (!line.trim()) continue;
|
|
485
|
+
try {
|
|
486
|
+
const obj = JSON.parse(line);
|
|
487
|
+
if (obj.type !== "assistant" || !obj.message) continue;
|
|
488
|
+
const model = obj.message.model;
|
|
489
|
+
if (isUsableModel(model)) {
|
|
490
|
+
lastModel = model;
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
473
493
|
}
|
|
474
|
-
} catch {
|
|
475
494
|
}
|
|
495
|
+
return lastModel;
|
|
496
|
+
} catch (err) {
|
|
497
|
+
if (err.code === "ENOENT") return void 0;
|
|
498
|
+
throw err;
|
|
499
|
+
} finally {
|
|
500
|
+
await fileHandle?.close();
|
|
476
501
|
}
|
|
477
|
-
return void 0;
|
|
478
502
|
}
|
|
479
503
|
async function getProjects() {
|
|
480
504
|
try {
|
|
@@ -603,17 +627,23 @@ async function getHistoricalSessions(projectPath) {
|
|
|
603
627
|
}
|
|
604
628
|
}
|
|
605
629
|
async function getSessionHistory(projectPath, sessionId) {
|
|
630
|
+
let fileHandle;
|
|
606
631
|
try {
|
|
607
632
|
const encodedPath = encodeDirName(projectPath);
|
|
608
633
|
const filePath = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
|
|
609
|
-
|
|
610
|
-
|
|
634
|
+
try {
|
|
635
|
+
fileHandle = await (0, import_promises.open)(filePath, "r");
|
|
636
|
+
} catch (err) {
|
|
637
|
+
if (err.code === "ENOENT") return { ok: true, value: [] };
|
|
611
638
|
throw err;
|
|
639
|
+
}
|
|
640
|
+
const rl = (0, import_readline.createInterface)({
|
|
641
|
+
input: fileHandle.createReadStream({ encoding: "utf-8" }),
|
|
642
|
+
crlfDelay: Infinity
|
|
612
643
|
});
|
|
613
|
-
if (raw === null) return { ok: true, value: [] };
|
|
614
|
-
const lines = raw.split("\n").filter((l) => l.trim());
|
|
615
644
|
const events = [];
|
|
616
|
-
for (const line of
|
|
645
|
+
for await (const line of rl) {
|
|
646
|
+
if (!line.trim()) continue;
|
|
617
647
|
try {
|
|
618
648
|
const obj = JSON.parse(line);
|
|
619
649
|
const type = obj.type;
|
|
@@ -686,6 +716,8 @@ async function getSessionHistory(projectPath, sessionId) {
|
|
|
686
716
|
ok: false,
|
|
687
717
|
error: err instanceof Error ? err : new Error(String(err))
|
|
688
718
|
};
|
|
719
|
+
} finally {
|
|
720
|
+
await fileHandle?.close();
|
|
689
721
|
}
|
|
690
722
|
}
|
|
691
723
|
async function extractLastTimestamp(filePath) {
|
|
@@ -1030,6 +1062,27 @@ var ProcessProvider = class {
|
|
|
1030
1062
|
}
|
|
1031
1063
|
return swept;
|
|
1032
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* 枚举可淘汰的老会话
|
|
1067
|
+
*
|
|
1068
|
+
* 进程已退出(已被空闲 GC kill)且空闲超过 maxIdleMs 的会话——其 entry 与各 Map
|
|
1069
|
+
* 仍长期占内存。调用方对返回 id 执行 killSession 彻底清除;淘汰后手机端发消息
|
|
1070
|
+
* 会自动走 resume 路径(--resume + JSONL),不影响继续对话。
|
|
1071
|
+
*
|
|
1072
|
+
* @returns 可淘汰的 sessionId 列表(仅枚举,不删除)
|
|
1073
|
+
*/
|
|
1074
|
+
listEvictableSessions(maxIdleMs) {
|
|
1075
|
+
if (maxIdleMs <= 0) return [];
|
|
1076
|
+
const now = Date.now();
|
|
1077
|
+
const evictable = [];
|
|
1078
|
+
for (const [sessionId, entry] of this.activeSessions) {
|
|
1079
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null) continue;
|
|
1080
|
+
if (entry.session.status === "running" || entry.session.status === "waiting_question" || entry.session.status === "waiting_approval") continue;
|
|
1081
|
+
if (now - entry.session.lastActiveAt < maxIdleMs) continue;
|
|
1082
|
+
evictable.push(sessionId);
|
|
1083
|
+
}
|
|
1084
|
+
return evictable;
|
|
1085
|
+
}
|
|
1033
1086
|
// ============================================
|
|
1034
1087
|
// 私有方法
|
|
1035
1088
|
// ============================================
|
|
@@ -1051,17 +1104,22 @@ var ProcessProvider = class {
|
|
|
1051
1104
|
} else {
|
|
1052
1105
|
args.push("--session-id", sessionId);
|
|
1053
1106
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1107
|
+
const safeModel = sanitizeModel(model);
|
|
1108
|
+
if (model && !safeModel) {
|
|
1109
|
+
console.warn(`[ProcessProvider] Session ${sessionId}: ignoring invalid model "${model}", falling back to CLI default`);
|
|
1110
|
+
}
|
|
1111
|
+
if (safeModel) {
|
|
1112
|
+
args.push("--model", safeModel);
|
|
1056
1113
|
}
|
|
1114
|
+
const safeFallbackModel = sanitizeModel(fallbackModel);
|
|
1057
1115
|
if (permissionMode && permissionMode !== "default") {
|
|
1058
1116
|
args.push("--permission-mode", permissionMode);
|
|
1059
1117
|
}
|
|
1060
1118
|
if (effort) {
|
|
1061
1119
|
args.push("--effort", effort);
|
|
1062
1120
|
}
|
|
1063
|
-
if (
|
|
1064
|
-
args.push("--fallback-model",
|
|
1121
|
+
if (safeFallbackModel) {
|
|
1122
|
+
args.push("--fallback-model", safeFallbackModel);
|
|
1065
1123
|
}
|
|
1066
1124
|
if (maxBudgetUsd != null) {
|
|
1067
1125
|
args.push("--max-budget-usd", String(maxBudgetUsd));
|
|
@@ -2046,10 +2104,18 @@ var SessionManager = class {
|
|
|
2046
2104
|
sessionAgentType = /* @__PURE__ */ new Map();
|
|
2047
2105
|
/** 事件回调列表(事件会被转发到 WsBridge) */
|
|
2048
2106
|
eventCallbacks = [];
|
|
2107
|
+
/** 会话被移除(kill / 淘汰)时的回调列表(用于释放外部模块的会话级状态,如 NotificationService) */
|
|
2108
|
+
sessionRemovedCallbacks = [];
|
|
2049
2109
|
/** 每个会话的事件流取消订阅函数 */
|
|
2050
2110
|
unsubscribeMap = /* @__PURE__ */ new Map();
|
|
2051
2111
|
/** 每个会话的事件缓冲区(用于新订阅者重放)*/
|
|
2052
2112
|
sessionEventBuffers = /* @__PURE__ */ new Map();
|
|
2113
|
+
/**
|
|
2114
|
+
* 每个会话最近一次 AskUserQuestion tool_use 的真实 id(从 claude_event 流捕获)。
|
|
2115
|
+
* PreToolUse hook payload 不含 tool_use_id,但内联卡片需要它来匹配状态,
|
|
2116
|
+
* 故在转发流事件时记录,askQuestion 时兜底回填。
|
|
2117
|
+
*/
|
|
2118
|
+
lastAskQuestionToolUseId = /* @__PURE__ */ new Map();
|
|
2053
2119
|
/** AskUserQuestion 问题映射:requestId → resolve 回调 + 原始问题内容 */
|
|
2054
2120
|
pendingQuestions = /* @__PURE__ */ new Map();
|
|
2055
2121
|
/**
|
|
@@ -2158,6 +2224,7 @@ var SessionManager = class {
|
|
|
2158
2224
|
this.bufferTruncated.delete(sessionId);
|
|
2159
2225
|
this.sessionProjectPaths.delete(sessionId);
|
|
2160
2226
|
this.sessionStats.delete(sessionId);
|
|
2227
|
+
this.lastAskQuestionToolUseId.delete(sessionId);
|
|
2161
2228
|
const pending = this.pendingAssistantEvents.get(sessionId);
|
|
2162
2229
|
if (pending) {
|
|
2163
2230
|
clearTimeout(pending.timer);
|
|
@@ -2166,6 +2233,13 @@ var SessionManager = class {
|
|
|
2166
2233
|
const provider = this.getProviderForSession(sessionId);
|
|
2167
2234
|
await provider.killSession(sessionId);
|
|
2168
2235
|
this.sessionAgentType.delete(sessionId);
|
|
2236
|
+
for (const cb of this.sessionRemovedCallbacks) {
|
|
2237
|
+
try {
|
|
2238
|
+
cb(sessionId);
|
|
2239
|
+
} catch (err) {
|
|
2240
|
+
console.error("[SessionManager] sessionRemoved callback failed:", err);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2169
2243
|
console.log(`[SessionManager] Session killed: ${sessionId}`);
|
|
2170
2244
|
}
|
|
2171
2245
|
/**
|
|
@@ -2356,6 +2430,21 @@ var SessionManager = class {
|
|
|
2356
2430
|
}
|
|
2357
2431
|
};
|
|
2358
2432
|
}
|
|
2433
|
+
/**
|
|
2434
|
+
* 注册"会话被移除"回调(会话 kill 或淘汰时触发,传入 sessionId)。
|
|
2435
|
+
* 用于让外部模块释放会话级状态,如 NotificationService.releaseSession。
|
|
2436
|
+
*
|
|
2437
|
+
* @returns 取消注册的函数
|
|
2438
|
+
*/
|
|
2439
|
+
onSessionRemoved(callback) {
|
|
2440
|
+
this.sessionRemovedCallbacks.push(callback);
|
|
2441
|
+
return () => {
|
|
2442
|
+
const index = this.sessionRemovedCallbacks.indexOf(callback);
|
|
2443
|
+
if (index !== -1) {
|
|
2444
|
+
this.sessionRemovedCallbacks.splice(index, 1);
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2359
2448
|
/**
|
|
2360
2449
|
* 清理所有资源
|
|
2361
2450
|
*/
|
|
@@ -2379,6 +2468,7 @@ var SessionManager = class {
|
|
|
2379
2468
|
this.pendingQuestions.clear();
|
|
2380
2469
|
this.lastBroadcastStatus.clear();
|
|
2381
2470
|
this.eventCallbacks.length = 0;
|
|
2471
|
+
this.sessionRemovedCallbacks.length = 0;
|
|
2382
2472
|
console.log("[SessionManager] Destroyed");
|
|
2383
2473
|
}
|
|
2384
2474
|
// ============================================
|
|
@@ -2427,6 +2517,13 @@ var SessionManager = class {
|
|
|
2427
2517
|
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
2518
|
}
|
|
2429
2519
|
}
|
|
2520
|
+
if (event.type === "assistant" && Array.isArray(event.message?.content)) {
|
|
2521
|
+
for (const block of event.message.content) {
|
|
2522
|
+
if (block.type === "tool_use" && (block.name === "AskUserQuestion" || block.name === "AskFollowupQuestion") && typeof block.id === "string") {
|
|
2523
|
+
this.lastAskQuestionToolUseId.set(sessionId, block.id);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2430
2527
|
switch (event.type) {
|
|
2431
2528
|
case "assistant":
|
|
2432
2529
|
this.bufferAssistantEvent(sessionId, event);
|
|
@@ -2547,10 +2644,11 @@ var SessionManager = class {
|
|
|
2547
2644
|
* 返回的 Promise 在 handleQuestionResponse 时 resolve。
|
|
2548
2645
|
*/
|
|
2549
2646
|
askQuestion(sessionId, toolUseId, questions, requestId) {
|
|
2647
|
+
const resolvedToolUseId = toolUseId || this.lastAskQuestionToolUseId.get(sessionId) || "";
|
|
2550
2648
|
const request = {
|
|
2551
2649
|
id: requestId,
|
|
2552
2650
|
sessionId,
|
|
2553
|
-
toolUseId,
|
|
2651
|
+
toolUseId: resolvedToolUseId,
|
|
2554
2652
|
question: questions[0]?.question ?? "",
|
|
2555
2653
|
options: questions[0]?.options?.map((o) => o.label),
|
|
2556
2654
|
questions,
|
|
@@ -2562,7 +2660,7 @@ var SessionManager = class {
|
|
|
2562
2660
|
return new Promise((resolve) => {
|
|
2563
2661
|
this.pendingQuestions.set(requestId, {
|
|
2564
2662
|
sessionId,
|
|
2565
|
-
toolUseId,
|
|
2663
|
+
toolUseId: resolvedToolUseId,
|
|
2566
2664
|
question: request.question,
|
|
2567
2665
|
options: request.options,
|
|
2568
2666
|
questions,
|
|
@@ -4084,7 +4182,8 @@ var HookInstaller = class {
|
|
|
4084
4182
|
const isLatestVersion = approvalScriptContent.includes("permissionDecision") && approvalScriptContent.includes("Sessix Approval Hook v2");
|
|
4085
4183
|
const settings = await this.readClaudeSettings();
|
|
4086
4184
|
const configExists = this.hasHookConfig(settings);
|
|
4087
|
-
|
|
4185
|
+
const hasLegacyHook = this.hasHookEntry(settings?.hooks?.PreToolUse, LEGACY_HOOK_COMMANDS[0]) || this.hasHookEntry(settings?.hooks?.PermissionRequest, LEGACY_HOOK_COMMANDS[1]);
|
|
4186
|
+
return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists && !hasLegacyHook;
|
|
4088
4187
|
}
|
|
4089
4188
|
// ============================================
|
|
4090
4189
|
// 内部方法
|
|
@@ -4096,8 +4195,14 @@ var HookInstaller = class {
|
|
|
4096
4195
|
let settings = await this.readClaudeSettings();
|
|
4097
4196
|
let changed = false;
|
|
4098
4197
|
for (const cmd of LEGACY_HOOK_COMMANDS) {
|
|
4099
|
-
this.
|
|
4100
|
-
|
|
4198
|
+
if (this.hasHookEntry(settings?.hooks?.PreToolUse, cmd)) {
|
|
4199
|
+
this.removeHookCommand(settings, "PreToolUse", cmd);
|
|
4200
|
+
changed = true;
|
|
4201
|
+
}
|
|
4202
|
+
if (this.hasHookEntry(settings?.hooks?.PermissionRequest, cmd)) {
|
|
4203
|
+
this.removeHookCommand(settings, "PermissionRequest", cmd);
|
|
4204
|
+
changed = true;
|
|
4205
|
+
}
|
|
4101
4206
|
}
|
|
4102
4207
|
if (!settings.hooks) {
|
|
4103
4208
|
settings.hooks = {};
|
|
@@ -4494,12 +4599,40 @@ var NotificationService = class _NotificationService {
|
|
|
4494
4599
|
this.latestAssistantText.clear();
|
|
4495
4600
|
for (const timer of this.activityPushTimers.values()) clearTimeout(timer);
|
|
4496
4601
|
this.activityPushTimers.clear();
|
|
4602
|
+
for (const timer of this.idleEndTimers.values()) clearTimeout(timer);
|
|
4603
|
+
this.idleEndTimers.clear();
|
|
4604
|
+
for (const timer of this.laHeartbeatTimers.values()) clearInterval(timer);
|
|
4605
|
+
this.laHeartbeatTimers.clear();
|
|
4497
4606
|
this.recentActivityState.clear();
|
|
4498
4607
|
this.lastActivityPushAt.clear();
|
|
4499
4608
|
this.pendingPriority.clear();
|
|
4500
4609
|
this.activityCounters.clear();
|
|
4501
4610
|
this.lastPushedFingerprint.clear();
|
|
4502
4611
|
}
|
|
4612
|
+
/**
|
|
4613
|
+
* 释放单个会话的全部内存状态(会话被 kill 或淘汰时调用)。
|
|
4614
|
+
* 由 SessionManager.onSessionRemoved 钩子触发,覆盖用户主动 kill 和自动淘汰两条路径。
|
|
4615
|
+
* 幂等:重复调用或对未知会话调用都安全。
|
|
4616
|
+
*/
|
|
4617
|
+
releaseSession(sessionId) {
|
|
4618
|
+
this.clearActivityPushTimer(sessionId);
|
|
4619
|
+
this.cancelIdleEndTimer(sessionId);
|
|
4620
|
+
this.stopLaHeartbeat(sessionId);
|
|
4621
|
+
this.clearSessionActivityState(sessionId);
|
|
4622
|
+
this.yoloModeState.delete(sessionId);
|
|
4623
|
+
this.lastActivityPushAt.delete(sessionId);
|
|
4624
|
+
this.lastPushedFingerprint.delete(sessionId);
|
|
4625
|
+
this.pendingPriority.delete(sessionId);
|
|
4626
|
+
}
|
|
4627
|
+
/**
|
|
4628
|
+
* 清空单会话可重建的重状态(recentActivity / 计数器 / 最新文本)。
|
|
4629
|
+
* 会话走到 idle 时调用即可释放内存——resume 后这些状态会随新事件自动重建。
|
|
4630
|
+
*/
|
|
4631
|
+
clearSessionActivityState(sessionId) {
|
|
4632
|
+
this.recentActivityState.delete(sessionId);
|
|
4633
|
+
this.activityCounters.delete(sessionId);
|
|
4634
|
+
this.latestAssistantText.delete(sessionId);
|
|
4635
|
+
}
|
|
4503
4636
|
// ============================================
|
|
4504
4637
|
// 内部方法
|
|
4505
4638
|
// ============================================
|
|
@@ -4537,6 +4670,7 @@ var NotificationService = class _NotificationService {
|
|
|
4537
4670
|
badge: this.getGlobalPendingCount(),
|
|
4538
4671
|
data: { type: "task_complete", sessionId: event.sessionId }
|
|
4539
4672
|
});
|
|
4673
|
+
this.clearSessionActivityState(event.sessionId);
|
|
4540
4674
|
}
|
|
4541
4675
|
} else if (event.status === "running" || event.status === "waiting_approval" || event.status === "waiting_question") {
|
|
4542
4676
|
this.cancelIdleEndTimer(event.sessionId);
|
|
@@ -4825,9 +4959,8 @@ var NotificationService = class _NotificationService {
|
|
|
4825
4959
|
});
|
|
4826
4960
|
}
|
|
4827
4961
|
this.stopLaHeartbeat(sessionId);
|
|
4828
|
-
this.
|
|
4962
|
+
this.clearSessionActivityState(sessionId);
|
|
4829
4963
|
this.lastActivityPushAt.delete(sessionId);
|
|
4830
|
-
this.activityCounters.delete(sessionId);
|
|
4831
4964
|
this.lastPushedFingerprint.delete(sessionId);
|
|
4832
4965
|
console.log(`[NotificationService] \u{1F3C1} LA end (${reason}) session=${sessionId.slice(0, 8)}\u2026`);
|
|
4833
4966
|
}
|
|
@@ -5020,6 +5153,36 @@ var DesktopNotificationChannel = class {
|
|
|
5020
5153
|
}
|
|
5021
5154
|
};
|
|
5022
5155
|
|
|
5156
|
+
// src/notification/pushTokenStore.ts
|
|
5157
|
+
var import_node_fs4 = require("fs");
|
|
5158
|
+
var import_node_os7 = require("os");
|
|
5159
|
+
var import_node_path6 = require("path");
|
|
5160
|
+
var DEFAULT_PUSH_TOKENS_FILE = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "push-tokens.json");
|
|
5161
|
+
function loadPushTokens(filePath = DEFAULT_PUSH_TOKENS_FILE) {
|
|
5162
|
+
try {
|
|
5163
|
+
const parsed = JSON.parse((0, import_node_fs4.readFileSync)(filePath, "utf-8"));
|
|
5164
|
+
if (!Array.isArray(parsed)) return [];
|
|
5165
|
+
return parsed.filter((t2) => typeof t2 === "string");
|
|
5166
|
+
} catch {
|
|
5167
|
+
return [];
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
function savePushTokens(tokens, filePath = DEFAULT_PUSH_TOKENS_FILE) {
|
|
5171
|
+
const deduped = [];
|
|
5172
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5173
|
+
for (const t2 of tokens) {
|
|
5174
|
+
if (typeof t2 !== "string" || seen.has(t2)) continue;
|
|
5175
|
+
seen.add(t2);
|
|
5176
|
+
deduped.push(t2);
|
|
5177
|
+
}
|
|
5178
|
+
try {
|
|
5179
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path6.dirname)(filePath), { recursive: true });
|
|
5180
|
+
(0, import_node_fs4.writeFileSync)(filePath, JSON.stringify(deduped, null, 2), "utf-8");
|
|
5181
|
+
} catch (err) {
|
|
5182
|
+
console.warn("[pushTokenStore] \u5199\u5165 push token \u5931\u8D25:", err);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
|
|
5023
5186
|
// src/notification/ExpoNotificationChannel.ts
|
|
5024
5187
|
var EXPO_PUSH_API = "https://exp.host/--/api/v2/push/send";
|
|
5025
5188
|
var EXPO_RECEIPT_API = "https://exp.host/--/api/v2/push/getReceipts";
|
|
@@ -5030,18 +5193,35 @@ var ExpoNotificationChannel = class {
|
|
|
5030
5193
|
tokenWsMap = /* @__PURE__ */ new Map();
|
|
5031
5194
|
/** per-token 通知音效偏好 */
|
|
5032
5195
|
soundPreferences = /* @__PURE__ */ new Map();
|
|
5196
|
+
/** push token 持久化文件路径 */
|
|
5197
|
+
pushTokensFile;
|
|
5198
|
+
constructor(opts = {}) {
|
|
5199
|
+
this.pushTokensFile = opts.pushTokensFile ?? DEFAULT_PUSH_TOKENS_FILE;
|
|
5200
|
+
for (const token of loadPushTokens(this.pushTokensFile)) {
|
|
5201
|
+
this.tokens.add(token);
|
|
5202
|
+
}
|
|
5203
|
+
if (this.tokens.size > 0) {
|
|
5204
|
+
console.log(`[ExpoNotificationChannel] \u4ECE\u78C1\u76D8\u91CD\u8F7D ${this.tokens.size} \u4E2A push token`);
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
/** 把当前 token 集合落盘,供进程重启后重载。 */
|
|
5208
|
+
persist() {
|
|
5209
|
+
savePushTokens(Array.from(this.tokens), this.pushTokensFile);
|
|
5210
|
+
}
|
|
5033
5211
|
isAvailable() {
|
|
5034
5212
|
return this.tokens.size > 0;
|
|
5035
5213
|
}
|
|
5036
5214
|
addToken(token, ws) {
|
|
5037
5215
|
this.tokens.add(token);
|
|
5038
5216
|
if (ws) this.tokenWsMap.set(token, ws);
|
|
5217
|
+
this.persist();
|
|
5039
5218
|
console.log(`[ExpoNotificationChannel] ${t("notification.tokenRegistered", { count: this.tokens.size })}`);
|
|
5040
5219
|
}
|
|
5041
5220
|
removeToken(token) {
|
|
5042
5221
|
this.tokens.delete(token);
|
|
5043
5222
|
this.tokenWsMap.delete(token);
|
|
5044
5223
|
this.soundPreferences.delete(token);
|
|
5224
|
+
this.persist();
|
|
5045
5225
|
console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
|
|
5046
5226
|
}
|
|
5047
5227
|
/** 更新某个 token 的音效偏好 */
|
|
@@ -5113,6 +5293,7 @@ var ExpoNotificationChannel = class {
|
|
|
5113
5293
|
this.tokens.delete(staleToken);
|
|
5114
5294
|
this.tokenWsMap.delete(staleToken);
|
|
5115
5295
|
this.soundPreferences.delete(staleToken);
|
|
5296
|
+
this.persist();
|
|
5116
5297
|
console.warn(`[ExpoNotificationChannel] \u26A0\uFE0F \u5DF2\u79FB\u9664\u5931\u6548 token\uFF08DeviceNotRegistered\uFF09\u3002\u82E5\u901A\u77E5\u672A\u6062\u590D\uFF0C\u8BF7\u91CD\u542F App \u91CD\u65B0\u6CE8\u518C push token\u3002`);
|
|
5117
5298
|
}
|
|
5118
5299
|
} else if (ticket.status === "ok" && typeof ticket.id === "string" && targetTokens[i]) {
|
|
@@ -5159,6 +5340,7 @@ var ExpoNotificationChannel = class {
|
|
|
5159
5340
|
this.tokens.delete(token);
|
|
5160
5341
|
this.tokenWsMap.delete(token);
|
|
5161
5342
|
this.soundPreferences.delete(token);
|
|
5343
|
+
this.persist();
|
|
5162
5344
|
console.warn("[ExpoNotificationChannel] \u26A0\uFE0F \u5DF2\u79FB\u9664\u5931\u6548 token\uFF08receipt DeviceNotRegistered\uFF09\u3002\u91CD\u542F App \u53EF\u91CD\u65B0\u6CE8\u518C\u3002");
|
|
5163
5345
|
} else if (errorCode === "InvalidCredentials" || errorCode === "MismatchSenderId") {
|
|
5164
5346
|
console.error(
|
|
@@ -5658,6 +5840,9 @@ var PairingManager = class {
|
|
|
5658
5840
|
|
|
5659
5841
|
// src/utils/shellPath.ts
|
|
5660
5842
|
var import_node_child_process7 = require("child_process");
|
|
5843
|
+
var import_node_fs5 = require("fs");
|
|
5844
|
+
var import_node_path7 = require("path");
|
|
5845
|
+
var import_node_os8 = require("os");
|
|
5661
5846
|
var fixed = false;
|
|
5662
5847
|
function fixShellPath() {
|
|
5663
5848
|
if (fixed || isWindows) {
|
|
@@ -5665,23 +5850,62 @@ function fixShellPath() {
|
|
|
5665
5850
|
return;
|
|
5666
5851
|
}
|
|
5667
5852
|
fixed = true;
|
|
5853
|
+
const fromShell = readLoginShellPath();
|
|
5854
|
+
if (fromShell) {
|
|
5855
|
+
process.env.PATH = mergePath(fromShell, process.env.PATH || "");
|
|
5856
|
+
}
|
|
5857
|
+
const stableNodeDir = resolveStableNodeDir();
|
|
5858
|
+
if (stableNodeDir) {
|
|
5859
|
+
process.env.PATH = mergePath(stableNodeDir, process.env.PATH || "");
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
function readLoginShellPath() {
|
|
5668
5863
|
const shell = process.env.SHELL || "/bin/zsh";
|
|
5669
5864
|
const isFish = /\/fish$/.test(shell);
|
|
5670
5865
|
const printPathCmd = isFish ? "string join : $PATH" : 'printf "%s" "$PATH"';
|
|
5671
|
-
|
|
5866
|
+
for (const flags of [["-i", "-l", "-c"], ["-l", "-c"]]) {
|
|
5867
|
+
try {
|
|
5868
|
+
const raw = (0, import_node_child_process7.execFileSync)(shell, [...flags, printPathCmd], {
|
|
5869
|
+
encoding: "utf8",
|
|
5870
|
+
timeout: 4e3,
|
|
5871
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5872
|
+
}).trim();
|
|
5873
|
+
if (raw) return raw;
|
|
5874
|
+
} catch {
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
return null;
|
|
5878
|
+
}
|
|
5879
|
+
function resolveStableNodeDir() {
|
|
5880
|
+
const exe = isWindows ? "node.exe" : "node";
|
|
5881
|
+
for (const dir of (process.env.PATH || "").split(":")) {
|
|
5882
|
+
if (!dir) continue;
|
|
5883
|
+
try {
|
|
5884
|
+
const p = (0, import_node_path7.join)(dir, exe);
|
|
5885
|
+
(0, import_node_fs5.accessSync)(p, import_node_fs5.constants.X_OK);
|
|
5886
|
+
return (0, import_node_path7.dirname)((0, import_node_fs5.realpathSync)(p));
|
|
5887
|
+
} catch {
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
return scanFnmNodeDir();
|
|
5891
|
+
}
|
|
5892
|
+
function scanFnmNodeDir() {
|
|
5893
|
+
const base = (0, import_node_path7.join)((0, import_node_os8.homedir)(), ".fnm", "node-versions");
|
|
5672
5894
|
try {
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5895
|
+
const versions = (0, import_node_fs5.readdirSync)(base).filter((v) => /^v?\d+\./.test(v)).sort(
|
|
5896
|
+
(a, b) => b.localeCompare(a, void 0, { numeric: true, sensitivity: "base" })
|
|
5897
|
+
);
|
|
5898
|
+
for (const v of versions) {
|
|
5899
|
+
const dir = (0, import_node_path7.join)(base, v, "installation", "bin");
|
|
5900
|
+
try {
|
|
5901
|
+
(0, import_node_fs5.accessSync)((0, import_node_path7.join)(dir, "node"), import_node_fs5.constants.X_OK);
|
|
5902
|
+
return dir;
|
|
5903
|
+
} catch {
|
|
5904
|
+
}
|
|
5905
|
+
}
|
|
5906
|
+
} catch {
|
|
5681
5907
|
}
|
|
5682
|
-
|
|
5683
|
-
if (!fromShell) return;
|
|
5684
|
-
process.env.PATH = mergePath(fromShell, process.env.PATH || "");
|
|
5908
|
+
return null;
|
|
5685
5909
|
}
|
|
5686
5910
|
function mergePath(primary, secondary) {
|
|
5687
5911
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -5915,20 +6139,19 @@ var TerminalExecutor = class {
|
|
|
5915
6139
|
var import_node_child_process9 = require("child_process");
|
|
5916
6140
|
var import_node_util = require("util");
|
|
5917
6141
|
var import_promises4 = require("fs/promises");
|
|
5918
|
-
var
|
|
5919
|
-
var
|
|
6142
|
+
var import_node_path8 = require("path");
|
|
6143
|
+
var import_node_os9 = require("os");
|
|
5920
6144
|
var import_uuid5 = require("uuid");
|
|
5921
6145
|
var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
|
|
5922
6146
|
var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
5923
6147
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
5924
|
-
var CONFIG_FILE = (0,
|
|
6148
|
+
var CONFIG_FILE = (0, import_node_path8.join)((0, import_node_os9.homedir)(), ".sessix", "xcode-config.json");
|
|
5925
6149
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5926
6150
|
"node_modules",
|
|
5927
6151
|
".git",
|
|
5928
6152
|
"DerivedData",
|
|
5929
6153
|
"Pods",
|
|
5930
6154
|
".build",
|
|
5931
|
-
"build",
|
|
5932
6155
|
"dist",
|
|
5933
6156
|
"__pycache__",
|
|
5934
6157
|
".next",
|
|
@@ -5975,7 +6198,7 @@ var XcodeBuildExecutor = class {
|
|
|
5975
6198
|
return this.configCache;
|
|
5976
6199
|
}
|
|
5977
6200
|
async writeConfigs(store) {
|
|
5978
|
-
await (0, import_promises4.mkdir)((0,
|
|
6201
|
+
await (0, import_promises4.mkdir)((0, import_node_path8.join)((0, import_node_os9.homedir)(), ".sessix"), { recursive: true });
|
|
5979
6202
|
await (0, import_promises4.writeFile)(CONFIG_FILE, JSON.stringify(store, null, 2), "utf8");
|
|
5980
6203
|
this.configCache = store;
|
|
5981
6204
|
}
|
|
@@ -6028,7 +6251,7 @@ var XcodeBuildExecutor = class {
|
|
|
6028
6251
|
if (SKIP_DIRS.has(name)) continue;
|
|
6029
6252
|
if (name.startsWith(".")) continue;
|
|
6030
6253
|
if (name.endsWith(".xcodeproj") || name.endsWith(".xcworkspace")) continue;
|
|
6031
|
-
const childPath = (0,
|
|
6254
|
+
const childPath = (0, import_node_path8.join)(currentPath, name);
|
|
6032
6255
|
await this.scanDir(rootPath, childPath, depth + 1, results);
|
|
6033
6256
|
}
|
|
6034
6257
|
}
|
|
@@ -6255,7 +6478,7 @@ ${e.stderr ?? ""}`);
|
|
|
6255
6478
|
if (!builtDir || !productName) {
|
|
6256
6479
|
throw new Error("\u65E0\u6CD5\u4ECE -showBuildSettings \u4E2D\u8BFB\u53D6 BUILT_PRODUCTS_DIR / FULL_PRODUCT_NAME");
|
|
6257
6480
|
}
|
|
6258
|
-
return (0,
|
|
6481
|
+
return (0, import_node_path8.join)(builtDir, productName);
|
|
6259
6482
|
}
|
|
6260
6483
|
// ============================================
|
|
6261
6484
|
// 清理
|
|
@@ -6327,7 +6550,7 @@ function kindOrder(k) {
|
|
|
6327
6550
|
|
|
6328
6551
|
// src/commands/CommandDiscovery.ts
|
|
6329
6552
|
var import_promises5 = require("fs/promises");
|
|
6330
|
-
var
|
|
6553
|
+
var import_node_path9 = require("path");
|
|
6331
6554
|
var import_node_crypto = require("crypto");
|
|
6332
6555
|
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
6333
6556
|
var MAX_README_BYTES = 256 * 1024;
|
|
@@ -6351,7 +6574,7 @@ var CommandDiscovery = class {
|
|
|
6351
6574
|
this.scanReadme(projectPath, "CLAUDE.md", "claude.md", collector)
|
|
6352
6575
|
]);
|
|
6353
6576
|
for (const sub of SUBPACKAGE_DIRS) {
|
|
6354
|
-
const subRoot = (0,
|
|
6577
|
+
const subRoot = (0, import_node_path9.join)(projectPath, sub);
|
|
6355
6578
|
let entries;
|
|
6356
6579
|
try {
|
|
6357
6580
|
entries = await (0, import_promises5.readdir)(subRoot);
|
|
@@ -6361,7 +6584,7 @@ var CommandDiscovery = class {
|
|
|
6361
6584
|
let scanned = 0;
|
|
6362
6585
|
for (const name of entries) {
|
|
6363
6586
|
if (name.startsWith(".") || scanned >= MAX_SCAN_PER_DIR) continue;
|
|
6364
|
-
const childAbs = (0,
|
|
6587
|
+
const childAbs = (0, import_node_path9.join)(subRoot, name);
|
|
6365
6588
|
try {
|
|
6366
6589
|
const s = await (0, import_promises5.stat)(childAbs);
|
|
6367
6590
|
if (!s.isDirectory()) continue;
|
|
@@ -6402,7 +6625,7 @@ var CommandDiscovery = class {
|
|
|
6402
6625
|
// ============================================
|
|
6403
6626
|
async scanPackageJson(rootPath, subDir, out) {
|
|
6404
6627
|
const file = subDir ? `${subDir}/package.json` : "package.json";
|
|
6405
|
-
const abs = (0,
|
|
6628
|
+
const abs = (0, import_node_path9.join)(rootPath, file);
|
|
6406
6629
|
let raw;
|
|
6407
6630
|
try {
|
|
6408
6631
|
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
@@ -6433,7 +6656,7 @@ var CommandDiscovery = class {
|
|
|
6433
6656
|
}
|
|
6434
6657
|
async scanMakefile(rootPath, subDir, out) {
|
|
6435
6658
|
const file = subDir ? `${subDir}/Makefile` : "Makefile";
|
|
6436
|
-
const abs = (0,
|
|
6659
|
+
const abs = (0, import_node_path9.join)(rootPath, file);
|
|
6437
6660
|
let raw;
|
|
6438
6661
|
try {
|
|
6439
6662
|
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
@@ -6474,7 +6697,7 @@ var CommandDiscovery = class {
|
|
|
6474
6697
|
}
|
|
6475
6698
|
async scanJustfile(rootPath, subDir, out) {
|
|
6476
6699
|
const file = subDir ? `${subDir}/justfile` : "justfile";
|
|
6477
|
-
const abs = (0,
|
|
6700
|
+
const abs = (0, import_node_path9.join)(rootPath, file);
|
|
6478
6701
|
let raw;
|
|
6479
6702
|
try {
|
|
6480
6703
|
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
@@ -6515,7 +6738,7 @@ var CommandDiscovery = class {
|
|
|
6515
6738
|
}
|
|
6516
6739
|
async scanCargo(rootPath, subDir, out) {
|
|
6517
6740
|
const file = subDir ? `${subDir}/Cargo.toml` : "Cargo.toml";
|
|
6518
|
-
const abs = (0,
|
|
6741
|
+
const abs = (0, import_node_path9.join)(rootPath, file);
|
|
6519
6742
|
try {
|
|
6520
6743
|
await (0, import_promises5.stat)(abs);
|
|
6521
6744
|
} catch {
|
|
@@ -6546,7 +6769,7 @@ var CommandDiscovery = class {
|
|
|
6546
6769
|
for (const name of ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"]) {
|
|
6547
6770
|
const file = subDir ? `${subDir}/${name}` : name;
|
|
6548
6771
|
try {
|
|
6549
|
-
await (0, import_promises5.stat)((0,
|
|
6772
|
+
await (0, import_promises5.stat)((0, import_node_path9.join)(rootPath, file));
|
|
6550
6773
|
} catch {
|
|
6551
6774
|
continue;
|
|
6552
6775
|
}
|
|
@@ -6572,7 +6795,7 @@ var CommandDiscovery = class {
|
|
|
6572
6795
|
}
|
|
6573
6796
|
}
|
|
6574
6797
|
async scanReadme(rootPath, fileName, source, out) {
|
|
6575
|
-
const abs = (0,
|
|
6798
|
+
const abs = (0, import_node_path9.join)(rootPath, fileName);
|
|
6576
6799
|
let raw;
|
|
6577
6800
|
try {
|
|
6578
6801
|
const s = await (0, import_promises5.stat)(abs);
|
|
@@ -6952,8 +7175,8 @@ var GitExecutor = class {
|
|
|
6952
7175
|
|
|
6953
7176
|
// src/scheduling/ScheduledSessionManager.ts
|
|
6954
7177
|
var import_promises6 = require("fs/promises");
|
|
6955
|
-
var
|
|
6956
|
-
var
|
|
7178
|
+
var import_node_os10 = require("os");
|
|
7179
|
+
var import_node_path10 = require("path");
|
|
6957
7180
|
var import_uuid7 = require("uuid");
|
|
6958
7181
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
6959
7182
|
var ScheduledSessionManager = class {
|
|
@@ -6964,7 +7187,7 @@ var ScheduledSessionManager = class {
|
|
|
6964
7187
|
onFired;
|
|
6965
7188
|
persistTimer = null;
|
|
6966
7189
|
constructor(opts) {
|
|
6967
|
-
this.storeFile = opts.storeFile ?? (0,
|
|
7190
|
+
this.storeFile = opts.storeFile ?? (0, import_node_path10.join)((0, import_node_os10.homedir)(), ".sessix", "scheduled-sessions.json");
|
|
6968
7191
|
this.onFire = opts.onFire;
|
|
6969
7192
|
this.onChange = opts.onChange;
|
|
6970
7193
|
this.onFired = opts.onFired;
|
|
@@ -7069,7 +7292,7 @@ var ScheduledSessionManager = class {
|
|
|
7069
7292
|
this.persistTimer = setTimeout(() => {
|
|
7070
7293
|
this.persistTimer = null;
|
|
7071
7294
|
const tasks = [...this.tasks.values()].map((e) => e.task);
|
|
7072
|
-
(0, import_promises6.mkdir)((0,
|
|
7295
|
+
(0, import_promises6.mkdir)((0, import_node_path10.join)(this.storeFile, ".."), { recursive: true }).then(() => (0, import_promises6.writeFile)(this.storeFile, JSON.stringify(tasks, null, 2), "utf8")).catch((err) => {
|
|
7073
7296
|
console.error("[ScheduledSessionManager] persist error:", err);
|
|
7074
7297
|
});
|
|
7075
7298
|
}, 500);
|
|
@@ -7096,10 +7319,11 @@ function isValidTask(value) {
|
|
|
7096
7319
|
// src/utils/cliCapabilities.ts
|
|
7097
7320
|
var import_node_child_process11 = require("child_process");
|
|
7098
7321
|
var DEFAULT_MODELS = [
|
|
7099
|
-
{ value: "opus", label: "Opus 4.
|
|
7100
|
-
{ value: "claude-opus-4-
|
|
7101
|
-
{ value: "
|
|
7102
|
-
{ value: "
|
|
7322
|
+
{ value: "opus", label: "Opus 4.8", sublabel: "Most capable for ambitious work", maxEffort: "max", defaultEffort: "xhigh" },
|
|
7323
|
+
{ value: "claude-opus-4-7", label: "Opus 4.7", sublabel: "Previous generation flagship", maxEffort: "max", defaultEffort: "xhigh" },
|
|
7324
|
+
{ value: "claude-opus-4-6", label: "Opus 4.6", sublabel: "Earlier generation flagship", maxEffort: "max", defaultEffort: "xhigh" },
|
|
7325
|
+
{ value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks", maxEffort: "high", defaultEffort: "high" },
|
|
7326
|
+
{ value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers", maxEffort: "medium", defaultEffort: "medium" }
|
|
7103
7327
|
];
|
|
7104
7328
|
var DEFAULT_CAPABILITIES = {
|
|
7105
7329
|
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
@@ -7172,7 +7396,7 @@ async function killPortProcess(port) {
|
|
|
7172
7396
|
}
|
|
7173
7397
|
}
|
|
7174
7398
|
async function loadApnsConfigFromFile() {
|
|
7175
|
-
const path2 = (0,
|
|
7399
|
+
const path2 = (0, import_node_path11.join)((0, import_node_os11.homedir)(), ".sessix", "apns.json");
|
|
7176
7400
|
try {
|
|
7177
7401
|
const raw = await (0, import_promises7.readFile)(path2, "utf8");
|
|
7178
7402
|
const cfg = JSON.parse(raw);
|
|
@@ -7219,8 +7443,8 @@ async function createWithRetry(label, port, factory) {
|
|
|
7219
7443
|
}
|
|
7220
7444
|
async function start(opts = {}) {
|
|
7221
7445
|
fixShellPath();
|
|
7222
|
-
const configDir = (0,
|
|
7223
|
-
const tokenFile = (0,
|
|
7446
|
+
const configDir = (0, import_node_path11.join)((0, import_node_os11.homedir)(), ".sessix");
|
|
7447
|
+
const tokenFile = (0, import_node_path11.join)(configDir, "token");
|
|
7224
7448
|
let token;
|
|
7225
7449
|
if (opts.token !== void 0) {
|
|
7226
7450
|
token = opts.token;
|
|
@@ -7267,6 +7491,7 @@ async function start(opts = {}) {
|
|
|
7267
7491
|
const notificationService = new NotificationService(sessionManager, expoChannel);
|
|
7268
7492
|
notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
|
|
7269
7493
|
notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
|
|
7494
|
+
sessionManager.onSessionRemoved((sessionId) => notificationService.releaseSession(sessionId));
|
|
7270
7495
|
const activityPushOpts = opts.activityPush ?? await loadApnsConfigFromFile();
|
|
7271
7496
|
if (activityPushOpts) {
|
|
7272
7497
|
try {
|
|
@@ -7284,7 +7509,7 @@ async function start(opts = {}) {
|
|
|
7284
7509
|
let mdnsService = null;
|
|
7285
7510
|
const pairingManager = new PairingManager({
|
|
7286
7511
|
token,
|
|
7287
|
-
serverName: (0,
|
|
7512
|
+
serverName: (0, import_node_os11.hostname)(),
|
|
7288
7513
|
version: "0.2.0",
|
|
7289
7514
|
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
7290
7515
|
});
|
|
@@ -7955,8 +8180,30 @@ async function start(opts = {}) {
|
|
|
7955
8180
|
const idleTimeoutMs = Number(process.env.SESSIX_IDLE_TIMEOUT_MS ?? 30 * 60 * 1e3);
|
|
7956
8181
|
const idleSweepIntervalMs = Number(process.env.SESSIX_IDLE_SWEEP_INTERVAL_MS ?? 5 * 60 * 1e3);
|
|
7957
8182
|
const maxActiveProcesses = Number(process.env.SESSIX_MAX_ACTIVE_PROCESSES ?? 15);
|
|
8183
|
+
const sessionEvictMs = Number(process.env.SESSIX_SESSION_EVICT_MS ?? 2 * 60 * 60 * 1e3);
|
|
8184
|
+
let gcFn;
|
|
8185
|
+
const maybeGc = () => {
|
|
8186
|
+
if (gcFn === void 0) {
|
|
8187
|
+
gcFn = globalThis.gc ?? null;
|
|
8188
|
+
if (!gcFn) {
|
|
8189
|
+
try {
|
|
8190
|
+
(0, import_node_v8.setFlagsFromString)("--expose-gc");
|
|
8191
|
+
const fn = (0, import_node_vm.runInNewContext)("gc");
|
|
8192
|
+
gcFn = typeof fn === "function" ? fn : null;
|
|
8193
|
+
} catch {
|
|
8194
|
+
gcFn = null;
|
|
8195
|
+
}
|
|
8196
|
+
}
|
|
8197
|
+
}
|
|
8198
|
+
if (gcFn) {
|
|
8199
|
+
try {
|
|
8200
|
+
gcFn();
|
|
8201
|
+
} catch {
|
|
8202
|
+
}
|
|
8203
|
+
}
|
|
8204
|
+
};
|
|
7958
8205
|
let idleSweepTimer = null;
|
|
7959
|
-
if (idleSweepIntervalMs > 0 && (idleTimeoutMs > 0 || maxActiveProcesses > 0)) {
|
|
8206
|
+
if (idleSweepIntervalMs > 0 && (idleTimeoutMs > 0 || maxActiveProcesses > 0 || sessionEvictMs > 0)) {
|
|
7960
8207
|
idleSweepTimer = setInterval(async () => {
|
|
7961
8208
|
try {
|
|
7962
8209
|
let totalSwept = 0;
|
|
@@ -7975,7 +8222,18 @@ async function start(opts = {}) {
|
|
|
7975
8222
|
swept.forEach(broadcastShrink);
|
|
7976
8223
|
totalSwept += swept.length;
|
|
7977
8224
|
}
|
|
8225
|
+
if (sessionEvictMs > 0 && typeof provider.listEvictableSessions === "function") {
|
|
8226
|
+
const evictable = provider.listEvictableSessions(sessionEvictMs);
|
|
8227
|
+
for (const id of evictable) {
|
|
8228
|
+
await sessionManager.killSession(id);
|
|
8229
|
+
}
|
|
8230
|
+
if (evictable.length > 0) {
|
|
8231
|
+
console.log(`[Server] Idle GC: evicted ${evictable.length} stale session(s)`);
|
|
8232
|
+
totalSwept += evictable.length;
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
7978
8235
|
}
|
|
8236
|
+
const hasRunning = sessionManager.getActiveSessions().some((s) => s.status === "running");
|
|
7979
8237
|
if (totalSwept > 0) {
|
|
7980
8238
|
console.log(`[Server] Idle GC: swept ${totalSwept} idle session(s)`);
|
|
7981
8239
|
wsBridge.broadcast({
|
|
@@ -7983,6 +8241,9 @@ async function start(opts = {}) {
|
|
|
7983
8241
|
sessions: sessionManager.getActiveSessions()
|
|
7984
8242
|
});
|
|
7985
8243
|
}
|
|
8244
|
+
if (totalSwept > 0 || !hasRunning) {
|
|
8245
|
+
maybeGc();
|
|
8246
|
+
}
|
|
7986
8247
|
} catch (err) {
|
|
7987
8248
|
console.error("[Server] Idle GC failed:", err);
|
|
7988
8249
|
}
|