sessix-server 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +369 -115
- package/dist/server.js +369 -115
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -952,9 +952,13 @@ ${context}`;
|
|
|
952
952
|
throw new Error(`Session ${sessionId} stdin unavailable`);
|
|
953
953
|
}
|
|
954
954
|
const toolResult = JSON.stringify({
|
|
955
|
-
type: "
|
|
956
|
-
|
|
957
|
-
|
|
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
|
|
958
962
|
});
|
|
959
963
|
await new Promise((resolve, reject) => {
|
|
960
964
|
entry.process.stdin.write(toolResult + "\n", (err) => {
|
|
@@ -2965,6 +2969,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2965
2969
|
this.handleApprovalHook(req, res);
|
|
2966
2970
|
} else if (req.method === "POST" && pathname === "/hook/notify") {
|
|
2967
2971
|
this.handleHookNotify(req, res);
|
|
2972
|
+
} else if (req.method === "POST" && pathname === "/api/resolve") {
|
|
2973
|
+
this.handleApiResolve(req, res);
|
|
2968
2974
|
} else if (req.method === "POST" && pathname === "/pair") {
|
|
2969
2975
|
this.handlePair(req, res);
|
|
2970
2976
|
} else if (req.method === "GET" && pathname === "/health") {
|
|
@@ -3030,6 +3036,34 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
3030
3036
|
this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
|
|
3031
3037
|
}
|
|
3032
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
|
+
}
|
|
3033
3067
|
/**
|
|
3034
3068
|
* 非阻塞 hook 通知端点
|
|
3035
3069
|
*
|
|
@@ -3741,7 +3775,7 @@ var HookInstaller = class {
|
|
|
3741
3775
|
// src/notification/NotificationService.ts
|
|
3742
3776
|
var import_node_path5 = require("path");
|
|
3743
3777
|
var RECENT_ACTIVITY_MAX = 6;
|
|
3744
|
-
var ACTIVITY_PUSH_THROTTLE_MS =
|
|
3778
|
+
var ACTIVITY_PUSH_THROTTLE_MS = 4e3;
|
|
3745
3779
|
var NotificationService = class {
|
|
3746
3780
|
constructor(sessionManager, expoChannel = null) {
|
|
3747
3781
|
this.sessionManager = sessionManager;
|
|
@@ -3766,8 +3800,24 @@ var NotificationService = class {
|
|
|
3766
3800
|
activityPushTimers = /* @__PURE__ */ new Map();
|
|
3767
3801
|
/** 上次推送 LA content 的时间戳(用于节流;首次立即推送) */
|
|
3768
3802
|
lastActivityPushAt = /* @__PURE__ */ new Map();
|
|
3803
|
+
/** 挂起的优先级提升请求(状态变化时设为 '10',flush 后清除) */
|
|
3804
|
+
pendingPriority = /* @__PURE__ */ new Map();
|
|
3805
|
+
/** sessionId → 累计活动计数器(用于 summary 模式的 activitySummary) */
|
|
3806
|
+
activityCounters = /* @__PURE__ */ new Map();
|
|
3769
3807
|
/** 提供器:根据 sessionId 取该会话的待审批列表(由 server.ts 注入) */
|
|
3770
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();
|
|
3771
3821
|
/** 添加通知渠道(id 唯一,可用于后续动态开关) */
|
|
3772
3822
|
addChannel(id, channel, enabled = true) {
|
|
3773
3823
|
this.channelMap.set(id, { channel, enabled });
|
|
@@ -3802,13 +3852,16 @@ var NotificationService = class {
|
|
|
3802
3852
|
this.activityPushChannel.addToken(sessionId, token);
|
|
3803
3853
|
console.log(`[NotificationService] \u2705 LA push token \u5DF2\u6CE8\u518C (session=${sessionId.slice(0, 8)}\u2026, token=${token.slice(0, 16)}\u2026)`);
|
|
3804
3854
|
this.scheduleActivityPush(sessionId, true);
|
|
3855
|
+
this.startLaHeartbeat(sessionId);
|
|
3805
3856
|
}
|
|
3806
3857
|
/** 移除 ActivityKit push token */
|
|
3807
3858
|
removeActivityPushToken(sessionId) {
|
|
3859
|
+
this.stopLaHeartbeat(sessionId);
|
|
3808
3860
|
this.activityPushChannel?.removeToken(sessionId);
|
|
3809
3861
|
this.clearActivityPushTimer(sessionId);
|
|
3810
3862
|
this.recentActivityState.delete(sessionId);
|
|
3811
3863
|
this.lastActivityPushAt.delete(sessionId);
|
|
3864
|
+
this.activityCounters.delete(sessionId);
|
|
3812
3865
|
}
|
|
3813
3866
|
/** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
|
|
3814
3867
|
setPendingApprovalsProvider(fn) {
|
|
@@ -3827,7 +3880,7 @@ var NotificationService = class {
|
|
|
3827
3880
|
this.yoloModeState.set(sessionId, enabled);
|
|
3828
3881
|
}
|
|
3829
3882
|
/** 直接触发审批通知(由 ApprovalProxy 回调调用) */
|
|
3830
|
-
notifyApproval(request, pendingCount) {
|
|
3883
|
+
async notifyApproval(request, pendingCount) {
|
|
3831
3884
|
if (this.yoloModeState.get(request.sessionId)) return;
|
|
3832
3885
|
const sessionTitle = this.getSessionTitle(request.sessionId);
|
|
3833
3886
|
const title = pendingCount > 1 ? t("notification.pendingApprovals", { title: sessionTitle, count: pendingCount }) : sessionTitle;
|
|
@@ -3837,7 +3890,8 @@ var NotificationService = class {
|
|
|
3837
3890
|
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
3838
3891
|
const recentActivity = this.getRecentActivity(request.sessionId);
|
|
3839
3892
|
const latestMessage = recentActivity[recentActivity.length - 1] ?? "";
|
|
3840
|
-
this.
|
|
3893
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
|
|
3894
|
+
const sent = await this.activityPushChannel.updateActivityWithAlert(
|
|
3841
3895
|
request.sessionId,
|
|
3842
3896
|
{
|
|
3843
3897
|
status: "waitingApproval",
|
|
@@ -3852,12 +3906,22 @@ var NotificationService = class {
|
|
|
3852
3906
|
pendingCount
|
|
3853
3907
|
},
|
|
3854
3908
|
isYoloMode,
|
|
3855
|
-
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)
|
|
3856
3914
|
},
|
|
3857
3915
|
{ title, body }
|
|
3858
3916
|
);
|
|
3859
|
-
|
|
3860
|
-
|
|
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}`);
|
|
3861
3925
|
}
|
|
3862
3926
|
const dangerLevel = this.getDangerLevel(request.toolName);
|
|
3863
3927
|
const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
|
|
@@ -3891,15 +3955,20 @@ var NotificationService = class {
|
|
|
3891
3955
|
if (this.activityPushChannel?.hasToken(request.sessionId)) {
|
|
3892
3956
|
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
3893
3957
|
const recentActivity = this.getRecentActivity(request.sessionId);
|
|
3958
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
|
|
3894
3959
|
this.activityPushChannel.updateActivityWithAlert(
|
|
3895
3960
|
request.sessionId,
|
|
3896
3961
|
{
|
|
3897
|
-
status: "
|
|
3962
|
+
status: "waitingQuestion",
|
|
3898
3963
|
sessionTitle,
|
|
3899
3964
|
latestMessage: request.question.slice(0, 80),
|
|
3900
3965
|
recentActivity,
|
|
3901
3966
|
isYoloMode,
|
|
3902
|
-
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)
|
|
3903
3972
|
},
|
|
3904
3973
|
{ title: sessionTitle, body }
|
|
3905
3974
|
);
|
|
@@ -3941,6 +4010,8 @@ var NotificationService = class {
|
|
|
3941
4010
|
this.activityPushTimers.clear();
|
|
3942
4011
|
this.recentActivityState.clear();
|
|
3943
4012
|
this.lastActivityPushAt.clear();
|
|
4013
|
+
this.pendingPriority.clear();
|
|
4014
|
+
this.activityCounters.clear();
|
|
3944
4015
|
}
|
|
3945
4016
|
// ============================================
|
|
3946
4017
|
// 内部方法
|
|
@@ -3964,22 +4035,14 @@ var NotificationService = class {
|
|
|
3964
4035
|
case "status_change": {
|
|
3965
4036
|
this.clearActivityPushTimer(event.sessionId);
|
|
3966
4037
|
if (event.status === "idle") {
|
|
3967
|
-
|
|
3968
|
-
const latestMsg = this.latestAssistantText.get(event.sessionId);
|
|
3969
|
-
const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
|
|
3970
|
-
const isYoloMode = this.getYoloMode(event.sessionId);
|
|
4038
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
3971
4039
|
if (this.activityPushChannel?.hasToken(event.sessionId)) {
|
|
3972
|
-
this.
|
|
3973
|
-
|
|
3974
|
-
sessionTitle,
|
|
3975
|
-
latestMessage: body,
|
|
3976
|
-
recentActivity: this.getRecentActivity(event.sessionId),
|
|
3977
|
-
isYoloMode,
|
|
3978
|
-
updatedAt: Date.now()
|
|
3979
|
-
});
|
|
3980
|
-
this.recentActivityState.delete(event.sessionId);
|
|
3981
|
-
this.lastActivityPushAt.delete(event.sessionId);
|
|
4040
|
+
this.scheduleActivityPush(event.sessionId, true, "10");
|
|
4041
|
+
this.scheduleIdleEnd(event.sessionId, 3e4);
|
|
3982
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");
|
|
3983
4046
|
this.notify({
|
|
3984
4047
|
title: sessionTitle,
|
|
3985
4048
|
body,
|
|
@@ -3988,31 +4051,12 @@ var NotificationService = class {
|
|
|
3988
4051
|
data: { type: "task_complete", sessionId: event.sessionId }
|
|
3989
4052
|
});
|
|
3990
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");
|
|
3991
4057
|
} else if (event.status === "error") {
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : t("notification.taskError");
|
|
3995
|
-
const isYoloMode = this.getYoloMode(event.sessionId);
|
|
3996
|
-
if (this.activityPushChannel?.hasToken(event.sessionId)) {
|
|
3997
|
-
this.activityPushChannel.endActivity(event.sessionId, {
|
|
3998
|
-
status: "error",
|
|
3999
|
-
sessionTitle,
|
|
4000
|
-
latestMessage: body,
|
|
4001
|
-
recentActivity: this.getRecentActivity(event.sessionId),
|
|
4002
|
-
isYoloMode,
|
|
4003
|
-
updatedAt: Date.now()
|
|
4004
|
-
});
|
|
4005
|
-
this.recentActivityState.delete(event.sessionId);
|
|
4006
|
-
this.lastActivityPushAt.delete(event.sessionId);
|
|
4007
|
-
} else {
|
|
4008
|
-
this.notify({
|
|
4009
|
-
title: sessionTitle,
|
|
4010
|
-
body,
|
|
4011
|
-
sound: "default",
|
|
4012
|
-
badge: this.getGlobalPendingCount(),
|
|
4013
|
-
data: { type: "task_error", sessionId: event.sessionId }
|
|
4014
|
-
});
|
|
4015
|
-
}
|
|
4058
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
4059
|
+
this.flushActivityEnd(event.sessionId, "error");
|
|
4016
4060
|
}
|
|
4017
4061
|
break;
|
|
4018
4062
|
}
|
|
@@ -4088,6 +4132,7 @@ var NotificationService = class {
|
|
|
4088
4132
|
} else if (block.type === "tool_use") {
|
|
4089
4133
|
const line = this.summarizeToolCall(block.name, block.input ?? {});
|
|
4090
4134
|
if (line) next.push(line);
|
|
4135
|
+
this.incrementCounter(sessionId, block.name);
|
|
4091
4136
|
}
|
|
4092
4137
|
}
|
|
4093
4138
|
state.currentEntries = next;
|
|
@@ -4099,9 +4144,66 @@ var NotificationService = class {
|
|
|
4099
4144
|
const combined = [...state.history, ...state.currentEntries];
|
|
4100
4145
|
return combined.slice(-RECENT_ACTIVITY_MAX);
|
|
4101
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
|
+
}
|
|
4102
4197
|
/** 节流调度 LA content push;首次立即推,后续合并到 throttle window 末尾 */
|
|
4103
|
-
scheduleActivityPush(sessionId, force = false) {
|
|
4104
|
-
if (!this.activityPushChannel
|
|
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
|
+
}
|
|
4105
4207
|
const now = Date.now();
|
|
4106
4208
|
const last = this.lastActivityPushAt.get(sessionId) ?? 0;
|
|
4107
4209
|
const elapsed = now - last;
|
|
@@ -4127,6 +4229,101 @@ var NotificationService = class {
|
|
|
4127
4229
|
this.activityPushTimers.delete(sessionId);
|
|
4128
4230
|
}
|
|
4129
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
|
+
}
|
|
4130
4327
|
/** 真正发送一次 LA content push(无 alert) */
|
|
4131
4328
|
flushActivityPush(sessionId) {
|
|
4132
4329
|
const channel = this.activityPushChannel;
|
|
@@ -4146,7 +4343,10 @@ var NotificationService = class {
|
|
|
4146
4343
|
latestMessage,
|
|
4147
4344
|
recentActivity,
|
|
4148
4345
|
isYoloMode,
|
|
4149
|
-
updatedAt: Date.now()
|
|
4346
|
+
updatedAt: Date.now(),
|
|
4347
|
+
displayMode: "summary",
|
|
4348
|
+
activitySummary: this.buildActivitySummary(sessionId),
|
|
4349
|
+
startedAt: session.createdAt
|
|
4150
4350
|
};
|
|
4151
4351
|
if (latestApproval) {
|
|
4152
4352
|
contentState.approvalInfo = {
|
|
@@ -4157,17 +4357,15 @@ var NotificationService = class {
|
|
|
4157
4357
|
pendingCount: pendingApprovals.length
|
|
4158
4358
|
};
|
|
4159
4359
|
}
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
totalOutputTokens: session.stats.totalOutputTokens,
|
|
4164
|
-
totalCostUsd: session.stats.totalCostUsd
|
|
4165
|
-
};
|
|
4166
|
-
}
|
|
4360
|
+
contentState.stats = this.buildStatsPayload(session);
|
|
4361
|
+
const priority = this.pendingPriority.get(sessionId) ?? "5";
|
|
4362
|
+
this.pendingPriority.delete(sessionId);
|
|
4167
4363
|
this.lastActivityPushAt.set(sessionId, Date.now());
|
|
4168
4364
|
const lineCount = recentActivity.length;
|
|
4169
|
-
channel.updateActivity(sessionId, contentState).then(() => {
|
|
4170
|
-
|
|
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
|
+
}
|
|
4171
4369
|
}).catch((err) => {
|
|
4172
4370
|
console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
|
|
4173
4371
|
});
|
|
@@ -4324,12 +4522,13 @@ var ExpoNotificationChannel = class {
|
|
|
4324
4522
|
}
|
|
4325
4523
|
async send(payload) {
|
|
4326
4524
|
if (this.tokens.size === 0) return;
|
|
4327
|
-
const
|
|
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) => {
|
|
4328
4527
|
const ws = this.tokenWsMap.get(token);
|
|
4329
4528
|
return !ws || ws.readyState !== ws.OPEN;
|
|
4330
4529
|
});
|
|
4331
|
-
if (
|
|
4332
|
-
const messages =
|
|
4530
|
+
if (targetTokens.length === 0) return;
|
|
4531
|
+
const messages = targetTokens.map((to) => {
|
|
4333
4532
|
let sound = payload.sound ?? "default";
|
|
4334
4533
|
const prefs = this.soundPreferences.get(to);
|
|
4335
4534
|
if (prefs) {
|
|
@@ -4351,7 +4550,7 @@ var ExpoNotificationChannel = class {
|
|
|
4351
4550
|
};
|
|
4352
4551
|
});
|
|
4353
4552
|
try {
|
|
4354
|
-
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${
|
|
4553
|
+
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${targetTokens.length}/${this.tokens.size} devices${isCompletionNotif ? ", forced" : ""})`, targetTokens);
|
|
4355
4554
|
const res = await fetch(EXPO_PUSH_API, {
|
|
4356
4555
|
method: "POST",
|
|
4357
4556
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
@@ -4395,6 +4594,12 @@ var ActivityPushChannel = class {
|
|
|
4395
4594
|
* 同时维护两个连接 + 探测机制,避免环境配错时静默失败。
|
|
4396
4595
|
*/
|
|
4397
4596
|
tokenEnv = /* @__PURE__ */ new Map();
|
|
4597
|
+
/**
|
|
4598
|
+
* 两个环境都拒绝的 token(Live Activity 已销毁/过期)。
|
|
4599
|
+
* 标记后跳过所有发送尝试,避免无意义的 HTTP/2 请求。
|
|
4600
|
+
* 当同一 session 注册新 token 时自动清除旧 token 的标记。
|
|
4601
|
+
*/
|
|
4602
|
+
deadTokens = /* @__PURE__ */ new Set();
|
|
4398
4603
|
/** 首次探测顺序(来自配置 hint),未配置则先试 sandbox(开发场景占多数) */
|
|
4399
4604
|
probeOrder;
|
|
4400
4605
|
teamId;
|
|
@@ -4431,6 +4636,11 @@ var ActivityPushChannel = class {
|
|
|
4431
4636
|
}
|
|
4432
4637
|
/** 注册 Activity push token */
|
|
4433
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
|
+
}
|
|
4434
4644
|
const existed = this.tokens.has(sessionId);
|
|
4435
4645
|
this.tokens.set(sessionId, token);
|
|
4436
4646
|
console.log(`[ActivityPushChannel] Token ${existed ? "updated" : "registered"}: session=${sessionId.slice(0, 8)}\u2026 token=${token.slice(0, 16)}\u2026`);
|
|
@@ -4439,65 +4649,82 @@ var ActivityPushChannel = class {
|
|
|
4439
4649
|
removeToken(sessionId) {
|
|
4440
4650
|
const tok = this.tokens.get(sessionId);
|
|
4441
4651
|
this.tokens.delete(sessionId);
|
|
4442
|
-
if (tok)
|
|
4652
|
+
if (tok) {
|
|
4653
|
+
this.tokenEnv.delete(tok);
|
|
4654
|
+
this.deadTokens.delete(tok);
|
|
4655
|
+
}
|
|
4443
4656
|
}
|
|
4444
|
-
/** 发送 content-state 更新到指定会话的 Live Activity
|
|
4445
|
-
async updateActivity(sessionId, contentState) {
|
|
4657
|
+
/** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知)。返回 true 表示实际发出 */
|
|
4658
|
+
async updateActivity(sessionId, contentState, opts) {
|
|
4446
4659
|
const token = this.tokens.get(sessionId);
|
|
4447
|
-
if (!token) return;
|
|
4660
|
+
if (!token) return false;
|
|
4448
4661
|
const now = Math.floor(Date.now() / 1e3);
|
|
4662
|
+
const priority = opts?.priority ?? "5";
|
|
4449
4663
|
const payload = {
|
|
4450
4664
|
aps: {
|
|
4451
4665
|
timestamp: now,
|
|
4452
4666
|
event: "update",
|
|
4453
4667
|
"content-state": contentState,
|
|
4454
|
-
|
|
4455
|
-
"stale-date": now + 120
|
|
4668
|
+
"stale-date": now + 600
|
|
4456
4669
|
}
|
|
4457
4670
|
};
|
|
4458
4671
|
try {
|
|
4459
|
-
await this.sendToAPNs(token, payload, {
|
|
4672
|
+
await this.sendToAPNs(token, payload, {
|
|
4673
|
+
priority,
|
|
4674
|
+
collapseId: `state-${sessionId.slice(0, 54)}`
|
|
4675
|
+
});
|
|
4676
|
+
return true;
|
|
4460
4677
|
} catch (err) {
|
|
4461
4678
|
console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
|
|
4679
|
+
return false;
|
|
4462
4680
|
}
|
|
4463
4681
|
}
|
|
4464
|
-
/** 发送带通知的 content-state
|
|
4682
|
+
/** 发送带通知的 content-state 更新(审批请求时使用),返回是否发送成功 */
|
|
4465
4683
|
async updateActivityWithAlert(sessionId, contentState, alert) {
|
|
4466
4684
|
const token = this.tokens.get(sessionId);
|
|
4467
|
-
if (!token) return;
|
|
4685
|
+
if (!token) return false;
|
|
4468
4686
|
const now = Math.floor(Date.now() / 1e3);
|
|
4469
4687
|
const payload = {
|
|
4470
4688
|
aps: {
|
|
4471
4689
|
timestamp: now,
|
|
4472
4690
|
event: "update",
|
|
4473
4691
|
"content-state": contentState,
|
|
4474
|
-
"stale-date": now +
|
|
4692
|
+
"stale-date": now + 600,
|
|
4475
4693
|
alert,
|
|
4476
4694
|
sound: "default"
|
|
4477
4695
|
}
|
|
4478
4696
|
};
|
|
4479
4697
|
try {
|
|
4480
4698
|
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4699
|
+
return true;
|
|
4481
4700
|
} catch (err) {
|
|
4482
4701
|
console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
|
|
4702
|
+
return false;
|
|
4483
4703
|
}
|
|
4484
4704
|
}
|
|
4485
|
-
/**
|
|
4486
|
-
|
|
4705
|
+
/**
|
|
4706
|
+
* 结束指定会话的 Live Activity。
|
|
4707
|
+
* 可选 alert:APNs event=end 时同时推送横幅通知 + 声音,用于在会话完成时提醒用户。
|
|
4708
|
+
*/
|
|
4709
|
+
async endActivity(sessionId, contentState, opts) {
|
|
4487
4710
|
const token = this.tokens.get(sessionId);
|
|
4488
4711
|
if (!token) return;
|
|
4489
4712
|
const now = Math.floor(Date.now() / 1e3);
|
|
4490
|
-
const
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
"content-state": contentState
|
|
4495
|
-
}
|
|
4713
|
+
const aps = {
|
|
4714
|
+
timestamp: now,
|
|
4715
|
+
event: "end",
|
|
4716
|
+
"content-state": contentState
|
|
4496
4717
|
};
|
|
4718
|
+
if (opts?.alert) {
|
|
4719
|
+
aps.alert = opts.alert;
|
|
4720
|
+
aps.sound = "default";
|
|
4721
|
+
}
|
|
4722
|
+
const payload = { aps };
|
|
4497
4723
|
try {
|
|
4498
4724
|
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4499
4725
|
} catch (err) {
|
|
4500
|
-
|
|
4726
|
+
this.tokens.delete(sessionId);
|
|
4727
|
+
throw err;
|
|
4501
4728
|
}
|
|
4502
4729
|
this.tokens.delete(sessionId);
|
|
4503
4730
|
}
|
|
@@ -4508,29 +4735,44 @@ var ActivityPushChannel = class {
|
|
|
4508
4735
|
/**
|
|
4509
4736
|
* 发送 APNs,自动处理环境探测。
|
|
4510
4737
|
* 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
|
|
4511
|
-
* 收到 BadDeviceToken
|
|
4738
|
+
* 收到 BadDeviceToken / BadEnvironmentKeyInToken 自动切到另一个环境,
|
|
4739
|
+
* 并把成功的环境绑定到该 token。
|
|
4512
4740
|
*/
|
|
4513
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
|
+
}
|
|
4514
4745
|
const known = this.tokenEnv.get(deviceToken);
|
|
4515
4746
|
if (known) {
|
|
4516
4747
|
return this.sendToAPNsOnce(deviceToken, payload, opts, known);
|
|
4517
4748
|
}
|
|
4749
|
+
const short = deviceToken.slice(0, 16);
|
|
4750
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
|
|
4518
4751
|
let lastErr = null;
|
|
4519
4752
|
for (const env of this.probeOrder) {
|
|
4520
4753
|
try {
|
|
4754
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe try ${env} token=${short}\u2026`);
|
|
4521
4755
|
await this.sendToAPNsOnce(deviceToken, payload, opts, env);
|
|
4522
4756
|
this.tokenEnv.set(deviceToken, env);
|
|
4523
|
-
|
|
4524
|
-
console.log(`[ActivityPushChannel] Token bound to ${env} after probe (token=${deviceToken.slice(0, 16)}\u2026)`);
|
|
4525
|
-
}
|
|
4757
|
+
console.log(`[ActivityPushChannel] \u2705 probe bound to ${env} (token=${short}\u2026)`);
|
|
4526
4758
|
return;
|
|
4527
4759
|
} catch (err) {
|
|
4528
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}`);
|
|
4529
4763
|
if (!isBadDeviceTokenError(err)) {
|
|
4530
4764
|
throw err;
|
|
4531
4765
|
}
|
|
4532
4766
|
}
|
|
4533
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
|
+
}
|
|
4534
4776
|
throw lastErr ?? new Error("APNs send failed: all environments rejected token");
|
|
4535
4777
|
}
|
|
4536
4778
|
/** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
|
|
@@ -4546,21 +4788,25 @@ var ActivityPushChannel = class {
|
|
|
4546
4788
|
} catch (err) {
|
|
4547
4789
|
return reject(err);
|
|
4548
4790
|
}
|
|
4549
|
-
const
|
|
4791
|
+
const headers = {
|
|
4550
4792
|
":method": "POST",
|
|
4551
4793
|
":path": `/3/device/${deviceToken}`,
|
|
4552
4794
|
"authorization": `bearer ${jwt}`,
|
|
4553
4795
|
"apns-topic": topic,
|
|
4554
4796
|
"apns-push-type": "liveactivity",
|
|
4555
4797
|
"apns-priority": priority,
|
|
4556
|
-
"apns-expiration": String(Math.floor(Date.now() / 1e3) +
|
|
4798
|
+
"apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
|
|
4557
4799
|
"content-type": "application/json",
|
|
4558
4800
|
"content-length": Buffer.byteLength(payloadStr)
|
|
4559
|
-
}
|
|
4801
|
+
};
|
|
4802
|
+
if (opts.collapseId) {
|
|
4803
|
+
headers["apns-collapse-id"] = opts.collapseId;
|
|
4804
|
+
}
|
|
4805
|
+
const req = client.request(headers);
|
|
4560
4806
|
let statusCode = 0;
|
|
4561
4807
|
let responseData = "";
|
|
4562
|
-
req.on("response", (
|
|
4563
|
-
statusCode = Number(
|
|
4808
|
+
req.on("response", (headers2) => {
|
|
4809
|
+
statusCode = Number(headers2[":status"] ?? 0);
|
|
4564
4810
|
});
|
|
4565
4811
|
req.on("data", (chunk) => {
|
|
4566
4812
|
responseData += chunk;
|
|
@@ -4601,7 +4847,7 @@ var ActivityPushChannel = class {
|
|
|
4601
4847
|
const signingInput = `${header}.${claims}`;
|
|
4602
4848
|
const sign = crypto.createSign("SHA256");
|
|
4603
4849
|
sign.update(signingInput);
|
|
4604
|
-
const signature = sign.sign(this.authKey, "base64url");
|
|
4850
|
+
const signature = sign.sign({ key: this.authKey, dsaEncoding: "ieee-p1363" }, "base64url");
|
|
4605
4851
|
const token = `${signingInput}.${signature}`;
|
|
4606
4852
|
this.cachedJwt = { token, expiresAt: now + 3e3 };
|
|
4607
4853
|
return token;
|
|
@@ -4617,10 +4863,10 @@ var ApnsError = class extends Error {
|
|
|
4617
4863
|
};
|
|
4618
4864
|
function isBadDeviceTokenError(err) {
|
|
4619
4865
|
if (!(err instanceof ApnsError)) return false;
|
|
4620
|
-
if (err.statusCode !== 400 && err.statusCode !== 410) return false;
|
|
4866
|
+
if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
|
|
4621
4867
|
try {
|
|
4622
4868
|
const parsed = JSON.parse(err.responseBody);
|
|
4623
|
-
return parsed.reason === "BadDeviceToken" || parsed.reason === "Unregistered";
|
|
4869
|
+
return parsed.reason === "BadDeviceToken" || parsed.reason === "BadEnvironmentKeyInToken" || parsed.reason === "Unregistered";
|
|
4624
4870
|
} catch {
|
|
4625
4871
|
return false;
|
|
4626
4872
|
}
|
|
@@ -5181,9 +5427,6 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
5181
5427
|
}
|
|
5182
5428
|
};
|
|
5183
5429
|
|
|
5184
|
-
// src/server.ts
|
|
5185
|
-
var import_promises8 = require("fs/promises");
|
|
5186
|
-
|
|
5187
5430
|
// src/terminal/TerminalExecutor.ts
|
|
5188
5431
|
var import_node_child_process8 = require("child_process");
|
|
5189
5432
|
var import_uuid5 = require("uuid");
|
|
@@ -6687,7 +6930,8 @@ async function start(opts = {}) {
|
|
|
6687
6930
|
onFire: async (task) => {
|
|
6688
6931
|
const p = task.payload;
|
|
6689
6932
|
if (p.kind === "create") {
|
|
6690
|
-
await (0, import_promises7.
|
|
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 });
|
|
6691
6935
|
const session = await sessionManager.createSession(
|
|
6692
6936
|
p.projectPath,
|
|
6693
6937
|
p.message,
|
|
@@ -6755,7 +6999,8 @@ async function start(opts = {}) {
|
|
|
6755
6999
|
try {
|
|
6756
7000
|
switch (event.type) {
|
|
6757
7001
|
case "create_session": {
|
|
6758
|
-
await (0, import_promises7.
|
|
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 });
|
|
6759
7004
|
const resumeId = event.resumeSessionId ?? event.newSessionId;
|
|
6760
7005
|
if (resumeId) sessionFileWatcher.unwatch(resumeId);
|
|
6761
7006
|
await sessionManager.createSession(
|
|
@@ -6815,38 +7060,44 @@ async function start(opts = {}) {
|
|
|
6815
7060
|
sessions: sessionManager.getActiveSessions()
|
|
6816
7061
|
});
|
|
6817
7062
|
sessionManager.flushPendingAssistant(event.sessionId);
|
|
6818
|
-
const bufferedEvents = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
6819
7063
|
if (sessionManager.isBufferTruncated(event.sessionId)) {
|
|
6820
7064
|
const projectPath = sessionManager.getSessionProjectPath(event.sessionId);
|
|
6821
7065
|
if (projectPath) {
|
|
6822
7066
|
const historyResult = await getSessionHistory(projectPath, event.sessionId);
|
|
7067
|
+
const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
6823
7068
|
if (historyResult.ok && historyResult.value.length > 0) {
|
|
6824
|
-
const merged = [...historyResult.value, ...
|
|
7069
|
+
const merged = [...historyResult.value, ...buffered];
|
|
6825
7070
|
wsBridge.send(ws, {
|
|
6826
7071
|
type: "session_history",
|
|
6827
7072
|
sessionId: event.sessionId,
|
|
6828
7073
|
events: merged
|
|
6829
7074
|
});
|
|
6830
|
-
} else if (
|
|
7075
|
+
} else if (buffered.length > 0) {
|
|
6831
7076
|
wsBridge.send(ws, {
|
|
6832
7077
|
type: "session_history",
|
|
6833
7078
|
sessionId: event.sessionId,
|
|
6834
|
-
events:
|
|
7079
|
+
events: buffered
|
|
6835
7080
|
});
|
|
6836
7081
|
}
|
|
6837
|
-
} else
|
|
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) {
|
|
6838
7095
|
wsBridge.send(ws, {
|
|
6839
7096
|
type: "session_history",
|
|
6840
7097
|
sessionId: event.sessionId,
|
|
6841
|
-
events:
|
|
7098
|
+
events: buffered
|
|
6842
7099
|
});
|
|
6843
7100
|
}
|
|
6844
|
-
} else if (bufferedEvents.length > 0) {
|
|
6845
|
-
wsBridge.send(ws, {
|
|
6846
|
-
type: "session_history",
|
|
6847
|
-
sessionId: event.sessionId,
|
|
6848
|
-
events: bufferedEvents
|
|
6849
|
-
});
|
|
6850
7101
|
}
|
|
6851
7102
|
for (const req of approvalProxy.getPendingRequestsForSession(event.sessionId)) {
|
|
6852
7103
|
wsBridge.send(ws, { type: "approval_request", request: req });
|
|
@@ -6943,7 +7194,7 @@ async function start(opts = {}) {
|
|
|
6943
7194
|
if (!isStreaming) {
|
|
6944
7195
|
const filePath = getSessionFilePath(event.projectPath, event.sessionId);
|
|
6945
7196
|
try {
|
|
6946
|
-
const fileStat = await (0,
|
|
7197
|
+
const fileStat = await (0, import_promises7.stat)(filePath);
|
|
6947
7198
|
sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
|
|
6948
7199
|
} catch {
|
|
6949
7200
|
}
|
|
@@ -7027,6 +7278,9 @@ async function start(opts = {}) {
|
|
|
7027
7278
|
wsBridge.clearViewingSession(ws);
|
|
7028
7279
|
break;
|
|
7029
7280
|
}
|
|
7281
|
+
case "approval_displayed": {
|
|
7282
|
+
break;
|
|
7283
|
+
}
|
|
7030
7284
|
case "always_allow_tool": {
|
|
7031
7285
|
approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
|
|
7032
7286
|
break;
|
|
@@ -7226,7 +7480,7 @@ async function start(opts = {}) {
|
|
|
7226
7480
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
7227
7481
|
const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
|
|
7228
7482
|
notificationService.notifyApproval(request, pendingCount);
|
|
7229
|
-
},
|
|
7483
|
+
}, 3e3);
|
|
7230
7484
|
setTimeout(() => {
|
|
7231
7485
|
if (!approvalProxy.isPending(request.id)) return;
|
|
7232
7486
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|