lark-remote 0.5.19 → 0.5.21
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/CHANGELOG.md +29 -0
- package/dist/bridge/approval-actions.d.ts +1 -1
- package/dist/bridge/approval-actions.js +1 -1
- package/dist/bridge/bash-run.d.ts +71 -0
- package/dist/bridge/bash-run.js +217 -0
- package/dist/bridge/compact.d.ts +155 -0
- package/dist/bridge/compact.js +450 -0
- package/dist/bridge/final-usage.js +4 -4
- package/dist/bridge/inbound-media.js +72 -46
- package/dist/bridge/index.d.ts +106 -153
- package/dist/bridge/index.js +400 -1000
- package/dist/bridge/job-watch.d.ts +68 -0
- package/dist/bridge/job-watch.js +146 -0
- package/dist/bridge/queue-manager.d.ts +3 -3
- package/dist/bridge/queue-manager.js +10 -29
- package/dist/card/approval-render.d.ts +2 -2
- package/dist/card/approval-render.js +141 -130
- package/dist/card/card-budget.js +123 -98
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +114 -159
- package/dist/card/run-state.d.ts +1 -15
- package/dist/card/run-state.js +358 -292
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +21 -17
- package/dist/card/usage-format.d.ts +22 -7
- package/dist/card/usage-format.js +119 -100
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/config/codex-config.js +130 -106
- package/dist/config/index.d.ts +16 -2
- package/dist/config/index.js +23 -8
- package/dist/connector/index.js +6 -6
- package/dist/error-classification.js +3 -2
- package/dist/inbound/placeholder.d.ts +1 -1
- package/dist/inbound/placeholder.js +1 -1
- package/dist/index.js +387 -281
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +254 -237
- package/dist/model-catalog.d.ts +2 -2
- package/dist/model-catalog.js +3 -8
- package/dist/platform/identity.js +1 -1
- package/dist/platform/path.d.ts +1 -1
- package/dist/platform/path.js +1 -1
- package/dist/platform/sleep-blocker.js +1 -1
- package/dist/platform/terminator-win32.js +2 -1
- package/dist/router/commands/ls.d.ts +3 -0
- package/dist/router/commands/ls.js +256 -175
- package/dist/router/commands/order.js +184 -159
- package/dist/router/commands/resume.js +190 -152
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/config/dsh.d.ts +4 -8
- package/dist/router/config/dsh.js +3 -10
- package/dist/router/config/kimi.js +1 -1
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/config-diff.js +1 -1
- package/dist/router/index.d.ts +56 -11
- package/dist/router/index.js +401 -391
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +6 -4
- package/dist/runner/codex/app-server/runner.d.ts +9 -2
- package/dist/runner/codex/app-server/runner.js +30 -25
- package/dist/runner/codex/app-server/translator.d.ts +2 -5
- package/dist/runner/codex/app-server/translator.js +291 -238
- package/dist/runner/common/acp/acp-approval.d.ts +1 -1
- package/dist/runner/common/acp/acp-approval.js +1 -1
- package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
- package/dist/runner/common/acp/base-acp-runner.js +26 -65
- package/dist/runner/common/acp/base-translator.d.ts +12 -2
- package/dist/runner/common/acp/base-translator.js +50 -26
- package/dist/runner/common/acp/protocol-types.d.ts +1 -1
- package/dist/runner/common/acp/protocol-types.js +2 -2
- package/dist/runner/common/connection-based-runner.d.ts +32 -1
- package/dist/runner/common/connection-based-runner.js +32 -2
- package/dist/runner/common/jsonl-stream.js +154 -187
- package/dist/runner/common/jsonrpc/client.d.ts +29 -0
- package/dist/runner/common/jsonrpc/client.js +43 -12
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
- package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
- package/dist/runner/common/spawning-runner.d.ts +6 -1
- package/dist/runner/common/spawning-runner.js +9 -4
- package/dist/runner/common/turn-started.d.ts +8 -0
- package/dist/runner/common/turn-started.js +11 -0
- package/dist/runner/dsh/approval.js +84 -51
- package/dist/runner/dsh/catalog-probe.d.ts +17 -0
- package/dist/runner/dsh/catalog-probe.js +10 -0
- package/dist/runner/dsh/client.d.ts +7 -11
- package/dist/runner/dsh/client.js +356 -289
- package/dist/runner/dsh/exit-cancel.d.ts +2 -2
- package/dist/runner/dsh/exit-cancel.js +2 -2
- package/dist/runner/dsh/inflight.d.ts +1 -1
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +44 -4
- package/dist/runner/dsh/runner.js +296 -199
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +1 -2
- package/dist/runner/kimi/acp/runner.js +82 -46
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
- package/dist/runner/pi/rpc/protocol-types.js +1 -1
- package/dist/runner/pi/rpc/runner.d.ts +3 -0
- package/dist/runner/pi/rpc/runner.js +27 -42
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/runner/types.d.ts +2 -2
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +160 -122
- package/dist/session/codex/rollout-reader.js +181 -182
- package/dist/session/common/throw-session-list-error.d.ts +10 -0
- package/dist/session/common/throw-session-list-error.js +16 -0
- package/dist/session/dsh/sessions.js +291 -235
- package/dist/session/kimi/sessions.d.ts +40 -8
- package/dist/session/kimi/sessions.js +373 -301
- package/dist/session/opencode/sessions.d.ts +142 -1
- package/dist/session/opencode/sessions.js +378 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +9 -1
- package/dist/session/session-store.js +99 -99
- package/dist/update/version-check.js +2 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
package/dist/bridge/index.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { sleep } from '../common/sleep.js';
|
|
3
2
|
import { isContentFilterFailure } from '../common/turn-failure.js';
|
|
4
3
|
import { resolveFinalUsage, errorMessage } from './final-usage.js';
|
|
5
4
|
import { getLogger } from '../logger/index.js';
|
|
6
5
|
import { RunCardSession } from '../card/run-card-session.js';
|
|
7
|
-
import {
|
|
6
|
+
import { renderRunCardDetailed } from '../card/run-renderer.js';
|
|
8
7
|
import { writeFinalAnswerFile, finalAnswerFileFailureCard } from '../card/final-answer-file.js';
|
|
9
8
|
import { bestEffortUnlink } from '../common/fs.js';
|
|
10
|
-
import {
|
|
11
|
-
import { BashCardSession, capBashOutput } from '../card/bash-card-session.js';
|
|
12
|
-
import { isLiveBackgroundJob, isRunActive, isRunTerminal, markBackgroundJobsUnknown, removeBackgroundJob, syncBackgroundJobs, } from '../card/run-state.js';
|
|
9
|
+
import { isRunActive, isRunTerminal } from '../card/run-state.js';
|
|
13
10
|
import { buildSessionHistoryCard } from '../card/card-helpers.js';
|
|
14
11
|
import { agentDisplayName, resumeUseButton } from '../card/card-shared.js';
|
|
15
12
|
import { enforceCardBudget } from '../card/card-budget.js';
|
|
16
13
|
import { RunUsageCollector } from './run-usage.js';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
14
|
+
import { CompactLifecycle, runnerSupportsCompact, terminalReactionEmoji } from './compact.js';
|
|
15
|
+
import { JobWatchLifecycle } from './job-watch.js';
|
|
16
|
+
import { BashRunLifecycle } from './bash-run.js';
|
|
20
17
|
import { QueueManager, } from './queue-manager.js';
|
|
21
18
|
import { ApprovalActions } from './approval-actions.js';
|
|
22
19
|
import { createRunApprovalCoordinator, remindPendingApproval } from './run-approval.js';
|
|
@@ -42,21 +39,6 @@ const MAX_CACHED_RUNNER_WORKSPACES = 10;
|
|
|
42
39
|
* message forever.
|
|
43
40
|
*/
|
|
44
41
|
const IDLE_TIMEOUT_MS = 15 * 60 * 1000;
|
|
45
|
-
/**
|
|
46
|
-
* Emoji reaction per run terminal state, so a glance at the user's original
|
|
47
|
-
* message distinguishes how the run ended. Unknown/non-terminal states fall
|
|
48
|
-
* back to 'Done' (default).
|
|
49
|
-
*/
|
|
50
|
-
const TERMINAL_REACTION_EMOJI = {
|
|
51
|
-
error: 'ERROR',
|
|
52
|
-
idle_timeout: 'Alarm',
|
|
53
|
-
interrupted: 'SHHH',
|
|
54
|
-
// 本轮结束但目标继续:任务还没完,不能打 ✓(Done)。
|
|
55
|
-
goal_continuing: 'OnIt',
|
|
56
|
-
};
|
|
57
|
-
function terminalReactionEmoji(terminal) {
|
|
58
|
-
return TERMINAL_REACTION_EMOJI[terminal] ?? 'Done';
|
|
59
|
-
}
|
|
60
42
|
/**
|
|
61
43
|
* 完成通知卡的标题 / 配色 / 结果行。
|
|
62
44
|
*
|
|
@@ -81,40 +63,36 @@ function completionNotificationPresentation(terminal) {
|
|
|
81
63
|
return { title: '✅ 会话已完成', template: 'green', result: 'completed' };
|
|
82
64
|
}
|
|
83
65
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*/
|
|
88
|
-
|
|
89
|
-
|
|
66
|
+
// --- approvalModeSettingsFor 的逐 agent 快照(字段口径单表 + 单函数) ---
|
|
67
|
+
// Map 查表避免 constructor/toString 之类键命中 Object.prototype;字段顺序
|
|
68
|
+
// 与原 switch 分支逐字一致。不碰 Bridge 实例状态,避免 this 绑定问题。
|
|
69
|
+
/** 各 agent 审批设置快照的字段口径(顺序 = 原分支逐字)。 */
|
|
70
|
+
const APPROVAL_SETTINGS_FIELDS = new Map([
|
|
71
|
+
['codex', ['approvalPolicy', 'sandbox']],
|
|
72
|
+
['kimi', ['permissionMode']],
|
|
73
|
+
['opencode', ['mode']],
|
|
74
|
+
['dsh', ['permissionPreset']],
|
|
75
|
+
]);
|
|
76
|
+
function approvalSettingsFor(agents, kind) {
|
|
77
|
+
const fields = APPROVAL_SETTINGS_FIELDS.get(kind);
|
|
78
|
+
if (!fields)
|
|
79
|
+
return undefined;
|
|
80
|
+
const cfg = agents?.[kind];
|
|
81
|
+
if (!cfg)
|
|
82
|
+
return undefined;
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const field of fields)
|
|
85
|
+
out[field] = cfg[field];
|
|
86
|
+
return out;
|
|
90
87
|
}
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
*
|
|
99
|
-
* 只做鸭子探测会把「漏实现」降级成静默隐藏按钮(dsh 就这样漏了),所以声明表
|
|
100
|
-
* 是必需项;只信声明表则可能渲染出点不动的死按钮,所以实现检查也保留。两者
|
|
101
|
-
* 不一致时打 error 日志,让缺陷在运行日志里可见。
|
|
102
|
-
*/
|
|
103
|
-
function runnerSupportsCompact(runner, kind) {
|
|
104
|
-
const declared = supportsCompact(kind);
|
|
105
|
-
const implemented = runnerHasRunCompact(runner);
|
|
106
|
-
if (declared !== implemented && !compactMismatchWarned.has(kind)) {
|
|
107
|
-
compactMismatchWarned.add(kind);
|
|
108
|
-
getLogger().error(`[lark-remote] compact capability mismatch kind=${kind} declared=${declared} implemented=${implemented}`);
|
|
109
|
-
}
|
|
110
|
-
return declared && implemented;
|
|
88
|
+
/** runner 是否使用 live result 事件(显式传 runner + usageAuthority 状态)。 */
|
|
89
|
+
function computeLiveResultEvents(runner, usageAuthority) {
|
|
90
|
+
return runner.usesLiveResultEvents?.() ?? usageAuthority === 'live';
|
|
91
|
+
}
|
|
92
|
+
/** 待审批计数(startIdleWatchdog 的 pendingApprovals 回调,显式传 coordinators 表)。 */
|
|
93
|
+
function pendingApprovalCount(coordinators, runId) {
|
|
94
|
+
return coordinators.get(runId)?.pendingCount() ?? 0;
|
|
111
95
|
}
|
|
112
|
-
/**
|
|
113
|
-
* 每个 cwd 保留的「可压缩 run」条数上限。Compact 按钮挂在终态 run 卡片上,
|
|
114
|
-
* 卡片会被后续消息不断顶上去;无上限会让长跑进程随 run 数无界增长,而用户
|
|
115
|
-
* 实际能点到的只有最近几张卡,20 条足以覆盖。
|
|
116
|
-
*/
|
|
117
|
-
const MAX_COMPACTABLE_RUNS_PER_CWD = 20;
|
|
118
96
|
/**
|
|
119
97
|
* Bridge owns the per-workspace serial processing queue (§9.6) and the
|
|
120
98
|
* single-card streaming forwarding loop (§9.12 watchdog + session sync).
|
|
@@ -149,7 +127,7 @@ export class Bridge {
|
|
|
149
127
|
mediaHandler;
|
|
150
128
|
/**
|
|
151
129
|
* Per-workspace runner instances, keyed by (cwd, agentKind).
|
|
152
|
-
*
|
|
130
|
+
* Each agentKind has its own cache slot, so switching
|
|
153
131
|
* defaultAgent creates a NEW runner without evicting the old one. This
|
|
154
132
|
* eliminates the need for clearRunners() on agent switch, preventing the
|
|
155
133
|
* regression where clearRunners() orphaned a running subprocess and
|
|
@@ -175,16 +153,11 @@ export class Bridge {
|
|
|
175
153
|
coordinatorFor: (runId) => this.approvalCoordinators.get(runId),
|
|
176
154
|
});
|
|
177
155
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* 为什么按 runId 存而不是「每 cwd 只留最新一条」:compact 卡片动作走串行
|
|
183
|
-
* 队列(会被正在跑的 run 挡住),点击到执行之间可能夹着别的 run 收尾。
|
|
184
|
-
* 单槽会被覆盖 —— 旧卡片的压缩必然报「该任务已结束或不属于当前会话」,
|
|
185
|
-
* 与 session 是否还在、能否压缩无关(2026-10-08 线上缺陷)。
|
|
156
|
+
* Compact 生命周期(见 compact.ts):可压缩 run 记账、终态卡 / resume 卡
|
|
157
|
+
* 两条 Compact 入口、kimi 在途检测与压缩流式卡。activeRuns 槽位表按引用
|
|
158
|
+
* 共用——压缩注册前确认槽位空闲,否则 /stop 停错对象。
|
|
186
159
|
*/
|
|
187
|
-
|
|
160
|
+
compact;
|
|
188
161
|
/**
|
|
189
162
|
* 终态卡片的渲染快照(runId → state + options):run 结束后 activeRuns 已
|
|
190
163
|
* 清空,而卡片上的「⏹ 停止」后台任务按钮(job.kill)还活着,需要按 runId
|
|
@@ -195,26 +168,17 @@ export class Bridge {
|
|
|
195
168
|
runCardSnapshots = new Map();
|
|
196
169
|
static RUN_CARD_SNAPSHOT_MAX = 8;
|
|
197
170
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* 受 RUN_CARD_SNAPSHOT_MAX 约束(一张卡最多一条流)。
|
|
171
|
+
* 终态卡后台任务订阅与停止(见 job-watch.ts):job.kill、名单订阅与
|
|
172
|
+
* PATCH 串行链收在该模块;jobCardPatches 链内渲染读 snapshot.state,
|
|
173
|
+
* 「停止」与「订阅帧」并发不会把卡片写回旧内容。
|
|
202
174
|
*/
|
|
203
|
-
|
|
175
|
+
jobWatch;
|
|
204
176
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* 后发先至的那条会把卡片写回旧内容(任务又变回「运行中」)。
|
|
209
|
-
*/
|
|
210
|
-
jobCardPatches = new Map();
|
|
211
|
-
/**
|
|
212
|
-
* Active `!` bash runs, keyed by runId (NOT cwd). Bash runs bypass the serial
|
|
213
|
-
* queue and may run in parallel with claude runs / other bash runs in the same
|
|
214
|
-
* cwd, so they live outside `activeRuns`. Tracked so `/stop` can reach
|
|
215
|
-
* the per-run BashRunner instance via interruptCurrentRun.
|
|
177
|
+
* `!` bash 命令执行(见 bash-run.ts):独立 runId 空间、独立登记表,不进
|
|
178
|
+
* activeRuns;绝不 re-chain 到 QueueManager(会自等待死锁)。/stop 的 bash
|
|
179
|
+
* 侧匹配与中断也收在该模块,避免误杀并行跑的 `!` 任务。
|
|
216
180
|
*/
|
|
217
|
-
|
|
181
|
+
bashRuns;
|
|
218
182
|
constructor(deps) {
|
|
219
183
|
this.connector = deps.connector;
|
|
220
184
|
this.sessionStore = deps.sessionStore;
|
|
@@ -228,11 +192,36 @@ export class Bridge {
|
|
|
228
192
|
resolveCwd: (userId) => this.resolveCwd(userId),
|
|
229
193
|
getConfig: () => this.config,
|
|
230
194
|
});
|
|
195
|
+
this.compact = new CompactLifecycle({
|
|
196
|
+
activeRuns: this.activeRuns,
|
|
197
|
+
connector: this.connector,
|
|
198
|
+
sessionStore: this.sessionStore,
|
|
199
|
+
sessionReaderRegistry: this.sessionReaderRegistry,
|
|
200
|
+
getConfig: () => this.config,
|
|
201
|
+
getRunner: (cwd, kind, opts) => this.getRunner(cwd, kind, opts),
|
|
202
|
+
sendResult: (result, ctx, opts) => this.sendResult(result, ctx, opts),
|
|
203
|
+
startIdleWatchdog: (opts) => this.startIdleWatchdog(opts),
|
|
204
|
+
finishTurnReactions: (ctx, emoji) => this.finishTurnReactions(ctx, emoji),
|
|
205
|
+
resolveCwd: (userId) => this.resolveCwd(userId),
|
|
206
|
+
});
|
|
207
|
+
this.jobWatch = new JobWatchLifecycle({
|
|
208
|
+
getRunner: (cwd, kind) => this.getRunner(cwd, kind),
|
|
209
|
+
getRunCardSnapshot: (runId) => this.runCardSnapshots.get(runId),
|
|
210
|
+
updateCardInPlace: (card, ctx) => this.updateCardInPlace(card, ctx),
|
|
211
|
+
patchCard: (messageId, card) => this.patchCard(messageId, card),
|
|
212
|
+
});
|
|
213
|
+
this.bashRuns = new BashRunLifecycle({
|
|
214
|
+
connector: this.connector,
|
|
215
|
+
sessionStore: this.sessionStore,
|
|
216
|
+
resolveCwd: (userId) => this.resolveCwd(userId),
|
|
217
|
+
sendResult: (result, ctx) => this.sendResult(result, ctx),
|
|
218
|
+
finishTurnReactions: (ctx, emoji) => this.finishTurnReactions(ctx, emoji),
|
|
219
|
+
});
|
|
231
220
|
}
|
|
232
221
|
/**
|
|
233
222
|
* Get or create a runner for the given cwd, keyed by current defaultAgent.
|
|
234
223
|
*
|
|
235
|
-
*
|
|
224
|
+
* Cache key includes agentKind, so switching defaultAgent
|
|
236
225
|
* creates a NEW runner under a different key without evicting the old one.
|
|
237
226
|
* This eliminates the need for clearRunners() on agent switch.
|
|
238
227
|
*/
|
|
@@ -317,38 +306,7 @@ export class Bridge {
|
|
|
317
306
|
}
|
|
318
307
|
/** Per-agent approval-mode settings snapshot from the current config. */
|
|
319
308
|
approvalModeSettingsFor(kind) {
|
|
320
|
-
|
|
321
|
-
switch (kind) {
|
|
322
|
-
case 'codex': {
|
|
323
|
-
const codexConfig = agents?.codex;
|
|
324
|
-
if (!codexConfig)
|
|
325
|
-
return undefined;
|
|
326
|
-
return {
|
|
327
|
-
approvalPolicy: codexConfig.approvalPolicy,
|
|
328
|
-
sandbox: codexConfig.sandbox,
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
case 'kimi': {
|
|
332
|
-
const kimiConfig = agents?.kimi;
|
|
333
|
-
if (!kimiConfig)
|
|
334
|
-
return undefined;
|
|
335
|
-
return { permissionMode: kimiConfig.permissionMode };
|
|
336
|
-
}
|
|
337
|
-
case 'opencode': {
|
|
338
|
-
const opencodeConfig = agents?.opencode;
|
|
339
|
-
if (!opencodeConfig)
|
|
340
|
-
return undefined;
|
|
341
|
-
return { mode: opencodeConfig.mode };
|
|
342
|
-
}
|
|
343
|
-
case 'dsh': {
|
|
344
|
-
const dshConfig = agents?.dsh;
|
|
345
|
-
if (!dshConfig)
|
|
346
|
-
return undefined;
|
|
347
|
-
return { permissionPreset: dshConfig.permissionPreset };
|
|
348
|
-
}
|
|
349
|
-
default:
|
|
350
|
-
return undefined;
|
|
351
|
-
}
|
|
309
|
+
return approvalSettingsFor(this.config.agents, kind);
|
|
352
310
|
}
|
|
353
311
|
/** Get the current runner for a cwd. Used by router for agent-specific commands (.2). */
|
|
354
312
|
getCurrentRunner(cwd) {
|
|
@@ -379,9 +337,9 @@ export class Bridge {
|
|
|
379
337
|
*/
|
|
380
338
|
clearRunners() {
|
|
381
339
|
// Skip workspaces with an active run: evicting their runner would orphan
|
|
382
|
-
// the instance that owns the live subprocess
|
|
383
|
-
//
|
|
384
|
-
//
|
|
340
|
+
// the instance that owns the live subprocess — the running runner would
|
|
341
|
+
// become unreachable, stop() would hit a fresh empty runner, the real
|
|
342
|
+
// subprocess would never be killed and the queue chain would block forever.
|
|
385
343
|
// The running run already started with its (possibly now-stale) config, so
|
|
386
344
|
// keeping it is correct; finalizeRun deletes the cache entry when the run
|
|
387
345
|
// ends, and the next run picks up the new config/agent via getRunner.
|
|
@@ -645,25 +603,7 @@ export class Bridge {
|
|
|
645
603
|
}
|
|
646
604
|
/** Get all active bash runs with their state for /active command. */
|
|
647
605
|
getActiveBashRuns() {
|
|
648
|
-
|
|
649
|
-
for (const [runId, bashRun] of this.activeBashRuns) {
|
|
650
|
-
const state = bashRun.cardSession.currentState;
|
|
651
|
-
// Only include non-terminal runs (bash has no finalizing state)
|
|
652
|
-
if (state.terminal === 'running') {
|
|
653
|
-
result.push({
|
|
654
|
-
runId,
|
|
655
|
-
cwd: bashRun.cwd,
|
|
656
|
-
userId: bashRun.userId,
|
|
657
|
-
chatId: bashRun.chatId,
|
|
658
|
-
terminal: state.terminal,
|
|
659
|
-
command: bashRun.command,
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
return result;
|
|
664
|
-
}
|
|
665
|
-
async interruptCurrentRun(input) {
|
|
666
|
-
return (await this.interruptCurrentRunDetailed(input)).stopped;
|
|
606
|
+
return this.bashRuns.getActiveBashRuns();
|
|
667
607
|
}
|
|
668
608
|
/**
|
|
669
609
|
* Same as {@link interruptCurrentRun}, plus what the runner's stop actually
|
|
@@ -719,45 +659,11 @@ export class Bridge {
|
|
|
719
659
|
stop = stopResult;
|
|
720
660
|
}
|
|
721
661
|
}
|
|
722
|
-
// Then check active bash runs (! commands).
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
// `!`
|
|
726
|
-
|
|
727
|
-
// 没有 claude run 命中时才回退到 bash,避免误杀用户并行跑的 `!` 任务。
|
|
728
|
-
const cardScoped = input.runId !== undefined || input.cardMessageId !== undefined;
|
|
729
|
-
const includeBashRuns = (input.runId === undefined && input.cardMessageId === undefined && input.cwd === undefined) ||
|
|
730
|
-
activeTargets.length === 0;
|
|
731
|
-
const bashTargets = includeBashRuns
|
|
732
|
-
? [...this.activeBashRuns.entries()].filter(([runId, b]) => {
|
|
733
|
-
if (input.cwd !== undefined && b.cwd !== input.cwd)
|
|
734
|
-
return false;
|
|
735
|
-
if (b.userId !== input.userId || b.chatId !== input.chatId)
|
|
736
|
-
return false;
|
|
737
|
-
if (input.runId) {
|
|
738
|
-
if (runId === input.runId)
|
|
739
|
-
return true;
|
|
740
|
-
// Scoped card clicks may fall back only through their own card id.
|
|
741
|
-
if (cardScoped)
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
if (input.cardMessageId) {
|
|
745
|
-
return b.cardSession.cardMessageId === input.cardMessageId;
|
|
746
|
-
}
|
|
747
|
-
return true;
|
|
748
|
-
})
|
|
749
|
-
: [];
|
|
750
|
-
for (const [runId, b] of bashTargets) {
|
|
751
|
-
getLogger().info(`[lark-remote] interrupt hit bash runId=${runId} cwd=${b.cwd} userId=${input.userId}`);
|
|
752
|
-
await b.bashRunner.stop({ immediate: true });
|
|
753
|
-
// Update card to show interrupted state. exitCode 不传(保持 null):
|
|
754
|
-
// bash-renderer 的「已手动终止」文案要求 exitCode === null;传 -1 会让
|
|
755
|
-
// 该分支不可达,中断卡退化成「退出码: -1」的失败样式。
|
|
756
|
-
await b.cardSession.finish('interrupted');
|
|
757
|
-
this.activeBashRuns.delete(runId);
|
|
758
|
-
getLogger().info(`[lark-remote] interrupt bash stop done runId=${runId}`);
|
|
759
|
-
stoppedCount++;
|
|
760
|
-
}
|
|
662
|
+
// Then check active bash runs (! commands). 实现在 bash-run.ts
|
|
663
|
+
// (bridge 结构治理第 3 刀):bash 用独立 runId 空间、独立登记表,匹配
|
|
664
|
+
// 与停止整体收在 stopMatchingBashRuns 里——`/stop` 语义与「不误杀并行
|
|
665
|
+
// `!` 任务」的守卫注释随迁。
|
|
666
|
+
stoppedCount += await this.bashRuns.stopMatchingBashRuns(input, activeTargets.length);
|
|
761
667
|
if (stoppedCount === 0) {
|
|
762
668
|
getLogger().warn(`[lark-remote] interrupt miss userId=${input.userId} ` +
|
|
763
669
|
`requestedRunId=${input.runId ?? '(none)'} ` +
|
|
@@ -804,8 +710,8 @@ export class Bridge {
|
|
|
804
710
|
// Stop the EXACT runner instance that is running (carried by activeRun),
|
|
805
711
|
// not whatever getRunner(cwd) would return now. clearRunners() may have
|
|
806
712
|
// emptied the cache mid-run, in which case getRunner creates a fresh empty
|
|
807
|
-
// runner whose stop() is a no-op and the real subprocess is never killed
|
|
808
|
-
//
|
|
713
|
+
// runner whose stop() is a no-op and the real subprocess is never killed,
|
|
714
|
+
// blocking the queue chain forever.
|
|
809
715
|
const runner = active.runner;
|
|
810
716
|
// Bind the queue reset to THIS task's execution slot: the stop window
|
|
811
717
|
// below may outlive the task's settle, letting the serial chain advance
|
|
@@ -813,18 +719,28 @@ export class Bridge {
|
|
|
813
719
|
// unconditional reset would then zero the successor's count and mark
|
|
814
720
|
// its slot interrupted, hiding it from queue cards.
|
|
815
721
|
const stoppedSlot = this.queueManager.getExecutingSlot(cwd);
|
|
816
|
-
const [, stopResult] = await Promise.allSettled([
|
|
722
|
+
const [finishResult, stopResult] = await Promise.allSettled([
|
|
817
723
|
// 用户主动中断(/stop、stop 卡按钮、queue.immediate)显式打标
|
|
818
724
|
// interruptedReason,与审批超时自动取消(approval_timeout)、审批
|
|
819
725
|
// 卡取消(approval_cancelled)在终态数据上可区分(方案 A)。
|
|
820
726
|
active.session.finish('interrupted', { interruptedReason: 'user_stop' }),
|
|
821
727
|
runner.stop({ immediate: true }),
|
|
822
728
|
]);
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
:
|
|
729
|
+
// allSettled 的两个结果都必须检查:finish 被飞书拒绝时卡片会永久停在
|
|
730
|
+
// 「运行中」;runner.stop 失败时进程可能没死而槽位已被释放——两者都
|
|
731
|
+
// 不能静默吞掉(此前第一个结果整体丢弃、stop 失败仍记 "stop done")。
|
|
732
|
+
if (finishResult.status === 'rejected') {
|
|
733
|
+
getLogger().error(`[lark-remote] stop: terminal card finish failed runId=${active.runId}:`, finishResult.reason);
|
|
734
|
+
}
|
|
735
|
+
let stop;
|
|
736
|
+
if (stopResult.status === 'rejected') {
|
|
737
|
+
getLogger().error(`[lark-remote] stop: runner.stop failed runId=${active.runId} — 运行可能未终止:`, stopResult.reason);
|
|
738
|
+
// 上报「可能仍在跑」,调用方按 unconfirmed 口径聚合(不承诺已停止)。
|
|
739
|
+
stop = { status: 'unconfirmed', detail: errorMessage(stopResult.reason) };
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
stop = stopResult.value ?? undefined;
|
|
743
|
+
}
|
|
828
744
|
// The chain may have advanced while the stop was in flight: a new run
|
|
829
745
|
// (from a task that began during the stop window) may now occupy this
|
|
830
746
|
// cwd. Only delete the entry we actually stopped.
|
|
@@ -840,8 +756,8 @@ export class Bridge {
|
|
|
840
756
|
// interruptCurrentRun 的 await 窗口内 queue 链会推进下一条消息,其
|
|
841
757
|
// getRunner 可能刚拿走本实例(缓存命中先于本行执行)——立即 evict 会
|
|
842
758
|
// 让那个 run 的长驻连接脱管:进程残留至 idle TTL 才回收,期间持有
|
|
843
|
-
// agent 侧会话写锁(codex "thread already has an active writer"
|
|
844
|
-
//
|
|
759
|
+
// agent 侧会话写锁(codex "thread already has an active writer")。
|
|
760
|
+
// 改为标 stale:下一次「即将开跑」的 getRunner 在槽位空闲时
|
|
845
761
|
// 换新实例(见 getRunner 的 replaceStale),本次 run 的 finalize 收尾时
|
|
846
762
|
// 也会消费该标记;两者都保证不会有活跃 run 使用被 dispose 的实例。
|
|
847
763
|
// run-per-message runner 无长驻连接,保持立即 evict 不变。
|
|
@@ -863,7 +779,9 @@ export class Bridge {
|
|
|
863
779
|
if (stoppedSlot !== undefined) {
|
|
864
780
|
this.queueManager.resetExecutingCount(cwd, stoppedSlot);
|
|
865
781
|
}
|
|
866
|
-
|
|
782
|
+
if (stop?.status !== 'unconfirmed') {
|
|
783
|
+
getLogger().info(`[lark-remote] interrupt stop done runId=${active.runId}`);
|
|
784
|
+
}
|
|
867
785
|
return stop;
|
|
868
786
|
}
|
|
869
787
|
/**
|
|
@@ -914,25 +832,22 @@ export class Bridge {
|
|
|
914
832
|
/**
|
|
915
833
|
* Deliver a final answer that cannot fit in the run card as a Markdown file
|
|
916
834
|
* attachment. 卡片正文已被标记替代,文件是唯一的内容通道;此处绝不发纯文本
|
|
917
|
-
*
|
|
835
|
+
* (纯文本与卡片重复,也丢 Markdown 结构)。
|
|
918
836
|
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
837
|
+
* 发送成功即清理临时文件;失败时发一张提示卡(不是纯文本),完整回答
|
|
838
|
+
* 留在临时文件里(路径见错误日志),便于事后找回。
|
|
921
839
|
*/
|
|
922
840
|
async sendFinalAnswerFile(text, ctx) {
|
|
923
841
|
let filePath;
|
|
924
842
|
try {
|
|
925
843
|
filePath = writeFinalAnswerFile(text);
|
|
926
844
|
await this.connector.sendFile(ctx.chatId, filePath);
|
|
845
|
+
bestEffortUnlink(filePath);
|
|
927
846
|
}
|
|
928
847
|
catch (err) {
|
|
929
848
|
getLogger().error(`[lark-remote] failed to deliver complete final answer as file (kept at ${filePath ?? 'not written'}):`, err);
|
|
930
849
|
await this.sendResult({ card: finalAnswerFileFailureCard(errorMessage(err)) }, ctx);
|
|
931
850
|
}
|
|
932
|
-
finally {
|
|
933
|
-
if (filePath)
|
|
934
|
-
bestEffortUnlink(filePath);
|
|
935
|
-
}
|
|
936
851
|
}
|
|
937
852
|
/**
|
|
938
853
|
* Update an existing card in place (原地更新) instead of sending a new one.
|
|
@@ -1005,138 +920,23 @@ export class Bridge {
|
|
|
1005
920
|
this.runCardSnapshots.set(runId, snapshot);
|
|
1006
921
|
while (this.runCardSnapshots.size > Bridge.RUN_CARD_SNAPSHOT_MAX) {
|
|
1007
922
|
const oldest = this.runCardSnapshots.keys().next().value;
|
|
1008
|
-
if (oldest === undefined)
|
|
1009
|
-
break;
|
|
1010
923
|
this.runCardSnapshots.delete(oldest);
|
|
1011
924
|
// 卡片已经不在窗口里:继续订阅只是往一个没人看的 messageId 打 PATCH。
|
|
1012
|
-
this.stopJobWatch(oldest);
|
|
925
|
+
this.jobWatch.stopJobWatch(oldest);
|
|
1013
926
|
}
|
|
1014
927
|
return snapshot;
|
|
1015
928
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
* 后台任务的生命周期跨 run(这就是它需要单独入口的原因),所以解析路径是
|
|
1026
|
-
* 终态快照(runId → sessionId/cwd/agentKind)+ 该工作区的 runner 实例,
|
|
1027
|
-
* 而不是 activeRuns。成功后同步摘掉 state 里的任务并就地重渲染,否则按钮
|
|
1028
|
-
* 会留在卡片上而任务早已结束。
|
|
1029
|
-
*/
|
|
929
|
+
// =============================================================================
|
|
930
|
+
// 后台任务停止与订阅
|
|
931
|
+
// =============================================================================
|
|
932
|
+
// 实现住在 job-watch.ts(bridge 结构治理第 2 刀):job.kill、名单订阅、
|
|
933
|
+
// markJobsUnknown 与 PATCH 串行链整体搬过去。这里只留 killBackgroundJob
|
|
934
|
+
// 薄转发——router 的 job.kill 卡片动作按名字分发到 bridge 公开 API;
|
|
935
|
+
// 快照淘汰(rememberRunCardSnapshot)与收尾订阅(finalizeRun)直接调
|
|
936
|
+
// this.jobWatch 的同名方法。
|
|
937
|
+
/** 停止终态卡上的一个后台任务(job.kill 卡片动作)。 */
|
|
1030
938
|
async killBackgroundJob(runId, jobId, ctx) {
|
|
1031
|
-
|
|
1032
|
-
const sessionId = snapshot?.state.sessionId;
|
|
1033
|
-
if (!snapshot || !sessionId)
|
|
1034
|
-
return false;
|
|
1035
|
-
const runner = this.getRunner(snapshot.cwd, snapshot.agentKind);
|
|
1036
|
-
if (typeof runner.killJob !== 'function')
|
|
1037
|
-
return false;
|
|
1038
|
-
const killed = await runner.killJob(sessionId, jobId);
|
|
1039
|
-
if (!killed)
|
|
1040
|
-
return false;
|
|
1041
|
-
snapshot.state = removeBackgroundJob(snapshot.state, jobId);
|
|
1042
|
-
// 与订阅帧共用一条串行链:两条 PATCH 并发时,后发先至的那条会把卡片写回
|
|
1043
|
-
// 旧内容(刚停掉的任务又显示成「运行中」)。
|
|
1044
|
-
await this.enqueueJobCardPatch(runId, async () => {
|
|
1045
|
-
await this.updateCardInPlace(renderRunCard(snapshot.state, snapshot.options), ctx);
|
|
1046
|
-
return true;
|
|
1047
|
-
});
|
|
1048
|
-
// 卡片上已经没有在跑的任务:不必再等宿主推下一帧,直接退订。
|
|
1049
|
-
if (!(snapshot.state.backgroundJobs ?? []).some(isLiveBackgroundJob)) {
|
|
1050
|
-
this.stopJobWatch(runId);
|
|
1051
|
-
}
|
|
1052
|
-
return true;
|
|
1053
|
-
}
|
|
1054
|
-
/**
|
|
1055
|
-
* 让终态卡的后台任务小节跟着宿主继续更新。
|
|
1056
|
-
*
|
|
1057
|
-
* run 的卡片在 `result` 那一帧就关闭了,但后台任务还能再跑很久:不订阅,
|
|
1058
|
-
* 卡片会一直写「运行中」并留着停止按钮,点了必然失败。退订条件写
|
|
1059
|
-
* 在 {@link consumeJobRoster} 里——名单清空、patch 失败、订阅中断、快照被
|
|
1060
|
-
* 淘汰,任一命中即退出;**绝不因为 patch 失败补发新卡**。
|
|
1061
|
-
*/
|
|
1062
|
-
startBackgroundJobWatch(runId, snapshot, runner) {
|
|
1063
|
-
const live = (snapshot.state.backgroundJobs ?? []).some(isLiveBackgroundJob);
|
|
1064
|
-
if (!live)
|
|
1065
|
-
return;
|
|
1066
|
-
const sessionId = snapshot.state.sessionId;
|
|
1067
|
-
const messageId = snapshot.messageId;
|
|
1068
|
-
const watchJobs = runner.watchJobs;
|
|
1069
|
-
if (!sessionId || !messageId || typeof watchJobs !== 'function') {
|
|
1070
|
-
// 没有卡 id 就没有 patch 的落点:保留收尾快照,不另发一张卡。
|
|
1071
|
-
getLogger().info(`[lark-remote] background jobs stay on the turn snapshot runId=${runId} ` +
|
|
1072
|
-
`(sessionId=${sessionId ?? '-'} messageId=${messageId ?? '-'} watchable=${typeof watchJobs === 'function'})`);
|
|
1073
|
-
return;
|
|
1074
|
-
}
|
|
1075
|
-
const controller = new AbortController();
|
|
1076
|
-
this.jobWatches.set(runId, controller);
|
|
1077
|
-
void this.consumeJobRoster(runId, snapshot, controller, (signal) => watchJobs.call(runner, sessionId, signal));
|
|
1078
|
-
}
|
|
1079
|
-
/** 消费宿主推来的整份任务名单,直到全结束 / 流断 / patch 失败 / 外部退订。 */
|
|
1080
|
-
async consumeJobRoster(runId, snapshot, controller, watch) {
|
|
1081
|
-
const messageId = snapshot.messageId;
|
|
1082
|
-
if (!messageId)
|
|
1083
|
-
return;
|
|
1084
|
-
let allEnded = false;
|
|
1085
|
-
try {
|
|
1086
|
-
for await (const roster of watch(controller.signal)) {
|
|
1087
|
-
const next = syncBackgroundJobs(snapshot.state, roster);
|
|
1088
|
-
// 名单没变化就不打 PATCH:宿主可能重复推同一份。
|
|
1089
|
-
if (next === snapshot.state)
|
|
1090
|
-
continue;
|
|
1091
|
-
snapshot.state = next;
|
|
1092
|
-
const ok = await this.enqueueJobCardPatch(runId, () => this.patchCard(messageId, renderRunCard(snapshot.state, snapshot.options)));
|
|
1093
|
-
if (!ok) {
|
|
1094
|
-
getLogger().warn(`[lark-remote] background job card update failed, stop watching runId=${runId}`);
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
if (!(snapshot.state.backgroundJobs ?? []).some(isLiveBackgroundJob)) {
|
|
1098
|
-
allEnded = true;
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
catch (err) {
|
|
1104
|
-
if (!controller.signal.aborted) {
|
|
1105
|
-
getLogger().warn(`[lark-remote] background job watch failed runId=${runId}: ${errorMessage(err)}`);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
finally {
|
|
1109
|
-
const externallyAborted = controller.signal.aborted;
|
|
1110
|
-
controller.abort();
|
|
1111
|
-
// 流断了而任务没结算:监控中断 ≠ 任务结束,标成未知而不是继续写「运行中」。
|
|
1112
|
-
if (!allEnded && !externallyAborted)
|
|
1113
|
-
await this.markJobsUnknown(runId, snapshot);
|
|
1114
|
-
this.jobWatches.delete(runId);
|
|
1115
|
-
this.jobCardPatches.delete(runId);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
/** 订阅断了但任务仍未结算:把在跑的任务标成「状态未知」并重渲染一次。 */
|
|
1119
|
-
async markJobsUnknown(runId, snapshot) {
|
|
1120
|
-
const next = markBackgroundJobsUnknown(snapshot.state);
|
|
1121
|
-
if (next === snapshot.state)
|
|
1122
|
-
return;
|
|
1123
|
-
snapshot.state = next;
|
|
1124
|
-
const messageId = snapshot.messageId;
|
|
1125
|
-
if (!messageId)
|
|
1126
|
-
return;
|
|
1127
|
-
await this.enqueueJobCardPatch(runId, () => this.patchCard(messageId, renderRunCard(snapshot.state, snapshot.options)));
|
|
1128
|
-
}
|
|
1129
|
-
/**
|
|
1130
|
-
* 一张终态卡的 PATCH 串行链。
|
|
1131
|
-
*
|
|
1132
|
-
* 渲染放在链内执行(`task` 里读 `snapshot.state`),后入链的任务必然看到更新
|
|
1133
|
-
* 后的状态;否则「停止」与「订阅帧」并发时卡片可能回退。
|
|
1134
|
-
*/
|
|
1135
|
-
enqueueJobCardPatch(runId, task) {
|
|
1136
|
-
const previous = this.jobCardPatches.get(runId) ?? Promise.resolve();
|
|
1137
|
-
const next = previous.then(task, task);
|
|
1138
|
-
this.jobCardPatches.set(runId, next.then(() => undefined, () => undefined));
|
|
1139
|
-
return next;
|
|
939
|
+
return this.jobWatch.killBackgroundJob(runId, jobId, ctx);
|
|
1140
940
|
}
|
|
1141
941
|
/** Send a file to the user via Feishu. */
|
|
1142
942
|
async sendFile(filePath, ctx) {
|
|
@@ -1210,7 +1010,7 @@ export class Bridge {
|
|
|
1210
1010
|
// sessionId 钉死语义——binding 有 sessionId 时用它(入队时刻快照),
|
|
1211
1011
|
// 否则跟随 live store(无 binding 或入队时无 session 的正常路径)。
|
|
1212
1012
|
const sessionId = opts?.binding?.sessionId ?? this.sessionStore.getSessionId(ctx.userId, agentKind);
|
|
1213
|
-
//
|
|
1013
|
+
// 会话代际快照:/new、/cd、/resume 会 bump
|
|
1214
1014
|
// epoch,该 run 后续 system.init 的 sessionId 写回即判 stale 跳过。
|
|
1215
1015
|
// 快照点优先取 binding.sessionEpoch(入队/commit 时刻,与 sessionId 同刻):
|
|
1216
1016
|
// 排队消息在「commit 后、开跑前」被 reset 时,执行起点捕获会漏判(epoch 已是
|
|
@@ -1220,7 +1020,7 @@ export class Bridge {
|
|
|
1220
1020
|
// Resolve the runner BEFORE creating the run session so the exact instance
|
|
1221
1021
|
// that will run is captured into activeRun. interruptCurrentRun stops THAT
|
|
1222
1022
|
// instance directly -- never a fresh runner re-resolved from a cache that
|
|
1223
|
-
// clearRunners() may have emptied mid-run
|
|
1023
|
+
// clearRunners() may have emptied mid-run.
|
|
1224
1024
|
const runner = this.getRunner(cwd, agentKind, { replaceStale: true });
|
|
1225
1025
|
// Step 1: Create run session (runId, cardSession, activeRun tracking)
|
|
1226
1026
|
const { runId, cardSession, activeRun } = this.createRunSession(ctx, cwd, runner, agentKind);
|
|
@@ -1252,7 +1052,7 @@ export class Bridge {
|
|
|
1252
1052
|
*
|
|
1253
1053
|
* DSH 把上游 `finish_reason: content_filter` 归一成 PI_AI_ERROR:既不在默认
|
|
1254
1054
|
* 重试码里(EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT),也不会换模型,
|
|
1255
|
-
* 于是一次(多数是误报的)判定就把整个 turn 判死。
|
|
1055
|
+
* 于是一次(多数是误报的)判定就把整个 turn 判死。60 步、
|
|
1256
1056
|
* 10 分钟的 turn 在最后一步被截断,已写入的文件还在,用户只看到「运行出错」
|
|
1257
1057
|
* 加一句英文报文。
|
|
1258
1058
|
*
|
|
@@ -1371,12 +1171,23 @@ export class Bridge {
|
|
|
1371
1171
|
}
|
|
1372
1172
|
stop();
|
|
1373
1173
|
getLogger().warn(`[lark-remote] claude idle timeout, stopping process runId=${opts.runId}`);
|
|
1174
|
+
// 看门狗清理的两个结果都必须留痕:卡片 patch 失败时终态会停在
|
|
1175
|
+
// 「运行中」;runner.stop 失败时挂死的 run 没被杀、cwd 槽位未释放,
|
|
1176
|
+
// 串行队列从此永久堵死(用户视角「机器人不回消息」,日志却只有
|
|
1177
|
+
// 前面一条 warn)。对照 claude session.ts idle timer 的 .catch(log)。
|
|
1374
1178
|
void Promise.allSettled([
|
|
1375
1179
|
opts.session.finish('idle_timeout', {
|
|
1376
1180
|
idleTimeoutMinutes: Math.max(1, Math.round(this.idleTimeoutMs / 60_000)),
|
|
1377
1181
|
}),
|
|
1378
1182
|
opts.runner.stop(),
|
|
1379
|
-
])
|
|
1183
|
+
]).then(([finishResult, stopResult]) => {
|
|
1184
|
+
if (finishResult.status === 'rejected') {
|
|
1185
|
+
getLogger().error(`[lark-remote] idle-timeout cleanup failed runId=${opts.runId} finish:`, finishResult.reason);
|
|
1186
|
+
}
|
|
1187
|
+
if (stopResult.status === 'rejected') {
|
|
1188
|
+
getLogger().error(`[lark-remote] idle-timeout cleanup failed runId=${opts.runId} stop:`, stopResult.reason);
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1380
1191
|
}, tickMs);
|
|
1381
1192
|
}
|
|
1382
1193
|
return {
|
|
@@ -1395,7 +1206,7 @@ export class Bridge {
|
|
|
1395
1206
|
* result 在 system.init 之后才到。
|
|
1396
1207
|
*
|
|
1397
1208
|
* 例外:app-server runner(usageAuthority==='live')的 init 是 runner 自己补发
|
|
1398
|
-
* 的 synthetic init
|
|
1209
|
+
* 的 synthetic init。此分支保留为防御性兜底:live runner 若
|
|
1399
1210
|
* 在无 init 时发出 result(旧构建等),session 写回照常执行,但 sawResult 不置
|
|
1400
1211
|
* 真——卡片不会把半初始化的 run 当作已收尾。
|
|
1401
1212
|
*
|
|
@@ -1474,7 +1285,7 @@ export class Bridge {
|
|
|
1474
1285
|
const { cardSession, usage, sawResult, sawAnyResult, pendingPreInitTerminal, agentKind, runId, } = opts;
|
|
1475
1286
|
const finalState = cardSession.currentState;
|
|
1476
1287
|
if (finalState.terminal === 'finalizing') {
|
|
1477
|
-
//
|
|
1288
|
+
// 三态终态映射:interrupted(审批超时/取消、/stop)是独立
|
|
1478
1289
|
// 终态,不再归因于 Agent 错误;error 保持「运行出错」;其余为 done。
|
|
1479
1290
|
const subtype = finalState.resultSubtype;
|
|
1480
1291
|
const isError = subtype === 'error' || !!finalState.errorMsg;
|
|
@@ -1540,203 +1351,259 @@ export class Bridge {
|
|
|
1540
1351
|
// 'jsonl'。result 事件的 live 语义(pre-init/写回)单独声明,不能和
|
|
1541
1352
|
// 用量来源共用一个字段。
|
|
1542
1353
|
const usageAuthority = runner.getUsageAuthority?.();
|
|
1543
|
-
const liveResultEvents = runner
|
|
1544
|
-
//
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1354
|
+
const liveResultEvents = computeLiveResultEvents(runner, usageAuthority);
|
|
1355
|
+
// 运行期状态打包成显式状态对象传给各抽出方法(等价替换原闭包捕获的局部变量)。
|
|
1356
|
+
const run = {
|
|
1357
|
+
runner,
|
|
1358
|
+
cardSession,
|
|
1359
|
+
activeRun,
|
|
1360
|
+
sessionId,
|
|
1361
|
+
sessionEpochAtStart,
|
|
1362
|
+
ctx,
|
|
1363
|
+
cwd,
|
|
1364
|
+
runId,
|
|
1365
|
+
message,
|
|
1366
|
+
agentKind,
|
|
1367
|
+
usage: new RunUsageCollector(),
|
|
1368
|
+
usageAuthority,
|
|
1369
|
+
liveResultEvents,
|
|
1370
|
+
state: {
|
|
1371
|
+
watchdog: null,
|
|
1372
|
+
sawResult: false,
|
|
1373
|
+
sawAnyResult: false,
|
|
1374
|
+
pendingPreInitTerminal: undefined,
|
|
1375
|
+
},
|
|
1376
|
+
};
|
|
1556
1377
|
try {
|
|
1557
|
-
|
|
1558
|
-
// queue.immediate 的 run-start hold:目标槽位可能在 handler 发最终
|
|
1559
|
-
// toast 之前就 begin(interrupt/PATCH 的 await 窗口)。activeRun 已
|
|
1560
|
-
// 登记(队列语义照旧),只把建卡这一步挡到 toast 之后——run 卡片才是
|
|
1561
|
-
// 点击后最后一条新消息,聊天列表预览跟随它的标题变化。
|
|
1562
|
-
await this.queueManager.awaitTaskRunStart(cwd);
|
|
1563
|
-
getLogger().debug(`[lark-remote] cardSession.start() begin runId=${runId}`);
|
|
1564
|
-
await cardSession.start();
|
|
1565
|
-
getLogger().info(`[lark-remote] cardSession.start() ok runId=${runId}`);
|
|
1566
|
-
}
|
|
1567
|
-
catch (err) {
|
|
1568
|
-
getLogger().warn('[lark-remote] card stream unavailable, will use static fallback:', err);
|
|
1569
|
-
// Card stream failed - continue with static fallback below, but ensure
|
|
1570
|
-
// we don't silently fail if the fallback also fails (covered in catch below).
|
|
1571
|
-
}
|
|
1572
|
-
// /stop(或队列「立即执行」的中断)可能落在建卡窗口内:卡片已定格为
|
|
1573
|
-
// 终态,但 runner 还没开始跑,runner.stop() 也没有进程可杀——spawn 型
|
|
1574
|
-
// 与连接型 runner 的 stop() 在 run() 之前都是空操作,且 run() 入口会
|
|
1575
|
-
// 重置停止标记。必须在这里收口,否则用户看到「已中断」,agent 仍会把
|
|
1576
|
-
// 这一轮跑完并改写工作区。finalizeRun 仍会照常收尾(settle / reaction)。
|
|
1577
|
-
if (isRunTerminal(cardSession.currentState.terminal)) {
|
|
1578
|
-
getLogger().info(`[lark-remote] card terminal before run start, skip runner runId=${runId} terminal=${cardSession.currentState.terminal}`);
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
// 单 interval 看门狗。事件到来时只刷新时间戳(无 timer 操作);
|
|
1582
|
-
// interval 周期检查静默时长,超时才 finish('idle_timeout') + stop()。
|
|
1583
|
-
// 武装点在建卡之后,与原实现一致(宽限期不含 cardSession.start() 耗时)。
|
|
1584
|
-
watchdog = this.startIdleWatchdog({
|
|
1585
|
-
runId,
|
|
1586
|
-
session: cardSession,
|
|
1587
|
-
runner,
|
|
1588
|
-
pendingApprovals: () => this.approvalCoordinators.get(runId)?.pendingCount() ?? 0,
|
|
1589
|
-
});
|
|
1590
|
-
getLogger().info(`[lark-remote] runner.run() begin runId=${runId} message=${message.slice(0, 100)}`);
|
|
1591
|
-
// Extract agent-specific options from config
|
|
1592
|
-
const agentOpts = this.getAgentRunOptions(agentKind);
|
|
1593
|
-
const runOpts = { cwd, sessionId, ...agentOpts };
|
|
1594
|
-
// Track whether system.init has been received for this run.
|
|
1595
|
-
// Claude CLI --resume emits a stale result (from the previous turn)
|
|
1596
|
-
// before sending system.init; bridge must not treat that as the run
|
|
1597
|
-
// ending (sawResult, usage capture, or cardSession.push would all
|
|
1598
|
-
// be wrong for the historical event).
|
|
1599
|
-
let sawInit = false;
|
|
1600
|
-
const writeback = {
|
|
1601
|
-
sessionStore: this.sessionStore,
|
|
1602
|
-
userId: ctx.userId,
|
|
1603
|
-
agentKind,
|
|
1604
|
-
cwd,
|
|
1605
|
-
sessionEpochAtStart,
|
|
1606
|
-
runId,
|
|
1607
|
-
};
|
|
1608
|
-
for await (const event of runner.run(message, runOpts)) {
|
|
1609
|
-
// 只有终态才阻止事件处理。finalizing 是非终态,应该继续处理
|
|
1610
|
-
// 这样在 result 事件后,后台任务输出仍能被正确处理
|
|
1611
|
-
const terminal = cardSession.currentState.terminal;
|
|
1612
|
-
if (isRunTerminal(terminal)) {
|
|
1613
|
-
continue;
|
|
1614
|
-
}
|
|
1615
|
-
if (event.type === 'system' && event.subtype === 'init') {
|
|
1616
|
-
sawInit = true;
|
|
1617
|
-
// 本轮真实使用的 session(不可变身份):指针在 run 中被 /new 等移动时,
|
|
1618
|
-
// 结束后的 usage 读取仍以它为准。
|
|
1619
|
-
activeRun.sessionId = event.session_id;
|
|
1620
|
-
writeBackInitSession(writeback, event);
|
|
1621
|
-
getLogger().info(`[lark-remote] system.init received runId=${runId} sessionId=${event.session_id} cwd=${event.cwd}`);
|
|
1622
|
-
}
|
|
1623
|
-
if (event.type === 'turn_started') {
|
|
1624
|
-
// app-server runner 在 turn setup 成功后补发 synthetic system.init
|
|
1625
|
-
// (§9.22 守卫前提),此处 turn_started 写回保留为双保险:两者都用
|
|
1626
|
-
// threadId 写回 sessionId(代际守卫一致),幂等。缺失会导致每次消息
|
|
1627
|
-
// 都新建线程(thread/start 而非 thread/resume),会话无法延续。
|
|
1628
|
-
// init 缺失时以 threadId 作为本轮身份兜底。
|
|
1629
|
-
activeRun.sessionId ??= event.threadId;
|
|
1630
|
-
writeBackTurnStarted(writeback, event);
|
|
1631
|
-
}
|
|
1632
|
-
// Track context length from compaction events
|
|
1633
|
-
if (event.type === 'system' &&
|
|
1634
|
-
event.subtype === 'compact_boundary' &&
|
|
1635
|
-
event.compactMetadata) {
|
|
1636
|
-
usage.observeCompaction(event.compactMetadata.postTokens);
|
|
1637
|
-
}
|
|
1638
|
-
if (event.type === 'result') {
|
|
1639
|
-
const outcome = this.observeResultEvent(event, {
|
|
1640
|
-
sawInit,
|
|
1641
|
-
liveResultEvents,
|
|
1642
|
-
usage,
|
|
1643
|
-
writeback,
|
|
1644
|
-
runId,
|
|
1645
|
-
});
|
|
1646
|
-
if (outcome.preInitTerminal)
|
|
1647
|
-
pendingPreInitTerminal ??= outcome.preInitTerminal;
|
|
1648
|
-
if (outcome.sawResult)
|
|
1649
|
-
sawResult = true;
|
|
1650
|
-
sawAnyResult = true;
|
|
1651
|
-
// 陈旧 result 不进卡片:run-state reducer 本来也会因 sessionId 未定而
|
|
1652
|
-
// 跳过,在 bridge 层就拦掉可以省下一次无谓的 push + render。
|
|
1653
|
-
if (outcome.dropFromCard) {
|
|
1654
|
-
// 仍然重置 idle 计时——CLI 活着并且在产事件。
|
|
1655
|
-
watchdog?.touch();
|
|
1656
|
-
continue;
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
// Approval events (Codex app-server mode)
|
|
1660
|
-
if (event.type === 'approval_requested' ||
|
|
1661
|
-
event.type === 'approval_resolved' ||
|
|
1662
|
-
event.type === 'approval_view_updated') {
|
|
1663
|
-
this.dispatchApprovalEvent(event, { runId, runner, cardSession, ctx, agentKind });
|
|
1664
|
-
}
|
|
1665
|
-
// §9.12 红线:每个事件(含 result)无条件刷新
|
|
1666
|
-
// lastEventTs 重新武装看门狗。result 后 CLI 进程做 jsonl flush/清理(finalizing
|
|
1667
|
-
// 过渡)期间,宽限期恢复为完整的 idleTimeoutMs,避免已产出结果的 turn 被误标
|
|
1668
|
-
// idle_timeout。旧实现 `else { resetIdle(); }` 漏掉 result 分支。
|
|
1669
|
-
watchdog?.touch();
|
|
1670
|
-
await cardSession.push(event);
|
|
1671
|
-
}
|
|
1672
|
-
getLogger().info(`[lark-remote] runner stream end runId=${runId} sawResult=${sawResult}`);
|
|
1673
|
-
// §9.12 红线:for-await 自然结束 = CLI 已退出,
|
|
1674
|
-
// 看门狗使命结束,立即摘除 interval(不再等 finally),堵掉「stream 已结束、
|
|
1675
|
-
// 收尾处理跨过死线被误标 idle_timeout」的同源小窗口(随本修复一并关闭)。
|
|
1676
|
-
watchdog?.stop();
|
|
1677
|
-
// stream 结束 = CLI 退出 = jsonl 已落盘。读 jsonl 拿准确的 contextLength + compactCount:
|
|
1678
|
-
// live stream-json 不发 compact_boundary,live contextLength 会被 result 的 input+output 兜底失真。
|
|
1679
|
-
// 但 result=error 时跳过: 进程可能立即退出未写新 usage (如 kimi 0.26+ 拒绝 -p+--auto
|
|
1680
|
-
// 立即 exit=1), 此时 jsonl 里是上一次成功 turn 的历史 usage, 会被误当作本次 usage 显示,
|
|
1681
|
-
// 误导用户以为本次出错的请求也消耗了 token。error 时只用 live 捕获的 usage (若有)。
|
|
1682
|
-
// 本轮使用的 session:run 期间 /new //resume //cd 会移动全局
|
|
1683
|
-
// 指针,结束时再读 store 会把别条会话的 token/context 显示到这张卡上。
|
|
1684
|
-
// 优先级:卡片状态(本轮 init 写入)→ activeRun(本轮 init/turn_started)
|
|
1685
|
-
// → 本轮开始时捕获的 sessionId。三者都没有就跳过 usage,绝不回读全局指针。
|
|
1686
|
-
const finalSessionId = cardSession.currentState.sessionId ?? activeRun.sessionId ?? sessionId;
|
|
1687
|
-
const finalUsage = usage.resultNotSuccess
|
|
1688
|
-
? undefined
|
|
1689
|
-
: resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, finalSessionId, cwd, agentKind);
|
|
1690
|
-
usage.finalize(finalUsage, usageAuthority);
|
|
1691
|
-
getLogger().info(usage.logSummary(runId));
|
|
1692
|
-
// for-await 自然结束 = stream 关闭 = CLI 进程退出 → 终态矩阵
|
|
1693
|
-
await this.finishRunAfterStream({
|
|
1694
|
-
cardSession,
|
|
1695
|
-
usage,
|
|
1696
|
-
sawResult,
|
|
1697
|
-
sawAnyResult,
|
|
1698
|
-
pendingPreInitTerminal,
|
|
1699
|
-
agentKind,
|
|
1700
|
-
runId,
|
|
1701
|
-
});
|
|
1378
|
+
await this.executeStream(run);
|
|
1702
1379
|
}
|
|
1703
1380
|
catch (err) {
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1381
|
+
await this.handleStreamFailure(err, run);
|
|
1382
|
+
}
|
|
1383
|
+
finally {
|
|
1384
|
+
this.cleanupStream(run);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* runAgentStreamToEnd 的 try 主体(建卡 → 事件循环 → 收尾),抽成独立方法
|
|
1389
|
+
* 降低外层圈复杂度;state 为跨方法共享的可变状态。逻辑与文案逐字保留。
|
|
1390
|
+
*/
|
|
1391
|
+
async executeStream(run) {
|
|
1392
|
+
const { runner, cardSession, activeRun, sessionId, sessionEpochAtStart, ctx, cwd, runId, message, agentKind, usage, usageAuthority, liveResultEvents, state, } = run;
|
|
1393
|
+
try {
|
|
1394
|
+
// queue.immediate 的 run-start hold:目标槽位可能在 handler 发最终
|
|
1395
|
+
// toast 之前就 begin(interrupt/PATCH 的 await 窗口)。activeRun 已
|
|
1396
|
+
// 登记(队列语义照旧),只把建卡这一步挡到 toast 之后——run 卡片才是
|
|
1397
|
+
// 点击后最后一条新消息,聊天列表预览跟随它的标题变化。
|
|
1398
|
+
await this.queueManager.awaitTaskRunStart(cwd);
|
|
1399
|
+
getLogger().debug(`[lark-remote] cardSession.start() begin runId=${runId}`);
|
|
1400
|
+
await cardSession.start();
|
|
1401
|
+
getLogger().info(`[lark-remote] cardSession.start() ok runId=${runId}`);
|
|
1402
|
+
}
|
|
1403
|
+
catch (err) {
|
|
1404
|
+
getLogger().warn('[lark-remote] card stream unavailable, will use static fallback:', err);
|
|
1405
|
+
// Card stream failed - continue with static fallback below, but ensure
|
|
1406
|
+
// we don't silently fail if the fallback also fails (covered in catch below).
|
|
1407
|
+
}
|
|
1408
|
+
// /stop(或队列「立即执行」的中断)可能落在建卡窗口内:卡片已定格为
|
|
1409
|
+
// 终态,但 runner 还没开始跑,runner.stop() 也没有进程可杀——spawn 型
|
|
1410
|
+
// 与连接型 runner 的 stop() 在 run() 之前都是空操作,且 run() 入口会
|
|
1411
|
+
// 重置停止标记。必须在这里收口,否则用户看到「已中断」,agent 仍会把
|
|
1412
|
+
// 这一轮跑完并改写工作区。finalizeRun 仍会照常收尾(settle / reaction)。
|
|
1413
|
+
if (isRunTerminal(cardSession.currentState.terminal)) {
|
|
1414
|
+
getLogger().info(`[lark-remote] card terminal before run start, skip runner runId=${runId} terminal=${cardSession.currentState.terminal}`);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
// 单 interval 看门狗。事件到来时只刷新时间戳(无 timer 操作);
|
|
1418
|
+
// interval 周期检查静默时长,超时才 finish('idle_timeout') + stop()。
|
|
1419
|
+
// 武装点在建卡之后,与原实现一致(宽限期不含 cardSession.start() 耗时)。
|
|
1420
|
+
state.watchdog = this.startIdleWatchdog({
|
|
1421
|
+
runId,
|
|
1422
|
+
session: cardSession,
|
|
1423
|
+
runner,
|
|
1424
|
+
pendingApprovals: pendingApprovalCount.bind(null, this.approvalCoordinators, runId),
|
|
1425
|
+
});
|
|
1426
|
+
getLogger().info(`[lark-remote] runner.run() begin runId=${runId} message=${message.slice(0, 100)}`);
|
|
1427
|
+
// Extract agent-specific options from config
|
|
1428
|
+
const agentOpts = this.getAgentRunOptions(agentKind);
|
|
1429
|
+
const runOpts = { cwd, sessionId, ...agentOpts };
|
|
1430
|
+
// Track whether system.init has been received for this run.
|
|
1431
|
+
// Claude CLI --resume emits a stale result (from the previous turn)
|
|
1432
|
+
// before sending system.init; bridge must not treat that as the run
|
|
1433
|
+
// ending (sawResult, usage capture, or cardSession.push would all
|
|
1434
|
+
// be wrong for the historical event).
|
|
1435
|
+
let sawInit = false;
|
|
1436
|
+
const writeback = {
|
|
1437
|
+
sessionStore: this.sessionStore,
|
|
1438
|
+
userId: ctx.userId,
|
|
1439
|
+
agentKind,
|
|
1440
|
+
cwd,
|
|
1441
|
+
sessionEpochAtStart,
|
|
1442
|
+
runId,
|
|
1443
|
+
};
|
|
1444
|
+
for await (const event of runner.run(message, runOpts)) {
|
|
1445
|
+
// 只有终态才阻止事件处理。finalizing 是非终态,应该继续处理
|
|
1446
|
+
// 这样在 result 事件后,后台任务输出仍能被正确处理
|
|
1447
|
+
const terminal = cardSession.currentState.terminal;
|
|
1448
|
+
if (isRunTerminal(terminal)) {
|
|
1449
|
+
continue;
|
|
1709
1450
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1451
|
+
// 归一化:init/turn_started 的 session 写回 + compact_boundary 用量观察。
|
|
1452
|
+
// 返回 true = 本轮 system.init 已到(调用方置 sawInit,供 pre-init 守卫用)。
|
|
1453
|
+
if (this.maybeSyncSession(event, { activeRun, writeback, usage, runId })) {
|
|
1454
|
+
sawInit = true;
|
|
1712
1455
|
}
|
|
1713
|
-
//
|
|
1714
|
-
//
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1456
|
+
// pre-init result 守卫(§9.22,observeResultEvent):resume 重放的陈旧
|
|
1457
|
+
// result 不置 sawResult、不进卡片。
|
|
1458
|
+
if (event.type === 'result') {
|
|
1459
|
+
const outcome = this.observeResultEvent(event, {
|
|
1460
|
+
sawInit,
|
|
1461
|
+
liveResultEvents,
|
|
1462
|
+
usage,
|
|
1463
|
+
writeback,
|
|
1464
|
+
runId,
|
|
1722
1465
|
});
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1466
|
+
if (outcome.preInitTerminal)
|
|
1467
|
+
state.pendingPreInitTerminal ??= outcome.preInitTerminal;
|
|
1468
|
+
if (outcome.sawResult)
|
|
1469
|
+
state.sawResult = true;
|
|
1470
|
+
state.sawAnyResult = true;
|
|
1471
|
+
// 陈旧 result 不进卡片:run-state reducer 本来也会因 sessionId 未定而
|
|
1472
|
+
// 跳过,在 bridge 层就拦掉可以省下一次无谓的 push + render。
|
|
1473
|
+
if (outcome.dropFromCard) {
|
|
1474
|
+
// 仍然重置 idle 计时——CLI 活着并且在产事件。
|
|
1475
|
+
this.handleEventForWatchdog(state.watchdog);
|
|
1476
|
+
continue;
|
|
1728
1477
|
}
|
|
1729
1478
|
}
|
|
1479
|
+
// Approval events (Codex app-server mode)。审批先到、item/started 后到的
|
|
1480
|
+
// 乱序流也在此补全(dispatchApprovalEvent 内 updateView)。
|
|
1481
|
+
if (event.type === 'approval_requested' ||
|
|
1482
|
+
event.type === 'approval_resolved' ||
|
|
1483
|
+
event.type === 'approval_view_updated') {
|
|
1484
|
+
this.dispatchApprovalEvent(event, { runId, runner, cardSession, ctx, agentKind });
|
|
1485
|
+
}
|
|
1486
|
+
// §9.12 红线:每个事件(含 result)无条件刷新
|
|
1487
|
+
// lastEventTs 重新武装看门狗。result 后 CLI 进程做 jsonl flush/清理(finalizing
|
|
1488
|
+
// 过渡)期间,宽限期恢复为完整的 idleTimeoutMs,避免已产出结果的 turn 被误标
|
|
1489
|
+
// idle_timeout。旧实现 `else { resetIdle(); }` 漏掉 result 分支。
|
|
1490
|
+
this.handleEventForWatchdog(state.watchdog);
|
|
1491
|
+
// 渲染:reducer(run-state)在 cardSession.push 内部。
|
|
1492
|
+
await cardSession.push(event);
|
|
1493
|
+
}
|
|
1494
|
+
getLogger().info(`[lark-remote] runner stream end runId=${runId} sawResult=${state.sawResult}`);
|
|
1495
|
+
// §9.12 红线:for-await 自然结束 = CLI 已退出,
|
|
1496
|
+
// 看门狗使命结束,立即摘除 interval(不再等 finally),堵掉「stream 已结束、
|
|
1497
|
+
// 收尾处理跨过死线被误标 idle_timeout」的同源小窗口(随本修复一并关闭)。
|
|
1498
|
+
state.watchdog?.stop();
|
|
1499
|
+
// stream 结束 = CLI 退出 = jsonl 已落盘。读 jsonl 拿准确的 contextLength + compactCount:
|
|
1500
|
+
// live stream-json 不发 compact_boundary,live contextLength 会被 result 的 input+output 兜底失真。
|
|
1501
|
+
// 但 result=error 时跳过: 进程可能立即退出未写新 usage (如 kimi 0.26+ 拒绝 -p+--auto
|
|
1502
|
+
// 立即 exit=1), 此时 jsonl 里是上一次成功 turn 的历史 usage, 会被误当作本次 usage 显示,
|
|
1503
|
+
// 误导用户以为本次出错的请求也消耗了 token。error 时只用 live 捕获的 usage (若有)。
|
|
1504
|
+
// 本轮使用的 session:run 期间 /new //resume //cd 会移动全局
|
|
1505
|
+
// 指针,结束时再读 store 会把别条会话的 token/context 显示到这张卡上。
|
|
1506
|
+
// 优先级:卡片状态(本轮 init 写入)→ activeRun(本轮 init/turn_started)
|
|
1507
|
+
// → 本轮开始时捕获的 sessionId。三者都没有就跳过 usage,绝不回读全局指针。
|
|
1508
|
+
const finalSessionId = cardSession.currentState.sessionId ?? activeRun.sessionId ?? sessionId;
|
|
1509
|
+
const finalUsage = usage.resultNotSuccess
|
|
1510
|
+
? undefined
|
|
1511
|
+
: resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, finalSessionId, cwd, agentKind);
|
|
1512
|
+
usage.finalize(finalUsage, usageAuthority);
|
|
1513
|
+
getLogger().info(usage.logSummary(runId));
|
|
1514
|
+
// for-await 自然结束 = stream 关闭 = CLI 进程退出 → 终态矩阵
|
|
1515
|
+
await this.finishRunAfterStream({
|
|
1516
|
+
cardSession,
|
|
1517
|
+
usage,
|
|
1518
|
+
sawResult: state.sawResult,
|
|
1519
|
+
sawAnyResult: state.sawAnyResult,
|
|
1520
|
+
pendingPreInitTerminal: state.pendingPreInitTerminal,
|
|
1521
|
+
agentKind,
|
|
1522
|
+
runId,
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
/** runAgentStreamToEnd 的 catch 主体(抽成独立方法,state.sawResult 沿用循环累计值)。 */
|
|
1526
|
+
async handleStreamFailure(err, run) {
|
|
1527
|
+
const { runner, cardSession, runId, usage, state } = run;
|
|
1528
|
+
getLogger().error(`[lark-remote] claude run error runId=${runId}:`, err);
|
|
1529
|
+
// 双保险:run() 的 finally 已负责杀子进程,但 catch 路径再补一次
|
|
1530
|
+
// runner.stop()(与 bash 路径对齐),覆盖任何 finally 之外的残留窗口。
|
|
1531
|
+
try {
|
|
1532
|
+
await runner.stop();
|
|
1730
1533
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1534
|
+
catch (stopErr) {
|
|
1535
|
+
getLogger().warn(`[lark-remote] runner stop failed:`, stopErr);
|
|
1536
|
+
}
|
|
1537
|
+
// 终态守卫:running 或 finalizing(非终态)-> 转 error;
|
|
1538
|
+
// 已终态(interrupted/idle_timeout/done/error)-> 保留首终态,仅补充 usage。
|
|
1539
|
+
const catchTerminal = cardSession.currentState.terminal;
|
|
1540
|
+
if (isRunActive(catchTerminal)) {
|
|
1541
|
+
await cardSession.finish('error', {
|
|
1542
|
+
...usage.meta(),
|
|
1543
|
+
inputTokens: usage.liveInputTokens,
|
|
1544
|
+
outputTokens: usage.liveOutputTokens,
|
|
1545
|
+
errorMsg: errorMessage(err),
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
else {
|
|
1549
|
+
getLogger().info(`[lark-remote] skip error finish: state already terminal (${catchTerminal}) runId=${runId}`);
|
|
1550
|
+
if (state.sawResult) {
|
|
1551
|
+
await cardSession.finish(catchTerminal, usage.meta());
|
|
1737
1552
|
}
|
|
1738
1553
|
}
|
|
1739
1554
|
}
|
|
1555
|
+
/** runAgentStreamToEnd 的 finally 主体(抽成独立方法)。 */
|
|
1556
|
+
cleanupStream(run) {
|
|
1557
|
+
const { runId, state } = run;
|
|
1558
|
+
state.watchdog?.stop();
|
|
1559
|
+
const coordinator = this.approvalCoordinators.get(runId);
|
|
1560
|
+
if (coordinator) {
|
|
1561
|
+
coordinator.onTurnEnded();
|
|
1562
|
+
this.approvalCoordinators.delete(runId);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* 事件循环的看门狗喂狗(runAgentStreamToEnd 抽出的纯向方法)。
|
|
1567
|
+
*
|
|
1568
|
+
* §9.12:事件到达只 `touch()` 刷新时间戳(无 timer 操作),interval 周期检查
|
|
1569
|
+
* 静默时长;喂狗点与红线口径见 runAgentStreamToEnd 循环内两处调用点注释
|
|
1570
|
+
* (每个事件含 result 都必须喂,被守卫丢弃的 result 也喂——CLI 活着就在产事件)。
|
|
1571
|
+
*/
|
|
1572
|
+
handleEventForWatchdog(watchdog) {
|
|
1573
|
+
watchdog?.touch();
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* 事件循环的 session 同步(runAgentStreamToEnd 抽出的纯向方法):
|
|
1577
|
+
* init / turn_started 两写回点 + compact_boundary 用量观察。
|
|
1578
|
+
*
|
|
1579
|
+
* @returns 事件是否为本轮 `system.init`(调用方据此置 sawInit,供 pre-init
|
|
1580
|
+
* result 守卫判定)。init 分支的全部副作用完成后才返回,与原内联顺序一致。
|
|
1581
|
+
*/
|
|
1582
|
+
maybeSyncSession(event, opts) {
|
|
1583
|
+
const { activeRun, writeback, usage, runId } = opts;
|
|
1584
|
+
if (event.type === 'system' && event.subtype === 'init') {
|
|
1585
|
+
// 本轮真实使用的 session(不可变身份):指针在 run 中被 /new 等移动时,
|
|
1586
|
+
// 结束后的 usage 读取仍以它为准。
|
|
1587
|
+
activeRun.sessionId = event.session_id;
|
|
1588
|
+
writeBackInitSession(writeback, event);
|
|
1589
|
+
getLogger().info(`[lark-remote] system.init received runId=${runId} sessionId=${event.session_id} cwd=${event.cwd}`);
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
if (event.type === 'turn_started') {
|
|
1593
|
+
// app-server runner 在 turn setup 成功后补发 synthetic system.init
|
|
1594
|
+
// (§9.22 守卫前提),此处 turn_started 写回保留为双保险:两者都用
|
|
1595
|
+
// threadId 写回 sessionId(代际守卫一致),幂等。缺失会导致每次消息
|
|
1596
|
+
// 都新建线程(thread/start 而非 thread/resume),会话无法延续。
|
|
1597
|
+
// init 缺失时以 threadId 作为本轮身份兜底。
|
|
1598
|
+
activeRun.sessionId ??= event.threadId;
|
|
1599
|
+
writeBackTurnStarted(writeback, event);
|
|
1600
|
+
}
|
|
1601
|
+
// Track context length from compaction events
|
|
1602
|
+
if (event.type === 'system' && event.subtype === 'compact_boundary' && event.compactMetadata) {
|
|
1603
|
+
usage.observeCompaction(event.compactMetadata.postTokens);
|
|
1604
|
+
}
|
|
1605
|
+
return false;
|
|
1606
|
+
}
|
|
1740
1607
|
/**
|
|
1741
1608
|
* 终态 reaction 收尾:撤掉 Typing、打上下方终态表情。
|
|
1742
1609
|
*
|
|
@@ -1806,7 +1673,7 @@ export class Bridge {
|
|
|
1806
1673
|
this.finishTurnReactions(ctx, terminalReactionEmoji(cardSession.currentState.terminal));
|
|
1807
1674
|
// 后台任务跨 turn:卡片收尾只是"这一轮的投影结束了",任务本身可能还在跑。
|
|
1808
1675
|
// 订阅放在最后:卡片已落地,patch 才有落点。
|
|
1809
|
-
this.startBackgroundJobWatch(runId, snapshot, activeRun.runner);
|
|
1676
|
+
this.jobWatch.startBackgroundJobWatch(runId, snapshot, activeRun.runner);
|
|
1810
1677
|
}
|
|
1811
1678
|
finally {
|
|
1812
1679
|
// cleanup must survive mid-finalize errors. The only unguarded
|
|
@@ -1822,7 +1689,7 @@ export class Bridge {
|
|
|
1822
1689
|
// runId,只能回「该任务已结束或不属于当前会话」。守卫只回答「cwd 槽位还
|
|
1823
1690
|
// 归不归本 run」(决定能不能删槽位/evict runner),记账回答「这次 run
|
|
1824
1691
|
// 结束了吗、用的哪个 sessionId」——后者与所有权无关,放在守卫外。
|
|
1825
|
-
this.recordCompactableRunForTerminal(cardSession, activeRun, ctx);
|
|
1692
|
+
this.compact.recordCompactableRunForTerminal(cardSession, activeRun, ctx);
|
|
1826
1693
|
if (this.activeRuns.get(cwd) === activeRun) {
|
|
1827
1694
|
this.activeRuns.delete(cwd);
|
|
1828
1695
|
// Workspace-lifetime runners (codex app-server) keep their persistent
|
|
@@ -1920,498 +1787,31 @@ export class Bridge {
|
|
|
1920
1787
|
async handleApprovalAnswerNote(opts) {
|
|
1921
1788
|
return this.approvalActions.handleApprovalAnswerNote(opts);
|
|
1922
1789
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
if (!byRunId) {
|
|
1932
|
-
byRunId = new Map();
|
|
1933
|
-
this.compactableRuns.set(cwd, byRunId);
|
|
1934
|
-
}
|
|
1935
|
-
byRunId.delete(runId);
|
|
1936
|
-
byRunId.set(runId, { sessionId, agentKind, cwd });
|
|
1937
|
-
while (byRunId.size > MAX_COMPACTABLE_RUNS_PER_CWD) {
|
|
1938
|
-
const oldest = byRunId.keys().next().value;
|
|
1939
|
-
if (oldest === undefined)
|
|
1940
|
-
break;
|
|
1941
|
-
byRunId.delete(oldest);
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
/**
|
|
1945
|
-
* 终态 Compact 记账:把本次 run 实际使用的 sessionId 记进
|
|
1946
|
-
* {@link compactableRuns},供终态卡上的 Compact 按钮按 runId 反查。
|
|
1947
|
-
*
|
|
1948
|
-
* 调用点固定在 `finalizeRun` 的 `finally` 里、**槽位所有权守卫之外**。
|
|
1949
|
-
* 所有权守卫只回答「cwd 槽位还归不归本 run」(决定能否删槽位 / evict
|
|
1950
|
-
* runner);记账回答「本次 run 结束了吗、用的哪个 sessionId」。两者在不同
|
|
1951
|
-
* 维度上:外部中断(`/stop`、停止卡按钮、`queue.immediate`)会先摘槽位,
|
|
1952
|
-
* 记账若跟着守卫走就永远不会落,中断卡片的按钮必然反查不到(中断
|
|
1953
|
-
* 后点 Compact 一律回「该任务已结束或不属于当前会话」)。
|
|
1954
|
-
*
|
|
1955
|
-
* 覆盖所有终态入口(正常结束、error、interrupted、idle_timeout、
|
|
1956
|
-
* goal_continuing)——这些卡片都渲染 Compact 按钮(`goal` 仍 active 时
|
|
1957
|
-
* 除外)。记账失败不能影响清理,故内部兜错。
|
|
1958
|
-
*/
|
|
1959
|
-
recordCompactableRunForTerminal(cardSession, activeRun, ctx) {
|
|
1960
|
-
try {
|
|
1961
|
-
if (!runnerSupportsCompact(activeRun.runner, activeRun.agentKind))
|
|
1962
|
-
return;
|
|
1963
|
-
if (activeRun.runner.lifetime !== 'workspace')
|
|
1964
|
-
return;
|
|
1965
|
-
const terminal = cardSession.currentState.terminal;
|
|
1966
|
-
// 与事件消费守卫、Compact 按钮共用同一份终态集合(run-state.ts)。
|
|
1967
|
-
if (!isRunTerminal(terminal))
|
|
1968
|
-
return;
|
|
1969
|
-
// 卡片状态优先(本轮 init/turn_started 写入);缺失时回退 store——
|
|
1970
|
-
// 启动失败但已 /stop 的 run 没有自己的 init,压缩此前会话仍是有意义
|
|
1971
|
-
// 的动作。
|
|
1972
|
-
const compactSessionId = cardSession.currentState.sessionId ??
|
|
1973
|
-
this.sessionStore.getSessionId(ctx.userId, activeRun.agentKind);
|
|
1974
|
-
if (!compactSessionId)
|
|
1975
|
-
return;
|
|
1976
|
-
this.recordCompactableRun(activeRun.cwd, activeRun.runId, compactSessionId, activeRun.agentKind);
|
|
1977
|
-
}
|
|
1978
|
-
catch (err) {
|
|
1979
|
-
getLogger().warn(`[lark-remote] compact record failed runId=${activeRun.runId}: ${errorMessage(err)}`);
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
/**
|
|
1983
|
-
* 按 runId 反查可压缩 run 记录(跨 cwd 扫描)。
|
|
1984
|
-
*
|
|
1985
|
-
* 为什么不带 cwd 过滤:按钮属于「那一次 run」,与用户此刻在哪个工作目录、
|
|
1986
|
-
* 排队期间有没有 /cd 无关。`/stop` 的 interruptCurrentRun 同样是全局按 runId
|
|
1987
|
-
* 找(从不查当前 cwd),compact 与它保持一致。用当前 cwd 收窄会让「切了工作区
|
|
1988
|
-
* 再点旧卡片」和「排队期间 /cd」两种情况误报「该任务已结束或不属于当前会话」。
|
|
1989
|
-
*/
|
|
1990
|
-
findCompactableRun(runId) {
|
|
1991
|
-
for (const byRunId of this.compactableRuns.values()) {
|
|
1992
|
-
const record = byRunId.get(runId);
|
|
1993
|
-
if (record)
|
|
1994
|
-
return record;
|
|
1995
|
-
}
|
|
1996
|
-
return undefined;
|
|
1997
|
-
}
|
|
1998
|
-
/**
|
|
1999
|
-
* Handle compact card action — trigger a compaction request for any
|
|
2000
|
-
* runCompact-capable runner(codex/kimi/opencode/pi/claude)。按钮带的是
|
|
2001
|
-
* 那次 run 的 runId,据此反查该 run 实际使用的 sessionId + agentKind
|
|
2002
|
-
* + cwd(compactableRuns,按 runId 记录),再调 runner 的 runCompact()。
|
|
2003
|
-
*/
|
|
1790
|
+
// =============================================================================
|
|
1791
|
+
// Compact 动作入口
|
|
1792
|
+
// =============================================================================
|
|
1793
|
+
// 实现住在 compact.ts(bridge 结构治理第 1 刀):可压缩 run 记账、终态卡 /
|
|
1794
|
+
// resume 卡两条入口、kimi 在途检测与压缩流式卡全部搬过去;这里只留薄转发,
|
|
1795
|
+
// router 的卡片动作分发(handleCardAction → bridge.handleCompactAction /
|
|
1796
|
+
// handleResumeCompact)与 finalizeRun 的终态记账调用路径不变。
|
|
1797
|
+
/** 终态卡 Compact 按钮:按 runId 反查那次 run 的会话并发起压缩。 */
|
|
2004
1798
|
async handleCompactAction(value, ctx) {
|
|
2005
|
-
|
|
2006
|
-
log.info(`[lark-remote] handleCompactAction userId=${ctx.userId} runId=${value.runId?.slice(0, 8)}...`);
|
|
2007
|
-
const { runId } = value;
|
|
2008
|
-
if (!runId) {
|
|
2009
|
-
await this.sendResult({ text: '⚠️ 无效的 Compact 请求,缺少 runId' }, ctx);
|
|
2010
|
-
return;
|
|
2011
|
-
}
|
|
2012
|
-
// 校验:run 结束后 activeRuns 已清空,只能靠按钮自带的 runId 反查记录。
|
|
2013
|
-
// 按 runId 存(不按「最新的那一条」),点击到执行之间夹了新 run 收尾也不失效;
|
|
2014
|
-
// 查找也不按「当前 cwd」收窄(见 findCompactableRun)。
|
|
2015
|
-
const target = this.findCompactableRun(runId);
|
|
2016
|
-
if (!target) {
|
|
2017
|
-
await this.sendResult({ text: '⚠️ 该任务已结束或不属于当前会话' }, ctx);
|
|
2018
|
-
return;
|
|
2019
|
-
}
|
|
2020
|
-
// 压缩必须用那次 run 自己的 cwd(session 文件 + runner 实例都按 cwd 解析)。
|
|
2021
|
-
const cwd = target.cwd;
|
|
2022
|
-
// §5.3: kimi 压缩在途检测(wire 状态机)。第二次点击在本地毫秒级如实回答,
|
|
2023
|
-
// 不开卡、不发 session/prompt——否则 kimi 服务端拒绝并产生「秒回」假完成。
|
|
2024
|
-
if (await this.rejectIfCompactionInFlight(target.sessionId, cwd, target.agentKind, ctx)) {
|
|
2025
|
-
return;
|
|
2026
|
-
}
|
|
2027
|
-
// Check that the runner has runCompact method(声明表 && runner 实现)
|
|
2028
|
-
const runner = this.getRunner(cwd, target.agentKind, { replaceStale: true });
|
|
2029
|
-
if (!runnerSupportsCompact(runner, target.agentKind)) {
|
|
2030
|
-
await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
|
|
2031
|
-
return;
|
|
2032
|
-
}
|
|
2033
|
-
log.info(`[lark-remote] handleCompactAction executing runCompact for runId=${runId} cwd=${cwd}`);
|
|
2034
|
-
await this.streamCompact({
|
|
2035
|
-
sessionId: target.sessionId,
|
|
2036
|
-
cwd,
|
|
2037
|
-
agentKind: target.agentKind,
|
|
2038
|
-
runner,
|
|
2039
|
-
ctx,
|
|
2040
|
-
});
|
|
2041
|
-
}
|
|
2042
|
-
/**
|
|
2043
|
-
* §5.3: kimi 压缩在途检测——reader 实现契约的 `readCompactionState` 时,若
|
|
2044
|
-
* wire 状态机判定当前有未终结的压缩(最后一次 begin 无 terminal 记录),
|
|
2045
|
-
* 直接回文本「已有一次 Compact 正在进行」并返回 true(调用方不开卡、不发
|
|
2046
|
-
* session/prompt)。codex/claude 等 reader 无此方法 → 返回 false,行为不变。
|
|
2047
|
-
*/
|
|
2048
|
-
async rejectIfCompactionInFlight(sessionId, cwd, agentKind, ctx) {
|
|
2049
|
-
const reader = this.sessionReaderRegistry.get(agentKind);
|
|
2050
|
-
if (!reader?.readCompactionState)
|
|
2051
|
-
return false;
|
|
2052
|
-
try {
|
|
2053
|
-
const state = reader.readCompactionState(sessionId, cwd);
|
|
2054
|
-
if (state?.inFlight) {
|
|
2055
|
-
const since = state.inFlightSince;
|
|
2056
|
-
let timeLabel = '未知时刻';
|
|
2057
|
-
if (since !== undefined) {
|
|
2058
|
-
const d = new Date(since);
|
|
2059
|
-
const hh = String(d.getHours()).padStart(2, '0');
|
|
2060
|
-
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
2061
|
-
timeLabel = `${hh}:${mm}`;
|
|
2062
|
-
}
|
|
2063
|
-
await this.sendResult({ text: `⚠️ 已有一次 Compact 正在进行(开始于 ${timeLabel}),请等待完成` }, ctx);
|
|
2064
|
-
return true;
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
catch (err) {
|
|
2068
|
-
getLogger().warn(`[lark-remote] readCompactionState failed sessionId=${sessionId}: ${errorMessage(err)}`);
|
|
2069
|
-
}
|
|
2070
|
-
return false;
|
|
1799
|
+
await this.compact.handleCompactAction(value, ctx);
|
|
2071
1800
|
}
|
|
2072
|
-
/**
|
|
2073
|
-
* Stream a compaction to a card: start a RunCardSession with
|
|
2074
|
-
* operationKind='compaction' (no recursive Compact button), consume
|
|
2075
|
-
* runner.runCompact() events, read authoritative jsonl usage, and finish
|
|
2076
|
-
* the card. Shared by handleCompactAction (run card button) and
|
|
2077
|
-
* handleResumeCompact (resume cards) so both flows stay in sync.
|
|
2078
|
-
*
|
|
2079
|
-
* Precondition: callers have already validated that `runner` implements
|
|
2080
|
-
* runCompact and that the session exists in `cwd`.
|
|
2081
|
-
*/
|
|
2082
|
-
async streamCompact(opts) {
|
|
2083
|
-
const log = getLogger();
|
|
2084
|
-
const { sessionId, cwd, agentKind, runner, ctx } = opts;
|
|
2085
|
-
log.info(`[lark-remote] streamCompact sessionId=${sessionId} cwd=${cwd}`);
|
|
2086
|
-
// 防御性能力检查:调用方已校验(runId / resume 两条路径),此处双保险并
|
|
2087
|
-
// 让 TS 收窄 runner 类型('runCompact' in runner 之后的 cast 才合法)。
|
|
2088
|
-
if (!runnerSupportsCompact(runner, agentKind) ||
|
|
2089
|
-
!('runCompact' in runner) ||
|
|
2090
|
-
typeof runner.runCompact !== 'function') {
|
|
2091
|
-
await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
|
|
2092
|
-
return;
|
|
2093
|
-
}
|
|
2094
|
-
// activeRuns 以 cwd 为键:注册前先确认槽位空闲,否则压缩会覆盖在跑的 run 的
|
|
2095
|
-
// 记录,令 /stop 只停到最后注册的那个。
|
|
2096
|
-
const busyWith = this.activeRuns.get(cwd);
|
|
2097
|
-
if (busyWith) {
|
|
2098
|
-
log.info(`[lark-remote] compact refused: cwd busy cwd=${cwd} runId=${busyWith.runId}`);
|
|
2099
|
-
await this.sendResult({ text: '⚠️ 该工作区正在运行任务,请等待结束或先 /stop' }, ctx);
|
|
2100
|
-
return;
|
|
2101
|
-
}
|
|
2102
|
-
await this.sendResult({ text: '🗜 Compact 已触发,正在压缩会话…' }, ctx);
|
|
2103
|
-
const compactRunId = randomUUID();
|
|
2104
|
-
// 渲染选项同时用于流式卡与「卡片流没建起来」时的静态兜底卡——两者必须
|
|
2105
|
-
// 同门控,否则兜底卡会渲染出与本次 run 不一致的按钮。
|
|
2106
|
-
const renderOptions = { agentKind };
|
|
2107
|
-
const cardSession = new RunCardSession({
|
|
2108
|
-
connector: this.connector,
|
|
2109
|
-
chatId: ctx.chatId,
|
|
2110
|
-
replyTo: ctx.messageId,
|
|
2111
|
-
runId: compactRunId,
|
|
2112
|
-
renderOptions,
|
|
2113
|
-
});
|
|
2114
|
-
// 压缩与普通 run 共用同一套生命周期——注册进 activeRuns(/stop 有对象可停、
|
|
2115
|
-
// /active 看得见)、挂 §9.12 空闲看门狗、终态后补 reaction。缺任何一环,卡住的
|
|
2116
|
-
// 压缩都只能等 runner 级超时,用户既停不掉也看不出它还在跑。
|
|
2117
|
-
const activeRun = {
|
|
2118
|
-
runId: compactRunId,
|
|
2119
|
-
userId: ctx.userId,
|
|
2120
|
-
chatId: ctx.chatId,
|
|
2121
|
-
session: cardSession,
|
|
2122
|
-
cwd,
|
|
2123
|
-
runner,
|
|
2124
|
-
agentKind,
|
|
2125
|
-
};
|
|
2126
|
-
this.activeRuns.set(cwd, activeRun);
|
|
2127
|
-
getLogger().info(`[lark-remote] activeRuns.set cwd=${cwd} runId=${compactRunId} compact=true`);
|
|
2128
|
-
let watchdog = null;
|
|
2129
|
-
try {
|
|
2130
|
-
await cardSession.start();
|
|
2131
|
-
watchdog = this.startIdleWatchdog({
|
|
2132
|
-
runId: compactRunId,
|
|
2133
|
-
session: cardSession,
|
|
2134
|
-
runner,
|
|
2135
|
-
});
|
|
2136
|
-
// operationKind=compaction:卡片不渲染 Compact 按钮(防递归 Compact)。
|
|
2137
|
-
await cardSession.push({
|
|
2138
|
-
type: 'turn_started',
|
|
2139
|
-
threadId: sessionId,
|
|
2140
|
-
turnId: '',
|
|
2141
|
-
operationKind: 'compaction',
|
|
2142
|
-
});
|
|
2143
|
-
let sawResult = false;
|
|
2144
|
-
let resultSubtype;
|
|
2145
|
-
let resultError;
|
|
2146
|
-
for await (const event of runner.runCompact('', { cwd, sessionId })) {
|
|
2147
|
-
// 已终态(/stop、空闲看门狗)后不再消费事件:与 runAgentStreamToEnd 同口径,
|
|
2148
|
-
// 否则迟到的 result 会把终态卡改写成「已完成」。
|
|
2149
|
-
const cardTerminal = cardSession.currentState.terminal;
|
|
2150
|
-
if (isRunTerminal(cardTerminal)) {
|
|
2151
|
-
continue;
|
|
2152
|
-
}
|
|
2153
|
-
if (event.type === 'result') {
|
|
2154
|
-
sawResult = true;
|
|
2155
|
-
resultSubtype = event.subtype;
|
|
2156
|
-
resultError = event.errorMessage;
|
|
2157
|
-
}
|
|
2158
|
-
watchdog?.touch();
|
|
2159
|
-
await cardSession.push(event);
|
|
2160
|
-
}
|
|
2161
|
-
// §5.3: 卡片终态必须等于引擎真实终态。旧实现只看 sawResult 布尔——error
|
|
2162
|
-
// subtype 的 result 也被 finish 'done'(放大器 bug,2026-08-31 事故)。
|
|
2163
|
-
const terminal = resultSubtype === 'success'
|
|
2164
|
-
? 'done'
|
|
2165
|
-
: resultSubtype === 'interrupted'
|
|
2166
|
-
? 'interrupted'
|
|
2167
|
-
: 'error';
|
|
2168
|
-
// 压缩结束后从会话 jsonl 读权威统计(压缩后上下文、压缩次数、本次压缩
|
|
2169
|
-
// token 消耗与会话累计)。thread/compacted 通知与 jsonl 落盘几乎同时
|
|
2170
|
-
// (实测差 ~9ms),compactCount 未读到则短重试,仍失败优雅降级(无统计)。
|
|
2171
|
-
// 不支持 compactCount 的 reader(opencode/dsh)不重试——否则每次 compact
|
|
2172
|
-
// 都会固定多跑两次完整 export/读取,且永远等不到该字段。
|
|
2173
|
-
const compactCountSupported = this.sessionReaderRegistry.tryGet(agentKind)?.supportsCompactCount !== false;
|
|
2174
|
-
let finalUsage = resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, sessionId, cwd, agentKind);
|
|
2175
|
-
for (let attempt = 0; compactCountSupported && !finalUsage?.compactCount && attempt < 2; attempt++) {
|
|
2176
|
-
await sleep(150);
|
|
2177
|
-
finalUsage = resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, sessionId, cwd, agentKind);
|
|
2178
|
-
}
|
|
2179
|
-
await cardSession.finish(terminal, {
|
|
2180
|
-
resultSubtype: resultSubtype ?? 'error',
|
|
2181
|
-
errorMsg: terminal === 'error' ? (resultError ?? '未收到压缩结果') : undefined,
|
|
2182
|
-
// finalUsage 的字段形态就是 FinishMeta 的用量部分(同为 UsageSnapshot),
|
|
2183
|
-
// 逐字段抄的那 14 行随单源一起去掉。
|
|
2184
|
-
...(finalUsage ?? {}),
|
|
2185
|
-
});
|
|
2186
|
-
log.info(`[lark-remote] streamCompact finished sessionId=${sessionId} sawResult=${sawResult} subtype=${resultSubtype ?? 'none'}`);
|
|
2187
|
-
}
|
|
2188
|
-
catch (err) {
|
|
2189
|
-
log.error(`[lark-remote] streamCompact failed: ${errorMessage(err)}`);
|
|
2190
|
-
// 终态守卫(与 runAgentStreamToEnd 同口径):running/finalizing 才转 error,
|
|
2191
|
-
// 已终态(interrupted/idle_timeout)保留首终态,不被迟到的异常改写。
|
|
2192
|
-
const catchTerminal = cardSession.currentState.terminal;
|
|
2193
|
-
if (!isRunTerminal(catchTerminal)) {
|
|
2194
|
-
await cardSession.finish('error', { errorMsg: errorMessage(err) });
|
|
2195
|
-
}
|
|
2196
|
-
else {
|
|
2197
|
-
log.info(`[lark-remote] skip compact error finish: state already terminal (${catchTerminal}) runId=${compactRunId}`);
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
finally {
|
|
2201
|
-
watchdog?.stop();
|
|
2202
|
-
// 与 run 路径同口径的降级:卡片流没建起来(start 失败 / stream 未收尾)时,
|
|
2203
|
-
// 终态只存在于内存里。settle() 释放 producer 并等 stream 收尾;'unsent'
|
|
2204
|
-
// 表示用户手里没有卡,必须补一张静态终态卡,否则只剩「Compact 已触发」
|
|
2205
|
-
// 和一条 reaction,用户既不知道失败也不知道原因。
|
|
2206
|
-
const settled = await cardSession.settle();
|
|
2207
|
-
if (settled === 'unsent') {
|
|
2208
|
-
const finalRender = renderRunCardDetailed(cardSession.currentState, renderOptions);
|
|
2209
|
-
await this.sendResult({ card: finalRender.card }, ctx);
|
|
2210
|
-
}
|
|
2211
|
-
// 只摘自己的槽位:/stop 可能已删掉本条并让排队的下一条占住同一 cwd。
|
|
2212
|
-
if (this.activeRuns.get(cwd) === activeRun) {
|
|
2213
|
-
this.activeRuns.delete(cwd);
|
|
2214
|
-
getLogger().info(`[lark-remote] activeRuns.delete cwd=${cwd} runId=${compactRunId}`);
|
|
2215
|
-
}
|
|
2216
|
-
// 终态 reaction:与普通 run 同一口径(done→Done / error→ERROR /
|
|
2217
|
-
// interrupted→SHHH / idle_timeout→Alarm)。
|
|
2218
|
-
this.finishTurnReactions(ctx, terminalReactionEmoji(cardSession.currentState.terminal));
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
/**
|
|
2222
|
-
* Handle resume.compact card action — compact a session directly from a
|
|
2223
|
-
* resume card (auto-resume / `/resume <id>`), without a runId.
|
|
2224
|
-
*
|
|
2225
|
-
* Unlike handleCompactAction (which validates against the last finished run's
|
|
2226
|
-
* runId), the resume card carries the sessionId + agent it was rendered for.
|
|
2227
|
-
* Validation: the session must exist in the current cwd (same rule as
|
|
2228
|
-
* cmdResume / resume.use) and the runner must implement runCompact.
|
|
2229
|
-
*/
|
|
1801
|
+
/** resume 卡 Compact 按钮:直接压缩卡上指定的 session(无需 runId)。 */
|
|
2230
1802
|
async handleResumeCompact(value, ctx) {
|
|
2231
|
-
|
|
2232
|
-
log.info(`[lark-remote] handleResumeCompact userId=${ctx.userId} sessionId=${value.sessionId?.slice(0, 16)} agent=${value.agent ?? 'default'}`);
|
|
2233
|
-
const { sessionId } = value;
|
|
2234
|
-
if (!sessionId) {
|
|
2235
|
-
await this.sendResult({ text: '⚠️ 无效的 Compact 请求,缺少 sessionId' }, ctx);
|
|
2236
|
-
return;
|
|
2237
|
-
}
|
|
2238
|
-
const cwd = this.resolveCwd(ctx.userId);
|
|
2239
|
-
if (!cwd) {
|
|
2240
|
-
await this.sendResult({ text: '⚠️ 未设置工作目录' }, ctx);
|
|
2241
|
-
return;
|
|
2242
|
-
}
|
|
2243
|
-
// Resolve agent kind the same way resume.use does: card value wins,
|
|
2244
|
-
// otherwise fall back to defaultAgent. 名单来自单源 AGENT_KINDS —— 手抄一份
|
|
2245
|
-
// 必然再次漂移(这份曾经漏 dsh:带 dsh 的卡片会静默退回 defaultAgent,
|
|
2246
|
-
// 用错 reader 报「未找到 session」)。
|
|
2247
|
-
const agentKind = value.agent && AGENT_KINDS.includes(value.agent)
|
|
2248
|
-
? value.agent
|
|
2249
|
-
: this.config.defaultAgent;
|
|
2250
|
-
// Session must exist in the current cwd (same rule as cmdResume /
|
|
2251
|
-
// resume.use): reject ghosts so a stale card cannot compact a session
|
|
2252
|
-
// from another cwd. Read failures get visible feedback (card click
|
|
2253
|
-
// 红线:不许静默失败)。
|
|
2254
|
-
let content;
|
|
2255
|
-
try {
|
|
2256
|
-
content = this.sessionReaderRegistry.get(agentKind).readSessionContent(sessionId, cwd);
|
|
2257
|
-
}
|
|
2258
|
-
catch (err) {
|
|
2259
|
-
await this.sendResult({ text: `⚠️ 读取 session ${sessionId} 失败: ${errorMessage(err)}` }, ctx);
|
|
2260
|
-
return;
|
|
2261
|
-
}
|
|
2262
|
-
if (content.events.length === 0 &&
|
|
2263
|
-
!content.usage &&
|
|
2264
|
-
!content.aiTitle &&
|
|
2265
|
-
!content.recap &&
|
|
2266
|
-
!content.displayTitle) {
|
|
2267
|
-
await this.sendResult({ text: `⚠️ 未找到 session ${sessionId}(当前目录: ${cwd})` }, ctx);
|
|
2268
|
-
return;
|
|
2269
|
-
}
|
|
2270
|
-
// §5.3: kimi 压缩在途检测(与 handleCompactAction 同一入口)。
|
|
2271
|
-
if (await this.rejectIfCompactionInFlight(sessionId, cwd, agentKind, ctx))
|
|
2272
|
-
return;
|
|
2273
|
-
// Check that the runner has runCompact(声明表 && runner 实现;
|
|
2274
|
-
// codex/kimi/opencode/pi/claude/dsh 均已实现)。
|
|
2275
|
-
const runner = this.getRunner(cwd, agentKind);
|
|
2276
|
-
if (!runnerSupportsCompact(runner, agentKind)) {
|
|
2277
|
-
await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
log.info(`[lark-remote] handleResumeCompact executing runCompact sessionId=${sessionId} cwd=${cwd}`);
|
|
2281
|
-
await this.streamCompact({ sessionId, cwd, agentKind, runner, ctx });
|
|
1803
|
+
await this.compact.handleResumeCompact(value, ctx);
|
|
2282
1804
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
1805
|
+
// =============================================================================
|
|
1806
|
+
// `!` bash 命令
|
|
1807
|
+
// =============================================================================
|
|
1808
|
+
// 实现住在 bash-run.ts(bridge 结构治理第 3 刀):executeBash 薄壳、事件流
|
|
1809
|
+
// 消费、独立登记表(activeBashRuns,不进 activeRuns)与 /stop 的 bash 侧
|
|
1810
|
+
// 匹配全部搬过去。这里只留薄转发——router 的 `!` 分发(executeBash)与
|
|
1811
|
+
// /active 投影(getActiveBashRuns)按 bridge 公开 API 调用。
|
|
1812
|
+
/** Execute a bash command and stream output to a card(`!` 命令入口)。 */
|
|
2289
1813
|
async executeBash(command, ctx) {
|
|
2290
|
-
|
|
2291
|
-
getLogger().info(`[lark-remote] executeBash start userId=${ctx.userId} sessionCwd=${this.sessionStore.get(ctx.userId)?.cwd} command="${command.slice(0, 30)}..."`);
|
|
2292
|
-
if (!cwd) {
|
|
2293
|
-
getLogger().info(`[lark-remote] executeBash no cwd, sending error`);
|
|
2294
|
-
await this.sendResult({ text: '请先使用 /cd <path> 设置工作目录,或保存一个 workspace (/ws save <name>)' }, ctx);
|
|
2295
|
-
return;
|
|
2296
|
-
}
|
|
2297
|
-
getLogger().info(`[lark-remote] executeBash proceeding cwd=${cwd}`);
|
|
2298
|
-
// `!` commands bypass the serial queue entirely (design.md §9.6):
|
|
2299
|
-
// index.ts dispatches `!` to router.handle → executeBash directly, NOT via
|
|
2300
|
-
// bridge.enqueue. bash runs in parallel with same-workspace claude runs.
|
|
2301
|
-
// bash runs are tracked in `activeBashRuns` (keyed by runId), separate from
|
|
2302
|
-
// claude's `activeRuns` (keyed by cwd) — they must not collide or `/stop`
|
|
2303
|
-
// would kill the wrong task. Do NOT re-chain onto the serial queue
|
|
2304
|
-
// (QueueManager) here: that would block on the still-pending outer promise
|
|
2305
|
-
// and deadlock self-wait.
|
|
2306
|
-
await this.executeBashInternal(command, ctx, cwd);
|
|
2307
|
-
}
|
|
2308
|
-
/**
|
|
2309
|
-
* Internal execution of bash command (called directly from executeBash,
|
|
2310
|
-
* bypassing the serial queue — see executeBash).
|
|
2311
|
-
*/
|
|
2312
|
-
async executeBashInternal(command, ctx, cwd) {
|
|
2313
|
-
getLogger().debug(`[lark-remote] executeBashInternal userId=${ctx.userId} chatId=${ctx.chatId} cwd=${cwd} command="${command.slice(0, 50)}..."`);
|
|
2314
|
-
// Note: We don't check activeRuns here because bash bypasses the serial
|
|
2315
|
-
// queue and runs in parallel with same-workspace agent runs (§9.6). The
|
|
2316
|
-
// activeRuns check in forwardToClaude is for a different purpose (detecting
|
|
2317
|
-
// concurrent agent runs that would corrupt session state). Bash commands
|
|
2318
|
-
// don't have that issue.
|
|
2319
|
-
const runId = randomUUID();
|
|
2320
|
-
const bashRunner = new BashProcessRunner();
|
|
2321
|
-
// Single streaming card across the whole run (initial → output patches →
|
|
2322
|
-
// terminal), instead of sending many independent cards. BashCardSession is
|
|
2323
|
-
// the bash analogue of RunCardSession.
|
|
2324
|
-
const cardSession = new BashCardSession({
|
|
2325
|
-
connector: this.connector,
|
|
2326
|
-
chatId: ctx.chatId,
|
|
2327
|
-
replyTo: ctx.messageId,
|
|
2328
|
-
runId,
|
|
2329
|
-
command,
|
|
2330
|
-
renderOptions: {},
|
|
2331
|
-
});
|
|
2332
|
-
// Track this bash run independently from claude's `activeRuns`. Bash runs
|
|
2333
|
-
// bypass the serial queue and may run in parallel with a claude run in the
|
|
2334
|
-
// same cwd, so they must NOT share activeRuns (which is keyed by cwd
|
|
2335
|
-
// and would let bash/claude overwrite each other). Keyed by runId so
|
|
2336
|
-
// multiple `!` commands can run concurrently. Tracked so /stop can reach
|
|
2337
|
-
// the bashRunner (which is otherwise a local) via interruptCurrentRun.
|
|
2338
|
-
this.activeBashRuns.set(runId, {
|
|
2339
|
-
bashRunner,
|
|
2340
|
-
cardSession,
|
|
2341
|
-
userId: ctx.userId,
|
|
2342
|
-
chatId: ctx.chatId,
|
|
2343
|
-
cwd,
|
|
2344
|
-
command,
|
|
2345
|
-
});
|
|
2346
|
-
let output = '';
|
|
2347
|
-
let stderr = '';
|
|
2348
|
-
let exitCode = null;
|
|
2349
|
-
try {
|
|
2350
|
-
getLogger().info(`[lark-remote] executeBashInternal about to start stream chatId=${ctx.chatId} messageId=${ctx.messageId}`);
|
|
2351
|
-
try {
|
|
2352
|
-
await cardSession.start();
|
|
2353
|
-
getLogger().info(`[lark-remote] executeBash stream started runId=${runId}`);
|
|
2354
|
-
}
|
|
2355
|
-
catch (err) {
|
|
2356
|
-
getLogger().warn('[lark-remote] bash card stream unavailable:', err);
|
|
2357
|
-
}
|
|
2358
|
-
for await (const event of bashRunner.run(command, { cwd })) {
|
|
2359
|
-
if (event.type === 'stdout') {
|
|
2360
|
-
// 层④:bridge 本地 output 也必须 store-time 截断——仅 session state
|
|
2361
|
-
// 有界并不会让 `output +=` 自然有界(「store 有界则本地拼接自然有界」
|
|
2362
|
-
// 不成立),!yes 洪峰下本地字符串仍全量驻留。capBashOutput 增量调用
|
|
2363
|
-
// O(CAP+chunk),不会带回 O(n²) 截断成本(output 恒 ≤ CAP,拼接后
|
|
2364
|
-
// ≤ CAP+chunk 再 slice 尾部)。
|
|
2365
|
-
output = capBashOutput(output + event.content);
|
|
2366
|
-
await cardSession.update({ output, stderr });
|
|
2367
|
-
}
|
|
2368
|
-
else if (event.type === 'stderr') {
|
|
2369
|
-
stderr = capBashOutput(stderr + event.content);
|
|
2370
|
-
await cardSession.update({ output, stderr });
|
|
2371
|
-
}
|
|
2372
|
-
else if (event.type === 'exit') {
|
|
2373
|
-
exitCode = event.exitCode ?? 0;
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
await cardSession.finish(exitCode === 0 ? 'done' : 'error', { exitCode, output, stderr });
|
|
2377
|
-
}
|
|
2378
|
-
catch (err) {
|
|
2379
|
-
getLogger().error(`[lark-remote] bash run error runId=${runId}:`, err);
|
|
2380
|
-
// If for-await threw mid-stream the bash child may still be running.
|
|
2381
|
-
// Stop it before finalizing the card — otherwise the finally block
|
|
2382
|
-
// removes the activeBashRuns entry and /stop can no longer reach it,
|
|
2383
|
-
// leaving an orphan process.
|
|
2384
|
-
try {
|
|
2385
|
-
await bashRunner.stop();
|
|
2386
|
-
}
|
|
2387
|
-
catch (stopErr) {
|
|
2388
|
-
getLogger().warn(`[lark-remote] bash stop failed:`, stopErr);
|
|
2389
|
-
}
|
|
2390
|
-
await cardSession.finish('error', {
|
|
2391
|
-
exitCode: 1,
|
|
2392
|
-
output,
|
|
2393
|
-
stderr: stderr + `\n执行错误: ${errorMessage(err)}`,
|
|
2394
|
-
});
|
|
2395
|
-
}
|
|
2396
|
-
finally {
|
|
2397
|
-
const settleResult = await cardSession.settle();
|
|
2398
|
-
if (settleResult === 'unsent') {
|
|
2399
|
-
// Stream never established a card — send a static terminal card as fallback.
|
|
2400
|
-
// Use 2.0 schema to match the streaming card (avoid 2.0→1.x downgrade).
|
|
2401
|
-
try {
|
|
2402
|
-
await this.connector.sendWithRetry(ctx.chatId, { card: renderBashCard(cardSession.currentState, {}) }, { replyTo: ctx.messageId });
|
|
2403
|
-
}
|
|
2404
|
-
catch (fallbackErr) {
|
|
2405
|
-
getLogger().error(`[lark-remote] bash terminal fallback card failed runId=${runId} chatId=${ctx.chatId}:`, fallbackErr);
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
if (this.activeBashRuns.has(runId)) {
|
|
2409
|
-
this.activeBashRuns.delete(runId);
|
|
2410
|
-
getLogger().info(`[lark-remote] executeBash done runId=${runId} exitCode=${exitCode}`);
|
|
2411
|
-
}
|
|
2412
|
-
// Add a Done emoji reaction to the user's original message to signal completion
|
|
2413
|
-
this.finishTurnReactions(ctx, 'Done');
|
|
2414
|
-
}
|
|
1814
|
+
await this.bashRuns.executeBash(command, ctx);
|
|
2415
1815
|
}
|
|
2416
1816
|
}
|
|
2417
1817
|
//# sourceMappingURL=index.js.map
|