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/server.js CHANGED
@@ -311,7 +311,7 @@ var import_uuid9 = require("uuid");
311
311
  var import_promises7 = require("fs/promises");
312
312
  var import_node_os9 = require("os");
313
313
  var import_node_path9 = require("path");
314
- var import_node_child_process11 = require("child_process");
314
+ var import_node_child_process12 = require("child_process");
315
315
  var import_node_util3 = require("util");
316
316
 
317
317
  // src/providers/ProcessProvider.ts
@@ -667,7 +667,24 @@ var ProcessProvider = class {
667
667
  writeUserMessage(proc, message, sessionId, images) {
668
668
  const content = [];
669
669
  if (images?.length) {
670
- for (const img of images) {
670
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
671
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
672
+ for (let i = 0; i < images.length; i++) {
673
+ const img = images[i];
674
+ if (!ALLOWED_TYPES.has(img.media_type)) {
675
+ if (sessionId) {
676
+ this.emitWriteError(sessionId, `Image #${i + 1} rejected: unsupported media_type "${img.media_type}". Only JPEG/PNG/WebP/GIF are accepted.`);
677
+ }
678
+ return;
679
+ }
680
+ const sizeBytes = Math.floor(img.data.length * 0.75);
681
+ if (sizeBytes > MAX_IMAGE_BYTES) {
682
+ if (sessionId) {
683
+ const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(1);
684
+ this.emitWriteError(sessionId, `Image #${i + 1} rejected: ${sizeMb}MB exceeds 5MB per-image limit.`);
685
+ }
686
+ return;
687
+ }
671
688
  content.push({
672
689
  type: "image",
673
690
  source: { type: "base64", media_type: img.media_type, data: img.data }
@@ -694,6 +711,14 @@ var ProcessProvider = class {
694
711
  this.emitWriteError(sessionId, `Failed to send message: ${err.message}`);
695
712
  }
696
713
  });
714
+ if (sessionId) {
715
+ const syntheticUser = {
716
+ type: "user",
717
+ session_id: sessionId,
718
+ message: { role: "user", content }
719
+ };
720
+ this.emitter.emit(this.getEventName(sessionId), syntheticUser);
721
+ }
697
722
  }
698
723
  /**
699
724
  * 发出写入失败的合成错误事件
@@ -927,9 +952,13 @@ ${context}`;
927
952
  throw new Error(`Session ${sessionId} stdin unavailable`);
928
953
  }
929
954
  const toolResult = JSON.stringify({
930
- type: "tool_result",
931
- tool_use_id: toolUseId,
932
- content: answer
955
+ type: "user",
956
+ session_id: "",
957
+ message: {
958
+ role: "user",
959
+ content: [{ type: "tool_result", tool_use_id: toolUseId, content: answer }]
960
+ },
961
+ parent_tool_use_id: toolUseId
933
962
  });
934
963
  await new Promise((resolve, reject) => {
935
964
  entry.process.stdin.write(toolResult + "\n", (err) => {
@@ -2940,6 +2969,8 @@ var ApprovalProxy = class _ApprovalProxy {
2940
2969
  this.handleApprovalHook(req, res);
2941
2970
  } else if (req.method === "POST" && pathname === "/hook/notify") {
2942
2971
  this.handleHookNotify(req, res);
2972
+ } else if (req.method === "POST" && pathname === "/api/resolve") {
2973
+ this.handleApiResolve(req, res);
2943
2974
  } else if (req.method === "POST" && pathname === "/pair") {
2944
2975
  this.handlePair(req, res);
2945
2976
  } else if (req.method === "GET" && pathname === "/health") {
@@ -3005,6 +3036,34 @@ var ApprovalProxy = class _ApprovalProxy {
3005
3036
  this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
3006
3037
  }
3007
3038
  }
3039
+ /**
3040
+ * 移动端 API 端点:Widget Extension / Watch 直接提交审批决策
3041
+ *
3042
+ * 绕过 WebSocket 链路,让 iOS Widget Extension 的 AppIntent 在 App 挂起时
3043
+ * 仍能直接将审批结果提交到服务端。使用 Bearer token 鉴权(与 WS 同一 token)。
3044
+ */
3045
+ async handleApiResolve(req, res) {
3046
+ const authHeader = req.headers.authorization ?? "";
3047
+ const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
3048
+ if (bearerToken !== this.token) {
3049
+ this.sendJson(res, 401, { ok: false, error: "Unauthorized" });
3050
+ return;
3051
+ }
3052
+ try {
3053
+ const body = await this.parseJsonBody(req);
3054
+ const requestId = String(body.requestId ?? "").trim();
3055
+ const decision = String(body.decision ?? "").trim();
3056
+ if (!requestId || decision !== "allow" && decision !== "deny") {
3057
+ this.sendJson(res, 400, { ok: false, error: "requestId and decision (allow|deny) required" });
3058
+ return;
3059
+ }
3060
+ const resolved = this.resolveApproval(requestId, { decision });
3061
+ this.sendJson(res, 200, { ok: resolved });
3062
+ } catch (err) {
3063
+ console.error("[ApprovalProxy] /api/resolve error:", err);
3064
+ this.sendJson(res, 500, { ok: false, error: "Internal error" });
3065
+ }
3066
+ }
3008
3067
  /**
3009
3068
  * 非阻塞 hook 通知端点
3010
3069
  *
@@ -3715,6 +3774,8 @@ var HookInstaller = class {
3715
3774
 
3716
3775
  // src/notification/NotificationService.ts
3717
3776
  var import_node_path5 = require("path");
3777
+ var RECENT_ACTIVITY_MAX = 6;
3778
+ var ACTIVITY_PUSH_THROTTLE_MS = 4e3;
3718
3779
  var NotificationService = class {
3719
3780
  constructor(sessionManager, expoChannel = null) {
3720
3781
  this.sessionManager = sessionManager;
@@ -3733,6 +3794,30 @@ var NotificationService = class {
3733
3794
  latestAssistantText = /* @__PURE__ */ new Map();
3734
3795
  /** 获取全局待审批总数的回调(跨所有会话) */
3735
3796
  globalPendingCountProvider = null;
3797
+ /** sessionId → 最近活动状态(用于 LA content push) */
3798
+ recentActivityState = /* @__PURE__ */ new Map();
3799
+ /** sessionId → 节流定时器(LA content push) */
3800
+ activityPushTimers = /* @__PURE__ */ new Map();
3801
+ /** 上次推送 LA content 的时间戳(用于节流;首次立即推送) */
3802
+ lastActivityPushAt = /* @__PURE__ */ new Map();
3803
+ /** 挂起的优先级提升请求(状态变化时设为 '10',flush 后清除) */
3804
+ pendingPriority = /* @__PURE__ */ new Map();
3805
+ /** sessionId → 累计活动计数器(用于 summary 模式的 activitySummary) */
3806
+ activityCounters = /* @__PURE__ */ new Map();
3807
+ /** 提供器:根据 sessionId 取该会话的待审批列表(由 server.ts 注入) */
3808
+ pendingApprovalsProvider = null;
3809
+ /**
3810
+ * sessionId → idle 结束定时器。
3811
+ * 会话变为 idle 时启动(30 秒);用户发新消息重回 running 时取消。
3812
+ * 30 秒内无新消息 → 调 endActivity 关闭 LA,同时发横幅通知告知完成。
3813
+ */
3814
+ idleEndTimers = /* @__PURE__ */ new Map();
3815
+ /**
3816
+ * sessionId → LA 心跳定时器(setInterval)。
3817
+ * 确保 Agent 子任务等长时间无 claude_event 的场景下 LA 仍持续更新。
3818
+ * token 注册时启动,flushActivityEnd / removeActivityPushToken 时停止。
3819
+ */
3820
+ laHeartbeatTimers = /* @__PURE__ */ new Map();
3736
3821
  /** 添加通知渠道(id 唯一,可用于后续动态开关) */
3737
3822
  addChannel(id, channel, enabled = true) {
3738
3823
  this.channelMap.set(id, { channel, enabled });
@@ -3760,11 +3845,27 @@ var NotificationService = class {
3760
3845
  }
3761
3846
  /** 注册 ActivityKit push token(由手机端启动 Live Activity 后上报) */
3762
3847
  addActivityPushToken(sessionId, token) {
3763
- this.activityPushChannel?.addToken(sessionId, token);
3848
+ if (!this.activityPushChannel) {
3849
+ 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`);
3850
+ return;
3851
+ }
3852
+ this.activityPushChannel.addToken(sessionId, token);
3853
+ console.log(`[NotificationService] \u2705 LA push token \u5DF2\u6CE8\u518C (session=${sessionId.slice(0, 8)}\u2026, token=${token.slice(0, 16)}\u2026)`);
3854
+ this.scheduleActivityPush(sessionId, true);
3855
+ this.startLaHeartbeat(sessionId);
3764
3856
  }
3765
3857
  /** 移除 ActivityKit push token */
3766
3858
  removeActivityPushToken(sessionId) {
3859
+ this.stopLaHeartbeat(sessionId);
3767
3860
  this.activityPushChannel?.removeToken(sessionId);
3861
+ this.clearActivityPushTimer(sessionId);
3862
+ this.recentActivityState.delete(sessionId);
3863
+ this.lastActivityPushAt.delete(sessionId);
3864
+ this.activityCounters.delete(sessionId);
3865
+ }
3866
+ /** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
3867
+ setPendingApprovalsProvider(fn) {
3868
+ this.pendingApprovalsProvider = fn;
3768
3869
  }
3769
3870
  /** 设置全局待审批总数提供者 */
3770
3871
  setGlobalPendingCountProvider(provider) {
@@ -3779,7 +3880,7 @@ var NotificationService = class {
3779
3880
  this.yoloModeState.set(sessionId, enabled);
3780
3881
  }
3781
3882
  /** 直接触发审批通知(由 ApprovalProxy 回调调用) */
3782
- notifyApproval(request, pendingCount) {
3883
+ async notifyApproval(request, pendingCount) {
3783
3884
  if (this.yoloModeState.get(request.sessionId)) return;
3784
3885
  const sessionTitle = this.getSessionTitle(request.sessionId);
3785
3886
  const title = pendingCount > 1 ? t("notification.pendingApprovals", { title: sessionTitle, count: pendingCount }) : sessionTitle;
@@ -3787,12 +3888,16 @@ var NotificationService = class {
3787
3888
  if (this.activityPushChannel?.hasToken(request.sessionId)) {
3788
3889
  const dangerLevel2 = this.getDangerLevel(request.toolName);
3789
3890
  const isYoloMode = this.getYoloMode(request.sessionId);
3790
- this.activityPushChannel.updateActivityWithAlert(
3891
+ const recentActivity = this.getRecentActivity(request.sessionId);
3892
+ const latestMessage = recentActivity[recentActivity.length - 1] ?? "";
3893
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
3894
+ const sent = await this.activityPushChannel.updateActivityWithAlert(
3791
3895
  request.sessionId,
3792
3896
  {
3793
3897
  status: "waitingApproval",
3794
3898
  sessionTitle,
3795
- latestMessage: "",
3899
+ latestMessage,
3900
+ recentActivity,
3796
3901
  approvalInfo: {
3797
3902
  requestId: request.id,
3798
3903
  toolName: request.toolName,
@@ -3801,11 +3906,22 @@ var NotificationService = class {
3801
3906
  pendingCount
3802
3907
  },
3803
3908
  isYoloMode,
3804
- updatedAt: Date.now()
3909
+ updatedAt: Date.now(),
3910
+ displayMode: "summary",
3911
+ activitySummary: this.buildActivitySummary(request.sessionId),
3912
+ startedAt: session?.createdAt,
3913
+ stats: this.buildStatsPayload(session)
3805
3914
  },
3806
3915
  { title, body }
3807
3916
  );
3808
- return;
3917
+ if (sent) {
3918
+ console.log(`[NotificationService] \u{1F4E1} approval via ActivityKit push session=${request.sessionId.slice(0, 8)}\u2026 tool=${request.toolName}`);
3919
+ this.lastActivityPushAt.set(request.sessionId, Date.now());
3920
+ return;
3921
+ }
3922
+ console.warn(`[NotificationService] \u26A0\uFE0F ActivityKit push \u5931\u8D25\uFF0C\u964D\u7EA7\u5230 Expo push session=${request.sessionId.slice(0, 8)}\u2026`);
3923
+ } else {
3924
+ console.log(`[NotificationService] \u{1F4F2} approval via Expo push session=${request.sessionId.slice(0, 8)}\u2026 tool=${request.toolName}`);
3809
3925
  }
3810
3926
  const dangerLevel = this.getDangerLevel(request.toolName);
3811
3927
  const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
@@ -3838,17 +3954,25 @@ var NotificationService = class {
3838
3954
  const body = `\u2753 ${request.question.slice(0, 80)}`;
3839
3955
  if (this.activityPushChannel?.hasToken(request.sessionId)) {
3840
3956
  const isYoloMode = this.getYoloMode(request.sessionId);
3957
+ const recentActivity = this.getRecentActivity(request.sessionId);
3958
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
3841
3959
  this.activityPushChannel.updateActivityWithAlert(
3842
3960
  request.sessionId,
3843
3961
  {
3844
- status: "waitingApproval",
3962
+ status: "waitingQuestion",
3845
3963
  sessionTitle,
3846
3964
  latestMessage: request.question.slice(0, 80),
3965
+ recentActivity,
3847
3966
  isYoloMode,
3848
- updatedAt: Date.now()
3967
+ updatedAt: Date.now(),
3968
+ displayMode: "summary",
3969
+ activitySummary: this.buildActivitySummary(request.sessionId),
3970
+ startedAt: session?.createdAt,
3971
+ stats: this.buildStatsPayload(session)
3849
3972
  },
3850
3973
  { title: sessionTitle, body }
3851
3974
  );
3975
+ this.lastActivityPushAt.set(request.sessionId, Date.now());
3852
3976
  return;
3853
3977
  }
3854
3978
  this.notify({
@@ -3882,6 +4006,12 @@ var NotificationService = class {
3882
4006
  this.unsubscribe = null;
3883
4007
  this.yoloModeState.clear();
3884
4008
  this.latestAssistantText.clear();
4009
+ for (const timer of this.activityPushTimers.values()) clearTimeout(timer);
4010
+ this.activityPushTimers.clear();
4011
+ this.recentActivityState.clear();
4012
+ this.lastActivityPushAt.clear();
4013
+ this.pendingPriority.clear();
4014
+ this.activityCounters.clear();
3885
4015
  }
3886
4016
  // ============================================
3887
4017
  // 内部方法
@@ -3890,29 +4020,29 @@ var NotificationService = class {
3890
4020
  switch (event.type) {
3891
4021
  case "claude_event": {
3892
4022
  this.trackAssistantText(event.sessionId, event.event);
4023
+ this.updateRecentActivity(event.sessionId, event.event);
4024
+ this.scheduleActivityPush(event.sessionId);
3893
4025
  break;
3894
4026
  }
3895
4027
  case "claude_events": {
3896
4028
  for (const e of event.events) {
3897
4029
  this.trackAssistantText(event.sessionId, e);
4030
+ this.updateRecentActivity(event.sessionId, e);
3898
4031
  }
4032
+ this.scheduleActivityPush(event.sessionId);
3899
4033
  break;
3900
4034
  }
3901
4035
  case "status_change": {
4036
+ this.clearActivityPushTimer(event.sessionId);
3902
4037
  if (event.status === "idle") {
3903
- const sessionTitle = this.getSessionTitle(event.sessionId);
3904
- const latestMsg = this.latestAssistantText.get(event.sessionId);
3905
- const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
3906
- const isYoloMode = this.getYoloMode(event.sessionId);
4038
+ this.cancelIdleEndTimer(event.sessionId);
3907
4039
  if (this.activityPushChannel?.hasToken(event.sessionId)) {
3908
- this.activityPushChannel.endActivity(event.sessionId, {
3909
- status: "idle",
3910
- sessionTitle,
3911
- latestMessage: body,
3912
- isYoloMode,
3913
- updatedAt: Date.now()
3914
- });
4040
+ this.scheduleActivityPush(event.sessionId, true, "10");
4041
+ this.scheduleIdleEnd(event.sessionId, 3e4);
3915
4042
  } else {
4043
+ const sessionTitle = this.getSessionTitle(event.sessionId);
4044
+ const latestMsg = this.latestAssistantText.get(event.sessionId);
4045
+ const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
3916
4046
  this.notify({
3917
4047
  title: sessionTitle,
3918
4048
  body,
@@ -3921,28 +4051,12 @@ var NotificationService = class {
3921
4051
  data: { type: "task_complete", sessionId: event.sessionId }
3922
4052
  });
3923
4053
  }
4054
+ } else if (event.status === "running" || event.status === "waiting_approval" || event.status === "waiting_question") {
4055
+ this.cancelIdleEndTimer(event.sessionId);
4056
+ this.scheduleActivityPush(event.sessionId, true, "10");
3924
4057
  } else if (event.status === "error") {
3925
- const sessionTitle = this.getSessionTitle(event.sessionId);
3926
- const latestMsg = this.latestAssistantText.get(event.sessionId);
3927
- const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : t("notification.taskError");
3928
- const isYoloMode = this.getYoloMode(event.sessionId);
3929
- if (this.activityPushChannel?.hasToken(event.sessionId)) {
3930
- this.activityPushChannel.endActivity(event.sessionId, {
3931
- status: "error",
3932
- sessionTitle,
3933
- latestMessage: body,
3934
- isYoloMode,
3935
- updatedAt: Date.now()
3936
- });
3937
- } else {
3938
- this.notify({
3939
- title: sessionTitle,
3940
- body,
3941
- sound: "default",
3942
- badge: this.getGlobalPendingCount(),
3943
- data: { type: "task_error", sessionId: event.sessionId }
3944
- });
3945
- }
4058
+ this.cancelIdleEndTimer(event.sessionId);
4059
+ this.flushActivityEnd(event.sessionId, "error");
3946
4060
  }
3947
4061
  break;
3948
4062
  }
@@ -3975,6 +4089,383 @@ var NotificationService = class {
3975
4089
  getYoloMode(sessionId) {
3976
4090
  return this.yoloModeState.get(sessionId) ?? false;
3977
4091
  }
4092
+ // ============================================
4093
+ // Live Activity 内容推送(后台 LA 实时刷新)
4094
+ // ============================================
4095
+ /**
4096
+ * 把一个 ClaudeStreamEvent 折算到 recentActivity 列表里。
4097
+ * 同一 message.id 内多次 assistant 事件视为流式更新,整段重建 currentEntries;
4098
+ * 切换 message.id 视为新 turn,旧条目沉淀到 history。
4099
+ */
4100
+ updateRecentActivity(sessionId, event) {
4101
+ if (event.type === "result") {
4102
+ const state2 = this.recentActivityState.get(sessionId);
4103
+ if (state2 && state2.currentEntries.length > 0) {
4104
+ state2.history.push(...state2.currentEntries);
4105
+ while (state2.history.length > RECENT_ACTIVITY_MAX) state2.history.shift();
4106
+ state2.currentEntries = [];
4107
+ state2.currentMessageId = null;
4108
+ }
4109
+ return;
4110
+ }
4111
+ if (event.type !== "assistant") return;
4112
+ const msg = event.message;
4113
+ if (!Array.isArray(msg.content)) return;
4114
+ let state = this.recentActivityState.get(sessionId);
4115
+ if (!state) {
4116
+ state = { history: [], currentMessageId: null, currentEntries: [] };
4117
+ this.recentActivityState.set(sessionId, state);
4118
+ }
4119
+ if (state.currentMessageId !== msg.id) {
4120
+ if (state.currentEntries.length > 0) {
4121
+ state.history.push(...state.currentEntries);
4122
+ while (state.history.length > RECENT_ACTIVITY_MAX) state.history.shift();
4123
+ }
4124
+ state.currentEntries = [];
4125
+ state.currentMessageId = msg.id;
4126
+ }
4127
+ const next = [];
4128
+ for (const block of msg.content) {
4129
+ if (block.type === "text") {
4130
+ const line = this.summarizeText(block.text);
4131
+ if (line.length >= 4) next.push(line);
4132
+ } else if (block.type === "tool_use") {
4133
+ const line = this.summarizeToolCall(block.name, block.input ?? {});
4134
+ if (line) next.push(line);
4135
+ this.incrementCounter(sessionId, block.name);
4136
+ }
4137
+ }
4138
+ state.currentEntries = next;
4139
+ }
4140
+ /** 取该会话当前的 recentActivity(history + currentEntries),保留末尾 N 条 */
4141
+ getRecentActivity(sessionId) {
4142
+ const state = this.recentActivityState.get(sessionId);
4143
+ if (!state) return [];
4144
+ const combined = [...state.history, ...state.currentEntries];
4145
+ return combined.slice(-RECENT_ACTIVITY_MAX);
4146
+ }
4147
+ /** 工具名 → 计数器类别映射 */
4148
+ incrementCounter(sessionId, toolName) {
4149
+ let c = this.activityCounters.get(sessionId);
4150
+ if (!c) {
4151
+ c = { filesEdited: 0, commandsRun: 0, searches: 0, filesRead: 0, messagesReceived: 0 };
4152
+ this.activityCounters.set(sessionId, c);
4153
+ }
4154
+ switch (toolName) {
4155
+ case "Edit":
4156
+ case "MultiEdit":
4157
+ case "Write":
4158
+ case "NotebookEdit":
4159
+ c.filesEdited++;
4160
+ break;
4161
+ case "Bash":
4162
+ c.commandsRun++;
4163
+ break;
4164
+ case "Grep":
4165
+ case "Glob":
4166
+ case "WebSearch":
4167
+ case "WebFetch":
4168
+ c.searches++;
4169
+ break;
4170
+ case "Read":
4171
+ c.filesRead++;
4172
+ break;
4173
+ default:
4174
+ break;
4175
+ }
4176
+ }
4177
+ /** 把累计计数器格式化为可读摘要(如"已编辑 3 个文件 · 运行 5 条命令") */
4178
+ buildActivitySummary(sessionId) {
4179
+ const c = this.activityCounters.get(sessionId);
4180
+ if (!c) return "";
4181
+ const parts = [];
4182
+ if (c.filesEdited > 0) parts.push(`\u5DF2\u7F16\u8F91 ${c.filesEdited} \u4E2A\u6587\u4EF6`);
4183
+ if (c.commandsRun > 0) parts.push(`\u8FD0\u884C ${c.commandsRun} \u6761\u547D\u4EE4`);
4184
+ if (c.searches > 0) parts.push(`\u641C\u7D22 ${c.searches} \u6B21`);
4185
+ if (c.filesRead > 0) parts.push(`\u9605\u8BFB ${c.filesRead} \u4E2A\u6587\u4EF6`);
4186
+ return parts.join(" \xB7 ") || "\u4F1A\u8BDD\u8FDB\u884C\u4E2D\u2026";
4187
+ }
4188
+ buildStatsPayload(session) {
4189
+ return {
4190
+ totalInputTokens: session?.stats?.totalInputTokens ?? 0,
4191
+ totalOutputTokens: session?.stats?.totalOutputTokens ?? 0,
4192
+ totalCostUsd: session?.stats?.totalCostUsd,
4193
+ totalDurationMs: session?.stats?.totalDurationMs,
4194
+ runningStartedAt: session?.stats?.runningStartedAt
4195
+ };
4196
+ }
4197
+ /** 节流调度 LA content push;首次立即推,后续合并到 throttle window 末尾 */
4198
+ scheduleActivityPush(sessionId, force = false, priority) {
4199
+ if (!this.activityPushChannel) return;
4200
+ if (!this.activityPushChannel.hasToken(sessionId)) {
4201
+ console.warn(`[NotificationService] \u26A0\uFE0F skip LA push: session=${sessionId.slice(0, 8)}\u2026 token \u672A\u6CE8\u518C`);
4202
+ return;
4203
+ }
4204
+ if (priority === "10") {
4205
+ this.pendingPriority.set(sessionId, "10");
4206
+ }
4207
+ const now = Date.now();
4208
+ const last = this.lastActivityPushAt.get(sessionId) ?? 0;
4209
+ const elapsed = now - last;
4210
+ if (force || elapsed >= ACTIVITY_PUSH_THROTTLE_MS) {
4211
+ this.clearActivityPushTimer(sessionId);
4212
+ this.flushActivityPush(sessionId);
4213
+ return;
4214
+ }
4215
+ if (this.activityPushTimers.has(sessionId)) return;
4216
+ const wait = ACTIVITY_PUSH_THROTTLE_MS - elapsed;
4217
+ this.activityPushTimers.set(
4218
+ sessionId,
4219
+ setTimeout(() => {
4220
+ this.activityPushTimers.delete(sessionId);
4221
+ this.flushActivityPush(sessionId);
4222
+ }, wait)
4223
+ );
4224
+ }
4225
+ clearActivityPushTimer(sessionId) {
4226
+ const timer = this.activityPushTimers.get(sessionId);
4227
+ if (timer) {
4228
+ clearTimeout(timer);
4229
+ this.activityPushTimers.delete(sessionId);
4230
+ }
4231
+ }
4232
+ /**
4233
+ * 启动 LA 心跳(每 ACTIVITY_PUSH_THROTTLE_MS 触发一次 scheduleActivityPush)。
4234
+ * 确保 Agent 子会话等长时间无 claude_event 的情况下 LA 仍持续更新。
4235
+ * 心跳只在会话活跃状态(running/waitingApproval/waitingQuestion)下发推送;
4236
+ * scheduleActivityPush 自带节流,心跳与正常事件驱动不冲突。
4237
+ */
4238
+ startLaHeartbeat(sessionId) {
4239
+ if (this.laHeartbeatTimers.has(sessionId)) return;
4240
+ const timer = setInterval(() => {
4241
+ if (!this.activityPushChannel?.hasToken(sessionId)) {
4242
+ this.stopLaHeartbeat(sessionId);
4243
+ return;
4244
+ }
4245
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
4246
+ if (!session) {
4247
+ this.stopLaHeartbeat(sessionId);
4248
+ return;
4249
+ }
4250
+ if (session.status === "running" || session.status === "waiting_approval" || session.status === "waiting_question") {
4251
+ this.scheduleActivityPush(sessionId);
4252
+ }
4253
+ }, ACTIVITY_PUSH_THROTTLE_MS);
4254
+ this.laHeartbeatTimers.set(sessionId, timer);
4255
+ }
4256
+ /** 停止 LA 心跳 */
4257
+ stopLaHeartbeat(sessionId) {
4258
+ const timer = this.laHeartbeatTimers.get(sessionId);
4259
+ if (timer) {
4260
+ clearInterval(timer);
4261
+ this.laHeartbeatTimers.delete(sessionId);
4262
+ }
4263
+ }
4264
+ /** 取消 idle 结束定时器 */
4265
+ cancelIdleEndTimer(sessionId) {
4266
+ const timer = this.idleEndTimers.get(sessionId);
4267
+ if (timer) {
4268
+ clearTimeout(timer);
4269
+ this.idleEndTimers.delete(sessionId);
4270
+ }
4271
+ }
4272
+ /**
4273
+ * 启动 idle 结束定时器:delayMs 后若会话仍 idle,调 endActivity + 发通知。
4274
+ * 保证多轮对话期间(idle→running→idle)LA 不会过早消失。
4275
+ */
4276
+ scheduleIdleEnd(sessionId, delayMs) {
4277
+ const timer = setTimeout(() => {
4278
+ this.idleEndTimers.delete(sessionId);
4279
+ this.flushActivityEnd(sessionId, "idle");
4280
+ }, delayMs);
4281
+ this.idleEndTimers.set(sessionId, timer);
4282
+ }
4283
+ /**
4284
+ * 结束 LA 并发完成通知。有 LA token → APNs event:end(带横幅);
4285
+ * 无 token → 普通 Expo push。清理所有相关状态。
4286
+ */
4287
+ flushActivityEnd(sessionId, reason) {
4288
+ const sessionTitle = this.getSessionTitle(sessionId);
4289
+ const latestMsg = this.latestAssistantText.get(sessionId);
4290
+ const isError = reason === "error";
4291
+ const body = isError ? `\u274C ${latestMsg?.slice(0, 80) ?? t("notification.taskError")}` : `\u2705 ${latestMsg?.slice(0, 80) ?? t("notification.taskComplete")}`;
4292
+ const isYoloMode = this.getYoloMode(sessionId);
4293
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
4294
+ this.notify({
4295
+ title: sessionTitle,
4296
+ body,
4297
+ sound: "default",
4298
+ badge: this.getGlobalPendingCount(),
4299
+ data: { type: isError ? "task_error" : "task_complete", sessionId }
4300
+ });
4301
+ if (this.activityPushChannel?.hasToken(sessionId)) {
4302
+ this.activityPushChannel.endActivity(
4303
+ sessionId,
4304
+ {
4305
+ status: isError ? "error" : "completed",
4306
+ sessionTitle,
4307
+ latestMessage: body,
4308
+ recentActivity: this.getRecentActivity(sessionId),
4309
+ isYoloMode,
4310
+ updatedAt: Date.now(),
4311
+ displayMode: "summary",
4312
+ activitySummary: this.buildActivitySummary(sessionId),
4313
+ startedAt: session?.createdAt,
4314
+ stats: this.buildStatsPayload(session)
4315
+ }
4316
+ // 不传 alert——Expo push 已处理通知,event:end 仅用于关闭 LA
4317
+ ).catch((err) => {
4318
+ console.warn("[NotificationService] endActivity (close LA) failed, LA may linger:", err);
4319
+ });
4320
+ }
4321
+ this.stopLaHeartbeat(sessionId);
4322
+ this.recentActivityState.delete(sessionId);
4323
+ this.lastActivityPushAt.delete(sessionId);
4324
+ this.activityCounters.delete(sessionId);
4325
+ console.log(`[NotificationService] \u{1F3C1} LA end (${reason}) session=${sessionId.slice(0, 8)}\u2026`);
4326
+ }
4327
+ /** 真正发送一次 LA content push(无 alert) */
4328
+ flushActivityPush(sessionId) {
4329
+ const channel = this.activityPushChannel;
4330
+ if (!channel?.hasToken(sessionId)) return;
4331
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
4332
+ if (!session) return;
4333
+ const recentActivity = this.getRecentActivity(sessionId);
4334
+ const latestMessage = recentActivity[recentActivity.length - 1] ?? this.latestAssistantText.get(sessionId) ?? "";
4335
+ const sessionTitle = this.getSessionTitle(sessionId);
4336
+ const isYoloMode = this.getYoloMode(sessionId);
4337
+ const pendingApprovals = this.pendingApprovalsProvider?.(sessionId) ?? [];
4338
+ const latestApproval = pendingApprovals[pendingApprovals.length - 1];
4339
+ const status = latestApproval ? "waitingApproval" : this.mapSessionStatus(session.status);
4340
+ const contentState = {
4341
+ status,
4342
+ sessionTitle,
4343
+ latestMessage,
4344
+ recentActivity,
4345
+ isYoloMode,
4346
+ updatedAt: Date.now(),
4347
+ displayMode: "summary",
4348
+ activitySummary: this.buildActivitySummary(sessionId),
4349
+ startedAt: session.createdAt
4350
+ };
4351
+ if (latestApproval) {
4352
+ contentState.approvalInfo = {
4353
+ requestId: latestApproval.id,
4354
+ toolName: latestApproval.toolName,
4355
+ description: String(latestApproval.description ?? "").slice(0, 80),
4356
+ dangerLevel: this.getDangerLevel(latestApproval.toolName),
4357
+ pendingCount: pendingApprovals.length
4358
+ };
4359
+ }
4360
+ contentState.stats = this.buildStatsPayload(session);
4361
+ const priority = this.pendingPriority.get(sessionId) ?? "5";
4362
+ this.pendingPriority.delete(sessionId);
4363
+ this.lastActivityPushAt.set(sessionId, Date.now());
4364
+ const lineCount = recentActivity.length;
4365
+ channel.updateActivity(sessionId, contentState, { priority }).then((ok) => {
4366
+ if (ok) {
4367
+ console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} p=${priority} lines=${lineCount}`);
4368
+ }
4369
+ }).catch((err) => {
4370
+ console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
4371
+ });
4372
+ }
4373
+ /** SessionStatus → LiveActivity status 字符串映射(与客户端 mapStatus 一致) */
4374
+ mapSessionStatus(status) {
4375
+ switch (status) {
4376
+ case "running":
4377
+ return "running";
4378
+ case "waiting_approval":
4379
+ return "waitingApproval";
4380
+ case "waiting_question":
4381
+ return "waitingQuestion";
4382
+ case "idle":
4383
+ return "completed";
4384
+ case "completed":
4385
+ return "completed";
4386
+ case "error":
4387
+ return "error";
4388
+ default:
4389
+ return "idle";
4390
+ }
4391
+ }
4392
+ /** 文本块清洗:去多余空白 + 截断到 70 字符 */
4393
+ summarizeText(raw) {
4394
+ if (typeof raw !== "string") return "";
4395
+ const cleaned = raw.replace(/\s+/g, " ").trim();
4396
+ return cleaned.length > 70 ? cleaned.slice(0, 70) + "\u2026" : cleaned;
4397
+ }
4398
+ /** 工具调用摘要(与客户端 summarizeToolCall 行为对齐,简化版只输出中文) */
4399
+ summarizeToolCall(name, input) {
4400
+ const str = (v) => typeof v === "string" ? v : "";
4401
+ const baseName = (p) => {
4402
+ const cleaned = p.split(/[?#]/)[0];
4403
+ const parts = cleaned.split("/");
4404
+ return parts[parts.length - 1] || cleaned;
4405
+ };
4406
+ const trunc = (s, n) => s.length > n ? s.slice(0, n) + "\u2026" : s;
4407
+ switch (name) {
4408
+ case "Bash": {
4409
+ const cmd = str(input.command).split("\n")[0];
4410
+ return cmd ? `\u8FD0\u884C: ${trunc(cmd, 60)}` : "\u6267\u884C\u547D\u4EE4";
4411
+ }
4412
+ case "Edit": {
4413
+ const fp = baseName(str(input.file_path));
4414
+ return fp ? `\u7F16\u8F91 ${fp}` : "\u7F16\u8F91\u6587\u4EF6";
4415
+ }
4416
+ case "MultiEdit": {
4417
+ const fp = baseName(str(input.file_path));
4418
+ return fp ? `\u6279\u91CF\u7F16\u8F91 ${fp}` : "\u6279\u91CF\u7F16\u8F91\u6587\u4EF6";
4419
+ }
4420
+ case "Write": {
4421
+ const fp = baseName(str(input.file_path));
4422
+ return fp ? `\u5199\u5165 ${fp}` : "\u5199\u5165\u6587\u4EF6";
4423
+ }
4424
+ case "Read":
4425
+ case "NotebookEdit": {
4426
+ const fp = baseName(str(input.file_path) || str(input.notebook_path));
4427
+ return fp ? `\u9605\u8BFB ${fp}` : "\u9605\u8BFB\u6587\u4EF6";
4428
+ }
4429
+ case "Grep": {
4430
+ const p = str(input.pattern);
4431
+ return p ? `\u641C\u7D22: ${trunc(p, 50)}` : "\u641C\u7D22\u4EE3\u7801";
4432
+ }
4433
+ case "Glob": {
4434
+ const p = str(input.pattern);
4435
+ return p ? `\u67E5\u627E: ${trunc(p, 50)}` : "\u67E5\u627E\u6587\u4EF6";
4436
+ }
4437
+ case "WebFetch": {
4438
+ const url = str(input.url);
4439
+ let host = url;
4440
+ try {
4441
+ host = new URL(url).hostname;
4442
+ } catch {
4443
+ }
4444
+ return host ? `\u8BF7\u6C42 ${trunc(host, 50)}` : "\u8BF7\u6C42\u7F51\u9875";
4445
+ }
4446
+ case "WebSearch": {
4447
+ const q = str(input.query);
4448
+ return q ? `\u641C\u7D22\u7F51\u9875: ${trunc(q, 50)}` : "\u641C\u7D22\u7F51\u9875";
4449
+ }
4450
+ case "TodoWrite":
4451
+ return "\u66F4\u65B0\u4EFB\u52A1\u6E05\u5355";
4452
+ case "Task":
4453
+ case "Agent": {
4454
+ const desc = str(input.description) || str(input.subagent_type);
4455
+ return desc ? `\u6D3E\u53D1\u4EFB\u52A1: ${trunc(desc, 50)}` : "\u6D3E\u53D1\u5B50\u4EFB\u52A1";
4456
+ }
4457
+ case "ExitPlanMode":
4458
+ return "\u63D0\u4EA4\u8BA1\u5212";
4459
+ case "Skill": {
4460
+ const skill = str(input.skill);
4461
+ return skill ? `\u8C03\u7528\u6280\u80FD: ${trunc(skill, 40)}` : "\u8C03\u7528\u6280\u80FD";
4462
+ }
4463
+ default: {
4464
+ const summary = trunc(JSON.stringify(input), 50);
4465
+ return name ? `${name}: ${summary}` : summary;
4466
+ }
4467
+ }
4468
+ }
3978
4469
  };
3979
4470
 
3980
4471
  // src/notification/DesktopNotificationChannel.ts
@@ -4031,12 +4522,13 @@ var ExpoNotificationChannel = class {
4031
4522
  }
4032
4523
  async send(payload) {
4033
4524
  if (this.tokens.size === 0) return;
4034
- const offlineTokens = Array.from(this.tokens).filter((token) => {
4525
+ const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
4526
+ const targetTokens = isCompletionNotif ? Array.from(this.tokens) : Array.from(this.tokens).filter((token) => {
4035
4527
  const ws = this.tokenWsMap.get(token);
4036
4528
  return !ws || ws.readyState !== ws.OPEN;
4037
4529
  });
4038
- if (offlineTokens.length === 0) return;
4039
- const messages = offlineTokens.map((to) => {
4530
+ if (targetTokens.length === 0) return;
4531
+ const messages = targetTokens.map((to) => {
4040
4532
  let sound = payload.sound ?? "default";
4041
4533
  const prefs = this.soundPreferences.get(to);
4042
4534
  if (prefs) {
@@ -4058,7 +4550,7 @@ var ExpoNotificationChannel = class {
4058
4550
  };
4059
4551
  });
4060
4552
  try {
4061
- console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${offlineTokens.length}/${this.tokens.size} devices)`, offlineTokens);
4553
+ console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${targetTokens.length}/${this.tokens.size} devices${isCompletionNotif ? ", forced" : ""})`, targetTokens);
4062
4554
  const res = await fetch(EXPO_PUSH_API, {
4063
4555
  method: "POST",
4064
4556
  headers: { "Content-Type": "application/json", Accept: "application/json" },
@@ -4088,100 +4580,151 @@ var ExpoNotificationChannel = class {
4088
4580
  var http2 = __toESM(require("http2"));
4089
4581
  var fs2 = __toESM(require("fs"));
4090
4582
  var crypto = __toESM(require("crypto"));
4583
+ var APNS_HOSTS = {
4584
+ production: "api.push.apple.com",
4585
+ sandbox: "api.sandbox.push.apple.com"
4586
+ };
4091
4587
  var ActivityPushChannel = class {
4092
4588
  /** sessionId -> activityPushToken */
4093
4589
  tokens = /* @__PURE__ */ new Map();
4590
+ /**
4591
+ * 每个 token 已确认工作的 APNs 环境。
4592
+ * Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
4593
+ * Release build (aps-environment=production) 的 token 仅在 production 端有效。
4594
+ * 同时维护两个连接 + 探测机制,避免环境配错时静默失败。
4595
+ */
4596
+ tokenEnv = /* @__PURE__ */ new Map();
4597
+ /**
4598
+ * 两个环境都拒绝的 token(Live Activity 已销毁/过期)。
4599
+ * 标记后跳过所有发送尝试,避免无意义的 HTTP/2 请求。
4600
+ * 当同一 session 注册新 token 时自动清除旧 token 的标记。
4601
+ */
4602
+ deadTokens = /* @__PURE__ */ new Set();
4603
+ /** 首次探测顺序(来自配置 hint),未配置则先试 sandbox(开发场景占多数) */
4604
+ probeOrder;
4094
4605
  teamId;
4095
4606
  keyId;
4096
4607
  authKey;
4097
- apnsHost;
4098
4608
  /** 缓存的 JWT token + 过期时间 */
4099
4609
  cachedJwt = null;
4100
- /** 复用的 HTTP/2 长连接 */
4101
- http2Client = null;
4610
+ /** 每个环境一条 HTTP/2 长连接 */
4611
+ http2Clients = {};
4102
4612
  constructor(config) {
4103
4613
  this.teamId = config.teamId;
4104
4614
  this.keyId = config.keyId;
4105
4615
  this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
4106
- this.apnsHost = config.sandbox ? "api.sandbox.push.apple.com" : "api.push.apple.com";
4107
- console.log(`[ActivityPushChannel] Initialized (${config.sandbox ? "sandbox" : "production"} mode)`);
4108
- }
4109
- /** 获取或新建 HTTP/2 长连接 */
4110
- getHttp2Client() {
4111
- if (this.http2Client && !this.http2Client.destroyed && !this.http2Client.closed) {
4112
- return this.http2Client;
4113
- }
4114
- this.http2Client = http2.connect(`https://${this.apnsHost}`);
4115
- this.http2Client.on("error", (err) => {
4116
- console.warn("[ActivityPushChannel] HTTP/2 connection error, will reconnect on next request:", err.message);
4117
- this.http2Client?.destroy();
4118
- this.http2Client = null;
4616
+ this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
4617
+ console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
4618
+ }
4619
+ /** 获取或新建指定环境的 HTTP/2 长连接 */
4620
+ getHttp2Client(env) {
4621
+ const existing = this.http2Clients[env];
4622
+ if (existing && !existing.destroyed && !existing.closed) {
4623
+ return existing;
4624
+ }
4625
+ const client = http2.connect(`https://${APNS_HOSTS[env]}`);
4626
+ client.on("error", (err) => {
4627
+ console.warn(`[ActivityPushChannel] HTTP/2 (${env}) error, will reconnect on next request:`, err.message);
4628
+ client.destroy();
4629
+ if (this.http2Clients[env] === client) delete this.http2Clients[env];
4119
4630
  });
4120
- this.http2Client.on("close", () => {
4121
- this.http2Client = null;
4631
+ client.on("close", () => {
4632
+ if (this.http2Clients[env] === client) delete this.http2Clients[env];
4122
4633
  });
4123
- return this.http2Client;
4634
+ this.http2Clients[env] = client;
4635
+ return client;
4124
4636
  }
4125
4637
  /** 注册 Activity push token */
4126
4638
  addToken(sessionId, token) {
4639
+ const oldToken = this.tokens.get(sessionId);
4640
+ if (oldToken && oldToken !== token) {
4641
+ this.tokenEnv.delete(oldToken);
4642
+ this.deadTokens.delete(oldToken);
4643
+ }
4644
+ const existed = this.tokens.has(sessionId);
4127
4645
  this.tokens.set(sessionId, token);
4128
- console.log(`[ActivityPushChannel] Token registered: session=${sessionId}`);
4646
+ console.log(`[ActivityPushChannel] Token ${existed ? "updated" : "registered"}: session=${sessionId.slice(0, 8)}\u2026 token=${token.slice(0, 16)}\u2026`);
4129
4647
  }
4130
4648
  /** 移除 Activity push token */
4131
4649
  removeToken(sessionId) {
4650
+ const tok = this.tokens.get(sessionId);
4132
4651
  this.tokens.delete(sessionId);
4652
+ if (tok) {
4653
+ this.tokenEnv.delete(tok);
4654
+ this.deadTokens.delete(tok);
4655
+ }
4133
4656
  }
4134
- /** 发送 content-state 更新到指定会话的 Live Activity */
4135
- async updateActivity(sessionId, contentState) {
4657
+ /** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知)。返回 true 表示实际发出 */
4658
+ async updateActivity(sessionId, contentState, opts) {
4136
4659
  const token = this.tokens.get(sessionId);
4137
- if (!token) return;
4660
+ if (!token) return false;
4661
+ const now = Math.floor(Date.now() / 1e3);
4662
+ const priority = opts?.priority ?? "5";
4138
4663
  const payload = {
4139
4664
  aps: {
4140
- timestamp: Math.floor(Date.now() / 1e3),
4665
+ timestamp: now,
4141
4666
  event: "update",
4142
- "content-state": contentState
4667
+ "content-state": contentState,
4668
+ "stale-date": now + 600
4143
4669
  }
4144
4670
  };
4145
4671
  try {
4146
- await this.sendToAPNs(token, payload);
4672
+ await this.sendToAPNs(token, payload, {
4673
+ priority,
4674
+ collapseId: `state-${sessionId.slice(0, 54)}`
4675
+ });
4676
+ return true;
4147
4677
  } catch (err) {
4148
4678
  console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
4679
+ return false;
4149
4680
  }
4150
4681
  }
4151
- /** 发送带通知的 content-state 更新(审批请求时使用) */
4682
+ /** 发送带通知的 content-state 更新(审批请求时使用),返回是否发送成功 */
4152
4683
  async updateActivityWithAlert(sessionId, contentState, alert) {
4153
4684
  const token = this.tokens.get(sessionId);
4154
- if (!token) return;
4685
+ if (!token) return false;
4686
+ const now = Math.floor(Date.now() / 1e3);
4155
4687
  const payload = {
4156
4688
  aps: {
4157
- timestamp: Math.floor(Date.now() / 1e3),
4689
+ timestamp: now,
4158
4690
  event: "update",
4159
4691
  "content-state": contentState,
4692
+ "stale-date": now + 600,
4160
4693
  alert,
4161
4694
  sound: "default"
4162
4695
  }
4163
4696
  };
4164
4697
  try {
4165
- await this.sendToAPNs(token, payload);
4698
+ await this.sendToAPNs(token, payload, { priority: "10" });
4699
+ return true;
4166
4700
  } catch (err) {
4167
4701
  console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
4702
+ return false;
4168
4703
  }
4169
4704
  }
4170
- /** 结束指定会话的 Live Activity */
4171
- async endActivity(sessionId, contentState) {
4705
+ /**
4706
+ * 结束指定会话的 Live Activity。
4707
+ * 可选 alert:APNs event=end 时同时推送横幅通知 + 声音,用于在会话完成时提醒用户。
4708
+ */
4709
+ async endActivity(sessionId, contentState, opts) {
4172
4710
  const token = this.tokens.get(sessionId);
4173
4711
  if (!token) return;
4174
- const payload = {
4175
- aps: {
4176
- timestamp: Math.floor(Date.now() / 1e3),
4177
- event: "end",
4178
- "content-state": contentState
4179
- }
4712
+ const now = Math.floor(Date.now() / 1e3);
4713
+ const aps = {
4714
+ timestamp: now,
4715
+ event: "end",
4716
+ "content-state": contentState
4180
4717
  };
4718
+ if (opts?.alert) {
4719
+ aps.alert = opts.alert;
4720
+ aps.sound = "default";
4721
+ }
4722
+ const payload = { aps };
4181
4723
  try {
4182
- await this.sendToAPNs(token, payload);
4724
+ await this.sendToAPNs(token, payload, { priority: "10" });
4183
4725
  } catch (err) {
4184
- console.warn(`[ActivityPushChannel] End failed session=${sessionId}:`, err);
4726
+ this.tokens.delete(sessionId);
4727
+ throw err;
4185
4728
  }
4186
4729
  this.tokens.delete(sessionId);
4187
4730
  }
@@ -4189,33 +4732,81 @@ var ActivityPushChannel = class {
4189
4732
  hasToken(sessionId) {
4190
4733
  return this.tokens.has(sessionId);
4191
4734
  }
4192
- /** 发送 APNs HTTP/2 请求 */
4193
- async sendToAPNs(deviceToken, payload) {
4735
+ /**
4736
+ * 发送 APNs,自动处理环境探测。
4737
+ * 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
4738
+ * 收到 BadDeviceToken / BadEnvironmentKeyInToken 自动切到另一个环境,
4739
+ * 并把成功的环境绑定到该 token。
4740
+ */
4741
+ async sendToAPNs(deviceToken, payload, opts = {}) {
4742
+ if (this.deadTokens.has(deviceToken)) {
4743
+ throw new Error(`token permanently dead (Activity ended): ${deviceToken.slice(0, 16)}\u2026`);
4744
+ }
4745
+ const known = this.tokenEnv.get(deviceToken);
4746
+ if (known) {
4747
+ return this.sendToAPNsOnce(deviceToken, payload, opts, known);
4748
+ }
4749
+ const short = deviceToken.slice(0, 16);
4750
+ console.log(`[ActivityPushChannel] \u{1F50D} probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
4751
+ let lastErr = null;
4752
+ for (const env of this.probeOrder) {
4753
+ try {
4754
+ console.log(`[ActivityPushChannel] \u{1F50D} probe try ${env} token=${short}\u2026`);
4755
+ await this.sendToAPNsOnce(deviceToken, payload, opts, env);
4756
+ this.tokenEnv.set(deviceToken, env);
4757
+ console.log(`[ActivityPushChannel] \u2705 probe bound to ${env} (token=${short}\u2026)`);
4758
+ return;
4759
+ } catch (err) {
4760
+ lastErr = err;
4761
+ const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
4762
+ console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
4763
+ if (!isBadDeviceTokenError(err)) {
4764
+ throw err;
4765
+ }
4766
+ }
4767
+ }
4768
+ this.deadTokens.add(deviceToken);
4769
+ for (const [sid, tok] of this.tokens) {
4770
+ if (tok === deviceToken) {
4771
+ this.tokens.delete(sid);
4772
+ console.warn(`[ActivityPushChannel] \u2620\uFE0F dead token, session removed: session=${sid.slice(0, 8)}\u2026 token=${short}\u2026 (Activity ended/iOS reinstalled)`);
4773
+ break;
4774
+ }
4775
+ }
4776
+ throw lastErr ?? new Error("APNs send failed: all environments rejected token");
4777
+ }
4778
+ /** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
4779
+ async sendToAPNsOnce(deviceToken, payload, opts, env) {
4194
4780
  const topic = "com.kachun.sessix.push-type.liveactivity";
4195
4781
  const jwt = this.getJWT();
4196
4782
  const payloadStr = JSON.stringify(payload);
4783
+ const priority = opts.priority ?? "10";
4197
4784
  return new Promise((resolve, reject) => {
4198
4785
  let client;
4199
4786
  try {
4200
- client = this.getHttp2Client();
4787
+ client = this.getHttp2Client(env);
4201
4788
  } catch (err) {
4202
4789
  return reject(err);
4203
4790
  }
4204
- const req = client.request({
4791
+ const headers = {
4205
4792
  ":method": "POST",
4206
4793
  ":path": `/3/device/${deviceToken}`,
4207
4794
  "authorization": `bearer ${jwt}`,
4208
4795
  "apns-topic": topic,
4209
4796
  "apns-push-type": "liveactivity",
4210
- "apns-priority": "10",
4211
- "apns-expiration": String(Math.floor(Date.now() / 1e3) + 30),
4797
+ "apns-priority": priority,
4798
+ "apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
4212
4799
  "content-type": "application/json",
4213
4800
  "content-length": Buffer.byteLength(payloadStr)
4214
- });
4801
+ };
4802
+ if (opts.collapseId) {
4803
+ headers["apns-collapse-id"] = opts.collapseId;
4804
+ }
4805
+ const req = client.request(headers);
4215
4806
  let statusCode = 0;
4216
4807
  let responseData = "";
4217
- req.on("response", (headers) => {
4218
- statusCode = Number(headers[":status"] ?? 0);
4808
+ req.on("response", (headers2) => {
4809
+ statusCode = Number(headers2[":status"] ?? 0);
4219
4810
  });
4220
4811
  req.on("data", (chunk) => {
4221
4812
  responseData += chunk;
@@ -4225,10 +4816,11 @@ var ActivityPushChannel = class {
4225
4816
  resolve();
4226
4817
  } else {
4227
4818
  if (statusCode === 0) {
4228
- this.http2Client?.destroy();
4229
- this.http2Client = null;
4819
+ const c = this.http2Clients[env];
4820
+ c?.destroy();
4821
+ delete this.http2Clients[env];
4230
4822
  }
4231
- reject(new Error(`APNs returned ${statusCode}: ${responseData}`));
4823
+ reject(new ApnsError(statusCode, responseData));
4232
4824
  }
4233
4825
  });
4234
4826
  req.on("error", (err) => {
@@ -4261,6 +4853,24 @@ var ActivityPushChannel = class {
4261
4853
  return token;
4262
4854
  }
4263
4855
  };
4856
+ var ApnsError = class extends Error {
4857
+ constructor(statusCode, responseBody) {
4858
+ super(`APNs returned ${statusCode}: ${responseBody}`);
4859
+ this.statusCode = statusCode;
4860
+ this.responseBody = responseBody;
4861
+ this.name = "ApnsError";
4862
+ }
4863
+ };
4864
+ function isBadDeviceTokenError(err) {
4865
+ if (!(err instanceof ApnsError)) return false;
4866
+ if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
4867
+ try {
4868
+ const parsed = JSON.parse(err.responseBody);
4869
+ return parsed.reason === "BadDeviceToken" || parsed.reason === "BadEnvironmentKeyInToken" || parsed.reason === "Unregistered";
4870
+ } catch {
4871
+ return false;
4872
+ }
4873
+ }
4264
4874
 
4265
4875
  // src/session/ProjectReader.ts
4266
4876
  var import_promises3 = require("fs/promises");
@@ -4660,6 +5270,45 @@ var PairingManager = class {
4660
5270
  }
4661
5271
  };
4662
5272
 
5273
+ // src/utils/shellPath.ts
5274
+ var import_node_child_process7 = require("child_process");
5275
+ var fixed = false;
5276
+ function fixShellPath() {
5277
+ if (fixed || isWindows) {
5278
+ fixed = true;
5279
+ return;
5280
+ }
5281
+ fixed = true;
5282
+ const shell = process.env.SHELL || "/bin/zsh";
5283
+ const isFish = /\/fish$/.test(shell);
5284
+ const printPathCmd = isFish ? "string join : $PATH" : 'printf "%s" "$PATH"';
5285
+ let raw;
5286
+ try {
5287
+ raw = (0, import_node_child_process7.execFileSync)(shell, ["-l", "-c", printPathCmd], {
5288
+ encoding: "utf8",
5289
+ timeout: 3e3,
5290
+ stdio: ["ignore", "pipe", "ignore"]
5291
+ });
5292
+ } catch (err) {
5293
+ console.warn("[fixShellPath] failed to read login shell PATH:", err);
5294
+ return;
5295
+ }
5296
+ const fromShell = raw.trim();
5297
+ if (!fromShell) return;
5298
+ process.env.PATH = mergePath(fromShell, process.env.PATH || "");
5299
+ }
5300
+ function mergePath(primary, secondary) {
5301
+ const seen = /* @__PURE__ */ new Set();
5302
+ const out = [];
5303
+ for (const seg of primary.split(":").concat(secondary.split(":"))) {
5304
+ if (!seg) continue;
5305
+ if (seen.has(seg)) continue;
5306
+ seen.add(seg);
5307
+ out.push(seg);
5308
+ }
5309
+ return out.join(":");
5310
+ }
5311
+
4663
5312
  // src/auth/AuthManager.ts
4664
5313
  var import_child_process3 = require("child_process");
4665
5314
  var import_child_process4 = require("child_process");
@@ -4778,13 +5427,10 @@ var AuthManager = class extends import_events3.EventEmitter {
4778
5427
  }
4779
5428
  };
4780
5429
 
4781
- // src/server.ts
4782
- var import_promises8 = require("fs/promises");
4783
-
4784
5430
  // src/terminal/TerminalExecutor.ts
4785
- var import_node_child_process7 = require("child_process");
5431
+ var import_node_child_process8 = require("child_process");
4786
5432
  var import_uuid5 = require("uuid");
4787
- var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
5433
+ var EXEC_TIMEOUT_MS = 30 * 60 * 1e3;
4788
5434
  var TerminalExecutor = class {
4789
5435
  processes = /* @__PURE__ */ new Map();
4790
5436
  eventCallbacks = [];
@@ -4806,9 +5452,9 @@ var TerminalExecutor = class {
4806
5452
  }
4807
5453
  exec(sessionId, command, cwd) {
4808
5454
  const execId = (0, import_uuid5.v4)();
4809
- const shell = isWindows ? "powershell" : "bash";
4810
- const args = isWindows ? ["-Command", command] : ["-c", command];
4811
- const proc = (0, import_node_child_process7.spawn)(shell, args, {
5455
+ const shell = isWindows ? "powershell" : process.env.SHELL || "/bin/zsh";
5456
+ const args = isWindows ? ["-Command", command] : ["-l", "-c", command];
5457
+ const proc = (0, import_node_child_process8.spawn)(shell, args, {
4812
5458
  cwd,
4813
5459
  stdio: ["ignore", "pipe", "pipe"],
4814
5460
  env: { ...process.env }
@@ -4845,6 +5491,14 @@ var TerminalExecutor = class {
4845
5491
  });
4846
5492
  const timer = setTimeout(() => {
4847
5493
  if (this.processes.has(execId)) {
5494
+ this.emit({
5495
+ type: "terminal_output",
5496
+ sessionId,
5497
+ execId,
5498
+ stream: "stderr",
5499
+ data: `[killed: timeout ${Math.round(EXEC_TIMEOUT_MS / 6e4)}m]
5500
+ `
5501
+ });
4848
5502
  killProcessCrossPlatform(proc);
4849
5503
  }
4850
5504
  }, EXEC_TIMEOUT_MS);
@@ -4869,13 +5523,13 @@ var TerminalExecutor = class {
4869
5523
  };
4870
5524
 
4871
5525
  // src/xcode/XcodeBuildExecutor.ts
4872
- var import_node_child_process8 = require("child_process");
5526
+ var import_node_child_process9 = require("child_process");
4873
5527
  var import_node_util = require("util");
4874
5528
  var import_promises4 = require("fs/promises");
4875
5529
  var import_node_path6 = require("path");
4876
5530
  var import_node_os7 = require("os");
4877
5531
  var import_uuid6 = require("uuid");
4878
- var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
5532
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
4879
5533
  var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
4880
5534
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
4881
5535
  var CONFIG_FILE = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "xcode-config.json");
@@ -5064,7 +5718,7 @@ ${e.stderr ?? ""}`);
5064
5718
  if (override) await this.saveConfig(projectPath, override);
5065
5719
  const buildId = (0, import_uuid6.v4)();
5066
5720
  const args = buildArgs(config);
5067
- const proc = (0, import_node_child_process8.spawn)("xcodebuild", args, {
5721
+ const proc = (0, import_node_child_process9.spawn)("xcodebuild", args, {
5068
5722
  cwd: projectPath,
5069
5723
  stdio: ["ignore", "pipe", "pipe"],
5070
5724
  env: { ...process.env, NSUnbufferedIO: "YES" }
@@ -5160,7 +5814,7 @@ ${e.stderr ?? ""}`);
5160
5814
 
5161
5815
  `
5162
5816
  });
5163
- const proc = (0, import_node_child_process8.spawn)(installCmd[0], installCmd.slice(1), {
5817
+ const proc = (0, import_node_child_process9.spawn)(installCmd[0], installCmd.slice(1), {
5164
5818
  cwd: projectPath,
5165
5819
  stdio: ["ignore", "pipe", "pipe"]
5166
5820
  });
@@ -5567,15 +6221,16 @@ var CommandDiscovery = class {
5567
6221
  const cmd = sanitizeBashLine(rawLine);
5568
6222
  if (!cmd) continue;
5569
6223
  const { command: cleanCmd, inlineComment } = splitInlineComment(cmd);
5570
- const title = synthesizeTitle(cleanCmd);
6224
+ const { cwd: cdCwd, command: finalCmd } = splitCdPrefix(cleanCmd);
6225
+ const title = synthesizeTitle(finalCmd);
5571
6226
  out.push(makeCommand({
5572
6227
  title,
5573
- command: cleanCmd,
5574
- cwd: "",
6228
+ command: finalCmd,
6229
+ cwd: cdCwd,
5575
6230
  source,
5576
6231
  sourceFile: fileName,
5577
6232
  description: inlineComment ?? blockHeading,
5578
- category: classifyByCommand(cleanCmd)
6233
+ category: classifyByCommand(finalCmd)
5579
6234
  }));
5580
6235
  }
5581
6236
  }
@@ -5620,6 +6275,19 @@ function synthesizeTitle(cmd) {
5620
6275
  const head = tokens.slice(0, 3).join(" ");
5621
6276
  return head.length > 60 ? head.slice(0, 60) + "\u2026" : head;
5622
6277
  }
6278
+ function splitCdPrefix(cmd) {
6279
+ const m = /^cd\s+(\S+)\s*&&\s*(.+)$/.exec(cmd);
6280
+ if (!m) return { cwd: "", command: cmd };
6281
+ const path2 = m[1];
6282
+ if (!path2) return { cwd: "", command: cmd };
6283
+ if (path2.startsWith("/") || path2.startsWith("~") || path2.startsWith("-")) {
6284
+ return { cwd: "", command: cmd };
6285
+ }
6286
+ if (path2.split("/").some((seg) => seg === "..")) {
6287
+ return { cwd: "", command: cmd };
6288
+ }
6289
+ return { cwd: path2, command: m[2].trim() };
6290
+ }
5623
6291
  function splitInlineComment(line) {
5624
6292
  let inSingle = false;
5625
6293
  let inDouble = false;
@@ -5681,10 +6349,10 @@ function sourceWeight(s) {
5681
6349
  }
5682
6350
 
5683
6351
  // src/git/GitExecutor.ts
5684
- var import_node_child_process9 = require("child_process");
6352
+ var import_node_child_process10 = require("child_process");
5685
6353
  var import_node_util2 = require("util");
5686
6354
  var import_uuid7 = require("uuid");
5687
- var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process9.exec);
6355
+ var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
5688
6356
  var STATUS_TIMEOUT_MS = 15e3;
5689
6357
  var COMMIT_TIMEOUT_MS = 6e4;
5690
6358
  var PUSH_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -5857,7 +6525,7 @@ var GitExecutor = class {
5857
6525
  });
5858
6526
  let proc;
5859
6527
  try {
5860
- proc = (0, import_node_child_process9.spawn)(cmd[0], cmd.slice(1), {
6528
+ proc = (0, import_node_child_process10.spawn)(cmd[0], cmd.slice(1), {
5861
6529
  cwd: projectPath,
5862
6530
  stdio: ["ignore", "pipe", "pipe"],
5863
6531
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
@@ -6037,9 +6705,15 @@ function isValidTask(value) {
6037
6705
  }
6038
6706
 
6039
6707
  // src/utils/cliCapabilities.ts
6040
- var import_node_child_process10 = require("child_process");
6708
+ var import_node_child_process11 = require("child_process");
6709
+ var DEFAULT_MODELS = [
6710
+ { value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
6711
+ { value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
6712
+ { value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
6713
+ ];
6041
6714
  var DEFAULT_CAPABILITIES = {
6042
- effortLevels: ["low", "medium", "high", "xhigh", "max"]
6715
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
6716
+ models: DEFAULT_MODELS
6043
6717
  };
6044
6718
  async function parseCliCapabilities() {
6045
6719
  const claudePath = findClaudePath();
@@ -6065,7 +6739,7 @@ async function parseCliCapabilities() {
6065
6739
  }
6066
6740
  function runCli(path2, args) {
6067
6741
  return new Promise((resolve) => {
6068
- (0, import_node_child_process10.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
6742
+ (0, import_node_child_process11.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
6069
6743
  if (err) {
6070
6744
  console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
6071
6745
  resolve(null);
@@ -6079,7 +6753,7 @@ function runCli(path2, args) {
6079
6753
  // src/server.ts
6080
6754
  var WS_PORT = 3745;
6081
6755
  var HTTP_PORT = 3746;
6082
- var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process11.exec);
6756
+ var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process12.exec);
6083
6757
  async function killPortProcess(port) {
6084
6758
  try {
6085
6759
  if (isWindows) {
@@ -6107,6 +6781,39 @@ async function killPortProcess(port) {
6107
6781
  } catch {
6108
6782
  }
6109
6783
  }
6784
+ async function loadApnsConfigFromFile() {
6785
+ const path2 = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix", "apns.json");
6786
+ try {
6787
+ const raw = await (0, import_promises7.readFile)(path2, "utf8");
6788
+ const cfg = JSON.parse(raw);
6789
+ if (typeof cfg.teamId !== "string" || typeof cfg.keyId !== "string" || typeof cfg.authKeyPath !== "string") {
6790
+ console.warn(`[Server] \u26A0\uFE0F ${path2} \u7F3A\u5C11\u5FC5\u9700\u5B57\u6BB5 (teamId / keyId / authKeyPath)\uFF0CLA \u540E\u53F0\u63A8\u9001\u5DF2\u7981\u7528`);
6791
+ return null;
6792
+ }
6793
+ try {
6794
+ await (0, import_promises7.readFile)(cfg.authKeyPath, "utf8");
6795
+ } catch (err) {
6796
+ console.warn(`[Server] \u26A0\uFE0F \u65E0\u6CD5\u8BFB\u53D6 APNs Auth Key: ${cfg.authKeyPath}`, err);
6797
+ return null;
6798
+ }
6799
+ console.log(`[Server] \u2705 \u5DF2\u52A0\u8F7D APNs \u914D\u7F6E (${path2})`);
6800
+ console.log(`[Server] teamId=${cfg.teamId} keyId=${cfg.keyId} sandbox=${cfg.sandbox === true}`);
6801
+ return {
6802
+ teamId: cfg.teamId,
6803
+ keyId: cfg.keyId,
6804
+ authKeyPath: cfg.authKeyPath,
6805
+ sandbox: cfg.sandbox === true
6806
+ };
6807
+ } catch (err) {
6808
+ const code = err.code;
6809
+ if (code === "ENOENT") {
6810
+ 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`);
6811
+ } else {
6812
+ console.warn(`[Server] \u26A0\uFE0F \u8BFB\u53D6 ${path2} \u5931\u8D25:`, err);
6813
+ }
6814
+ return null;
6815
+ }
6816
+ }
6110
6817
  async function createWithRetry(label, port, factory) {
6111
6818
  try {
6112
6819
  return await factory();
@@ -6121,6 +6828,7 @@ async function createWithRetry(label, port, factory) {
6121
6828
  }
6122
6829
  }
6123
6830
  async function start(opts = {}) {
6831
+ fixShellPath();
6124
6832
  const configDir = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix");
6125
6833
  const tokenFile = (0, import_node_path9.join)(configDir, "token");
6126
6834
  let token;
@@ -6169,9 +6877,10 @@ async function start(opts = {}) {
6169
6877
  const notificationService = new NotificationService(sessionManager, expoChannel);
6170
6878
  notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
6171
6879
  notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
6172
- if (opts.activityPush) {
6880
+ const activityPushOpts = opts.activityPush ?? await loadApnsConfigFromFile();
6881
+ if (activityPushOpts) {
6173
6882
  try {
6174
- const activityChannel = new ActivityPushChannel(opts.activityPush);
6883
+ const activityChannel = new ActivityPushChannel(activityPushOpts);
6175
6884
  notificationService.setActivityPushChannel(activityChannel);
6176
6885
  console.log(`[Server] ${t("server.activityPushEnabled")}`);
6177
6886
  } catch (err) {
@@ -6206,6 +6915,9 @@ async function start(opts = {}) {
6206
6915
  notificationService.setGlobalPendingCountProvider(
6207
6916
  () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
6208
6917
  );
6918
+ notificationService.setPendingApprovalsProvider(
6919
+ (sessionId) => approvalProxy.getPendingRequestsForSession(sessionId)
6920
+ );
6209
6921
  let cliCapabilities = null;
6210
6922
  parseCliCapabilities().then((caps) => {
6211
6923
  cliCapabilities = caps;
@@ -6218,7 +6930,8 @@ async function start(opts = {}) {
6218
6930
  onFire: async (task) => {
6219
6931
  const p = task.payload;
6220
6932
  if (p.kind === "create") {
6221
- await (0, import_promises7.mkdir)(p.projectPath, { recursive: true });
6933
+ const dirExists = await (0, import_promises7.stat)(p.projectPath).then((s) => s.isDirectory()).catch(() => false);
6934
+ if (!dirExists) await (0, import_promises7.mkdir)(p.projectPath, { recursive: true });
6222
6935
  const session = await sessionManager.createSession(
6223
6936
  p.projectPath,
6224
6937
  p.message,
@@ -6276,7 +6989,7 @@ async function start(opts = {}) {
6276
6989
  wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
6277
6990
  }
6278
6991
  if (cliCapabilities) {
6279
- wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version });
6992
+ wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version, models: cliCapabilities.models });
6280
6993
  }
6281
6994
  if (scheduledManager) {
6282
6995
  wsBridge.send(ws, { type: "scheduled_session_list", tasks: scheduledManager.list() });
@@ -6286,7 +6999,8 @@ async function start(opts = {}) {
6286
6999
  try {
6287
7000
  switch (event.type) {
6288
7001
  case "create_session": {
6289
- await (0, import_promises7.mkdir)(event.projectPath, { recursive: true });
7002
+ const dirExists = await (0, import_promises7.stat)(event.projectPath).then((s) => s.isDirectory()).catch(() => false);
7003
+ if (!dirExists) await (0, import_promises7.mkdir)(event.projectPath, { recursive: true });
6290
7004
  const resumeId = event.resumeSessionId ?? event.newSessionId;
6291
7005
  if (resumeId) sessionFileWatcher.unwatch(resumeId);
6292
7006
  await sessionManager.createSession(
@@ -6346,38 +7060,44 @@ async function start(opts = {}) {
6346
7060
  sessions: sessionManager.getActiveSessions()
6347
7061
  });
6348
7062
  sessionManager.flushPendingAssistant(event.sessionId);
6349
- const bufferedEvents = [...sessionManager.getSessionEvents(event.sessionId)];
6350
7063
  if (sessionManager.isBufferTruncated(event.sessionId)) {
6351
7064
  const projectPath = sessionManager.getSessionProjectPath(event.sessionId);
6352
7065
  if (projectPath) {
6353
7066
  const historyResult = await getSessionHistory(projectPath, event.sessionId);
7067
+ const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
6354
7068
  if (historyResult.ok && historyResult.value.length > 0) {
6355
- const merged = [...historyResult.value, ...bufferedEvents];
7069
+ const merged = [...historyResult.value, ...buffered];
6356
7070
  wsBridge.send(ws, {
6357
7071
  type: "session_history",
6358
7072
  sessionId: event.sessionId,
6359
7073
  events: merged
6360
7074
  });
6361
- } else if (bufferedEvents.length > 0) {
7075
+ } else if (buffered.length > 0) {
6362
7076
  wsBridge.send(ws, {
6363
7077
  type: "session_history",
6364
7078
  sessionId: event.sessionId,
6365
- events: bufferedEvents
7079
+ events: buffered
6366
7080
  });
6367
7081
  }
6368
- } else if (bufferedEvents.length > 0) {
7082
+ } else {
7083
+ const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
7084
+ if (buffered.length > 0) {
7085
+ wsBridge.send(ws, {
7086
+ type: "session_history",
7087
+ sessionId: event.sessionId,
7088
+ events: buffered
7089
+ });
7090
+ }
7091
+ }
7092
+ } else {
7093
+ const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
7094
+ if (buffered.length > 0) {
6369
7095
  wsBridge.send(ws, {
6370
7096
  type: "session_history",
6371
7097
  sessionId: event.sessionId,
6372
- events: bufferedEvents
7098
+ events: buffered
6373
7099
  });
6374
7100
  }
6375
- } else if (bufferedEvents.length > 0) {
6376
- wsBridge.send(ws, {
6377
- type: "session_history",
6378
- sessionId: event.sessionId,
6379
- events: bufferedEvents
6380
- });
6381
7101
  }
6382
7102
  for (const req of approvalProxy.getPendingRequestsForSession(event.sessionId)) {
6383
7103
  wsBridge.send(ws, { type: "approval_request", request: req });
@@ -6474,7 +7194,7 @@ async function start(opts = {}) {
6474
7194
  if (!isStreaming) {
6475
7195
  const filePath = getSessionFilePath(event.projectPath, event.sessionId);
6476
7196
  try {
6477
- const fileStat = await (0, import_promises8.stat)(filePath);
7197
+ const fileStat = await (0, import_promises7.stat)(filePath);
6478
7198
  sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
6479
7199
  } catch {
6480
7200
  }
@@ -6558,6 +7278,9 @@ async function start(opts = {}) {
6558
7278
  wsBridge.clearViewingSession(ws);
6559
7279
  break;
6560
7280
  }
7281
+ case "approval_displayed": {
7282
+ break;
7283
+ }
6561
7284
  case "always_allow_tool": {
6562
7285
  approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
6563
7286
  break;
@@ -6755,14 +7478,12 @@ async function start(opts = {}) {
6755
7478
  setTimeout(() => {
6756
7479
  if (!approvalProxy.isPending(request.id)) return;
6757
7480
  if (wsBridge.isViewingSession(request.sessionId)) return;
6758
- if (wsBridge.getConnectionCount() > 0) return;
6759
7481
  const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
6760
7482
  notificationService.notifyApproval(request, pendingCount);
6761
- }, 5e3);
7483
+ }, 3e3);
6762
7484
  setTimeout(() => {
6763
7485
  if (!approvalProxy.isPending(request.id)) return;
6764
7486
  if (wsBridge.isViewingSession(request.sessionId)) return;
6765
- if (wsBridge.getConnectionCount() > 0) return;
6766
7487
  console.log(`[Server] ${t("server.approvalRetry", { id: request.id })}`);
6767
7488
  const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
6768
7489
  notificationService.notifyApproval(request, pendingCount);
@@ -6774,13 +7495,11 @@ async function start(opts = {}) {
6774
7495
  setTimeout(() => {
6775
7496
  if (!sessionManager.isQuestionPending(request.id)) return;
6776
7497
  if (wsBridge.isViewingSession(request.sessionId)) return;
6777
- if (wsBridge.getConnectionCount() > 0) return;
6778
7498
  notificationService.notifyQuestion(request);
6779
7499
  }, 5e3);
6780
7500
  setTimeout(() => {
6781
7501
  if (!sessionManager.isQuestionPending(request.id)) return;
6782
7502
  if (wsBridge.isViewingSession(request.sessionId)) return;
6783
- if (wsBridge.getConnectionCount() > 0) return;
6784
7503
  console.log(`[Server] Question ${request.id} not answered in 60s, retrying push`);
6785
7504
  notificationService.notifyQuestion(request);
6786
7505
  }, 6e4);