lark-remote 0.5.20 → 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 +10 -0
- package/dist/bridge/inbound-media.js +71 -45
- package/dist/bridge/index.d.ts +9 -0
- package/dist/bridge/index.js +230 -203
- package/dist/card/approval-render.js +32 -33
- package/dist/card/card-budget.js +123 -98
- package/dist/card/run-renderer.js +82 -68
- package/dist/card/run-state.d.ts +0 -14
- package/dist/card/run-state.js +335 -207
- package/dist/card/tool-render.js +20 -16
- package/dist/card/usage-format.d.ts +12 -9
- package/dist/card/usage-format.js +111 -97
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/index.js +226 -190
- package/dist/message-handlers.js +253 -236
- 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 +189 -133
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/index.js +192 -201
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +287 -221
- package/dist/runner/common/acp/base-translator.js +19 -17
- 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/spawning-runner.d.ts +5 -0
- package/dist/runner/common/spawning-runner.js +8 -3
- 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/client.d.ts +7 -9
- package/dist/runner/dsh/client.js +356 -287
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +4 -3
- package/dist/runner/dsh/runner.js +88 -64
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/kimi/acp/runner.js +81 -44
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/session/claude/sessions.js +113 -101
- package/dist/session/codex/rollout-reader.js +179 -180
- package/dist/session/dsh/sessions.js +281 -231
- package/dist/session/kimi/sessions.d.ts +0 -7
- package/dist/session/kimi/sessions.js +129 -109
- package/dist/session/opencode/sessions.d.ts +0 -11
- package/dist/session/opencode/sessions.js +161 -137
- package/dist/session/session-store.d.ts +8 -0
- package/dist/session/session-store.js +98 -98
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.21] - 2026-11-05
|
|
9
|
+
|
|
10
|
+
### 维护
|
|
11
|
+
|
|
12
|
+
- **高复杂度函数按职责拆分**:router 的 `/ws` `/ls` `/order` 命令装配、会话存储与各 agent 的会话目录读取、卡片渲染与预算降级等函数拆成可单独测试的小函数,调用关系与输出不变;拆分风险由下面那条结构等价快照兜住。无行为变化
|
|
13
|
+
- **新增结构等价快照**:`structure-equivalence` 用例直接调用命令层与卡片层的公共出口,把卡片元素形状、文案、callback 的字段与顺序、分页栏、副作用调用记录整份录进外部快照,实现拆分后不带 `-u` 重跑必须逐字不变。快照里的动态值(临时目录及其父目录、路径分隔符、随机目录名、文件 mtime)在比较前统一归一,并加了漏归一守卫——宿主专属路径有残留直接抛错,同一份快照在 macOS / Linux / Windows 上逐字一致。无行为变化
|
|
14
|
+
- **补齐此前无用例的模块**:审批协调与审批事件、DSH 多路复用流与 translator、工具入参摘要、后台命令生命周期、各家 agent 的会话目录读取分支新增独立用例文件。无行为变化
|
|
15
|
+
- **用例不再向宿主操作系统要事实**:opencode 的日志错误监视原先靠「日志父路径被普通文件占住」制造 `ENOTDIR`,而 Windows 的 stat 对同一形态报 `ENOENT`——那正是「日志还没生成」的正常空态出口,故障分支只在单一系统走得到,现在改成按目标路径注入给定错误码;DSH 同步连接的断言不再硬编码 `/dev/null`(实现按平台传 `-o NUL`),只钉住 `-o` 后面必须是宿主空设备。无行为变化
|
|
16
|
+
- **pi 的中断用例消掉收尾竞态**:`abort` 是发了就不管的通知,写进子进程 stdin 不等于它已被读到。原先用例在 `stop()` 之后立刻收尾,清理时可能先杀掉模拟进程,接收日志里只剩 `get_state` 与 `prompt` 两条;现在等到 `abort` 真落进接收日志再走完事件循环,末事件仍断言为已中断。无行为变化
|
|
17
|
+
|
|
8
18
|
## [0.5.20] - 2026-11-04
|
|
9
19
|
|
|
10
20
|
### 修复
|
|
@@ -92,55 +92,81 @@ function sanitizeFileName(name) {
|
|
|
92
92
|
* 资源扩展名(图片/视频/语音):优先 MIME 映射,未知时按魔数兜底;
|
|
93
93
|
* 两者都无法识别时返回 undefined(调用方省略扩展名,避免错误标注格式)。
|
|
94
94
|
*/
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return 'webp';
|
|
103
|
-
}
|
|
104
|
-
if (head.length >= 12 &&
|
|
95
|
+
function normalizeMimeType(mimeType) {
|
|
96
|
+
return mimeType?.split(';')[0]?.trim().toLowerCase();
|
|
97
|
+
}
|
|
98
|
+
// --- 魔数判定原语(模块级命名函数,供查表顺序调用) ---
|
|
99
|
+
// 每个 matcher 逐字保留原 extensionFor 内联 if 的长度门槛与比较条件。
|
|
100
|
+
function isRiffFormat(format, head) {
|
|
101
|
+
return (head.length >= 12 &&
|
|
105
102
|
head.subarray(0, 4).toString('latin1') === 'RIFF' &&
|
|
106
|
-
head.subarray(8, 12).toString('latin1') ===
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
head.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
103
|
+
head.subarray(8, 12).toString('latin1') === format);
|
|
104
|
+
}
|
|
105
|
+
function isPng(head) {
|
|
106
|
+
return (head.length >= 8 &&
|
|
107
|
+
head.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])));
|
|
108
|
+
}
|
|
109
|
+
function isJpeg(head) {
|
|
110
|
+
return head.length >= 3 && head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff;
|
|
111
|
+
}
|
|
112
|
+
function isGif(head) {
|
|
113
|
+
return head.length >= 4 && head.subarray(0, 4).toString('latin1') === 'GIF8';
|
|
114
|
+
}
|
|
115
|
+
function isOgg(head) {
|
|
116
|
+
return head.length >= 4 && head.subarray(0, 4).toString('latin1') === 'OggS';
|
|
117
|
+
}
|
|
118
|
+
/** MP4/MOV 家族:ISO BMFF 在偏移 4 处写 'ftyp'。 */
|
|
119
|
+
function isIsoBmff(head) {
|
|
120
|
+
return head.length >= 8 && head.subarray(4, 8).toString('latin1') === 'ftyp';
|
|
121
|
+
}
|
|
122
|
+
function isBmp(head) {
|
|
123
|
+
return head.length >= 2 && head[0] === 0x42 && head[1] === 0x4d;
|
|
124
|
+
}
|
|
125
|
+
function isId3(head) {
|
|
126
|
+
return head.length >= 3 && head.subarray(0, 3).toString('latin1') === 'ID3';
|
|
127
|
+
}
|
|
128
|
+
/** MPEG audio frame sync(0xFFE0 掩码)+ 常见 layer/bitrate 组合。 */
|
|
129
|
+
function isMpegFrameSync(head) {
|
|
130
|
+
return head.length >= 2 && head[0] === 0xff && (head[1] & 0xfe) === 0xfa;
|
|
131
|
+
}
|
|
132
|
+
function isAmr(head) {
|
|
133
|
+
return head.length >= 5 && head.subarray(0, 5).toString('latin1') === '#!AMR';
|
|
134
|
+
}
|
|
135
|
+
function isMatroska(head) {
|
|
136
|
+
return head.length >= 4 && head.readUInt32BE(0) === 0x1a45dfa3;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 魔数规则表:键为唯一规则标识,值携带输出扩展名与判定函数。用 Map 而非对象
|
|
140
|
+
* 字面量(避免 `constructor` / `toString` 等键命中 Object.prototype),且 Map
|
|
141
|
+
* 迭代顺序 = 插入顺序,与原 if 链的短路优先级逐字一致(mp3 的两个签名占两条)。
|
|
142
|
+
*/
|
|
143
|
+
const MAGIC_EXTENSION_RULES = new Map([
|
|
144
|
+
['riff-webp', { ext: 'webp', matches: isRiffFormat.bind(null, 'WEBP') }],
|
|
145
|
+
['riff-wave', { ext: 'wav', matches: isRiffFormat.bind(null, 'WAVE') }],
|
|
146
|
+
['png', { ext: 'png', matches: isPng }],
|
|
147
|
+
['jpeg', { ext: 'jpg', matches: isJpeg }],
|
|
148
|
+
['gif', { ext: 'gif', matches: isGif }],
|
|
149
|
+
['ogg', { ext: 'opus', matches: isOgg }],
|
|
150
|
+
['iso-bmff', { ext: 'mp4', matches: isIsoBmff }],
|
|
151
|
+
['bmp', { ext: 'bmp', matches: isBmp }],
|
|
152
|
+
['id3', { ext: 'mp3', matches: isId3 }],
|
|
153
|
+
['mpeg-frame-sync', { ext: 'mp3', matches: isMpegFrameSync }],
|
|
154
|
+
['amr', { ext: 'amr', matches: isAmr }],
|
|
155
|
+
['matroska', { ext: 'mkv', matches: isMatroska }],
|
|
156
|
+
]);
|
|
157
|
+
function magicExtensionFor(head) {
|
|
158
|
+
for (const { ext, matches } of MAGIC_EXTENSION_RULES.values()) {
|
|
159
|
+
if (matches(head))
|
|
160
|
+
return ext;
|
|
141
161
|
}
|
|
142
162
|
return undefined;
|
|
143
163
|
}
|
|
164
|
+
function extensionFor(_kind, mimeType, head) {
|
|
165
|
+
const normalized = normalizeMimeType(mimeType);
|
|
166
|
+
if (normalized && MIME_TO_EXT[normalized])
|
|
167
|
+
return MIME_TO_EXT[normalized];
|
|
168
|
+
return magicExtensionFor(head);
|
|
169
|
+
}
|
|
144
170
|
/**
|
|
145
171
|
* 文件名长度限制(字节级,保留扩展名),避免 ENAMETOOLONG 整文件失败。
|
|
146
172
|
* 极罕见情况(扩展名本身超限)直接截断全名。
|
package/dist/bridge/index.d.ts
CHANGED
|
@@ -569,6 +569,15 @@ export declare class Bridge {
|
|
|
569
569
|
* Handles idle watchdog, event processing, session sync, and state transitions.
|
|
570
570
|
*/
|
|
571
571
|
private runAgentStreamToEnd;
|
|
572
|
+
/**
|
|
573
|
+
* runAgentStreamToEnd 的 try 主体(建卡 → 事件循环 → 收尾),抽成独立方法
|
|
574
|
+
* 降低外层圈复杂度;state 为跨方法共享的可变状态。逻辑与文案逐字保留。
|
|
575
|
+
*/
|
|
576
|
+
private executeStream;
|
|
577
|
+
/** runAgentStreamToEnd 的 catch 主体(抽成独立方法,state.sawResult 沿用循环累计值)。 */
|
|
578
|
+
private handleStreamFailure;
|
|
579
|
+
/** runAgentStreamToEnd 的 finally 主体(抽成独立方法)。 */
|
|
580
|
+
private cleanupStream;
|
|
572
581
|
/**
|
|
573
582
|
* 事件循环的看门狗喂狗(runAgentStreamToEnd 抽出的纯向方法)。
|
|
574
583
|
*
|
package/dist/bridge/index.js
CHANGED
|
@@ -63,6 +63,36 @@ function completionNotificationPresentation(terminal) {
|
|
|
63
63
|
return { title: '✅ 会话已完成', template: 'green', result: 'completed' };
|
|
64
64
|
}
|
|
65
65
|
}
|
|
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;
|
|
87
|
+
}
|
|
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;
|
|
95
|
+
}
|
|
66
96
|
/**
|
|
67
97
|
* Bridge owns the per-workspace serial processing queue (§9.6) and the
|
|
68
98
|
* single-card streaming forwarding loop (§9.12 watchdog + session sync).
|
|
@@ -276,38 +306,7 @@ export class Bridge {
|
|
|
276
306
|
}
|
|
277
307
|
/** Per-agent approval-mode settings snapshot from the current config. */
|
|
278
308
|
approvalModeSettingsFor(kind) {
|
|
279
|
-
|
|
280
|
-
switch (kind) {
|
|
281
|
-
case 'codex': {
|
|
282
|
-
const codexConfig = agents?.codex;
|
|
283
|
-
if (!codexConfig)
|
|
284
|
-
return undefined;
|
|
285
|
-
return {
|
|
286
|
-
approvalPolicy: codexConfig.approvalPolicy,
|
|
287
|
-
sandbox: codexConfig.sandbox,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
case 'kimi': {
|
|
291
|
-
const kimiConfig = agents?.kimi;
|
|
292
|
-
if (!kimiConfig)
|
|
293
|
-
return undefined;
|
|
294
|
-
return { permissionMode: kimiConfig.permissionMode };
|
|
295
|
-
}
|
|
296
|
-
case 'opencode': {
|
|
297
|
-
const opencodeConfig = agents?.opencode;
|
|
298
|
-
if (!opencodeConfig)
|
|
299
|
-
return undefined;
|
|
300
|
-
return { mode: opencodeConfig.mode };
|
|
301
|
-
}
|
|
302
|
-
case 'dsh': {
|
|
303
|
-
const dshConfig = agents?.dsh;
|
|
304
|
-
if (!dshConfig)
|
|
305
|
-
return undefined;
|
|
306
|
-
return { permissionPreset: dshConfig.permissionPreset };
|
|
307
|
-
}
|
|
308
|
-
default:
|
|
309
|
-
return undefined;
|
|
310
|
-
}
|
|
309
|
+
return approvalSettingsFor(this.config.agents, kind);
|
|
311
310
|
}
|
|
312
311
|
/** Get the current runner for a cwd. Used by router for agent-specific commands (.2). */
|
|
313
312
|
getCurrentRunner(cwd) {
|
|
@@ -1352,189 +1351,217 @@ export class Bridge {
|
|
|
1352
1351
|
// 'jsonl'。result 事件的 live 语义(pre-init/写回)单独声明,不能和
|
|
1353
1352
|
// 用量来源共用一个字段。
|
|
1354
1353
|
const usageAuthority = runner.getUsageAuthority?.();
|
|
1355
|
-
const liveResultEvents = runner
|
|
1356
|
-
//
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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
|
+
};
|
|
1368
1377
|
try {
|
|
1369
|
-
|
|
1370
|
-
// queue.immediate 的 run-start hold:目标槽位可能在 handler 发最终
|
|
1371
|
-
// toast 之前就 begin(interrupt/PATCH 的 await 窗口)。activeRun 已
|
|
1372
|
-
// 登记(队列语义照旧),只把建卡这一步挡到 toast 之后——run 卡片才是
|
|
1373
|
-
// 点击后最后一条新消息,聊天列表预览跟随它的标题变化。
|
|
1374
|
-
await this.queueManager.awaitTaskRunStart(cwd);
|
|
1375
|
-
getLogger().debug(`[lark-remote] cardSession.start() begin runId=${runId}`);
|
|
1376
|
-
await cardSession.start();
|
|
1377
|
-
getLogger().info(`[lark-remote] cardSession.start() ok runId=${runId}`);
|
|
1378
|
-
}
|
|
1379
|
-
catch (err) {
|
|
1380
|
-
getLogger().warn('[lark-remote] card stream unavailable, will use static fallback:', err);
|
|
1381
|
-
// Card stream failed - continue with static fallback below, but ensure
|
|
1382
|
-
// we don't silently fail if the fallback also fails (covered in catch below).
|
|
1383
|
-
}
|
|
1384
|
-
// /stop(或队列「立即执行」的中断)可能落在建卡窗口内:卡片已定格为
|
|
1385
|
-
// 终态,但 runner 还没开始跑,runner.stop() 也没有进程可杀——spawn 型
|
|
1386
|
-
// 与连接型 runner 的 stop() 在 run() 之前都是空操作,且 run() 入口会
|
|
1387
|
-
// 重置停止标记。必须在这里收口,否则用户看到「已中断」,agent 仍会把
|
|
1388
|
-
// 这一轮跑完并改写工作区。finalizeRun 仍会照常收尾(settle / reaction)。
|
|
1389
|
-
if (isRunTerminal(cardSession.currentState.terminal)) {
|
|
1390
|
-
getLogger().info(`[lark-remote] card terminal before run start, skip runner runId=${runId} terminal=${cardSession.currentState.terminal}`);
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
// 单 interval 看门狗。事件到来时只刷新时间戳(无 timer 操作);
|
|
1394
|
-
// interval 周期检查静默时长,超时才 finish('idle_timeout') + stop()。
|
|
1395
|
-
// 武装点在建卡之后,与原实现一致(宽限期不含 cardSession.start() 耗时)。
|
|
1396
|
-
watchdog = this.startIdleWatchdog({
|
|
1397
|
-
runId,
|
|
1398
|
-
session: cardSession,
|
|
1399
|
-
runner,
|
|
1400
|
-
pendingApprovals: () => this.approvalCoordinators.get(runId)?.pendingCount() ?? 0,
|
|
1401
|
-
});
|
|
1402
|
-
getLogger().info(`[lark-remote] runner.run() begin runId=${runId} message=${message.slice(0, 100)}`);
|
|
1403
|
-
// Extract agent-specific options from config
|
|
1404
|
-
const agentOpts = this.getAgentRunOptions(agentKind);
|
|
1405
|
-
const runOpts = { cwd, sessionId, ...agentOpts };
|
|
1406
|
-
// Track whether system.init has been received for this run.
|
|
1407
|
-
// Claude CLI --resume emits a stale result (from the previous turn)
|
|
1408
|
-
// before sending system.init; bridge must not treat that as the run
|
|
1409
|
-
// ending (sawResult, usage capture, or cardSession.push would all
|
|
1410
|
-
// be wrong for the historical event).
|
|
1411
|
-
let sawInit = false;
|
|
1412
|
-
const writeback = {
|
|
1413
|
-
sessionStore: this.sessionStore,
|
|
1414
|
-
userId: ctx.userId,
|
|
1415
|
-
agentKind,
|
|
1416
|
-
cwd,
|
|
1417
|
-
sessionEpochAtStart,
|
|
1418
|
-
runId,
|
|
1419
|
-
};
|
|
1420
|
-
for await (const event of runner.run(message, runOpts)) {
|
|
1421
|
-
// 只有终态才阻止事件处理。finalizing 是非终态,应该继续处理
|
|
1422
|
-
// 这样在 result 事件后,后台任务输出仍能被正确处理
|
|
1423
|
-
const terminal = cardSession.currentState.terminal;
|
|
1424
|
-
if (isRunTerminal(terminal)) {
|
|
1425
|
-
continue;
|
|
1426
|
-
}
|
|
1427
|
-
// 归一化:init/turn_started 的 session 写回 + compact_boundary 用量观察。
|
|
1428
|
-
// 返回 true = 本轮 system.init 已到(调用方置 sawInit,供 pre-init 守卫用)。
|
|
1429
|
-
if (this.maybeSyncSession(event, { activeRun, writeback, usage, runId })) {
|
|
1430
|
-
sawInit = true;
|
|
1431
|
-
}
|
|
1432
|
-
// pre-init result 守卫(§9.22,observeResultEvent):resume 重放的陈旧
|
|
1433
|
-
// result 不置 sawResult、不进卡片。
|
|
1434
|
-
if (event.type === 'result') {
|
|
1435
|
-
const outcome = this.observeResultEvent(event, {
|
|
1436
|
-
sawInit,
|
|
1437
|
-
liveResultEvents,
|
|
1438
|
-
usage,
|
|
1439
|
-
writeback,
|
|
1440
|
-
runId,
|
|
1441
|
-
});
|
|
1442
|
-
if (outcome.preInitTerminal)
|
|
1443
|
-
pendingPreInitTerminal ??= outcome.preInitTerminal;
|
|
1444
|
-
if (outcome.sawResult)
|
|
1445
|
-
sawResult = true;
|
|
1446
|
-
sawAnyResult = true;
|
|
1447
|
-
// 陈旧 result 不进卡片:run-state reducer 本来也会因 sessionId 未定而
|
|
1448
|
-
// 跳过,在 bridge 层就拦掉可以省下一次无谓的 push + render。
|
|
1449
|
-
if (outcome.dropFromCard) {
|
|
1450
|
-
// 仍然重置 idle 计时——CLI 活着并且在产事件。
|
|
1451
|
-
this.handleEventForWatchdog(watchdog);
|
|
1452
|
-
continue;
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
// Approval events (Codex app-server mode)。审批先到、item/started 后到的
|
|
1456
|
-
// 乱序流也在此补全(dispatchApprovalEvent 内 updateView)。
|
|
1457
|
-
if (event.type === 'approval_requested' ||
|
|
1458
|
-
event.type === 'approval_resolved' ||
|
|
1459
|
-
event.type === 'approval_view_updated') {
|
|
1460
|
-
this.dispatchApprovalEvent(event, { runId, runner, cardSession, ctx, agentKind });
|
|
1461
|
-
}
|
|
1462
|
-
// §9.12 红线:每个事件(含 result)无条件刷新
|
|
1463
|
-
// lastEventTs 重新武装看门狗。result 后 CLI 进程做 jsonl flush/清理(finalizing
|
|
1464
|
-
// 过渡)期间,宽限期恢复为完整的 idleTimeoutMs,避免已产出结果的 turn 被误标
|
|
1465
|
-
// idle_timeout。旧实现 `else { resetIdle(); }` 漏掉 result 分支。
|
|
1466
|
-
this.handleEventForWatchdog(watchdog);
|
|
1467
|
-
// 渲染:reducer(run-state)在 cardSession.push 内部。
|
|
1468
|
-
await cardSession.push(event);
|
|
1469
|
-
}
|
|
1470
|
-
getLogger().info(`[lark-remote] runner stream end runId=${runId} sawResult=${sawResult}`);
|
|
1471
|
-
// §9.12 红线:for-await 自然结束 = CLI 已退出,
|
|
1472
|
-
// 看门狗使命结束,立即摘除 interval(不再等 finally),堵掉「stream 已结束、
|
|
1473
|
-
// 收尾处理跨过死线被误标 idle_timeout」的同源小窗口(随本修复一并关闭)。
|
|
1474
|
-
watchdog?.stop();
|
|
1475
|
-
// stream 结束 = CLI 退出 = jsonl 已落盘。读 jsonl 拿准确的 contextLength + compactCount:
|
|
1476
|
-
// live stream-json 不发 compact_boundary,live contextLength 会被 result 的 input+output 兜底失真。
|
|
1477
|
-
// 但 result=error 时跳过: 进程可能立即退出未写新 usage (如 kimi 0.26+ 拒绝 -p+--auto
|
|
1478
|
-
// 立即 exit=1), 此时 jsonl 里是上一次成功 turn 的历史 usage, 会被误当作本次 usage 显示,
|
|
1479
|
-
// 误导用户以为本次出错的请求也消耗了 token。error 时只用 live 捕获的 usage (若有)。
|
|
1480
|
-
// 本轮使用的 session:run 期间 /new //resume //cd 会移动全局
|
|
1481
|
-
// 指针,结束时再读 store 会把别条会话的 token/context 显示到这张卡上。
|
|
1482
|
-
// 优先级:卡片状态(本轮 init 写入)→ activeRun(本轮 init/turn_started)
|
|
1483
|
-
// → 本轮开始时捕获的 sessionId。三者都没有就跳过 usage,绝不回读全局指针。
|
|
1484
|
-
const finalSessionId = cardSession.currentState.sessionId ?? activeRun.sessionId ?? sessionId;
|
|
1485
|
-
const finalUsage = usage.resultNotSuccess
|
|
1486
|
-
? undefined
|
|
1487
|
-
: resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, finalSessionId, cwd, agentKind);
|
|
1488
|
-
usage.finalize(finalUsage, usageAuthority);
|
|
1489
|
-
getLogger().info(usage.logSummary(runId));
|
|
1490
|
-
// for-await 自然结束 = stream 关闭 = CLI 进程退出 → 终态矩阵
|
|
1491
|
-
await this.finishRunAfterStream({
|
|
1492
|
-
cardSession,
|
|
1493
|
-
usage,
|
|
1494
|
-
sawResult,
|
|
1495
|
-
sawAnyResult,
|
|
1496
|
-
pendingPreInitTerminal,
|
|
1497
|
-
agentKind,
|
|
1498
|
-
runId,
|
|
1499
|
-
});
|
|
1378
|
+
await this.executeStream(run);
|
|
1500
1379
|
}
|
|
1501
1380
|
catch (err) {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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;
|
|
1507
1450
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
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;
|
|
1510
1455
|
}
|
|
1511
|
-
//
|
|
1512
|
-
//
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
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,
|
|
1520
1465
|
});
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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;
|
|
1526
1477
|
}
|
|
1527
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();
|
|
1528
1533
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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());
|
|
1535
1552
|
}
|
|
1536
1553
|
}
|
|
1537
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
|
+
}
|
|
1538
1565
|
/**
|
|
1539
1566
|
* 事件循环的看门狗喂狗(runAgentStreamToEnd 抽出的纯向方法)。
|
|
1540
1567
|
*
|