sessix-server 0.4.3 → 0.4.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 +868 -149
  2. package/dist/server.js +866 -147
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_node_os10 = require("os");
28
28
  var import_node_fs4 = require("fs");
29
29
  var import_node_path10 = require("path");
30
- var import_node_child_process12 = require("child_process");
30
+ var import_node_child_process13 = require("child_process");
31
31
 
32
32
  // src/i18n/locales/zh.ts
33
33
  var zh = {
@@ -306,7 +306,7 @@ var import_uuid9 = require("uuid");
306
306
  var import_promises7 = require("fs/promises");
307
307
  var import_node_os9 = require("os");
308
308
  var import_node_path9 = require("path");
309
- var import_node_child_process11 = require("child_process");
309
+ var import_node_child_process12 = require("child_process");
310
310
  var import_node_util3 = require("util");
311
311
 
312
312
  // src/providers/ProcessProvider.ts
@@ -662,7 +662,24 @@ var ProcessProvider = class {
662
662
  writeUserMessage(proc, message, sessionId, images) {
663
663
  const content = [];
664
664
  if (images?.length) {
665
- for (const img of images) {
665
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
666
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
667
+ for (let i = 0; i < images.length; i++) {
668
+ const img = images[i];
669
+ if (!ALLOWED_TYPES.has(img.media_type)) {
670
+ if (sessionId) {
671
+ this.emitWriteError(sessionId, `Image #${i + 1} rejected: unsupported media_type "${img.media_type}". Only JPEG/PNG/WebP/GIF are accepted.`);
672
+ }
673
+ return;
674
+ }
675
+ const sizeBytes = Math.floor(img.data.length * 0.75);
676
+ if (sizeBytes > MAX_IMAGE_BYTES) {
677
+ if (sessionId) {
678
+ const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(1);
679
+ this.emitWriteError(sessionId, `Image #${i + 1} rejected: ${sizeMb}MB exceeds 5MB per-image limit.`);
680
+ }
681
+ return;
682
+ }
666
683
  content.push({
667
684
  type: "image",
668
685
  source: { type: "base64", media_type: img.media_type, data: img.data }
@@ -689,6 +706,14 @@ var ProcessProvider = class {
689
706
  this.emitWriteError(sessionId, `Failed to send message: ${err.message}`);
690
707
  }
691
708
  });
709
+ if (sessionId) {
710
+ const syntheticUser = {
711
+ type: "user",
712
+ session_id: sessionId,
713
+ message: { role: "user", content }
714
+ };
715
+ this.emitter.emit(this.getEventName(sessionId), syntheticUser);
716
+ }
692
717
  }
693
718
  /**
694
719
  * 发出写入失败的合成错误事件
@@ -922,9 +947,13 @@ ${context}`;
922
947
  throw new Error(`Session ${sessionId} stdin unavailable`);
923
948
  }
924
949
  const toolResult = JSON.stringify({
925
- type: "tool_result",
926
- tool_use_id: toolUseId,
927
- content: answer
950
+ type: "user",
951
+ session_id: "",
952
+ message: {
953
+ role: "user",
954
+ content: [{ type: "tool_result", tool_use_id: toolUseId, content: answer }]
955
+ },
956
+ parent_tool_use_id: toolUseId
928
957
  });
929
958
  await new Promise((resolve, reject) => {
930
959
  entry.process.stdin.write(toolResult + "\n", (err) => {
@@ -2935,6 +2964,8 @@ var ApprovalProxy = class _ApprovalProxy {
2935
2964
  this.handleApprovalHook(req, res);
2936
2965
  } else if (req.method === "POST" && pathname === "/hook/notify") {
2937
2966
  this.handleHookNotify(req, res);
2967
+ } else if (req.method === "POST" && pathname === "/api/resolve") {
2968
+ this.handleApiResolve(req, res);
2938
2969
  } else if (req.method === "POST" && pathname === "/pair") {
2939
2970
  this.handlePair(req, res);
2940
2971
  } else if (req.method === "GET" && pathname === "/health") {
@@ -3000,6 +3031,34 @@ var ApprovalProxy = class _ApprovalProxy {
3000
3031
  this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
3001
3032
  }
3002
3033
  }
3034
+ /**
3035
+ * 移动端 API 端点:Widget Extension / Watch 直接提交审批决策
3036
+ *
3037
+ * 绕过 WebSocket 链路,让 iOS Widget Extension 的 AppIntent 在 App 挂起时
3038
+ * 仍能直接将审批结果提交到服务端。使用 Bearer token 鉴权(与 WS 同一 token)。
3039
+ */
3040
+ async handleApiResolve(req, res) {
3041
+ const authHeader = req.headers.authorization ?? "";
3042
+ const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
3043
+ if (bearerToken !== this.token) {
3044
+ this.sendJson(res, 401, { ok: false, error: "Unauthorized" });
3045
+ return;
3046
+ }
3047
+ try {
3048
+ const body = await this.parseJsonBody(req);
3049
+ const requestId = String(body.requestId ?? "").trim();
3050
+ const decision = String(body.decision ?? "").trim();
3051
+ if (!requestId || decision !== "allow" && decision !== "deny") {
3052
+ this.sendJson(res, 400, { ok: false, error: "requestId and decision (allow|deny) required" });
3053
+ return;
3054
+ }
3055
+ const resolved = this.resolveApproval(requestId, { decision });
3056
+ this.sendJson(res, 200, { ok: resolved });
3057
+ } catch (err) {
3058
+ console.error("[ApprovalProxy] /api/resolve error:", err);
3059
+ this.sendJson(res, 500, { ok: false, error: "Internal error" });
3060
+ }
3061
+ }
3003
3062
  /**
3004
3063
  * 非阻塞 hook 通知端点
3005
3064
  *
@@ -3710,6 +3769,8 @@ var HookInstaller = class {
3710
3769
 
3711
3770
  // src/notification/NotificationService.ts
3712
3771
  var import_node_path5 = require("path");
3772
+ var RECENT_ACTIVITY_MAX = 6;
3773
+ var ACTIVITY_PUSH_THROTTLE_MS = 4e3;
3713
3774
  var NotificationService = class {
3714
3775
  constructor(sessionManager, expoChannel = null) {
3715
3776
  this.sessionManager = sessionManager;
@@ -3728,6 +3789,30 @@ var NotificationService = class {
3728
3789
  latestAssistantText = /* @__PURE__ */ new Map();
3729
3790
  /** 获取全局待审批总数的回调(跨所有会话) */
3730
3791
  globalPendingCountProvider = null;
3792
+ /** sessionId → 最近活动状态(用于 LA content push) */
3793
+ recentActivityState = /* @__PURE__ */ new Map();
3794
+ /** sessionId → 节流定时器(LA content push) */
3795
+ activityPushTimers = /* @__PURE__ */ new Map();
3796
+ /** 上次推送 LA content 的时间戳(用于节流;首次立即推送) */
3797
+ lastActivityPushAt = /* @__PURE__ */ new Map();
3798
+ /** 挂起的优先级提升请求(状态变化时设为 '10',flush 后清除) */
3799
+ pendingPriority = /* @__PURE__ */ new Map();
3800
+ /** sessionId → 累计活动计数器(用于 summary 模式的 activitySummary) */
3801
+ activityCounters = /* @__PURE__ */ new Map();
3802
+ /** 提供器:根据 sessionId 取该会话的待审批列表(由 server.ts 注入) */
3803
+ pendingApprovalsProvider = null;
3804
+ /**
3805
+ * sessionId → idle 结束定时器。
3806
+ * 会话变为 idle 时启动(30 秒);用户发新消息重回 running 时取消。
3807
+ * 30 秒内无新消息 → 调 endActivity 关闭 LA,同时发横幅通知告知完成。
3808
+ */
3809
+ idleEndTimers = /* @__PURE__ */ new Map();
3810
+ /**
3811
+ * sessionId → LA 心跳定时器(setInterval)。
3812
+ * 确保 Agent 子任务等长时间无 claude_event 的场景下 LA 仍持续更新。
3813
+ * token 注册时启动,flushActivityEnd / removeActivityPushToken 时停止。
3814
+ */
3815
+ laHeartbeatTimers = /* @__PURE__ */ new Map();
3731
3816
  /** 添加通知渠道(id 唯一,可用于后续动态开关) */
3732
3817
  addChannel(id, channel, enabled = true) {
3733
3818
  this.channelMap.set(id, { channel, enabled });
@@ -3755,11 +3840,27 @@ var NotificationService = class {
3755
3840
  }
3756
3841
  /** 注册 ActivityKit push token(由手机端启动 Live Activity 后上报) */
3757
3842
  addActivityPushToken(sessionId, token) {
3758
- this.activityPushChannel?.addToken(sessionId, token);
3843
+ if (!this.activityPushChannel) {
3844
+ console.warn(`[NotificationService] \u26A0\uFE0F \u6536\u5230 LA push token \u4F46 ActivityPushChannel \u672A\u521D\u59CB\u5316 (session=${sessionId.slice(0, 8)}\u2026) \u2014 \u68C0\u67E5 ~/.sessix/apns.json`);
3845
+ return;
3846
+ }
3847
+ this.activityPushChannel.addToken(sessionId, token);
3848
+ console.log(`[NotificationService] \u2705 LA push token \u5DF2\u6CE8\u518C (session=${sessionId.slice(0, 8)}\u2026, token=${token.slice(0, 16)}\u2026)`);
3849
+ this.scheduleActivityPush(sessionId, true);
3850
+ this.startLaHeartbeat(sessionId);
3759
3851
  }
3760
3852
  /** 移除 ActivityKit push token */
3761
3853
  removeActivityPushToken(sessionId) {
3854
+ this.stopLaHeartbeat(sessionId);
3762
3855
  this.activityPushChannel?.removeToken(sessionId);
3856
+ this.clearActivityPushTimer(sessionId);
3857
+ this.recentActivityState.delete(sessionId);
3858
+ this.lastActivityPushAt.delete(sessionId);
3859
+ this.activityCounters.delete(sessionId);
3860
+ }
3861
+ /** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
3862
+ setPendingApprovalsProvider(fn) {
3863
+ this.pendingApprovalsProvider = fn;
3763
3864
  }
3764
3865
  /** 设置全局待审批总数提供者 */
3765
3866
  setGlobalPendingCountProvider(provider) {
@@ -3774,7 +3875,7 @@ var NotificationService = class {
3774
3875
  this.yoloModeState.set(sessionId, enabled);
3775
3876
  }
3776
3877
  /** 直接触发审批通知(由 ApprovalProxy 回调调用) */
3777
- notifyApproval(request, pendingCount) {
3878
+ async notifyApproval(request, pendingCount) {
3778
3879
  if (this.yoloModeState.get(request.sessionId)) return;
3779
3880
  const sessionTitle = this.getSessionTitle(request.sessionId);
3780
3881
  const title = pendingCount > 1 ? t("notification.pendingApprovals", { title: sessionTitle, count: pendingCount }) : sessionTitle;
@@ -3782,12 +3883,16 @@ var NotificationService = class {
3782
3883
  if (this.activityPushChannel?.hasToken(request.sessionId)) {
3783
3884
  const dangerLevel2 = this.getDangerLevel(request.toolName);
3784
3885
  const isYoloMode = this.getYoloMode(request.sessionId);
3785
- this.activityPushChannel.updateActivityWithAlert(
3886
+ const recentActivity = this.getRecentActivity(request.sessionId);
3887
+ const latestMessage = recentActivity[recentActivity.length - 1] ?? "";
3888
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
3889
+ const sent = await this.activityPushChannel.updateActivityWithAlert(
3786
3890
  request.sessionId,
3787
3891
  {
3788
3892
  status: "waitingApproval",
3789
3893
  sessionTitle,
3790
- latestMessage: "",
3894
+ latestMessage,
3895
+ recentActivity,
3791
3896
  approvalInfo: {
3792
3897
  requestId: request.id,
3793
3898
  toolName: request.toolName,
@@ -3796,11 +3901,22 @@ var NotificationService = class {
3796
3901
  pendingCount
3797
3902
  },
3798
3903
  isYoloMode,
3799
- updatedAt: Date.now()
3904
+ updatedAt: Date.now(),
3905
+ displayMode: "summary",
3906
+ activitySummary: this.buildActivitySummary(request.sessionId),
3907
+ startedAt: session?.createdAt,
3908
+ stats: this.buildStatsPayload(session)
3800
3909
  },
3801
3910
  { title, body }
3802
3911
  );
3803
- return;
3912
+ if (sent) {
3913
+ console.log(`[NotificationService] \u{1F4E1} approval via ActivityKit push session=${request.sessionId.slice(0, 8)}\u2026 tool=${request.toolName}`);
3914
+ this.lastActivityPushAt.set(request.sessionId, Date.now());
3915
+ return;
3916
+ }
3917
+ console.warn(`[NotificationService] \u26A0\uFE0F ActivityKit push \u5931\u8D25\uFF0C\u964D\u7EA7\u5230 Expo push session=${request.sessionId.slice(0, 8)}\u2026`);
3918
+ } else {
3919
+ console.log(`[NotificationService] \u{1F4F2} approval via Expo push session=${request.sessionId.slice(0, 8)}\u2026 tool=${request.toolName}`);
3804
3920
  }
3805
3921
  const dangerLevel = this.getDangerLevel(request.toolName);
3806
3922
  const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
@@ -3833,17 +3949,25 @@ var NotificationService = class {
3833
3949
  const body = `\u2753 ${request.question.slice(0, 80)}`;
3834
3950
  if (this.activityPushChannel?.hasToken(request.sessionId)) {
3835
3951
  const isYoloMode = this.getYoloMode(request.sessionId);
3952
+ const recentActivity = this.getRecentActivity(request.sessionId);
3953
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
3836
3954
  this.activityPushChannel.updateActivityWithAlert(
3837
3955
  request.sessionId,
3838
3956
  {
3839
- status: "waitingApproval",
3957
+ status: "waitingQuestion",
3840
3958
  sessionTitle,
3841
3959
  latestMessage: request.question.slice(0, 80),
3960
+ recentActivity,
3842
3961
  isYoloMode,
3843
- updatedAt: Date.now()
3962
+ updatedAt: Date.now(),
3963
+ displayMode: "summary",
3964
+ activitySummary: this.buildActivitySummary(request.sessionId),
3965
+ startedAt: session?.createdAt,
3966
+ stats: this.buildStatsPayload(session)
3844
3967
  },
3845
3968
  { title: sessionTitle, body }
3846
3969
  );
3970
+ this.lastActivityPushAt.set(request.sessionId, Date.now());
3847
3971
  return;
3848
3972
  }
3849
3973
  this.notify({
@@ -3877,6 +4001,12 @@ var NotificationService = class {
3877
4001
  this.unsubscribe = null;
3878
4002
  this.yoloModeState.clear();
3879
4003
  this.latestAssistantText.clear();
4004
+ for (const timer of this.activityPushTimers.values()) clearTimeout(timer);
4005
+ this.activityPushTimers.clear();
4006
+ this.recentActivityState.clear();
4007
+ this.lastActivityPushAt.clear();
4008
+ this.pendingPriority.clear();
4009
+ this.activityCounters.clear();
3880
4010
  }
3881
4011
  // ============================================
3882
4012
  // 内部方法
@@ -3885,29 +4015,29 @@ var NotificationService = class {
3885
4015
  switch (event.type) {
3886
4016
  case "claude_event": {
3887
4017
  this.trackAssistantText(event.sessionId, event.event);
4018
+ this.updateRecentActivity(event.sessionId, event.event);
4019
+ this.scheduleActivityPush(event.sessionId);
3888
4020
  break;
3889
4021
  }
3890
4022
  case "claude_events": {
3891
4023
  for (const e of event.events) {
3892
4024
  this.trackAssistantText(event.sessionId, e);
4025
+ this.updateRecentActivity(event.sessionId, e);
3893
4026
  }
4027
+ this.scheduleActivityPush(event.sessionId);
3894
4028
  break;
3895
4029
  }
3896
4030
  case "status_change": {
4031
+ this.clearActivityPushTimer(event.sessionId);
3897
4032
  if (event.status === "idle") {
3898
- const sessionTitle = this.getSessionTitle(event.sessionId);
3899
- const latestMsg = this.latestAssistantText.get(event.sessionId);
3900
- const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
3901
- const isYoloMode = this.getYoloMode(event.sessionId);
4033
+ this.cancelIdleEndTimer(event.sessionId);
3902
4034
  if (this.activityPushChannel?.hasToken(event.sessionId)) {
3903
- this.activityPushChannel.endActivity(event.sessionId, {
3904
- status: "idle",
3905
- sessionTitle,
3906
- latestMessage: body,
3907
- isYoloMode,
3908
- updatedAt: Date.now()
3909
- });
4035
+ this.scheduleActivityPush(event.sessionId, true, "10");
4036
+ this.scheduleIdleEnd(event.sessionId, 3e4);
3910
4037
  } else {
4038
+ const sessionTitle = this.getSessionTitle(event.sessionId);
4039
+ const latestMsg = this.latestAssistantText.get(event.sessionId);
4040
+ const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
3911
4041
  this.notify({
3912
4042
  title: sessionTitle,
3913
4043
  body,
@@ -3916,28 +4046,12 @@ var NotificationService = class {
3916
4046
  data: { type: "task_complete", sessionId: event.sessionId }
3917
4047
  });
3918
4048
  }
4049
+ } else if (event.status === "running" || event.status === "waiting_approval" || event.status === "waiting_question") {
4050
+ this.cancelIdleEndTimer(event.sessionId);
4051
+ this.scheduleActivityPush(event.sessionId, true, "10");
3919
4052
  } else if (event.status === "error") {
3920
- const sessionTitle = this.getSessionTitle(event.sessionId);
3921
- const latestMsg = this.latestAssistantText.get(event.sessionId);
3922
- const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : t("notification.taskError");
3923
- const isYoloMode = this.getYoloMode(event.sessionId);
3924
- if (this.activityPushChannel?.hasToken(event.sessionId)) {
3925
- this.activityPushChannel.endActivity(event.sessionId, {
3926
- status: "error",
3927
- sessionTitle,
3928
- latestMessage: body,
3929
- isYoloMode,
3930
- updatedAt: Date.now()
3931
- });
3932
- } else {
3933
- this.notify({
3934
- title: sessionTitle,
3935
- body,
3936
- sound: "default",
3937
- badge: this.getGlobalPendingCount(),
3938
- data: { type: "task_error", sessionId: event.sessionId }
3939
- });
3940
- }
4053
+ this.cancelIdleEndTimer(event.sessionId);
4054
+ this.flushActivityEnd(event.sessionId, "error");
3941
4055
  }
3942
4056
  break;
3943
4057
  }
@@ -3970,6 +4084,383 @@ var NotificationService = class {
3970
4084
  getYoloMode(sessionId) {
3971
4085
  return this.yoloModeState.get(sessionId) ?? false;
3972
4086
  }
4087
+ // ============================================
4088
+ // Live Activity 内容推送(后台 LA 实时刷新)
4089
+ // ============================================
4090
+ /**
4091
+ * 把一个 ClaudeStreamEvent 折算到 recentActivity 列表里。
4092
+ * 同一 message.id 内多次 assistant 事件视为流式更新,整段重建 currentEntries;
4093
+ * 切换 message.id 视为新 turn,旧条目沉淀到 history。
4094
+ */
4095
+ updateRecentActivity(sessionId, event) {
4096
+ if (event.type === "result") {
4097
+ const state2 = this.recentActivityState.get(sessionId);
4098
+ if (state2 && state2.currentEntries.length > 0) {
4099
+ state2.history.push(...state2.currentEntries);
4100
+ while (state2.history.length > RECENT_ACTIVITY_MAX) state2.history.shift();
4101
+ state2.currentEntries = [];
4102
+ state2.currentMessageId = null;
4103
+ }
4104
+ return;
4105
+ }
4106
+ if (event.type !== "assistant") return;
4107
+ const msg = event.message;
4108
+ if (!Array.isArray(msg.content)) return;
4109
+ let state = this.recentActivityState.get(sessionId);
4110
+ if (!state) {
4111
+ state = { history: [], currentMessageId: null, currentEntries: [] };
4112
+ this.recentActivityState.set(sessionId, state);
4113
+ }
4114
+ if (state.currentMessageId !== msg.id) {
4115
+ if (state.currentEntries.length > 0) {
4116
+ state.history.push(...state.currentEntries);
4117
+ while (state.history.length > RECENT_ACTIVITY_MAX) state.history.shift();
4118
+ }
4119
+ state.currentEntries = [];
4120
+ state.currentMessageId = msg.id;
4121
+ }
4122
+ const next = [];
4123
+ for (const block of msg.content) {
4124
+ if (block.type === "text") {
4125
+ const line = this.summarizeText(block.text);
4126
+ if (line.length >= 4) next.push(line);
4127
+ } else if (block.type === "tool_use") {
4128
+ const line = this.summarizeToolCall(block.name, block.input ?? {});
4129
+ if (line) next.push(line);
4130
+ this.incrementCounter(sessionId, block.name);
4131
+ }
4132
+ }
4133
+ state.currentEntries = next;
4134
+ }
4135
+ /** 取该会话当前的 recentActivity(history + currentEntries),保留末尾 N 条 */
4136
+ getRecentActivity(sessionId) {
4137
+ const state = this.recentActivityState.get(sessionId);
4138
+ if (!state) return [];
4139
+ const combined = [...state.history, ...state.currentEntries];
4140
+ return combined.slice(-RECENT_ACTIVITY_MAX);
4141
+ }
4142
+ /** 工具名 → 计数器类别映射 */
4143
+ incrementCounter(sessionId, toolName) {
4144
+ let c = this.activityCounters.get(sessionId);
4145
+ if (!c) {
4146
+ c = { filesEdited: 0, commandsRun: 0, searches: 0, filesRead: 0, messagesReceived: 0 };
4147
+ this.activityCounters.set(sessionId, c);
4148
+ }
4149
+ switch (toolName) {
4150
+ case "Edit":
4151
+ case "MultiEdit":
4152
+ case "Write":
4153
+ case "NotebookEdit":
4154
+ c.filesEdited++;
4155
+ break;
4156
+ case "Bash":
4157
+ c.commandsRun++;
4158
+ break;
4159
+ case "Grep":
4160
+ case "Glob":
4161
+ case "WebSearch":
4162
+ case "WebFetch":
4163
+ c.searches++;
4164
+ break;
4165
+ case "Read":
4166
+ c.filesRead++;
4167
+ break;
4168
+ default:
4169
+ break;
4170
+ }
4171
+ }
4172
+ /** 把累计计数器格式化为可读摘要(如"已编辑 3 个文件 · 运行 5 条命令") */
4173
+ buildActivitySummary(sessionId) {
4174
+ const c = this.activityCounters.get(sessionId);
4175
+ if (!c) return "";
4176
+ const parts = [];
4177
+ if (c.filesEdited > 0) parts.push(`\u5DF2\u7F16\u8F91 ${c.filesEdited} \u4E2A\u6587\u4EF6`);
4178
+ if (c.commandsRun > 0) parts.push(`\u8FD0\u884C ${c.commandsRun} \u6761\u547D\u4EE4`);
4179
+ if (c.searches > 0) parts.push(`\u641C\u7D22 ${c.searches} \u6B21`);
4180
+ if (c.filesRead > 0) parts.push(`\u9605\u8BFB ${c.filesRead} \u4E2A\u6587\u4EF6`);
4181
+ return parts.join(" \xB7 ") || "\u4F1A\u8BDD\u8FDB\u884C\u4E2D\u2026";
4182
+ }
4183
+ buildStatsPayload(session) {
4184
+ return {
4185
+ totalInputTokens: session?.stats?.totalInputTokens ?? 0,
4186
+ totalOutputTokens: session?.stats?.totalOutputTokens ?? 0,
4187
+ totalCostUsd: session?.stats?.totalCostUsd,
4188
+ totalDurationMs: session?.stats?.totalDurationMs,
4189
+ runningStartedAt: session?.stats?.runningStartedAt
4190
+ };
4191
+ }
4192
+ /** 节流调度 LA content push;首次立即推,后续合并到 throttle window 末尾 */
4193
+ scheduleActivityPush(sessionId, force = false, priority) {
4194
+ if (!this.activityPushChannel) return;
4195
+ if (!this.activityPushChannel.hasToken(sessionId)) {
4196
+ console.warn(`[NotificationService] \u26A0\uFE0F skip LA push: session=${sessionId.slice(0, 8)}\u2026 token \u672A\u6CE8\u518C`);
4197
+ return;
4198
+ }
4199
+ if (priority === "10") {
4200
+ this.pendingPriority.set(sessionId, "10");
4201
+ }
4202
+ const now = Date.now();
4203
+ const last = this.lastActivityPushAt.get(sessionId) ?? 0;
4204
+ const elapsed = now - last;
4205
+ if (force || elapsed >= ACTIVITY_PUSH_THROTTLE_MS) {
4206
+ this.clearActivityPushTimer(sessionId);
4207
+ this.flushActivityPush(sessionId);
4208
+ return;
4209
+ }
4210
+ if (this.activityPushTimers.has(sessionId)) return;
4211
+ const wait = ACTIVITY_PUSH_THROTTLE_MS - elapsed;
4212
+ this.activityPushTimers.set(
4213
+ sessionId,
4214
+ setTimeout(() => {
4215
+ this.activityPushTimers.delete(sessionId);
4216
+ this.flushActivityPush(sessionId);
4217
+ }, wait)
4218
+ );
4219
+ }
4220
+ clearActivityPushTimer(sessionId) {
4221
+ const timer = this.activityPushTimers.get(sessionId);
4222
+ if (timer) {
4223
+ clearTimeout(timer);
4224
+ this.activityPushTimers.delete(sessionId);
4225
+ }
4226
+ }
4227
+ /**
4228
+ * 启动 LA 心跳(每 ACTIVITY_PUSH_THROTTLE_MS 触发一次 scheduleActivityPush)。
4229
+ * 确保 Agent 子会话等长时间无 claude_event 的情况下 LA 仍持续更新。
4230
+ * 心跳只在会话活跃状态(running/waitingApproval/waitingQuestion)下发推送;
4231
+ * scheduleActivityPush 自带节流,心跳与正常事件驱动不冲突。
4232
+ */
4233
+ startLaHeartbeat(sessionId) {
4234
+ if (this.laHeartbeatTimers.has(sessionId)) return;
4235
+ const timer = setInterval(() => {
4236
+ if (!this.activityPushChannel?.hasToken(sessionId)) {
4237
+ this.stopLaHeartbeat(sessionId);
4238
+ return;
4239
+ }
4240
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
4241
+ if (!session) {
4242
+ this.stopLaHeartbeat(sessionId);
4243
+ return;
4244
+ }
4245
+ if (session.status === "running" || session.status === "waiting_approval" || session.status === "waiting_question") {
4246
+ this.scheduleActivityPush(sessionId);
4247
+ }
4248
+ }, ACTIVITY_PUSH_THROTTLE_MS);
4249
+ this.laHeartbeatTimers.set(sessionId, timer);
4250
+ }
4251
+ /** 停止 LA 心跳 */
4252
+ stopLaHeartbeat(sessionId) {
4253
+ const timer = this.laHeartbeatTimers.get(sessionId);
4254
+ if (timer) {
4255
+ clearInterval(timer);
4256
+ this.laHeartbeatTimers.delete(sessionId);
4257
+ }
4258
+ }
4259
+ /** 取消 idle 结束定时器 */
4260
+ cancelIdleEndTimer(sessionId) {
4261
+ const timer = this.idleEndTimers.get(sessionId);
4262
+ if (timer) {
4263
+ clearTimeout(timer);
4264
+ this.idleEndTimers.delete(sessionId);
4265
+ }
4266
+ }
4267
+ /**
4268
+ * 启动 idle 结束定时器:delayMs 后若会话仍 idle,调 endActivity + 发通知。
4269
+ * 保证多轮对话期间(idle→running→idle)LA 不会过早消失。
4270
+ */
4271
+ scheduleIdleEnd(sessionId, delayMs) {
4272
+ const timer = setTimeout(() => {
4273
+ this.idleEndTimers.delete(sessionId);
4274
+ this.flushActivityEnd(sessionId, "idle");
4275
+ }, delayMs);
4276
+ this.idleEndTimers.set(sessionId, timer);
4277
+ }
4278
+ /**
4279
+ * 结束 LA 并发完成通知。有 LA token → APNs event:end(带横幅);
4280
+ * 无 token → 普通 Expo push。清理所有相关状态。
4281
+ */
4282
+ flushActivityEnd(sessionId, reason) {
4283
+ const sessionTitle = this.getSessionTitle(sessionId);
4284
+ const latestMsg = this.latestAssistantText.get(sessionId);
4285
+ const isError = reason === "error";
4286
+ const body = isError ? `\u274C ${latestMsg?.slice(0, 80) ?? t("notification.taskError")}` : `\u2705 ${latestMsg?.slice(0, 80) ?? t("notification.taskComplete")}`;
4287
+ const isYoloMode = this.getYoloMode(sessionId);
4288
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
4289
+ this.notify({
4290
+ title: sessionTitle,
4291
+ body,
4292
+ sound: "default",
4293
+ badge: this.getGlobalPendingCount(),
4294
+ data: { type: isError ? "task_error" : "task_complete", sessionId }
4295
+ });
4296
+ if (this.activityPushChannel?.hasToken(sessionId)) {
4297
+ this.activityPushChannel.endActivity(
4298
+ sessionId,
4299
+ {
4300
+ status: isError ? "error" : "completed",
4301
+ sessionTitle,
4302
+ latestMessage: body,
4303
+ recentActivity: this.getRecentActivity(sessionId),
4304
+ isYoloMode,
4305
+ updatedAt: Date.now(),
4306
+ displayMode: "summary",
4307
+ activitySummary: this.buildActivitySummary(sessionId),
4308
+ startedAt: session?.createdAt,
4309
+ stats: this.buildStatsPayload(session)
4310
+ }
4311
+ // 不传 alert——Expo push 已处理通知,event:end 仅用于关闭 LA
4312
+ ).catch((err) => {
4313
+ console.warn("[NotificationService] endActivity (close LA) failed, LA may linger:", err);
4314
+ });
4315
+ }
4316
+ this.stopLaHeartbeat(sessionId);
4317
+ this.recentActivityState.delete(sessionId);
4318
+ this.lastActivityPushAt.delete(sessionId);
4319
+ this.activityCounters.delete(sessionId);
4320
+ console.log(`[NotificationService] \u{1F3C1} LA end (${reason}) session=${sessionId.slice(0, 8)}\u2026`);
4321
+ }
4322
+ /** 真正发送一次 LA content push(无 alert) */
4323
+ flushActivityPush(sessionId) {
4324
+ const channel = this.activityPushChannel;
4325
+ if (!channel?.hasToken(sessionId)) return;
4326
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
4327
+ if (!session) return;
4328
+ const recentActivity = this.getRecentActivity(sessionId);
4329
+ const latestMessage = recentActivity[recentActivity.length - 1] ?? this.latestAssistantText.get(sessionId) ?? "";
4330
+ const sessionTitle = this.getSessionTitle(sessionId);
4331
+ const isYoloMode = this.getYoloMode(sessionId);
4332
+ const pendingApprovals = this.pendingApprovalsProvider?.(sessionId) ?? [];
4333
+ const latestApproval = pendingApprovals[pendingApprovals.length - 1];
4334
+ const status = latestApproval ? "waitingApproval" : this.mapSessionStatus(session.status);
4335
+ const contentState = {
4336
+ status,
4337
+ sessionTitle,
4338
+ latestMessage,
4339
+ recentActivity,
4340
+ isYoloMode,
4341
+ updatedAt: Date.now(),
4342
+ displayMode: "summary",
4343
+ activitySummary: this.buildActivitySummary(sessionId),
4344
+ startedAt: session.createdAt
4345
+ };
4346
+ if (latestApproval) {
4347
+ contentState.approvalInfo = {
4348
+ requestId: latestApproval.id,
4349
+ toolName: latestApproval.toolName,
4350
+ description: String(latestApproval.description ?? "").slice(0, 80),
4351
+ dangerLevel: this.getDangerLevel(latestApproval.toolName),
4352
+ pendingCount: pendingApprovals.length
4353
+ };
4354
+ }
4355
+ contentState.stats = this.buildStatsPayload(session);
4356
+ const priority = this.pendingPriority.get(sessionId) ?? "5";
4357
+ this.pendingPriority.delete(sessionId);
4358
+ this.lastActivityPushAt.set(sessionId, Date.now());
4359
+ const lineCount = recentActivity.length;
4360
+ channel.updateActivity(sessionId, contentState, { priority }).then((ok) => {
4361
+ if (ok) {
4362
+ console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} p=${priority} lines=${lineCount}`);
4363
+ }
4364
+ }).catch((err) => {
4365
+ console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
4366
+ });
4367
+ }
4368
+ /** SessionStatus → LiveActivity status 字符串映射(与客户端 mapStatus 一致) */
4369
+ mapSessionStatus(status) {
4370
+ switch (status) {
4371
+ case "running":
4372
+ return "running";
4373
+ case "waiting_approval":
4374
+ return "waitingApproval";
4375
+ case "waiting_question":
4376
+ return "waitingQuestion";
4377
+ case "idle":
4378
+ return "completed";
4379
+ case "completed":
4380
+ return "completed";
4381
+ case "error":
4382
+ return "error";
4383
+ default:
4384
+ return "idle";
4385
+ }
4386
+ }
4387
+ /** 文本块清洗:去多余空白 + 截断到 70 字符 */
4388
+ summarizeText(raw) {
4389
+ if (typeof raw !== "string") return "";
4390
+ const cleaned = raw.replace(/\s+/g, " ").trim();
4391
+ return cleaned.length > 70 ? cleaned.slice(0, 70) + "\u2026" : cleaned;
4392
+ }
4393
+ /** 工具调用摘要(与客户端 summarizeToolCall 行为对齐,简化版只输出中文) */
4394
+ summarizeToolCall(name, input) {
4395
+ const str = (v) => typeof v === "string" ? v : "";
4396
+ const baseName = (p) => {
4397
+ const cleaned = p.split(/[?#]/)[0];
4398
+ const parts = cleaned.split("/");
4399
+ return parts[parts.length - 1] || cleaned;
4400
+ };
4401
+ const trunc = (s, n) => s.length > n ? s.slice(0, n) + "\u2026" : s;
4402
+ switch (name) {
4403
+ case "Bash": {
4404
+ const cmd = str(input.command).split("\n")[0];
4405
+ return cmd ? `\u8FD0\u884C: ${trunc(cmd, 60)}` : "\u6267\u884C\u547D\u4EE4";
4406
+ }
4407
+ case "Edit": {
4408
+ const fp = baseName(str(input.file_path));
4409
+ return fp ? `\u7F16\u8F91 ${fp}` : "\u7F16\u8F91\u6587\u4EF6";
4410
+ }
4411
+ case "MultiEdit": {
4412
+ const fp = baseName(str(input.file_path));
4413
+ return fp ? `\u6279\u91CF\u7F16\u8F91 ${fp}` : "\u6279\u91CF\u7F16\u8F91\u6587\u4EF6";
4414
+ }
4415
+ case "Write": {
4416
+ const fp = baseName(str(input.file_path));
4417
+ return fp ? `\u5199\u5165 ${fp}` : "\u5199\u5165\u6587\u4EF6";
4418
+ }
4419
+ case "Read":
4420
+ case "NotebookEdit": {
4421
+ const fp = baseName(str(input.file_path) || str(input.notebook_path));
4422
+ return fp ? `\u9605\u8BFB ${fp}` : "\u9605\u8BFB\u6587\u4EF6";
4423
+ }
4424
+ case "Grep": {
4425
+ const p = str(input.pattern);
4426
+ return p ? `\u641C\u7D22: ${trunc(p, 50)}` : "\u641C\u7D22\u4EE3\u7801";
4427
+ }
4428
+ case "Glob": {
4429
+ const p = str(input.pattern);
4430
+ return p ? `\u67E5\u627E: ${trunc(p, 50)}` : "\u67E5\u627E\u6587\u4EF6";
4431
+ }
4432
+ case "WebFetch": {
4433
+ const url = str(input.url);
4434
+ let host = url;
4435
+ try {
4436
+ host = new URL(url).hostname;
4437
+ } catch {
4438
+ }
4439
+ return host ? `\u8BF7\u6C42 ${trunc(host, 50)}` : "\u8BF7\u6C42\u7F51\u9875";
4440
+ }
4441
+ case "WebSearch": {
4442
+ const q = str(input.query);
4443
+ return q ? `\u641C\u7D22\u7F51\u9875: ${trunc(q, 50)}` : "\u641C\u7D22\u7F51\u9875";
4444
+ }
4445
+ case "TodoWrite":
4446
+ return "\u66F4\u65B0\u4EFB\u52A1\u6E05\u5355";
4447
+ case "Task":
4448
+ case "Agent": {
4449
+ const desc = str(input.description) || str(input.subagent_type);
4450
+ return desc ? `\u6D3E\u53D1\u4EFB\u52A1: ${trunc(desc, 50)}` : "\u6D3E\u53D1\u5B50\u4EFB\u52A1";
4451
+ }
4452
+ case "ExitPlanMode":
4453
+ return "\u63D0\u4EA4\u8BA1\u5212";
4454
+ case "Skill": {
4455
+ const skill = str(input.skill);
4456
+ return skill ? `\u8C03\u7528\u6280\u80FD: ${trunc(skill, 40)}` : "\u8C03\u7528\u6280\u80FD";
4457
+ }
4458
+ default: {
4459
+ const summary = trunc(JSON.stringify(input), 50);
4460
+ return name ? `${name}: ${summary}` : summary;
4461
+ }
4462
+ }
4463
+ }
3973
4464
  };
3974
4465
 
3975
4466
  // src/notification/DesktopNotificationChannel.ts
@@ -4026,12 +4517,13 @@ var ExpoNotificationChannel = class {
4026
4517
  }
4027
4518
  async send(payload) {
4028
4519
  if (this.tokens.size === 0) return;
4029
- const offlineTokens = Array.from(this.tokens).filter((token) => {
4520
+ const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
4521
+ const targetTokens = isCompletionNotif ? Array.from(this.tokens) : Array.from(this.tokens).filter((token) => {
4030
4522
  const ws = this.tokenWsMap.get(token);
4031
4523
  return !ws || ws.readyState !== ws.OPEN;
4032
4524
  });
4033
- if (offlineTokens.length === 0) return;
4034
- const messages = offlineTokens.map((to) => {
4525
+ if (targetTokens.length === 0) return;
4526
+ const messages = targetTokens.map((to) => {
4035
4527
  let sound = payload.sound ?? "default";
4036
4528
  const prefs = this.soundPreferences.get(to);
4037
4529
  if (prefs) {
@@ -4053,7 +4545,7 @@ var ExpoNotificationChannel = class {
4053
4545
  };
4054
4546
  });
4055
4547
  try {
4056
- console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${offlineTokens.length}/${this.tokens.size} devices)`, offlineTokens);
4548
+ console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${targetTokens.length}/${this.tokens.size} devices${isCompletionNotif ? ", forced" : ""})`, targetTokens);
4057
4549
  const res = await fetch(EXPO_PUSH_API, {
4058
4550
  method: "POST",
4059
4551
  headers: { "Content-Type": "application/json", Accept: "application/json" },
@@ -4083,100 +4575,151 @@ var ExpoNotificationChannel = class {
4083
4575
  var http2 = __toESM(require("http2"));
4084
4576
  var fs2 = __toESM(require("fs"));
4085
4577
  var crypto = __toESM(require("crypto"));
4578
+ var APNS_HOSTS = {
4579
+ production: "api.push.apple.com",
4580
+ sandbox: "api.sandbox.push.apple.com"
4581
+ };
4086
4582
  var ActivityPushChannel = class {
4087
4583
  /** sessionId -> activityPushToken */
4088
4584
  tokens = /* @__PURE__ */ new Map();
4585
+ /**
4586
+ * 每个 token 已确认工作的 APNs 环境。
4587
+ * Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
4588
+ * Release build (aps-environment=production) 的 token 仅在 production 端有效。
4589
+ * 同时维护两个连接 + 探测机制,避免环境配错时静默失败。
4590
+ */
4591
+ tokenEnv = /* @__PURE__ */ new Map();
4592
+ /**
4593
+ * 两个环境都拒绝的 token(Live Activity 已销毁/过期)。
4594
+ * 标记后跳过所有发送尝试,避免无意义的 HTTP/2 请求。
4595
+ * 当同一 session 注册新 token 时自动清除旧 token 的标记。
4596
+ */
4597
+ deadTokens = /* @__PURE__ */ new Set();
4598
+ /** 首次探测顺序(来自配置 hint),未配置则先试 sandbox(开发场景占多数) */
4599
+ probeOrder;
4089
4600
  teamId;
4090
4601
  keyId;
4091
4602
  authKey;
4092
- apnsHost;
4093
4603
  /** 缓存的 JWT token + 过期时间 */
4094
4604
  cachedJwt = null;
4095
- /** 复用的 HTTP/2 长连接 */
4096
- http2Client = null;
4605
+ /** 每个环境一条 HTTP/2 长连接 */
4606
+ http2Clients = {};
4097
4607
  constructor(config) {
4098
4608
  this.teamId = config.teamId;
4099
4609
  this.keyId = config.keyId;
4100
4610
  this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
4101
- this.apnsHost = config.sandbox ? "api.sandbox.push.apple.com" : "api.push.apple.com";
4102
- console.log(`[ActivityPushChannel] Initialized (${config.sandbox ? "sandbox" : "production"} mode)`);
4103
- }
4104
- /** 获取或新建 HTTP/2 长连接 */
4105
- getHttp2Client() {
4106
- if (this.http2Client && !this.http2Client.destroyed && !this.http2Client.closed) {
4107
- return this.http2Client;
4108
- }
4109
- this.http2Client = http2.connect(`https://${this.apnsHost}`);
4110
- this.http2Client.on("error", (err) => {
4111
- console.warn("[ActivityPushChannel] HTTP/2 connection error, will reconnect on next request:", err.message);
4112
- this.http2Client?.destroy();
4113
- this.http2Client = null;
4611
+ this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
4612
+ console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
4613
+ }
4614
+ /** 获取或新建指定环境的 HTTP/2 长连接 */
4615
+ getHttp2Client(env) {
4616
+ const existing = this.http2Clients[env];
4617
+ if (existing && !existing.destroyed && !existing.closed) {
4618
+ return existing;
4619
+ }
4620
+ const client = http2.connect(`https://${APNS_HOSTS[env]}`);
4621
+ client.on("error", (err) => {
4622
+ console.warn(`[ActivityPushChannel] HTTP/2 (${env}) error, will reconnect on next request:`, err.message);
4623
+ client.destroy();
4624
+ if (this.http2Clients[env] === client) delete this.http2Clients[env];
4114
4625
  });
4115
- this.http2Client.on("close", () => {
4116
- this.http2Client = null;
4626
+ client.on("close", () => {
4627
+ if (this.http2Clients[env] === client) delete this.http2Clients[env];
4117
4628
  });
4118
- return this.http2Client;
4629
+ this.http2Clients[env] = client;
4630
+ return client;
4119
4631
  }
4120
4632
  /** 注册 Activity push token */
4121
4633
  addToken(sessionId, token) {
4634
+ const oldToken = this.tokens.get(sessionId);
4635
+ if (oldToken && oldToken !== token) {
4636
+ this.tokenEnv.delete(oldToken);
4637
+ this.deadTokens.delete(oldToken);
4638
+ }
4639
+ const existed = this.tokens.has(sessionId);
4122
4640
  this.tokens.set(sessionId, token);
4123
- console.log(`[ActivityPushChannel] Token registered: session=${sessionId}`);
4641
+ console.log(`[ActivityPushChannel] Token ${existed ? "updated" : "registered"}: session=${sessionId.slice(0, 8)}\u2026 token=${token.slice(0, 16)}\u2026`);
4124
4642
  }
4125
4643
  /** 移除 Activity push token */
4126
4644
  removeToken(sessionId) {
4645
+ const tok = this.tokens.get(sessionId);
4127
4646
  this.tokens.delete(sessionId);
4647
+ if (tok) {
4648
+ this.tokenEnv.delete(tok);
4649
+ this.deadTokens.delete(tok);
4650
+ }
4128
4651
  }
4129
- /** 发送 content-state 更新到指定会话的 Live Activity */
4130
- async updateActivity(sessionId, contentState) {
4652
+ /** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知)。返回 true 表示实际发出 */
4653
+ async updateActivity(sessionId, contentState, opts) {
4131
4654
  const token = this.tokens.get(sessionId);
4132
- if (!token) return;
4655
+ if (!token) return false;
4656
+ const now = Math.floor(Date.now() / 1e3);
4657
+ const priority = opts?.priority ?? "5";
4133
4658
  const payload = {
4134
4659
  aps: {
4135
- timestamp: Math.floor(Date.now() / 1e3),
4660
+ timestamp: now,
4136
4661
  event: "update",
4137
- "content-state": contentState
4662
+ "content-state": contentState,
4663
+ "stale-date": now + 600
4138
4664
  }
4139
4665
  };
4140
4666
  try {
4141
- await this.sendToAPNs(token, payload);
4667
+ await this.sendToAPNs(token, payload, {
4668
+ priority,
4669
+ collapseId: `state-${sessionId.slice(0, 54)}`
4670
+ });
4671
+ return true;
4142
4672
  } catch (err) {
4143
4673
  console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
4674
+ return false;
4144
4675
  }
4145
4676
  }
4146
- /** 发送带通知的 content-state 更新(审批请求时使用) */
4677
+ /** 发送带通知的 content-state 更新(审批请求时使用),返回是否发送成功 */
4147
4678
  async updateActivityWithAlert(sessionId, contentState, alert) {
4148
4679
  const token = this.tokens.get(sessionId);
4149
- if (!token) return;
4680
+ if (!token) return false;
4681
+ const now = Math.floor(Date.now() / 1e3);
4150
4682
  const payload = {
4151
4683
  aps: {
4152
- timestamp: Math.floor(Date.now() / 1e3),
4684
+ timestamp: now,
4153
4685
  event: "update",
4154
4686
  "content-state": contentState,
4687
+ "stale-date": now + 600,
4155
4688
  alert,
4156
4689
  sound: "default"
4157
4690
  }
4158
4691
  };
4159
4692
  try {
4160
- await this.sendToAPNs(token, payload);
4693
+ await this.sendToAPNs(token, payload, { priority: "10" });
4694
+ return true;
4161
4695
  } catch (err) {
4162
4696
  console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
4697
+ return false;
4163
4698
  }
4164
4699
  }
4165
- /** 结束指定会话的 Live Activity */
4166
- async endActivity(sessionId, contentState) {
4700
+ /**
4701
+ * 结束指定会话的 Live Activity。
4702
+ * 可选 alert:APNs event=end 时同时推送横幅通知 + 声音,用于在会话完成时提醒用户。
4703
+ */
4704
+ async endActivity(sessionId, contentState, opts) {
4167
4705
  const token = this.tokens.get(sessionId);
4168
4706
  if (!token) return;
4169
- const payload = {
4170
- aps: {
4171
- timestamp: Math.floor(Date.now() / 1e3),
4172
- event: "end",
4173
- "content-state": contentState
4174
- }
4707
+ const now = Math.floor(Date.now() / 1e3);
4708
+ const aps = {
4709
+ timestamp: now,
4710
+ event: "end",
4711
+ "content-state": contentState
4175
4712
  };
4713
+ if (opts?.alert) {
4714
+ aps.alert = opts.alert;
4715
+ aps.sound = "default";
4716
+ }
4717
+ const payload = { aps };
4176
4718
  try {
4177
- await this.sendToAPNs(token, payload);
4719
+ await this.sendToAPNs(token, payload, { priority: "10" });
4178
4720
  } catch (err) {
4179
- console.warn(`[ActivityPushChannel] End failed session=${sessionId}:`, err);
4721
+ this.tokens.delete(sessionId);
4722
+ throw err;
4180
4723
  }
4181
4724
  this.tokens.delete(sessionId);
4182
4725
  }
@@ -4184,33 +4727,81 @@ var ActivityPushChannel = class {
4184
4727
  hasToken(sessionId) {
4185
4728
  return this.tokens.has(sessionId);
4186
4729
  }
4187
- /** 发送 APNs HTTP/2 请求 */
4188
- async sendToAPNs(deviceToken, payload) {
4730
+ /**
4731
+ * 发送 APNs,自动处理环境探测。
4732
+ * 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
4733
+ * 收到 BadDeviceToken / BadEnvironmentKeyInToken 自动切到另一个环境,
4734
+ * 并把成功的环境绑定到该 token。
4735
+ */
4736
+ async sendToAPNs(deviceToken, payload, opts = {}) {
4737
+ if (this.deadTokens.has(deviceToken)) {
4738
+ throw new Error(`token permanently dead (Activity ended): ${deviceToken.slice(0, 16)}\u2026`);
4739
+ }
4740
+ const known = this.tokenEnv.get(deviceToken);
4741
+ if (known) {
4742
+ return this.sendToAPNsOnce(deviceToken, payload, opts, known);
4743
+ }
4744
+ const short = deviceToken.slice(0, 16);
4745
+ console.log(`[ActivityPushChannel] \u{1F50D} probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
4746
+ let lastErr = null;
4747
+ for (const env of this.probeOrder) {
4748
+ try {
4749
+ console.log(`[ActivityPushChannel] \u{1F50D} probe try ${env} token=${short}\u2026`);
4750
+ await this.sendToAPNsOnce(deviceToken, payload, opts, env);
4751
+ this.tokenEnv.set(deviceToken, env);
4752
+ console.log(`[ActivityPushChannel] \u2705 probe bound to ${env} (token=${short}\u2026)`);
4753
+ return;
4754
+ } catch (err) {
4755
+ lastErr = err;
4756
+ const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
4757
+ console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
4758
+ if (!isBadDeviceTokenError(err)) {
4759
+ throw err;
4760
+ }
4761
+ }
4762
+ }
4763
+ this.deadTokens.add(deviceToken);
4764
+ for (const [sid, tok] of this.tokens) {
4765
+ if (tok === deviceToken) {
4766
+ this.tokens.delete(sid);
4767
+ console.warn(`[ActivityPushChannel] \u2620\uFE0F dead token, session removed: session=${sid.slice(0, 8)}\u2026 token=${short}\u2026 (Activity ended/iOS reinstalled)`);
4768
+ break;
4769
+ }
4770
+ }
4771
+ throw lastErr ?? new Error("APNs send failed: all environments rejected token");
4772
+ }
4773
+ /** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
4774
+ async sendToAPNsOnce(deviceToken, payload, opts, env) {
4189
4775
  const topic = "com.kachun.sessix.push-type.liveactivity";
4190
4776
  const jwt = this.getJWT();
4191
4777
  const payloadStr = JSON.stringify(payload);
4778
+ const priority = opts.priority ?? "10";
4192
4779
  return new Promise((resolve, reject) => {
4193
4780
  let client;
4194
4781
  try {
4195
- client = this.getHttp2Client();
4782
+ client = this.getHttp2Client(env);
4196
4783
  } catch (err) {
4197
4784
  return reject(err);
4198
4785
  }
4199
- const req = client.request({
4786
+ const headers = {
4200
4787
  ":method": "POST",
4201
4788
  ":path": `/3/device/${deviceToken}`,
4202
4789
  "authorization": `bearer ${jwt}`,
4203
4790
  "apns-topic": topic,
4204
4791
  "apns-push-type": "liveactivity",
4205
- "apns-priority": "10",
4206
- "apns-expiration": String(Math.floor(Date.now() / 1e3) + 30),
4792
+ "apns-priority": priority,
4793
+ "apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
4207
4794
  "content-type": "application/json",
4208
4795
  "content-length": Buffer.byteLength(payloadStr)
4209
- });
4796
+ };
4797
+ if (opts.collapseId) {
4798
+ headers["apns-collapse-id"] = opts.collapseId;
4799
+ }
4800
+ const req = client.request(headers);
4210
4801
  let statusCode = 0;
4211
4802
  let responseData = "";
4212
- req.on("response", (headers) => {
4213
- statusCode = Number(headers[":status"] ?? 0);
4803
+ req.on("response", (headers2) => {
4804
+ statusCode = Number(headers2[":status"] ?? 0);
4214
4805
  });
4215
4806
  req.on("data", (chunk) => {
4216
4807
  responseData += chunk;
@@ -4220,10 +4811,11 @@ var ActivityPushChannel = class {
4220
4811
  resolve();
4221
4812
  } else {
4222
4813
  if (statusCode === 0) {
4223
- this.http2Client?.destroy();
4224
- this.http2Client = null;
4814
+ const c = this.http2Clients[env];
4815
+ c?.destroy();
4816
+ delete this.http2Clients[env];
4225
4817
  }
4226
- reject(new Error(`APNs returned ${statusCode}: ${responseData}`));
4818
+ reject(new ApnsError(statusCode, responseData));
4227
4819
  }
4228
4820
  });
4229
4821
  req.on("error", (err) => {
@@ -4256,6 +4848,24 @@ var ActivityPushChannel = class {
4256
4848
  return token;
4257
4849
  }
4258
4850
  };
4851
+ var ApnsError = class extends Error {
4852
+ constructor(statusCode, responseBody) {
4853
+ super(`APNs returned ${statusCode}: ${responseBody}`);
4854
+ this.statusCode = statusCode;
4855
+ this.responseBody = responseBody;
4856
+ this.name = "ApnsError";
4857
+ }
4858
+ };
4859
+ function isBadDeviceTokenError(err) {
4860
+ if (!(err instanceof ApnsError)) return false;
4861
+ if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
4862
+ try {
4863
+ const parsed = JSON.parse(err.responseBody);
4864
+ return parsed.reason === "BadDeviceToken" || parsed.reason === "BadEnvironmentKeyInToken" || parsed.reason === "Unregistered";
4865
+ } catch {
4866
+ return false;
4867
+ }
4868
+ }
4259
4869
 
4260
4870
  // src/session/ProjectReader.ts
4261
4871
  var import_promises3 = require("fs/promises");
@@ -4655,6 +5265,45 @@ var PairingManager = class {
4655
5265
  }
4656
5266
  };
4657
5267
 
5268
+ // src/utils/shellPath.ts
5269
+ var import_node_child_process7 = require("child_process");
5270
+ var fixed = false;
5271
+ function fixShellPath() {
5272
+ if (fixed || isWindows) {
5273
+ fixed = true;
5274
+ return;
5275
+ }
5276
+ fixed = true;
5277
+ const shell = process.env.SHELL || "/bin/zsh";
5278
+ const isFish = /\/fish$/.test(shell);
5279
+ const printPathCmd = isFish ? "string join : $PATH" : 'printf "%s" "$PATH"';
5280
+ let raw;
5281
+ try {
5282
+ raw = (0, import_node_child_process7.execFileSync)(shell, ["-l", "-c", printPathCmd], {
5283
+ encoding: "utf8",
5284
+ timeout: 3e3,
5285
+ stdio: ["ignore", "pipe", "ignore"]
5286
+ });
5287
+ } catch (err) {
5288
+ console.warn("[fixShellPath] failed to read login shell PATH:", err);
5289
+ return;
5290
+ }
5291
+ const fromShell = raw.trim();
5292
+ if (!fromShell) return;
5293
+ process.env.PATH = mergePath(fromShell, process.env.PATH || "");
5294
+ }
5295
+ function mergePath(primary, secondary) {
5296
+ const seen = /* @__PURE__ */ new Set();
5297
+ const out = [];
5298
+ for (const seg of primary.split(":").concat(secondary.split(":"))) {
5299
+ if (!seg) continue;
5300
+ if (seen.has(seg)) continue;
5301
+ seen.add(seg);
5302
+ out.push(seg);
5303
+ }
5304
+ return out.join(":");
5305
+ }
5306
+
4658
5307
  // src/auth/AuthManager.ts
4659
5308
  var import_child_process3 = require("child_process");
4660
5309
  var import_child_process4 = require("child_process");
@@ -4773,13 +5422,10 @@ var AuthManager = class extends import_events3.EventEmitter {
4773
5422
  }
4774
5423
  };
4775
5424
 
4776
- // src/server.ts
4777
- var import_promises8 = require("fs/promises");
4778
-
4779
5425
  // src/terminal/TerminalExecutor.ts
4780
- var import_node_child_process7 = require("child_process");
5426
+ var import_node_child_process8 = require("child_process");
4781
5427
  var import_uuid5 = require("uuid");
4782
- var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
5428
+ var EXEC_TIMEOUT_MS = 30 * 60 * 1e3;
4783
5429
  var TerminalExecutor = class {
4784
5430
  processes = /* @__PURE__ */ new Map();
4785
5431
  eventCallbacks = [];
@@ -4801,9 +5447,9 @@ var TerminalExecutor = class {
4801
5447
  }
4802
5448
  exec(sessionId, command, cwd) {
4803
5449
  const execId = (0, import_uuid5.v4)();
4804
- const shell = isWindows ? "powershell" : "bash";
4805
- const args = isWindows ? ["-Command", command] : ["-c", command];
4806
- const proc = (0, import_node_child_process7.spawn)(shell, args, {
5450
+ const shell = isWindows ? "powershell" : process.env.SHELL || "/bin/zsh";
5451
+ const args = isWindows ? ["-Command", command] : ["-l", "-c", command];
5452
+ const proc = (0, import_node_child_process8.spawn)(shell, args, {
4807
5453
  cwd,
4808
5454
  stdio: ["ignore", "pipe", "pipe"],
4809
5455
  env: { ...process.env }
@@ -4840,6 +5486,14 @@ var TerminalExecutor = class {
4840
5486
  });
4841
5487
  const timer = setTimeout(() => {
4842
5488
  if (this.processes.has(execId)) {
5489
+ this.emit({
5490
+ type: "terminal_output",
5491
+ sessionId,
5492
+ execId,
5493
+ stream: "stderr",
5494
+ data: `[killed: timeout ${Math.round(EXEC_TIMEOUT_MS / 6e4)}m]
5495
+ `
5496
+ });
4843
5497
  killProcessCrossPlatform(proc);
4844
5498
  }
4845
5499
  }, EXEC_TIMEOUT_MS);
@@ -4864,13 +5518,13 @@ var TerminalExecutor = class {
4864
5518
  };
4865
5519
 
4866
5520
  // src/xcode/XcodeBuildExecutor.ts
4867
- var import_node_child_process8 = require("child_process");
5521
+ var import_node_child_process9 = require("child_process");
4868
5522
  var import_node_util = require("util");
4869
5523
  var import_promises4 = require("fs/promises");
4870
5524
  var import_node_path6 = require("path");
4871
5525
  var import_node_os7 = require("os");
4872
5526
  var import_uuid6 = require("uuid");
4873
- var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
5527
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
4874
5528
  var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
4875
5529
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
4876
5530
  var CONFIG_FILE = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "xcode-config.json");
@@ -5059,7 +5713,7 @@ ${e.stderr ?? ""}`);
5059
5713
  if (override) await this.saveConfig(projectPath, override);
5060
5714
  const buildId = (0, import_uuid6.v4)();
5061
5715
  const args = buildArgs(config);
5062
- const proc = (0, import_node_child_process8.spawn)("xcodebuild", args, {
5716
+ const proc = (0, import_node_child_process9.spawn)("xcodebuild", args, {
5063
5717
  cwd: projectPath,
5064
5718
  stdio: ["ignore", "pipe", "pipe"],
5065
5719
  env: { ...process.env, NSUnbufferedIO: "YES" }
@@ -5155,7 +5809,7 @@ ${e.stderr ?? ""}`);
5155
5809
 
5156
5810
  `
5157
5811
  });
5158
- const proc = (0, import_node_child_process8.spawn)(installCmd[0], installCmd.slice(1), {
5812
+ const proc = (0, import_node_child_process9.spawn)(installCmd[0], installCmd.slice(1), {
5159
5813
  cwd: projectPath,
5160
5814
  stdio: ["ignore", "pipe", "pipe"]
5161
5815
  });
@@ -5562,15 +6216,16 @@ var CommandDiscovery = class {
5562
6216
  const cmd = sanitizeBashLine(rawLine);
5563
6217
  if (!cmd) continue;
5564
6218
  const { command: cleanCmd, inlineComment } = splitInlineComment(cmd);
5565
- const title = synthesizeTitle(cleanCmd);
6219
+ const { cwd: cdCwd, command: finalCmd } = splitCdPrefix(cleanCmd);
6220
+ const title = synthesizeTitle(finalCmd);
5566
6221
  out.push(makeCommand({
5567
6222
  title,
5568
- command: cleanCmd,
5569
- cwd: "",
6223
+ command: finalCmd,
6224
+ cwd: cdCwd,
5570
6225
  source,
5571
6226
  sourceFile: fileName,
5572
6227
  description: inlineComment ?? blockHeading,
5573
- category: classifyByCommand(cleanCmd)
6228
+ category: classifyByCommand(finalCmd)
5574
6229
  }));
5575
6230
  }
5576
6231
  }
@@ -5615,6 +6270,19 @@ function synthesizeTitle(cmd) {
5615
6270
  const head = tokens.slice(0, 3).join(" ");
5616
6271
  return head.length > 60 ? head.slice(0, 60) + "\u2026" : head;
5617
6272
  }
6273
+ function splitCdPrefix(cmd) {
6274
+ const m = /^cd\s+(\S+)\s*&&\s*(.+)$/.exec(cmd);
6275
+ if (!m) return { cwd: "", command: cmd };
6276
+ const path2 = m[1];
6277
+ if (!path2) return { cwd: "", command: cmd };
6278
+ if (path2.startsWith("/") || path2.startsWith("~") || path2.startsWith("-")) {
6279
+ return { cwd: "", command: cmd };
6280
+ }
6281
+ if (path2.split("/").some((seg) => seg === "..")) {
6282
+ return { cwd: "", command: cmd };
6283
+ }
6284
+ return { cwd: path2, command: m[2].trim() };
6285
+ }
5618
6286
  function splitInlineComment(line) {
5619
6287
  let inSingle = false;
5620
6288
  let inDouble = false;
@@ -5676,10 +6344,10 @@ function sourceWeight(s) {
5676
6344
  }
5677
6345
 
5678
6346
  // src/git/GitExecutor.ts
5679
- var import_node_child_process9 = require("child_process");
6347
+ var import_node_child_process10 = require("child_process");
5680
6348
  var import_node_util2 = require("util");
5681
6349
  var import_uuid7 = require("uuid");
5682
- var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process9.exec);
6350
+ var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
5683
6351
  var STATUS_TIMEOUT_MS = 15e3;
5684
6352
  var COMMIT_TIMEOUT_MS = 6e4;
5685
6353
  var PUSH_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -5852,7 +6520,7 @@ var GitExecutor = class {
5852
6520
  });
5853
6521
  let proc;
5854
6522
  try {
5855
- proc = (0, import_node_child_process9.spawn)(cmd[0], cmd.slice(1), {
6523
+ proc = (0, import_node_child_process10.spawn)(cmd[0], cmd.slice(1), {
5856
6524
  cwd: projectPath,
5857
6525
  stdio: ["ignore", "pipe", "pipe"],
5858
6526
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
@@ -6032,9 +6700,15 @@ function isValidTask(value) {
6032
6700
  }
6033
6701
 
6034
6702
  // src/utils/cliCapabilities.ts
6035
- var import_node_child_process10 = require("child_process");
6703
+ var import_node_child_process11 = require("child_process");
6704
+ var DEFAULT_MODELS = [
6705
+ { value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
6706
+ { value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
6707
+ { value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
6708
+ ];
6036
6709
  var DEFAULT_CAPABILITIES = {
6037
- effortLevels: ["low", "medium", "high", "xhigh", "max"]
6710
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
6711
+ models: DEFAULT_MODELS
6038
6712
  };
6039
6713
  async function parseCliCapabilities() {
6040
6714
  const claudePath = findClaudePath();
@@ -6060,7 +6734,7 @@ async function parseCliCapabilities() {
6060
6734
  }
6061
6735
  function runCli(path2, args) {
6062
6736
  return new Promise((resolve) => {
6063
- (0, import_node_child_process10.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
6737
+ (0, import_node_child_process11.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
6064
6738
  if (err) {
6065
6739
  console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
6066
6740
  resolve(null);
@@ -6074,7 +6748,7 @@ function runCli(path2, args) {
6074
6748
  // src/server.ts
6075
6749
  var WS_PORT = 3745;
6076
6750
  var HTTP_PORT = 3746;
6077
- var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process11.exec);
6751
+ var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process12.exec);
6078
6752
  async function killPortProcess(port) {
6079
6753
  try {
6080
6754
  if (isWindows) {
@@ -6102,6 +6776,39 @@ async function killPortProcess(port) {
6102
6776
  } catch {
6103
6777
  }
6104
6778
  }
6779
+ async function loadApnsConfigFromFile() {
6780
+ const path2 = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix", "apns.json");
6781
+ try {
6782
+ const raw = await (0, import_promises7.readFile)(path2, "utf8");
6783
+ const cfg = JSON.parse(raw);
6784
+ if (typeof cfg.teamId !== "string" || typeof cfg.keyId !== "string" || typeof cfg.authKeyPath !== "string") {
6785
+ console.warn(`[Server] \u26A0\uFE0F ${path2} \u7F3A\u5C11\u5FC5\u9700\u5B57\u6BB5 (teamId / keyId / authKeyPath)\uFF0CLA \u540E\u53F0\u63A8\u9001\u5DF2\u7981\u7528`);
6786
+ return null;
6787
+ }
6788
+ try {
6789
+ await (0, import_promises7.readFile)(cfg.authKeyPath, "utf8");
6790
+ } catch (err) {
6791
+ console.warn(`[Server] \u26A0\uFE0F \u65E0\u6CD5\u8BFB\u53D6 APNs Auth Key: ${cfg.authKeyPath}`, err);
6792
+ return null;
6793
+ }
6794
+ console.log(`[Server] \u2705 \u5DF2\u52A0\u8F7D APNs \u914D\u7F6E (${path2})`);
6795
+ console.log(`[Server] teamId=${cfg.teamId} keyId=${cfg.keyId} sandbox=${cfg.sandbox === true}`);
6796
+ return {
6797
+ teamId: cfg.teamId,
6798
+ keyId: cfg.keyId,
6799
+ authKeyPath: cfg.authKeyPath,
6800
+ sandbox: cfg.sandbox === true
6801
+ };
6802
+ } catch (err) {
6803
+ const code = err.code;
6804
+ if (code === "ENOENT") {
6805
+ console.log(`[Server] \u2139\uFE0F ${path2} \u4E0D\u5B58\u5728\uFF0CLA \u540E\u53F0\u63A8\u9001\u672A\u542F\u7528\uFF08\u524D\u53F0 App \u4ECD\u80FD\u7528\u672C\u5730 Activity.update\uFF09`);
6806
+ } else {
6807
+ console.warn(`[Server] \u26A0\uFE0F \u8BFB\u53D6 ${path2} \u5931\u8D25:`, err);
6808
+ }
6809
+ return null;
6810
+ }
6811
+ }
6105
6812
  async function createWithRetry(label, port, factory) {
6106
6813
  try {
6107
6814
  return await factory();
@@ -6116,6 +6823,7 @@ async function createWithRetry(label, port, factory) {
6116
6823
  }
6117
6824
  }
6118
6825
  async function start(opts = {}) {
6826
+ fixShellPath();
6119
6827
  const configDir = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix");
6120
6828
  const tokenFile = (0, import_node_path9.join)(configDir, "token");
6121
6829
  let token;
@@ -6164,9 +6872,10 @@ async function start(opts = {}) {
6164
6872
  const notificationService = new NotificationService(sessionManager, expoChannel);
6165
6873
  notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
6166
6874
  notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
6167
- if (opts.activityPush) {
6875
+ const activityPushOpts = opts.activityPush ?? await loadApnsConfigFromFile();
6876
+ if (activityPushOpts) {
6168
6877
  try {
6169
- const activityChannel = new ActivityPushChannel(opts.activityPush);
6878
+ const activityChannel = new ActivityPushChannel(activityPushOpts);
6170
6879
  notificationService.setActivityPushChannel(activityChannel);
6171
6880
  console.log(`[Server] ${t("server.activityPushEnabled")}`);
6172
6881
  } catch (err) {
@@ -6201,6 +6910,9 @@ async function start(opts = {}) {
6201
6910
  notificationService.setGlobalPendingCountProvider(
6202
6911
  () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
6203
6912
  );
6913
+ notificationService.setPendingApprovalsProvider(
6914
+ (sessionId) => approvalProxy.getPendingRequestsForSession(sessionId)
6915
+ );
6204
6916
  let cliCapabilities = null;
6205
6917
  parseCliCapabilities().then((caps) => {
6206
6918
  cliCapabilities = caps;
@@ -6213,7 +6925,8 @@ async function start(opts = {}) {
6213
6925
  onFire: async (task) => {
6214
6926
  const p = task.payload;
6215
6927
  if (p.kind === "create") {
6216
- await (0, import_promises7.mkdir)(p.projectPath, { recursive: true });
6928
+ const dirExists = await (0, import_promises7.stat)(p.projectPath).then((s) => s.isDirectory()).catch(() => false);
6929
+ if (!dirExists) await (0, import_promises7.mkdir)(p.projectPath, { recursive: true });
6217
6930
  const session = await sessionManager.createSession(
6218
6931
  p.projectPath,
6219
6932
  p.message,
@@ -6271,7 +6984,7 @@ async function start(opts = {}) {
6271
6984
  wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
6272
6985
  }
6273
6986
  if (cliCapabilities) {
6274
- wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version });
6987
+ wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version, models: cliCapabilities.models });
6275
6988
  }
6276
6989
  if (scheduledManager) {
6277
6990
  wsBridge.send(ws, { type: "scheduled_session_list", tasks: scheduledManager.list() });
@@ -6281,7 +6994,8 @@ async function start(opts = {}) {
6281
6994
  try {
6282
6995
  switch (event.type) {
6283
6996
  case "create_session": {
6284
- await (0, import_promises7.mkdir)(event.projectPath, { recursive: true });
6997
+ const dirExists = await (0, import_promises7.stat)(event.projectPath).then((s) => s.isDirectory()).catch(() => false);
6998
+ if (!dirExists) await (0, import_promises7.mkdir)(event.projectPath, { recursive: true });
6285
6999
  const resumeId = event.resumeSessionId ?? event.newSessionId;
6286
7000
  if (resumeId) sessionFileWatcher.unwatch(resumeId);
6287
7001
  await sessionManager.createSession(
@@ -6341,38 +7055,44 @@ async function start(opts = {}) {
6341
7055
  sessions: sessionManager.getActiveSessions()
6342
7056
  });
6343
7057
  sessionManager.flushPendingAssistant(event.sessionId);
6344
- const bufferedEvents = [...sessionManager.getSessionEvents(event.sessionId)];
6345
7058
  if (sessionManager.isBufferTruncated(event.sessionId)) {
6346
7059
  const projectPath = sessionManager.getSessionProjectPath(event.sessionId);
6347
7060
  if (projectPath) {
6348
7061
  const historyResult = await getSessionHistory(projectPath, event.sessionId);
7062
+ const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
6349
7063
  if (historyResult.ok && historyResult.value.length > 0) {
6350
- const merged = [...historyResult.value, ...bufferedEvents];
7064
+ const merged = [...historyResult.value, ...buffered];
6351
7065
  wsBridge.send(ws, {
6352
7066
  type: "session_history",
6353
7067
  sessionId: event.sessionId,
6354
7068
  events: merged
6355
7069
  });
6356
- } else if (bufferedEvents.length > 0) {
7070
+ } else if (buffered.length > 0) {
7071
+ wsBridge.send(ws, {
7072
+ type: "session_history",
7073
+ sessionId: event.sessionId,
7074
+ events: buffered
7075
+ });
7076
+ }
7077
+ } else {
7078
+ const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
7079
+ if (buffered.length > 0) {
6357
7080
  wsBridge.send(ws, {
6358
7081
  type: "session_history",
6359
7082
  sessionId: event.sessionId,
6360
- events: bufferedEvents
7083
+ events: buffered
6361
7084
  });
6362
7085
  }
6363
- } else if (bufferedEvents.length > 0) {
7086
+ }
7087
+ } else {
7088
+ const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
7089
+ if (buffered.length > 0) {
6364
7090
  wsBridge.send(ws, {
6365
7091
  type: "session_history",
6366
7092
  sessionId: event.sessionId,
6367
- events: bufferedEvents
7093
+ events: buffered
6368
7094
  });
6369
7095
  }
6370
- } else if (bufferedEvents.length > 0) {
6371
- wsBridge.send(ws, {
6372
- type: "session_history",
6373
- sessionId: event.sessionId,
6374
- events: bufferedEvents
6375
- });
6376
7096
  }
6377
7097
  for (const req of approvalProxy.getPendingRequestsForSession(event.sessionId)) {
6378
7098
  wsBridge.send(ws, { type: "approval_request", request: req });
@@ -6469,7 +7189,7 @@ async function start(opts = {}) {
6469
7189
  if (!isStreaming) {
6470
7190
  const filePath = getSessionFilePath(event.projectPath, event.sessionId);
6471
7191
  try {
6472
- const fileStat = await (0, import_promises8.stat)(filePath);
7192
+ const fileStat = await (0, import_promises7.stat)(filePath);
6473
7193
  sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
6474
7194
  } catch {
6475
7195
  }
@@ -6553,6 +7273,9 @@ async function start(opts = {}) {
6553
7273
  wsBridge.clearViewingSession(ws);
6554
7274
  break;
6555
7275
  }
7276
+ case "approval_displayed": {
7277
+ break;
7278
+ }
6556
7279
  case "always_allow_tool": {
6557
7280
  approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
6558
7281
  break;
@@ -6750,14 +7473,12 @@ async function start(opts = {}) {
6750
7473
  setTimeout(() => {
6751
7474
  if (!approvalProxy.isPending(request.id)) return;
6752
7475
  if (wsBridge.isViewingSession(request.sessionId)) return;
6753
- if (wsBridge.getConnectionCount() > 0) return;
6754
7476
  const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
6755
7477
  notificationService.notifyApproval(request, pendingCount);
6756
- }, 5e3);
7478
+ }, 3e3);
6757
7479
  setTimeout(() => {
6758
7480
  if (!approvalProxy.isPending(request.id)) return;
6759
7481
  if (wsBridge.isViewingSession(request.sessionId)) return;
6760
- if (wsBridge.getConnectionCount() > 0) return;
6761
7482
  console.log(`[Server] ${t("server.approvalRetry", { id: request.id })}`);
6762
7483
  const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
6763
7484
  notificationService.notifyApproval(request, pendingCount);
@@ -6769,13 +7490,11 @@ async function start(opts = {}) {
6769
7490
  setTimeout(() => {
6770
7491
  if (!sessionManager.isQuestionPending(request.id)) return;
6771
7492
  if (wsBridge.isViewingSession(request.sessionId)) return;
6772
- if (wsBridge.getConnectionCount() > 0) return;
6773
7493
  notificationService.notifyQuestion(request);
6774
7494
  }, 5e3);
6775
7495
  setTimeout(() => {
6776
7496
  if (!sessionManager.isQuestionPending(request.id)) return;
6777
7497
  if (wsBridge.isViewingSession(request.sessionId)) return;
6778
- if (wsBridge.getConnectionCount() > 0) return;
6779
7498
  console.log(`[Server] Question ${request.id} not answered in 60s, retrying push`);
6780
7499
  notificationService.notifyQuestion(request);
6781
7500
  }, 6e4);
@@ -6964,7 +7683,7 @@ async function autoUpdateIfNeeded() {
6964
7683
  console.log(` \u{1F4E6} ${t("startup.autoUpdating", { current: PKG_VERSION, latest })}`);
6965
7684
  console.log();
6966
7685
  try {
6967
- (0, import_node_child_process12.execFileSync)("npx", [`sessix-server@${latest}`], {
7686
+ (0, import_node_child_process13.execFileSync)("npx", [`sessix-server@${latest}`], {
6968
7687
  stdio: "inherit",
6969
7688
  env: { ...process.env, __SESSIX_UPDATED: "1" }
6970
7689
  });