sessix-server 0.4.5 → 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.
- package/dist/index.js +368 -114
- package/dist/server.js +368 -114
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -947,9 +947,13 @@ ${context}`;
|
|
|
947
947
|
throw new Error(`Session ${sessionId} stdin unavailable`);
|
|
948
948
|
}
|
|
949
949
|
const toolResult = JSON.stringify({
|
|
950
|
-
type: "
|
|
951
|
-
|
|
952
|
-
|
|
950
|
+
type: "user",
|
|
951
|
+
session_id: "",
|
|
952
|
+
message: {
|
|
953
|
+
role: "user",
|
|
954
|
+
content: [{ type: "tool_result", tool_use_id: toolUseId, content: answer }]
|
|
955
|
+
},
|
|
956
|
+
parent_tool_use_id: toolUseId
|
|
953
957
|
});
|
|
954
958
|
await new Promise((resolve, reject) => {
|
|
955
959
|
entry.process.stdin.write(toolResult + "\n", (err) => {
|
|
@@ -2960,6 +2964,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2960
2964
|
this.handleApprovalHook(req, res);
|
|
2961
2965
|
} else if (req.method === "POST" && pathname === "/hook/notify") {
|
|
2962
2966
|
this.handleHookNotify(req, res);
|
|
2967
|
+
} else if (req.method === "POST" && pathname === "/api/resolve") {
|
|
2968
|
+
this.handleApiResolve(req, res);
|
|
2963
2969
|
} else if (req.method === "POST" && pathname === "/pair") {
|
|
2964
2970
|
this.handlePair(req, res);
|
|
2965
2971
|
} else if (req.method === "GET" && pathname === "/health") {
|
|
@@ -3025,6 +3031,34 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
3025
3031
|
this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
|
|
3026
3032
|
}
|
|
3027
3033
|
}
|
|
3034
|
+
/**
|
|
3035
|
+
* 移动端 API 端点:Widget Extension / Watch 直接提交审批决策
|
|
3036
|
+
*
|
|
3037
|
+
* 绕过 WebSocket 链路,让 iOS Widget Extension 的 AppIntent 在 App 挂起时
|
|
3038
|
+
* 仍能直接将审批结果提交到服务端。使用 Bearer token 鉴权(与 WS 同一 token)。
|
|
3039
|
+
*/
|
|
3040
|
+
async handleApiResolve(req, res) {
|
|
3041
|
+
const authHeader = req.headers.authorization ?? "";
|
|
3042
|
+
const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
3043
|
+
if (bearerToken !== this.token) {
|
|
3044
|
+
this.sendJson(res, 401, { ok: false, error: "Unauthorized" });
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
try {
|
|
3048
|
+
const body = await this.parseJsonBody(req);
|
|
3049
|
+
const requestId = String(body.requestId ?? "").trim();
|
|
3050
|
+
const decision = String(body.decision ?? "").trim();
|
|
3051
|
+
if (!requestId || decision !== "allow" && decision !== "deny") {
|
|
3052
|
+
this.sendJson(res, 400, { ok: false, error: "requestId and decision (allow|deny) required" });
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
const resolved = this.resolveApproval(requestId, { decision });
|
|
3056
|
+
this.sendJson(res, 200, { ok: resolved });
|
|
3057
|
+
} catch (err) {
|
|
3058
|
+
console.error("[ApprovalProxy] /api/resolve error:", err);
|
|
3059
|
+
this.sendJson(res, 500, { ok: false, error: "Internal error" });
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3028
3062
|
/**
|
|
3029
3063
|
* 非阻塞 hook 通知端点
|
|
3030
3064
|
*
|
|
@@ -3736,7 +3770,7 @@ var HookInstaller = class {
|
|
|
3736
3770
|
// src/notification/NotificationService.ts
|
|
3737
3771
|
var import_node_path5 = require("path");
|
|
3738
3772
|
var RECENT_ACTIVITY_MAX = 6;
|
|
3739
|
-
var ACTIVITY_PUSH_THROTTLE_MS =
|
|
3773
|
+
var ACTIVITY_PUSH_THROTTLE_MS = 4e3;
|
|
3740
3774
|
var NotificationService = class {
|
|
3741
3775
|
constructor(sessionManager, expoChannel = null) {
|
|
3742
3776
|
this.sessionManager = sessionManager;
|
|
@@ -3761,8 +3795,24 @@ var NotificationService = class {
|
|
|
3761
3795
|
activityPushTimers = /* @__PURE__ */ new Map();
|
|
3762
3796
|
/** 上次推送 LA content 的时间戳(用于节流;首次立即推送) */
|
|
3763
3797
|
lastActivityPushAt = /* @__PURE__ */ new Map();
|
|
3798
|
+
/** 挂起的优先级提升请求(状态变化时设为 '10',flush 后清除) */
|
|
3799
|
+
pendingPriority = /* @__PURE__ */ new Map();
|
|
3800
|
+
/** sessionId → 累计活动计数器(用于 summary 模式的 activitySummary) */
|
|
3801
|
+
activityCounters = /* @__PURE__ */ new Map();
|
|
3764
3802
|
/** 提供器:根据 sessionId 取该会话的待审批列表(由 server.ts 注入) */
|
|
3765
3803
|
pendingApprovalsProvider = null;
|
|
3804
|
+
/**
|
|
3805
|
+
* sessionId → idle 结束定时器。
|
|
3806
|
+
* 会话变为 idle 时启动(30 秒);用户发新消息重回 running 时取消。
|
|
3807
|
+
* 30 秒内无新消息 → 调 endActivity 关闭 LA,同时发横幅通知告知完成。
|
|
3808
|
+
*/
|
|
3809
|
+
idleEndTimers = /* @__PURE__ */ new Map();
|
|
3810
|
+
/**
|
|
3811
|
+
* sessionId → LA 心跳定时器(setInterval)。
|
|
3812
|
+
* 确保 Agent 子任务等长时间无 claude_event 的场景下 LA 仍持续更新。
|
|
3813
|
+
* token 注册时启动,flushActivityEnd / removeActivityPushToken 时停止。
|
|
3814
|
+
*/
|
|
3815
|
+
laHeartbeatTimers = /* @__PURE__ */ new Map();
|
|
3766
3816
|
/** 添加通知渠道(id 唯一,可用于后续动态开关) */
|
|
3767
3817
|
addChannel(id, channel, enabled = true) {
|
|
3768
3818
|
this.channelMap.set(id, { channel, enabled });
|
|
@@ -3797,13 +3847,16 @@ var NotificationService = class {
|
|
|
3797
3847
|
this.activityPushChannel.addToken(sessionId, token);
|
|
3798
3848
|
console.log(`[NotificationService] \u2705 LA push token \u5DF2\u6CE8\u518C (session=${sessionId.slice(0, 8)}\u2026, token=${token.slice(0, 16)}\u2026)`);
|
|
3799
3849
|
this.scheduleActivityPush(sessionId, true);
|
|
3850
|
+
this.startLaHeartbeat(sessionId);
|
|
3800
3851
|
}
|
|
3801
3852
|
/** 移除 ActivityKit push token */
|
|
3802
3853
|
removeActivityPushToken(sessionId) {
|
|
3854
|
+
this.stopLaHeartbeat(sessionId);
|
|
3803
3855
|
this.activityPushChannel?.removeToken(sessionId);
|
|
3804
3856
|
this.clearActivityPushTimer(sessionId);
|
|
3805
3857
|
this.recentActivityState.delete(sessionId);
|
|
3806
3858
|
this.lastActivityPushAt.delete(sessionId);
|
|
3859
|
+
this.activityCounters.delete(sessionId);
|
|
3807
3860
|
}
|
|
3808
3861
|
/** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
|
|
3809
3862
|
setPendingApprovalsProvider(fn) {
|
|
@@ -3822,7 +3875,7 @@ var NotificationService = class {
|
|
|
3822
3875
|
this.yoloModeState.set(sessionId, enabled);
|
|
3823
3876
|
}
|
|
3824
3877
|
/** 直接触发审批通知(由 ApprovalProxy 回调调用) */
|
|
3825
|
-
notifyApproval(request, pendingCount) {
|
|
3878
|
+
async notifyApproval(request, pendingCount) {
|
|
3826
3879
|
if (this.yoloModeState.get(request.sessionId)) return;
|
|
3827
3880
|
const sessionTitle = this.getSessionTitle(request.sessionId);
|
|
3828
3881
|
const title = pendingCount > 1 ? t("notification.pendingApprovals", { title: sessionTitle, count: pendingCount }) : sessionTitle;
|
|
@@ -3832,7 +3885,8 @@ var NotificationService = class {
|
|
|
3832
3885
|
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
3833
3886
|
const recentActivity = this.getRecentActivity(request.sessionId);
|
|
3834
3887
|
const latestMessage = recentActivity[recentActivity.length - 1] ?? "";
|
|
3835
|
-
this.
|
|
3888
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
|
|
3889
|
+
const sent = await this.activityPushChannel.updateActivityWithAlert(
|
|
3836
3890
|
request.sessionId,
|
|
3837
3891
|
{
|
|
3838
3892
|
status: "waitingApproval",
|
|
@@ -3847,12 +3901,22 @@ var NotificationService = class {
|
|
|
3847
3901
|
pendingCount
|
|
3848
3902
|
},
|
|
3849
3903
|
isYoloMode,
|
|
3850
|
-
updatedAt: Date.now()
|
|
3904
|
+
updatedAt: Date.now(),
|
|
3905
|
+
displayMode: "summary",
|
|
3906
|
+
activitySummary: this.buildActivitySummary(request.sessionId),
|
|
3907
|
+
startedAt: session?.createdAt,
|
|
3908
|
+
stats: this.buildStatsPayload(session)
|
|
3851
3909
|
},
|
|
3852
3910
|
{ title, body }
|
|
3853
3911
|
);
|
|
3854
|
-
|
|
3855
|
-
|
|
3912
|
+
if (sent) {
|
|
3913
|
+
console.log(`[NotificationService] \u{1F4E1} approval via ActivityKit push session=${request.sessionId.slice(0, 8)}\u2026 tool=${request.toolName}`);
|
|
3914
|
+
this.lastActivityPushAt.set(request.sessionId, Date.now());
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
console.warn(`[NotificationService] \u26A0\uFE0F ActivityKit push \u5931\u8D25\uFF0C\u964D\u7EA7\u5230 Expo push session=${request.sessionId.slice(0, 8)}\u2026`);
|
|
3918
|
+
} else {
|
|
3919
|
+
console.log(`[NotificationService] \u{1F4F2} approval via Expo push session=${request.sessionId.slice(0, 8)}\u2026 tool=${request.toolName}`);
|
|
3856
3920
|
}
|
|
3857
3921
|
const dangerLevel = this.getDangerLevel(request.toolName);
|
|
3858
3922
|
const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
|
|
@@ -3886,15 +3950,20 @@ var NotificationService = class {
|
|
|
3886
3950
|
if (this.activityPushChannel?.hasToken(request.sessionId)) {
|
|
3887
3951
|
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
3888
3952
|
const recentActivity = this.getRecentActivity(request.sessionId);
|
|
3953
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
|
|
3889
3954
|
this.activityPushChannel.updateActivityWithAlert(
|
|
3890
3955
|
request.sessionId,
|
|
3891
3956
|
{
|
|
3892
|
-
status: "
|
|
3957
|
+
status: "waitingQuestion",
|
|
3893
3958
|
sessionTitle,
|
|
3894
3959
|
latestMessage: request.question.slice(0, 80),
|
|
3895
3960
|
recentActivity,
|
|
3896
3961
|
isYoloMode,
|
|
3897
|
-
updatedAt: Date.now()
|
|
3962
|
+
updatedAt: Date.now(),
|
|
3963
|
+
displayMode: "summary",
|
|
3964
|
+
activitySummary: this.buildActivitySummary(request.sessionId),
|
|
3965
|
+
startedAt: session?.createdAt,
|
|
3966
|
+
stats: this.buildStatsPayload(session)
|
|
3898
3967
|
},
|
|
3899
3968
|
{ title: sessionTitle, body }
|
|
3900
3969
|
);
|
|
@@ -3936,6 +4005,8 @@ var NotificationService = class {
|
|
|
3936
4005
|
this.activityPushTimers.clear();
|
|
3937
4006
|
this.recentActivityState.clear();
|
|
3938
4007
|
this.lastActivityPushAt.clear();
|
|
4008
|
+
this.pendingPriority.clear();
|
|
4009
|
+
this.activityCounters.clear();
|
|
3939
4010
|
}
|
|
3940
4011
|
// ============================================
|
|
3941
4012
|
// 内部方法
|
|
@@ -3959,22 +4030,14 @@ var NotificationService = class {
|
|
|
3959
4030
|
case "status_change": {
|
|
3960
4031
|
this.clearActivityPushTimer(event.sessionId);
|
|
3961
4032
|
if (event.status === "idle") {
|
|
3962
|
-
|
|
3963
|
-
const latestMsg = this.latestAssistantText.get(event.sessionId);
|
|
3964
|
-
const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
|
|
3965
|
-
const isYoloMode = this.getYoloMode(event.sessionId);
|
|
4033
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
3966
4034
|
if (this.activityPushChannel?.hasToken(event.sessionId)) {
|
|
3967
|
-
this.
|
|
3968
|
-
|
|
3969
|
-
sessionTitle,
|
|
3970
|
-
latestMessage: body,
|
|
3971
|
-
recentActivity: this.getRecentActivity(event.sessionId),
|
|
3972
|
-
isYoloMode,
|
|
3973
|
-
updatedAt: Date.now()
|
|
3974
|
-
});
|
|
3975
|
-
this.recentActivityState.delete(event.sessionId);
|
|
3976
|
-
this.lastActivityPushAt.delete(event.sessionId);
|
|
4035
|
+
this.scheduleActivityPush(event.sessionId, true, "10");
|
|
4036
|
+
this.scheduleIdleEnd(event.sessionId, 3e4);
|
|
3977
4037
|
} else {
|
|
4038
|
+
const sessionTitle = this.getSessionTitle(event.sessionId);
|
|
4039
|
+
const latestMsg = this.latestAssistantText.get(event.sessionId);
|
|
4040
|
+
const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
|
|
3978
4041
|
this.notify({
|
|
3979
4042
|
title: sessionTitle,
|
|
3980
4043
|
body,
|
|
@@ -3983,31 +4046,12 @@ var NotificationService = class {
|
|
|
3983
4046
|
data: { type: "task_complete", sessionId: event.sessionId }
|
|
3984
4047
|
});
|
|
3985
4048
|
}
|
|
4049
|
+
} else if (event.status === "running" || event.status === "waiting_approval" || event.status === "waiting_question") {
|
|
4050
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
4051
|
+
this.scheduleActivityPush(event.sessionId, true, "10");
|
|
3986
4052
|
} else if (event.status === "error") {
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : t("notification.taskError");
|
|
3990
|
-
const isYoloMode = this.getYoloMode(event.sessionId);
|
|
3991
|
-
if (this.activityPushChannel?.hasToken(event.sessionId)) {
|
|
3992
|
-
this.activityPushChannel.endActivity(event.sessionId, {
|
|
3993
|
-
status: "error",
|
|
3994
|
-
sessionTitle,
|
|
3995
|
-
latestMessage: body,
|
|
3996
|
-
recentActivity: this.getRecentActivity(event.sessionId),
|
|
3997
|
-
isYoloMode,
|
|
3998
|
-
updatedAt: Date.now()
|
|
3999
|
-
});
|
|
4000
|
-
this.recentActivityState.delete(event.sessionId);
|
|
4001
|
-
this.lastActivityPushAt.delete(event.sessionId);
|
|
4002
|
-
} else {
|
|
4003
|
-
this.notify({
|
|
4004
|
-
title: sessionTitle,
|
|
4005
|
-
body,
|
|
4006
|
-
sound: "default",
|
|
4007
|
-
badge: this.getGlobalPendingCount(),
|
|
4008
|
-
data: { type: "task_error", sessionId: event.sessionId }
|
|
4009
|
-
});
|
|
4010
|
-
}
|
|
4053
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
4054
|
+
this.flushActivityEnd(event.sessionId, "error");
|
|
4011
4055
|
}
|
|
4012
4056
|
break;
|
|
4013
4057
|
}
|
|
@@ -4083,6 +4127,7 @@ var NotificationService = class {
|
|
|
4083
4127
|
} else if (block.type === "tool_use") {
|
|
4084
4128
|
const line = this.summarizeToolCall(block.name, block.input ?? {});
|
|
4085
4129
|
if (line) next.push(line);
|
|
4130
|
+
this.incrementCounter(sessionId, block.name);
|
|
4086
4131
|
}
|
|
4087
4132
|
}
|
|
4088
4133
|
state.currentEntries = next;
|
|
@@ -4094,9 +4139,66 @@ var NotificationService = class {
|
|
|
4094
4139
|
const combined = [...state.history, ...state.currentEntries];
|
|
4095
4140
|
return combined.slice(-RECENT_ACTIVITY_MAX);
|
|
4096
4141
|
}
|
|
4142
|
+
/** 工具名 → 计数器类别映射 */
|
|
4143
|
+
incrementCounter(sessionId, toolName) {
|
|
4144
|
+
let c = this.activityCounters.get(sessionId);
|
|
4145
|
+
if (!c) {
|
|
4146
|
+
c = { filesEdited: 0, commandsRun: 0, searches: 0, filesRead: 0, messagesReceived: 0 };
|
|
4147
|
+
this.activityCounters.set(sessionId, c);
|
|
4148
|
+
}
|
|
4149
|
+
switch (toolName) {
|
|
4150
|
+
case "Edit":
|
|
4151
|
+
case "MultiEdit":
|
|
4152
|
+
case "Write":
|
|
4153
|
+
case "NotebookEdit":
|
|
4154
|
+
c.filesEdited++;
|
|
4155
|
+
break;
|
|
4156
|
+
case "Bash":
|
|
4157
|
+
c.commandsRun++;
|
|
4158
|
+
break;
|
|
4159
|
+
case "Grep":
|
|
4160
|
+
case "Glob":
|
|
4161
|
+
case "WebSearch":
|
|
4162
|
+
case "WebFetch":
|
|
4163
|
+
c.searches++;
|
|
4164
|
+
break;
|
|
4165
|
+
case "Read":
|
|
4166
|
+
c.filesRead++;
|
|
4167
|
+
break;
|
|
4168
|
+
default:
|
|
4169
|
+
break;
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
/** 把累计计数器格式化为可读摘要(如"已编辑 3 个文件 · 运行 5 条命令") */
|
|
4173
|
+
buildActivitySummary(sessionId) {
|
|
4174
|
+
const c = this.activityCounters.get(sessionId);
|
|
4175
|
+
if (!c) return "";
|
|
4176
|
+
const parts = [];
|
|
4177
|
+
if (c.filesEdited > 0) parts.push(`\u5DF2\u7F16\u8F91 ${c.filesEdited} \u4E2A\u6587\u4EF6`);
|
|
4178
|
+
if (c.commandsRun > 0) parts.push(`\u8FD0\u884C ${c.commandsRun} \u6761\u547D\u4EE4`);
|
|
4179
|
+
if (c.searches > 0) parts.push(`\u641C\u7D22 ${c.searches} \u6B21`);
|
|
4180
|
+
if (c.filesRead > 0) parts.push(`\u9605\u8BFB ${c.filesRead} \u4E2A\u6587\u4EF6`);
|
|
4181
|
+
return parts.join(" \xB7 ") || "\u4F1A\u8BDD\u8FDB\u884C\u4E2D\u2026";
|
|
4182
|
+
}
|
|
4183
|
+
buildStatsPayload(session) {
|
|
4184
|
+
return {
|
|
4185
|
+
totalInputTokens: session?.stats?.totalInputTokens ?? 0,
|
|
4186
|
+
totalOutputTokens: session?.stats?.totalOutputTokens ?? 0,
|
|
4187
|
+
totalCostUsd: session?.stats?.totalCostUsd,
|
|
4188
|
+
totalDurationMs: session?.stats?.totalDurationMs,
|
|
4189
|
+
runningStartedAt: session?.stats?.runningStartedAt
|
|
4190
|
+
};
|
|
4191
|
+
}
|
|
4097
4192
|
/** 节流调度 LA content push;首次立即推,后续合并到 throttle window 末尾 */
|
|
4098
|
-
scheduleActivityPush(sessionId, force = false) {
|
|
4099
|
-
if (!this.activityPushChannel
|
|
4193
|
+
scheduleActivityPush(sessionId, force = false, priority) {
|
|
4194
|
+
if (!this.activityPushChannel) return;
|
|
4195
|
+
if (!this.activityPushChannel.hasToken(sessionId)) {
|
|
4196
|
+
console.warn(`[NotificationService] \u26A0\uFE0F skip LA push: session=${sessionId.slice(0, 8)}\u2026 token \u672A\u6CE8\u518C`);
|
|
4197
|
+
return;
|
|
4198
|
+
}
|
|
4199
|
+
if (priority === "10") {
|
|
4200
|
+
this.pendingPriority.set(sessionId, "10");
|
|
4201
|
+
}
|
|
4100
4202
|
const now = Date.now();
|
|
4101
4203
|
const last = this.lastActivityPushAt.get(sessionId) ?? 0;
|
|
4102
4204
|
const elapsed = now - last;
|
|
@@ -4122,6 +4224,101 @@ var NotificationService = class {
|
|
|
4122
4224
|
this.activityPushTimers.delete(sessionId);
|
|
4123
4225
|
}
|
|
4124
4226
|
}
|
|
4227
|
+
/**
|
|
4228
|
+
* 启动 LA 心跳(每 ACTIVITY_PUSH_THROTTLE_MS 触发一次 scheduleActivityPush)。
|
|
4229
|
+
* 确保 Agent 子会话等长时间无 claude_event 的情况下 LA 仍持续更新。
|
|
4230
|
+
* 心跳只在会话活跃状态(running/waitingApproval/waitingQuestion)下发推送;
|
|
4231
|
+
* scheduleActivityPush 自带节流,心跳与正常事件驱动不冲突。
|
|
4232
|
+
*/
|
|
4233
|
+
startLaHeartbeat(sessionId) {
|
|
4234
|
+
if (this.laHeartbeatTimers.has(sessionId)) return;
|
|
4235
|
+
const timer = setInterval(() => {
|
|
4236
|
+
if (!this.activityPushChannel?.hasToken(sessionId)) {
|
|
4237
|
+
this.stopLaHeartbeat(sessionId);
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4240
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4241
|
+
if (!session) {
|
|
4242
|
+
this.stopLaHeartbeat(sessionId);
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
if (session.status === "running" || session.status === "waiting_approval" || session.status === "waiting_question") {
|
|
4246
|
+
this.scheduleActivityPush(sessionId);
|
|
4247
|
+
}
|
|
4248
|
+
}, ACTIVITY_PUSH_THROTTLE_MS);
|
|
4249
|
+
this.laHeartbeatTimers.set(sessionId, timer);
|
|
4250
|
+
}
|
|
4251
|
+
/** 停止 LA 心跳 */
|
|
4252
|
+
stopLaHeartbeat(sessionId) {
|
|
4253
|
+
const timer = this.laHeartbeatTimers.get(sessionId);
|
|
4254
|
+
if (timer) {
|
|
4255
|
+
clearInterval(timer);
|
|
4256
|
+
this.laHeartbeatTimers.delete(sessionId);
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
/** 取消 idle 结束定时器 */
|
|
4260
|
+
cancelIdleEndTimer(sessionId) {
|
|
4261
|
+
const timer = this.idleEndTimers.get(sessionId);
|
|
4262
|
+
if (timer) {
|
|
4263
|
+
clearTimeout(timer);
|
|
4264
|
+
this.idleEndTimers.delete(sessionId);
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
/**
|
|
4268
|
+
* 启动 idle 结束定时器:delayMs 后若会话仍 idle,调 endActivity + 发通知。
|
|
4269
|
+
* 保证多轮对话期间(idle→running→idle)LA 不会过早消失。
|
|
4270
|
+
*/
|
|
4271
|
+
scheduleIdleEnd(sessionId, delayMs) {
|
|
4272
|
+
const timer = setTimeout(() => {
|
|
4273
|
+
this.idleEndTimers.delete(sessionId);
|
|
4274
|
+
this.flushActivityEnd(sessionId, "idle");
|
|
4275
|
+
}, delayMs);
|
|
4276
|
+
this.idleEndTimers.set(sessionId, timer);
|
|
4277
|
+
}
|
|
4278
|
+
/**
|
|
4279
|
+
* 结束 LA 并发完成通知。有 LA token → APNs event:end(带横幅);
|
|
4280
|
+
* 无 token → 普通 Expo push。清理所有相关状态。
|
|
4281
|
+
*/
|
|
4282
|
+
flushActivityEnd(sessionId, reason) {
|
|
4283
|
+
const sessionTitle = this.getSessionTitle(sessionId);
|
|
4284
|
+
const latestMsg = this.latestAssistantText.get(sessionId);
|
|
4285
|
+
const isError = reason === "error";
|
|
4286
|
+
const body = isError ? `\u274C ${latestMsg?.slice(0, 80) ?? t("notification.taskError")}` : `\u2705 ${latestMsg?.slice(0, 80) ?? t("notification.taskComplete")}`;
|
|
4287
|
+
const isYoloMode = this.getYoloMode(sessionId);
|
|
4288
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4289
|
+
this.notify({
|
|
4290
|
+
title: sessionTitle,
|
|
4291
|
+
body,
|
|
4292
|
+
sound: "default",
|
|
4293
|
+
badge: this.getGlobalPendingCount(),
|
|
4294
|
+
data: { type: isError ? "task_error" : "task_complete", sessionId }
|
|
4295
|
+
});
|
|
4296
|
+
if (this.activityPushChannel?.hasToken(sessionId)) {
|
|
4297
|
+
this.activityPushChannel.endActivity(
|
|
4298
|
+
sessionId,
|
|
4299
|
+
{
|
|
4300
|
+
status: isError ? "error" : "completed",
|
|
4301
|
+
sessionTitle,
|
|
4302
|
+
latestMessage: body,
|
|
4303
|
+
recentActivity: this.getRecentActivity(sessionId),
|
|
4304
|
+
isYoloMode,
|
|
4305
|
+
updatedAt: Date.now(),
|
|
4306
|
+
displayMode: "summary",
|
|
4307
|
+
activitySummary: this.buildActivitySummary(sessionId),
|
|
4308
|
+
startedAt: session?.createdAt,
|
|
4309
|
+
stats: this.buildStatsPayload(session)
|
|
4310
|
+
}
|
|
4311
|
+
// 不传 alert——Expo push 已处理通知,event:end 仅用于关闭 LA
|
|
4312
|
+
).catch((err) => {
|
|
4313
|
+
console.warn("[NotificationService] endActivity (close LA) failed, LA may linger:", err);
|
|
4314
|
+
});
|
|
4315
|
+
}
|
|
4316
|
+
this.stopLaHeartbeat(sessionId);
|
|
4317
|
+
this.recentActivityState.delete(sessionId);
|
|
4318
|
+
this.lastActivityPushAt.delete(sessionId);
|
|
4319
|
+
this.activityCounters.delete(sessionId);
|
|
4320
|
+
console.log(`[NotificationService] \u{1F3C1} LA end (${reason}) session=${sessionId.slice(0, 8)}\u2026`);
|
|
4321
|
+
}
|
|
4125
4322
|
/** 真正发送一次 LA content push(无 alert) */
|
|
4126
4323
|
flushActivityPush(sessionId) {
|
|
4127
4324
|
const channel = this.activityPushChannel;
|
|
@@ -4141,7 +4338,10 @@ var NotificationService = class {
|
|
|
4141
4338
|
latestMessage,
|
|
4142
4339
|
recentActivity,
|
|
4143
4340
|
isYoloMode,
|
|
4144
|
-
updatedAt: Date.now()
|
|
4341
|
+
updatedAt: Date.now(),
|
|
4342
|
+
displayMode: "summary",
|
|
4343
|
+
activitySummary: this.buildActivitySummary(sessionId),
|
|
4344
|
+
startedAt: session.createdAt
|
|
4145
4345
|
};
|
|
4146
4346
|
if (latestApproval) {
|
|
4147
4347
|
contentState.approvalInfo = {
|
|
@@ -4152,17 +4352,15 @@ var NotificationService = class {
|
|
|
4152
4352
|
pendingCount: pendingApprovals.length
|
|
4153
4353
|
};
|
|
4154
4354
|
}
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
totalOutputTokens: session.stats.totalOutputTokens,
|
|
4159
|
-
totalCostUsd: session.stats.totalCostUsd
|
|
4160
|
-
};
|
|
4161
|
-
}
|
|
4355
|
+
contentState.stats = this.buildStatsPayload(session);
|
|
4356
|
+
const priority = this.pendingPriority.get(sessionId) ?? "5";
|
|
4357
|
+
this.pendingPriority.delete(sessionId);
|
|
4162
4358
|
this.lastActivityPushAt.set(sessionId, Date.now());
|
|
4163
4359
|
const lineCount = recentActivity.length;
|
|
4164
|
-
channel.updateActivity(sessionId, contentState).then(() => {
|
|
4165
|
-
|
|
4360
|
+
channel.updateActivity(sessionId, contentState, { priority }).then((ok) => {
|
|
4361
|
+
if (ok) {
|
|
4362
|
+
console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} p=${priority} lines=${lineCount}`);
|
|
4363
|
+
}
|
|
4166
4364
|
}).catch((err) => {
|
|
4167
4365
|
console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
|
|
4168
4366
|
});
|
|
@@ -4319,12 +4517,13 @@ var ExpoNotificationChannel = class {
|
|
|
4319
4517
|
}
|
|
4320
4518
|
async send(payload) {
|
|
4321
4519
|
if (this.tokens.size === 0) return;
|
|
4322
|
-
const
|
|
4520
|
+
const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
|
|
4521
|
+
const targetTokens = isCompletionNotif ? Array.from(this.tokens) : Array.from(this.tokens).filter((token) => {
|
|
4323
4522
|
const ws = this.tokenWsMap.get(token);
|
|
4324
4523
|
return !ws || ws.readyState !== ws.OPEN;
|
|
4325
4524
|
});
|
|
4326
|
-
if (
|
|
4327
|
-
const messages =
|
|
4525
|
+
if (targetTokens.length === 0) return;
|
|
4526
|
+
const messages = targetTokens.map((to) => {
|
|
4328
4527
|
let sound = payload.sound ?? "default";
|
|
4329
4528
|
const prefs = this.soundPreferences.get(to);
|
|
4330
4529
|
if (prefs) {
|
|
@@ -4346,7 +4545,7 @@ var ExpoNotificationChannel = class {
|
|
|
4346
4545
|
};
|
|
4347
4546
|
});
|
|
4348
4547
|
try {
|
|
4349
|
-
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${
|
|
4548
|
+
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${targetTokens.length}/${this.tokens.size} devices${isCompletionNotif ? ", forced" : ""})`, targetTokens);
|
|
4350
4549
|
const res = await fetch(EXPO_PUSH_API, {
|
|
4351
4550
|
method: "POST",
|
|
4352
4551
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
@@ -4390,6 +4589,12 @@ var ActivityPushChannel = class {
|
|
|
4390
4589
|
* 同时维护两个连接 + 探测机制,避免环境配错时静默失败。
|
|
4391
4590
|
*/
|
|
4392
4591
|
tokenEnv = /* @__PURE__ */ new Map();
|
|
4592
|
+
/**
|
|
4593
|
+
* 两个环境都拒绝的 token(Live Activity 已销毁/过期)。
|
|
4594
|
+
* 标记后跳过所有发送尝试,避免无意义的 HTTP/2 请求。
|
|
4595
|
+
* 当同一 session 注册新 token 时自动清除旧 token 的标记。
|
|
4596
|
+
*/
|
|
4597
|
+
deadTokens = /* @__PURE__ */ new Set();
|
|
4393
4598
|
/** 首次探测顺序(来自配置 hint),未配置则先试 sandbox(开发场景占多数) */
|
|
4394
4599
|
probeOrder;
|
|
4395
4600
|
teamId;
|
|
@@ -4426,6 +4631,11 @@ var ActivityPushChannel = class {
|
|
|
4426
4631
|
}
|
|
4427
4632
|
/** 注册 Activity push token */
|
|
4428
4633
|
addToken(sessionId, token) {
|
|
4634
|
+
const oldToken = this.tokens.get(sessionId);
|
|
4635
|
+
if (oldToken && oldToken !== token) {
|
|
4636
|
+
this.tokenEnv.delete(oldToken);
|
|
4637
|
+
this.deadTokens.delete(oldToken);
|
|
4638
|
+
}
|
|
4429
4639
|
const existed = this.tokens.has(sessionId);
|
|
4430
4640
|
this.tokens.set(sessionId, token);
|
|
4431
4641
|
console.log(`[ActivityPushChannel] Token ${existed ? "updated" : "registered"}: session=${sessionId.slice(0, 8)}\u2026 token=${token.slice(0, 16)}\u2026`);
|
|
@@ -4434,65 +4644,82 @@ var ActivityPushChannel = class {
|
|
|
4434
4644
|
removeToken(sessionId) {
|
|
4435
4645
|
const tok = this.tokens.get(sessionId);
|
|
4436
4646
|
this.tokens.delete(sessionId);
|
|
4437
|
-
if (tok)
|
|
4647
|
+
if (tok) {
|
|
4648
|
+
this.tokenEnv.delete(tok);
|
|
4649
|
+
this.deadTokens.delete(tok);
|
|
4650
|
+
}
|
|
4438
4651
|
}
|
|
4439
|
-
/** 发送 content-state 更新到指定会话的 Live Activity
|
|
4440
|
-
async updateActivity(sessionId, contentState) {
|
|
4652
|
+
/** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知)。返回 true 表示实际发出 */
|
|
4653
|
+
async updateActivity(sessionId, contentState, opts) {
|
|
4441
4654
|
const token = this.tokens.get(sessionId);
|
|
4442
|
-
if (!token) return;
|
|
4655
|
+
if (!token) return false;
|
|
4443
4656
|
const now = Math.floor(Date.now() / 1e3);
|
|
4657
|
+
const priority = opts?.priority ?? "5";
|
|
4444
4658
|
const payload = {
|
|
4445
4659
|
aps: {
|
|
4446
4660
|
timestamp: now,
|
|
4447
4661
|
event: "update",
|
|
4448
4662
|
"content-state": contentState,
|
|
4449
|
-
|
|
4450
|
-
"stale-date": now + 120
|
|
4663
|
+
"stale-date": now + 600
|
|
4451
4664
|
}
|
|
4452
4665
|
};
|
|
4453
4666
|
try {
|
|
4454
|
-
await this.sendToAPNs(token, payload, {
|
|
4667
|
+
await this.sendToAPNs(token, payload, {
|
|
4668
|
+
priority,
|
|
4669
|
+
collapseId: `state-${sessionId.slice(0, 54)}`
|
|
4670
|
+
});
|
|
4671
|
+
return true;
|
|
4455
4672
|
} catch (err) {
|
|
4456
4673
|
console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
|
|
4674
|
+
return false;
|
|
4457
4675
|
}
|
|
4458
4676
|
}
|
|
4459
|
-
/** 发送带通知的 content-state
|
|
4677
|
+
/** 发送带通知的 content-state 更新(审批请求时使用),返回是否发送成功 */
|
|
4460
4678
|
async updateActivityWithAlert(sessionId, contentState, alert) {
|
|
4461
4679
|
const token = this.tokens.get(sessionId);
|
|
4462
|
-
if (!token) return;
|
|
4680
|
+
if (!token) return false;
|
|
4463
4681
|
const now = Math.floor(Date.now() / 1e3);
|
|
4464
4682
|
const payload = {
|
|
4465
4683
|
aps: {
|
|
4466
4684
|
timestamp: now,
|
|
4467
4685
|
event: "update",
|
|
4468
4686
|
"content-state": contentState,
|
|
4469
|
-
"stale-date": now +
|
|
4687
|
+
"stale-date": now + 600,
|
|
4470
4688
|
alert,
|
|
4471
4689
|
sound: "default"
|
|
4472
4690
|
}
|
|
4473
4691
|
};
|
|
4474
4692
|
try {
|
|
4475
4693
|
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4694
|
+
return true;
|
|
4476
4695
|
} catch (err) {
|
|
4477
4696
|
console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
|
|
4697
|
+
return false;
|
|
4478
4698
|
}
|
|
4479
4699
|
}
|
|
4480
|
-
/**
|
|
4481
|
-
|
|
4700
|
+
/**
|
|
4701
|
+
* 结束指定会话的 Live Activity。
|
|
4702
|
+
* 可选 alert:APNs event=end 时同时推送横幅通知 + 声音,用于在会话完成时提醒用户。
|
|
4703
|
+
*/
|
|
4704
|
+
async endActivity(sessionId, contentState, opts) {
|
|
4482
4705
|
const token = this.tokens.get(sessionId);
|
|
4483
4706
|
if (!token) return;
|
|
4484
4707
|
const now = Math.floor(Date.now() / 1e3);
|
|
4485
|
-
const
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
"content-state": contentState
|
|
4490
|
-
}
|
|
4708
|
+
const aps = {
|
|
4709
|
+
timestamp: now,
|
|
4710
|
+
event: "end",
|
|
4711
|
+
"content-state": contentState
|
|
4491
4712
|
};
|
|
4713
|
+
if (opts?.alert) {
|
|
4714
|
+
aps.alert = opts.alert;
|
|
4715
|
+
aps.sound = "default";
|
|
4716
|
+
}
|
|
4717
|
+
const payload = { aps };
|
|
4492
4718
|
try {
|
|
4493
4719
|
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4494
4720
|
} catch (err) {
|
|
4495
|
-
|
|
4721
|
+
this.tokens.delete(sessionId);
|
|
4722
|
+
throw err;
|
|
4496
4723
|
}
|
|
4497
4724
|
this.tokens.delete(sessionId);
|
|
4498
4725
|
}
|
|
@@ -4503,29 +4730,44 @@ var ActivityPushChannel = class {
|
|
|
4503
4730
|
/**
|
|
4504
4731
|
* 发送 APNs,自动处理环境探测。
|
|
4505
4732
|
* 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
|
|
4506
|
-
* 收到 BadDeviceToken
|
|
4733
|
+
* 收到 BadDeviceToken / BadEnvironmentKeyInToken 自动切到另一个环境,
|
|
4734
|
+
* 并把成功的环境绑定到该 token。
|
|
4507
4735
|
*/
|
|
4508
4736
|
async sendToAPNs(deviceToken, payload, opts = {}) {
|
|
4737
|
+
if (this.deadTokens.has(deviceToken)) {
|
|
4738
|
+
throw new Error(`token permanently dead (Activity ended): ${deviceToken.slice(0, 16)}\u2026`);
|
|
4739
|
+
}
|
|
4509
4740
|
const known = this.tokenEnv.get(deviceToken);
|
|
4510
4741
|
if (known) {
|
|
4511
4742
|
return this.sendToAPNsOnce(deviceToken, payload, opts, known);
|
|
4512
4743
|
}
|
|
4744
|
+
const short = deviceToken.slice(0, 16);
|
|
4745
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
|
|
4513
4746
|
let lastErr = null;
|
|
4514
4747
|
for (const env of this.probeOrder) {
|
|
4515
4748
|
try {
|
|
4749
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe try ${env} token=${short}\u2026`);
|
|
4516
4750
|
await this.sendToAPNsOnce(deviceToken, payload, opts, env);
|
|
4517
4751
|
this.tokenEnv.set(deviceToken, env);
|
|
4518
|
-
|
|
4519
|
-
console.log(`[ActivityPushChannel] Token bound to ${env} after probe (token=${deviceToken.slice(0, 16)}\u2026)`);
|
|
4520
|
-
}
|
|
4752
|
+
console.log(`[ActivityPushChannel] \u2705 probe bound to ${env} (token=${short}\u2026)`);
|
|
4521
4753
|
return;
|
|
4522
4754
|
} catch (err) {
|
|
4523
4755
|
lastErr = err;
|
|
4756
|
+
const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
|
|
4757
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
|
|
4524
4758
|
if (!isBadDeviceTokenError(err)) {
|
|
4525
4759
|
throw err;
|
|
4526
4760
|
}
|
|
4527
4761
|
}
|
|
4528
4762
|
}
|
|
4763
|
+
this.deadTokens.add(deviceToken);
|
|
4764
|
+
for (const [sid, tok] of this.tokens) {
|
|
4765
|
+
if (tok === deviceToken) {
|
|
4766
|
+
this.tokens.delete(sid);
|
|
4767
|
+
console.warn(`[ActivityPushChannel] \u2620\uFE0F dead token, session removed: session=${sid.slice(0, 8)}\u2026 token=${short}\u2026 (Activity ended/iOS reinstalled)`);
|
|
4768
|
+
break;
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4529
4771
|
throw lastErr ?? new Error("APNs send failed: all environments rejected token");
|
|
4530
4772
|
}
|
|
4531
4773
|
/** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
|
|
@@ -4541,21 +4783,25 @@ var ActivityPushChannel = class {
|
|
|
4541
4783
|
} catch (err) {
|
|
4542
4784
|
return reject(err);
|
|
4543
4785
|
}
|
|
4544
|
-
const
|
|
4786
|
+
const headers = {
|
|
4545
4787
|
":method": "POST",
|
|
4546
4788
|
":path": `/3/device/${deviceToken}`,
|
|
4547
4789
|
"authorization": `bearer ${jwt}`,
|
|
4548
4790
|
"apns-topic": topic,
|
|
4549
4791
|
"apns-push-type": "liveactivity",
|
|
4550
4792
|
"apns-priority": priority,
|
|
4551
|
-
"apns-expiration": String(Math.floor(Date.now() / 1e3) +
|
|
4793
|
+
"apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
|
|
4552
4794
|
"content-type": "application/json",
|
|
4553
4795
|
"content-length": Buffer.byteLength(payloadStr)
|
|
4554
|
-
}
|
|
4796
|
+
};
|
|
4797
|
+
if (opts.collapseId) {
|
|
4798
|
+
headers["apns-collapse-id"] = opts.collapseId;
|
|
4799
|
+
}
|
|
4800
|
+
const req = client.request(headers);
|
|
4555
4801
|
let statusCode = 0;
|
|
4556
4802
|
let responseData = "";
|
|
4557
|
-
req.on("response", (
|
|
4558
|
-
statusCode = Number(
|
|
4803
|
+
req.on("response", (headers2) => {
|
|
4804
|
+
statusCode = Number(headers2[":status"] ?? 0);
|
|
4559
4805
|
});
|
|
4560
4806
|
req.on("data", (chunk) => {
|
|
4561
4807
|
responseData += chunk;
|
|
@@ -4612,10 +4858,10 @@ var ApnsError = class extends Error {
|
|
|
4612
4858
|
};
|
|
4613
4859
|
function isBadDeviceTokenError(err) {
|
|
4614
4860
|
if (!(err instanceof ApnsError)) return false;
|
|
4615
|
-
if (err.statusCode !== 400 && err.statusCode !== 410) return false;
|
|
4861
|
+
if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
|
|
4616
4862
|
try {
|
|
4617
4863
|
const parsed = JSON.parse(err.responseBody);
|
|
4618
|
-
return parsed.reason === "BadDeviceToken" || parsed.reason === "Unregistered";
|
|
4864
|
+
return parsed.reason === "BadDeviceToken" || parsed.reason === "BadEnvironmentKeyInToken" || parsed.reason === "Unregistered";
|
|
4619
4865
|
} catch {
|
|
4620
4866
|
return false;
|
|
4621
4867
|
}
|
|
@@ -5176,9 +5422,6 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
5176
5422
|
}
|
|
5177
5423
|
};
|
|
5178
5424
|
|
|
5179
|
-
// src/server.ts
|
|
5180
|
-
var import_promises8 = require("fs/promises");
|
|
5181
|
-
|
|
5182
5425
|
// src/terminal/TerminalExecutor.ts
|
|
5183
5426
|
var import_node_child_process8 = require("child_process");
|
|
5184
5427
|
var import_uuid5 = require("uuid");
|
|
@@ -6682,7 +6925,8 @@ async function start(opts = {}) {
|
|
|
6682
6925
|
onFire: async (task) => {
|
|
6683
6926
|
const p = task.payload;
|
|
6684
6927
|
if (p.kind === "create") {
|
|
6685
|
-
await (0, import_promises7.
|
|
6928
|
+
const dirExists = await (0, import_promises7.stat)(p.projectPath).then((s) => s.isDirectory()).catch(() => false);
|
|
6929
|
+
if (!dirExists) await (0, import_promises7.mkdir)(p.projectPath, { recursive: true });
|
|
6686
6930
|
const session = await sessionManager.createSession(
|
|
6687
6931
|
p.projectPath,
|
|
6688
6932
|
p.message,
|
|
@@ -6750,7 +6994,8 @@ async function start(opts = {}) {
|
|
|
6750
6994
|
try {
|
|
6751
6995
|
switch (event.type) {
|
|
6752
6996
|
case "create_session": {
|
|
6753
|
-
await (0, import_promises7.
|
|
6997
|
+
const dirExists = await (0, import_promises7.stat)(event.projectPath).then((s) => s.isDirectory()).catch(() => false);
|
|
6998
|
+
if (!dirExists) await (0, import_promises7.mkdir)(event.projectPath, { recursive: true });
|
|
6754
6999
|
const resumeId = event.resumeSessionId ?? event.newSessionId;
|
|
6755
7000
|
if (resumeId) sessionFileWatcher.unwatch(resumeId);
|
|
6756
7001
|
await sessionManager.createSession(
|
|
@@ -6810,38 +7055,44 @@ async function start(opts = {}) {
|
|
|
6810
7055
|
sessions: sessionManager.getActiveSessions()
|
|
6811
7056
|
});
|
|
6812
7057
|
sessionManager.flushPendingAssistant(event.sessionId);
|
|
6813
|
-
const bufferedEvents = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
6814
7058
|
if (sessionManager.isBufferTruncated(event.sessionId)) {
|
|
6815
7059
|
const projectPath = sessionManager.getSessionProjectPath(event.sessionId);
|
|
6816
7060
|
if (projectPath) {
|
|
6817
7061
|
const historyResult = await getSessionHistory(projectPath, event.sessionId);
|
|
7062
|
+
const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
6818
7063
|
if (historyResult.ok && historyResult.value.length > 0) {
|
|
6819
|
-
const merged = [...historyResult.value, ...
|
|
7064
|
+
const merged = [...historyResult.value, ...buffered];
|
|
6820
7065
|
wsBridge.send(ws, {
|
|
6821
7066
|
type: "session_history",
|
|
6822
7067
|
sessionId: event.sessionId,
|
|
6823
7068
|
events: merged
|
|
6824
7069
|
});
|
|
6825
|
-
} else if (
|
|
7070
|
+
} else if (buffered.length > 0) {
|
|
6826
7071
|
wsBridge.send(ws, {
|
|
6827
7072
|
type: "session_history",
|
|
6828
7073
|
sessionId: event.sessionId,
|
|
6829
|
-
events:
|
|
7074
|
+
events: buffered
|
|
6830
7075
|
});
|
|
6831
7076
|
}
|
|
6832
|
-
} else
|
|
7077
|
+
} else {
|
|
7078
|
+
const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
7079
|
+
if (buffered.length > 0) {
|
|
7080
|
+
wsBridge.send(ws, {
|
|
7081
|
+
type: "session_history",
|
|
7082
|
+
sessionId: event.sessionId,
|
|
7083
|
+
events: buffered
|
|
7084
|
+
});
|
|
7085
|
+
}
|
|
7086
|
+
}
|
|
7087
|
+
} else {
|
|
7088
|
+
const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
7089
|
+
if (buffered.length > 0) {
|
|
6833
7090
|
wsBridge.send(ws, {
|
|
6834
7091
|
type: "session_history",
|
|
6835
7092
|
sessionId: event.sessionId,
|
|
6836
|
-
events:
|
|
7093
|
+
events: buffered
|
|
6837
7094
|
});
|
|
6838
7095
|
}
|
|
6839
|
-
} else if (bufferedEvents.length > 0) {
|
|
6840
|
-
wsBridge.send(ws, {
|
|
6841
|
-
type: "session_history",
|
|
6842
|
-
sessionId: event.sessionId,
|
|
6843
|
-
events: bufferedEvents
|
|
6844
|
-
});
|
|
6845
7096
|
}
|
|
6846
7097
|
for (const req of approvalProxy.getPendingRequestsForSession(event.sessionId)) {
|
|
6847
7098
|
wsBridge.send(ws, { type: "approval_request", request: req });
|
|
@@ -6938,7 +7189,7 @@ async function start(opts = {}) {
|
|
|
6938
7189
|
if (!isStreaming) {
|
|
6939
7190
|
const filePath = getSessionFilePath(event.projectPath, event.sessionId);
|
|
6940
7191
|
try {
|
|
6941
|
-
const fileStat = await (0,
|
|
7192
|
+
const fileStat = await (0, import_promises7.stat)(filePath);
|
|
6942
7193
|
sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
|
|
6943
7194
|
} catch {
|
|
6944
7195
|
}
|
|
@@ -7022,6 +7273,9 @@ async function start(opts = {}) {
|
|
|
7022
7273
|
wsBridge.clearViewingSession(ws);
|
|
7023
7274
|
break;
|
|
7024
7275
|
}
|
|
7276
|
+
case "approval_displayed": {
|
|
7277
|
+
break;
|
|
7278
|
+
}
|
|
7025
7279
|
case "always_allow_tool": {
|
|
7026
7280
|
approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
|
|
7027
7281
|
break;
|
|
@@ -7221,7 +7475,7 @@ async function start(opts = {}) {
|
|
|
7221
7475
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
7222
7476
|
const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
|
|
7223
7477
|
notificationService.notifyApproval(request, pendingCount);
|
|
7224
|
-
},
|
|
7478
|
+
}, 3e3);
|
|
7225
7479
|
setTimeout(() => {
|
|
7226
7480
|
if (!approvalProxy.isPending(request.id)) return;
|
|
7227
7481
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|