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.
Files changed (3) hide show
  1. package/dist/index.js +342 -81
  2. package/dist/server.js +337 -76
  3. 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 import_node_os9 = require("os");
313
- var import_node_path9 = require("path");
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
- const raw = await (0, import_promises.readFile)(filePath, "utf-8").catch((err) => {
459
- if (err.code === "ENOENT") return null;
460
- throw err;
461
- });
462
- if (raw === null) return void 0;
463
- const lines = raw.split("\n");
464
- for (let i = lines.length - 1; i >= 0; i--) {
465
- const line = lines[i].trim();
466
- if (!line) continue;
467
- try {
468
- const obj = JSON.parse(line);
469
- if (obj.type !== "assistant" || !obj.message) continue;
470
- const model = obj.message.model;
471
- if (typeof model === "string" && model && model !== "unknown") {
472
- return model;
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
- const raw = await (0, import_promises.readFile)(filePath, "utf-8").catch((err) => {
610
- if (err.code === "ENOENT") return null;
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 lines) {
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
- if (model) {
1055
- args.push("--model", model);
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 (fallbackModel) {
1064
- args.push("--fallback-model", fallbackModel);
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
- return isLatestVersion && permissionScriptExists && compactScriptExists && postCompactScriptExists && permissionDeniedScriptExists && configExists;
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.removeHookCommand(settings, "PreToolUse", cmd);
4100
- this.removeHookCommand(settings, "PermissionRequest", cmd);
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.recentActivityState.delete(sessionId);
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
- let raw;
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
- raw = (0, import_node_child_process7.execFileSync)(shell, ["-l", "-c", printPathCmd], {
5674
- encoding: "utf8",
5675
- timeout: 3e3,
5676
- stdio: ["ignore", "pipe", "ignore"]
5677
- });
5678
- } catch (err) {
5679
- console.warn("[fixShellPath] failed to read login shell PATH:", err);
5680
- return;
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
- const fromShell = raw.trim();
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 import_node_path6 = require("path");
5919
- var import_node_os7 = require("os");
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, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "xcode-config.json");
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, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix"), { recursive: true });
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, import_node_path6.join)(currentPath, name);
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, import_node_path6.join)(builtDir, productName);
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 import_node_path7 = require("path");
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, import_node_path7.join)(projectPath, sub);
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, import_node_path7.join)(subRoot, name);
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, import_node_path7.join)(rootPath, file);
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, import_node_path7.join)(rootPath, file);
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, import_node_path7.join)(rootPath, file);
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, import_node_path7.join)(rootPath, file);
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, import_node_path7.join)(rootPath, file));
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, import_node_path7.join)(rootPath, fileName);
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 import_node_os8 = require("os");
6956
- var import_node_path8 = require("path");
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, import_node_path8.join)((0, import_node_os8.homedir)(), ".sessix", "scheduled-sessions.json");
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, import_node_path8.join)(this.storeFile, ".."), { recursive: true }).then(() => (0, import_promises6.writeFile)(this.storeFile, JSON.stringify(tasks, null, 2), "utf8")).catch((err) => {
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.7", sublabel: "Most capable for ambitious work" },
7100
- { value: "claude-opus-4-6", label: "Opus 4.6", sublabel: "Previous generation flagship" },
7101
- { value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
7102
- { value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
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, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix", "apns.json");
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, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix");
7223
- const tokenFile = (0, import_node_path9.join)(configDir, "token");
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, import_node_os9.hostname)(),
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
  }