lark-remote 0.4.2 → 0.4.4
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 +33 -0
- package/dist/bridge/approval-coordinator.d.ts +2 -0
- package/dist/bridge/approval-coordinator.js +16 -7
- package/dist/bridge/index.d.ts +22 -0
- package/dist/bridge/index.js +138 -56
- package/dist/card/card-budget.js +39 -26
- package/dist/card/run-renderer.js +9 -2
- package/dist/card/run-state.js +81 -31
- package/dist/card/text-truncate.d.ts +9 -10
- package/dist/card/text-truncate.js +48 -43
- package/dist/card/tool-render.js +6 -2
- package/dist/cli.js +17 -0
- package/dist/config/codex-config.d.ts +3 -0
- package/dist/config/codex-config.js +4 -1
- package/dist/config/dir.js +6 -6
- package/dist/config/index.d.ts +7 -5
- package/dist/config/index.js +52 -13
- package/dist/config/kimi-config.d.ts +4 -1
- package/dist/config/kimi-config.js +5 -2
- package/dist/config/pi-config.d.ts +2 -7
- package/dist/config/pi-config.js +6 -6
- package/dist/config/wizard.js +8 -4
- package/dist/connector/index.d.ts +19 -0
- package/dist/connector/index.js +134 -23
- package/dist/inbound/placeholder.d.ts +3 -2
- package/dist/inbound/placeholder.js +17 -7
- package/dist/index.js +28 -6
- package/dist/instance-lock.d.ts +31 -15
- package/dist/instance-lock.js +64 -62
- package/dist/platform/agent-stopper.d.ts +29 -7
- package/dist/platform/agent-stopper.js +40 -8
- package/dist/platform/identity.d.ts +59 -3
- package/dist/platform/identity.js +105 -30
- package/dist/platform/path.d.ts +0 -7
- package/dist/platform/path.js +1 -51
- package/dist/platform/shell.d.ts +14 -0
- package/dist/platform/shell.js +35 -2
- package/dist/platform/sleep-blocker.d.ts +6 -0
- package/dist/platform/sleep-blocker.js +31 -5
- package/dist/platform/spawn.d.ts +1 -1
- package/dist/platform/spawn.js +1 -1
- package/dist/platform/terminator-posix.d.ts +1 -29
- package/dist/platform/terminator-posix.js +32 -30
- package/dist/platform/terminator-win32.js +4 -2
- package/dist/platform/terminator.d.ts +2 -2
- package/dist/platform/terminator.js +17 -2
- package/dist/platform/types.d.ts +3 -0
- package/dist/router/card-action-feedback.d.ts +17 -0
- package/dist/router/card-action-feedback.js +31 -0
- package/dist/router/config/claude.js +8 -5
- package/dist/router/index.d.ts +2 -0
- package/dist/router/index.js +35 -15
- package/dist/runner/claude/runner.js +1 -1
- package/dist/runner/claude/session.d.ts +2 -6
- package/dist/runner/claude/session.js +7 -8
- package/dist/runner/codex/app-server/protocol-types.d.ts +4 -0
- package/dist/runner/codex/app-server/protocol-types.js +4 -0
- package/dist/runner/codex/app-server/runner.js +3 -0
- package/dist/runner/common/acp/base-acp-runner.js +7 -0
- package/dist/runner/common/acp/base-translator.d.ts +43 -0
- package/dist/runner/common/acp/base-translator.js +83 -18
- package/dist/runner/common/connection-based-runner.d.ts +18 -0
- package/dist/runner/common/connection-based-runner.js +31 -0
- package/dist/runner/common/jsonrpc/client.js +6 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +20 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +27 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +29 -1
- package/dist/runner/common/jsonrpc/transport.js +79 -10
- package/dist/runner/common/spawning-runner.d.ts +49 -19
- package/dist/runner/common/spawning-runner.js +94 -54
- package/dist/runner/dsh/client.d.ts +1 -3
- package/dist/runner/dsh/client.js +0 -4
- package/dist/runner/dsh/types.d.ts +0 -36
- package/dist/runner/kimi/acp/runner.d.ts +18 -1
- package/dist/runner/kimi/acp/runner.js +100 -42
- package/dist/runner/kimi/acp/translator.d.ts +0 -1
- package/dist/runner/opencode/acp/translator.d.ts +22 -2
- package/dist/runner/opencode/acp/translator.js +60 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +4 -0
- package/dist/runner/pi/rpc/runner.js +3 -0
- package/dist/runner/resolve-agent-choices.js +0 -5
- package/package.json +2 -2
- package/dist/runner/common/process-stopper.d.ts +0 -7
- package/dist/runner/common/process-stopper.js +0 -8
- package/dist/runner/dsh/fake-dsh-server.d.ts +0 -66
- package/dist/runner/dsh/fake-dsh-server.js +0 -162
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ 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.4.4] - 2026-10-04
|
|
9
|
+
|
|
10
|
+
### 修复
|
|
11
|
+
|
|
12
|
+
- **opencode 的工具卡片能显示完整命令了**:opencode 的 ACP 在参数流式阶段先发 `tool_call`(参数为空、标题只回落成工具名),真正的命令/路径随后由 `tool_call_update` 携带。此前卡片只显示 `Bash` 看不到执行了什么;现在按调用 id 原地回填参数,命令与路径正常展示。顺带修掉同一命令被重复渲染成 `ls — ls` 的摘要去重问题
|
|
13
|
+
- **思考内容的省略计数不再虚增**:增量刷新时,旧逻辑把「前 N 字符已省略」标记自身的文本也计进被丢弃的正文,每帧约虚增 14 字符。现在只累计真实丢弃的正文,显示量始终不超过上限
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
|
|
17
|
+
- **`!` 命令失败用例去掉宿主 locale 依赖**:原先匹配 stderr 文案(`command not found`),中文 locale 下 bash 输出「未找到命令」导致用例稳定误红。改为断言确定性信号——命令名回显、非零退出码、诊断块呈现
|
|
18
|
+
|
|
19
|
+
## [0.4.3] - 2026-09-24
|
|
20
|
+
|
|
21
|
+
### 新增
|
|
22
|
+
|
|
23
|
+
- **富文本消息的顶层附件也能落盘了**:`@larksuite/channel` 从 `0.3.0` 升到 `0.7.1`。富文本(`post`)消息在**正文之外**的顶层附件区(`<file key=… name=…/>`,文件夹为 `<folder …/>`)此前不产出资源、会被当噪声丢掉;现在按序号识别并保存到当前工作目录,附件绝对路径照常写进 agent 提示词。往群里发一条「正文 + PDF」的富文本消息,agent 拿到的和普通文件消息一致(文件夹不产资源,只保留正文)
|
|
24
|
+
- **Windows 上收摊改走协议通道**:进程终止不再依赖跨进程 `SIGTERM`(win32 上本就不可靠),各 runner 统一登记「协议停止请求」,由 agent 自己响应后退出
|
|
25
|
+
|
|
26
|
+
### 修复
|
|
27
|
+
|
|
28
|
+
- **审批重投不再永不过期**:同一个 `requestId` 被重复投递时重新计时,并保证一个 requestId 最多只有一条待触发计时器。此前重投会留下两条计时器——要么审批永远等不到超时(server 无限等待),要么旧计时器提前把新窗口咬掉
|
|
29
|
+
- **Windows 防休眠此前从未生效**:`SetThreadExecutionState` 的 flags 因 Int32 溢出被截断,脚本一直静默失败。现按无符号 32 位、以十进制字面量传参,并把非终止错误升级为终止错误,让失败能落到告警分支而不是无声无息
|
|
30
|
+
- **资源下载超时不再静默**:超时只停止等待,底层下载照跑,晚到的结果由回调补做清理——避免「界面说超时、文件其实落了盘却没人管」
|
|
31
|
+
- **租户 token 失效会自动恢复**:识别 `tenant_access_token` 失效(飞书业务码 `99991663` 与 HTTP 401)并刷新后重试,不再让整条消息处理链失败
|
|
32
|
+
- **卡片 patch 失败不再成为无人接的 rejection**:流式卡片的 patch 由 throttle 触发、脱离 await 链(`Throttle.fireSoon` 丢掉 promise),业务码异常时改为记 warn 并原样透传返回值;需要抛出的场景收敛到带 await 的更新入口
|
|
33
|
+
|
|
34
|
+
### 变更
|
|
35
|
+
|
|
36
|
+
- **平台能力收口**:进程终止统一走 `Terminator` 抽象(删除 `ProcessStopper` 兼容壳),路径、进程身份、休眠能力按平台各自实现
|
|
37
|
+
- **claude 权限模式取值口径修正**:`permissionMode` 的 `default` 表示**省略该参数**(不是 CLI 的取值),与其他 agent 的配置语义对齐
|
|
38
|
+
- **测试套件平台无关化**:消除用例与 fixture 对宿主平台的隐性依赖(Windows 上全量失败从 25 条降到 0);同步 CLI 探测通道短路,不再依赖本机装了哪些 agent CLI
|
|
39
|
+
- **测试临时目录统一自清**:文件级钩子 + 全局兜底清扫,消除 `%TEMP%` 泄漏,被强杀或超时的场景也能收拾干净
|
|
40
|
+
|
|
8
41
|
## [0.4.2] - 2026-09-19
|
|
9
42
|
|
|
10
43
|
### 新增
|
|
@@ -142,6 +142,8 @@ export declare class ApprovalCoordinator {
|
|
|
142
142
|
*/
|
|
143
143
|
pendingCount(): number;
|
|
144
144
|
private clearTimer;
|
|
145
|
+
/** 装超时计时器;先清掉旧的,保证一个 requestId 最多只有一条待触发的计时。 */
|
|
146
|
+
private armTimeout;
|
|
145
147
|
/** nonce 去重:同一 nonce 只允许生效一次(防连点/飞书重投递)。 */
|
|
146
148
|
private assertFreshNonce;
|
|
147
149
|
/** 记录问题答案到 tracked.answers(懒初始化 Map)。 */
|
|
@@ -30,28 +30,30 @@ export class ApprovalCoordinator {
|
|
|
30
30
|
* Handle an approval_requested event.
|
|
31
31
|
*/
|
|
32
32
|
onRequested(event) {
|
|
33
|
+
// 事件级 timeoutMs 优先(Codex autoResolutionMs / Pi extension timeout),
|
|
34
|
+
// 缺省回落 run 级默认超时。放在事件上而非桥按 agentKind 分支,公共层
|
|
35
|
+
// 保持无 agent 分支。
|
|
36
|
+
const timeoutMs = event.timeoutMs ?? this.approvalTimeoutMs;
|
|
33
37
|
const existing = this.approvals.get(event.requestId);
|
|
34
38
|
if (existing) {
|
|
35
39
|
// Already tracked — update state
|
|
36
40
|
existing.state = 'pending';
|
|
37
41
|
existing.view = event.view;
|
|
42
|
+
// 重投必须重新计时:上一路的计时器在 resolved/expired 时已清掉,不重装
|
|
43
|
+
// 这条审批就永不过期,server 只能无限等待(红线「审批超时自动 cancel」)。
|
|
44
|
+
this.armTimeout(existing, timeoutMs);
|
|
38
45
|
return;
|
|
39
46
|
}
|
|
40
|
-
// 事件级 timeoutMs 优先(Codex autoResolutionMs / Pi extension timeout),
|
|
41
|
-
// 缺省回落 run 级默认超时。放在事件上而非桥按 agentKind 分支,公共层
|
|
42
|
-
// 保持无 agent 分支。
|
|
43
|
-
const timer = setTimeout(() => {
|
|
44
|
-
this.expireApproval(event.requestId);
|
|
45
|
-
}, event.timeoutMs ?? this.approvalTimeoutMs);
|
|
46
47
|
const tracked = {
|
|
47
48
|
requestId: event.requestId,
|
|
48
49
|
kind: event.kind,
|
|
49
50
|
state: 'pending',
|
|
50
51
|
view: event.view,
|
|
51
52
|
createdAt: Date.now(),
|
|
52
|
-
timeoutTimer:
|
|
53
|
+
timeoutTimer: null,
|
|
53
54
|
};
|
|
54
55
|
this.approvals.set(event.requestId, tracked);
|
|
56
|
+
this.armTimeout(tracked, timeoutMs);
|
|
55
57
|
}
|
|
56
58
|
/**
|
|
57
59
|
* Handle an approval_resolved event.
|
|
@@ -329,6 +331,13 @@ export class ApprovalCoordinator {
|
|
|
329
331
|
tracked.timeoutTimer = null;
|
|
330
332
|
}
|
|
331
333
|
}
|
|
334
|
+
/** 装超时计时器;先清掉旧的,保证一个 requestId 最多只有一条待触发的计时。 */
|
|
335
|
+
armTimeout(tracked, timeoutMs) {
|
|
336
|
+
this.clearTimer(tracked);
|
|
337
|
+
tracked.timeoutTimer = setTimeout(() => {
|
|
338
|
+
this.expireApproval(tracked.requestId);
|
|
339
|
+
}, timeoutMs);
|
|
340
|
+
}
|
|
332
341
|
/** nonce 去重:同一 nonce 只允许生效一次(防连点/飞书重投递)。 */
|
|
333
342
|
assertFreshNonce(requestId, nonce) {
|
|
334
343
|
if (nonce === undefined)
|
package/dist/bridge/index.d.ts
CHANGED
|
@@ -33,6 +33,12 @@ interface BridgeContext {
|
|
|
33
33
|
userId: string;
|
|
34
34
|
chatId: string;
|
|
35
35
|
messageId: string;
|
|
36
|
+
/**
|
|
37
|
+
* 本轮(装配窗口)全部入站 messageId,`messageId` 是其中最后一条。
|
|
38
|
+
* 只有走入站装配器的路径会填;卡片动作等单消息路径留空,收尾退化为只处理
|
|
39
|
+
* `messageId`。缺它会导致合并轮次里先到的消息挂着 Typing 不撤(§B8)。
|
|
40
|
+
*/
|
|
41
|
+
turnMessageIds?: string[];
|
|
36
42
|
}
|
|
37
43
|
/** A sendable result payload (mirror of the router's CommandResult shape). */
|
|
38
44
|
interface BridgeResult {
|
|
@@ -347,11 +353,27 @@ export declare class Bridge {
|
|
|
347
353
|
* Returns runId, cardSession, and activeRun object for tracking.
|
|
348
354
|
*/
|
|
349
355
|
private createRunSession;
|
|
356
|
+
/**
|
|
357
|
+
* §9.12 空闲看门狗:单 interval + 截止时间判定(P2-1),普通 run 与 compact
|
|
358
|
+
* 共用同一套兜底。事件到达只 `touch()` 刷新时间戳(无 timer 操作);静默超过
|
|
359
|
+
* `idleTimeoutMs` 才 finish('idle_timeout') + runner.stop(),让该工作区的串行
|
|
360
|
+
* 队列继续推进(否则一个挂死的 run 永久堵住后续消息)。
|
|
361
|
+
*/
|
|
362
|
+
private startIdleWatchdog;
|
|
350
363
|
/**
|
|
351
364
|
* Step 2 of forwardToClaude: Run the agent and consume its event stream.
|
|
352
365
|
* Handles idle watchdog, event processing, session sync, and state transitions.
|
|
353
366
|
*/
|
|
354
367
|
private runAgentStreamToEnd;
|
|
368
|
+
/**
|
|
369
|
+
* 终态 reaction 收尾:撤掉 Typing、打上下方终态表情。
|
|
370
|
+
*
|
|
371
|
+
* 逐条挂 Typing(`src/index.ts` 对每条入站消息都加),但 ctx.messageId 只是
|
|
372
|
+
* 装配窗口里的最后一条——只对它收尾会让先到的那几条永远停在「正在输入」。
|
|
373
|
+
* connector 的两个 reaction 方法自带 try/catch(缺失 reaction 是 no-op),
|
|
374
|
+
* 所以这里 fire-and-forget 即可,不额外兜错。
|
|
375
|
+
*/
|
|
376
|
+
private finishTurnReactions;
|
|
355
377
|
/**
|
|
356
378
|
* Step 3 of forwardToClaude: Finalize the run.
|
|
357
379
|
* Handles fallback card sending, completion notification, and cleanup.
|
package/dist/bridge/index.js
CHANGED
|
@@ -782,6 +782,51 @@ export class Bridge {
|
|
|
782
782
|
getLogger().info(`[lark-remote] activeRuns.set cwd=${cwd} runId=${runId}`);
|
|
783
783
|
return { runId, cardSession, activeRun };
|
|
784
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* §9.12 空闲看门狗:单 interval + 截止时间判定(P2-1),普通 run 与 compact
|
|
787
|
+
* 共用同一套兜底。事件到达只 `touch()` 刷新时间戳(无 timer 操作);静默超过
|
|
788
|
+
* `idleTimeoutMs` 才 finish('idle_timeout') + runner.stop(),让该工作区的串行
|
|
789
|
+
* 队列继续推进(否则一个挂死的 run 永久堵住后续消息)。
|
|
790
|
+
*/
|
|
791
|
+
startIdleWatchdog(opts) {
|
|
792
|
+
let lastEventTs = Date.now();
|
|
793
|
+
let interval = null;
|
|
794
|
+
const stop = () => {
|
|
795
|
+
if (interval)
|
|
796
|
+
clearInterval(interval);
|
|
797
|
+
interval = null;
|
|
798
|
+
};
|
|
799
|
+
if (this.idleTimeoutMs > 0) {
|
|
800
|
+
// 周期取 idleTimeoutMs/2:停滞后最多在 1.5× idleTimeoutMs 内触发
|
|
801
|
+
// (半个窗口延迟,看门狗语义可接受)
|
|
802
|
+
const tickMs = Math.max(1, Math.floor(this.idleTimeoutMs / 2));
|
|
803
|
+
interval = setInterval(() => {
|
|
804
|
+
if (Date.now() - lastEventTs <= this.idleTimeoutMs)
|
|
805
|
+
return;
|
|
806
|
+
// 权限等待期间暂停空闲看门狗:claude/codex 等待人工决策时无 stdout
|
|
807
|
+
// 事件。审批由 ApprovalCoordinator 的超时(默认 5 分钟 < 看门狗窗口)
|
|
808
|
+
// 自愈:cancel 送达 + 中断 turn,不会无限挂起。
|
|
809
|
+
if ((opts.pendingApprovals?.() ?? 0) > 0) {
|
|
810
|
+
lastEventTs = Date.now();
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
stop();
|
|
814
|
+
getLogger().warn(`[lark-remote] claude idle timeout, stopping process runId=${opts.runId}`);
|
|
815
|
+
void Promise.allSettled([
|
|
816
|
+
opts.session.finish('idle_timeout', {
|
|
817
|
+
idleTimeoutMinutes: Math.max(1, Math.round(this.idleTimeoutMs / 60_000)),
|
|
818
|
+
}),
|
|
819
|
+
opts.runner.stop(),
|
|
820
|
+
]);
|
|
821
|
+
}, tickMs);
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
touch: () => {
|
|
825
|
+
lastEventTs = Date.now();
|
|
826
|
+
},
|
|
827
|
+
stop,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
785
830
|
/**
|
|
786
831
|
* Step 2 of forwardToClaude: Run the agent and consume its event stream.
|
|
787
832
|
* Handles idle watchdog, event processing, session sync, and state transitions.
|
|
@@ -790,13 +835,8 @@ export class Bridge {
|
|
|
790
835
|
// Runner 声明的 usage 权威来源(review P3-7):codex app-server = 'live'
|
|
791
836
|
// (turn/started 的 tokenUsage.last 是本 turn 增量)。其余 runner 未声明。
|
|
792
837
|
const usageAuthority = runner.getUsageAuthority?.();
|
|
793
|
-
//
|
|
794
|
-
|
|
795
|
-
// 成正比(N 事件 = N 次 setTimeout + N 个闭包/秒);interval 方案只建一个
|
|
796
|
-
// interval,事件到来时仅刷新时间戳(无 timer 操作)。语义不变:事件流停滞
|
|
797
|
-
// 超过 idleTimeoutMs 仍触发 runner.stop()(§9.12)。
|
|
798
|
-
let lastEventTs = Date.now();
|
|
799
|
-
let idleInterval = null;
|
|
838
|
+
// §9.12 空闲看门狗:判定口径见 startIdleWatchdog;武装点在 cardSession.start() 之后。
|
|
839
|
+
let watchdog = null;
|
|
800
840
|
let sawResult = false;
|
|
801
841
|
// 记录 result 是否为 error: error 时跳过 jsonl usage 读取 (见 stream end 处说明)
|
|
802
842
|
let resultNotSuccess = false;
|
|
@@ -867,44 +907,15 @@ export class Bridge {
|
|
|
867
907
|
// Card session did not enter running state - still try to send static fallback
|
|
868
908
|
getLogger().info(`[lark-remote] cardSession not running, using static fallback`);
|
|
869
909
|
}
|
|
870
|
-
// P2-1: 单 interval
|
|
871
|
-
// interval
|
|
872
|
-
//
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
// 挂起。
|
|
880
|
-
const pendingApproval = this.approvalCoordinators.get(runId)?.pendingCount() ?? 0;
|
|
881
|
-
if (pendingApproval > 0) {
|
|
882
|
-
lastEventTs = Date.now();
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
if (idleInterval)
|
|
886
|
-
clearInterval(idleInterval);
|
|
887
|
-
idleInterval = null;
|
|
888
|
-
getLogger().warn(`[lark-remote] claude idle timeout, stopping process runId=${runId}`);
|
|
889
|
-
void Promise.allSettled([
|
|
890
|
-
cardSession.finish('idle_timeout', {
|
|
891
|
-
idleTimeoutMinutes: Math.max(1, Math.round(this.idleTimeoutMs / 60_000)),
|
|
892
|
-
}),
|
|
893
|
-
runner.stop(),
|
|
894
|
-
]);
|
|
895
|
-
};
|
|
896
|
-
const resetIdle = () => {
|
|
897
|
-
lastEventTs = Date.now();
|
|
898
|
-
};
|
|
899
|
-
if (this.idleTimeoutMs > 0) {
|
|
900
|
-
const tickMs = Math.max(1, Math.floor(this.idleTimeoutMs / 2));
|
|
901
|
-
idleInterval = setInterval(() => {
|
|
902
|
-
if (Date.now() - lastEventTs > this.idleTimeoutMs) {
|
|
903
|
-
fireIdleTimeout();
|
|
904
|
-
}
|
|
905
|
-
}, tickMs);
|
|
906
|
-
}
|
|
907
|
-
resetIdle();
|
|
910
|
+
// P2-1: 单 interval 看门狗。事件到来时只刷新时间戳(无 timer 操作);
|
|
911
|
+
// interval 周期检查静默时长,超时才 finish('idle_timeout') + stop()。
|
|
912
|
+
// 武装点在建卡之后,与原实现一致(宽限期不含 cardSession.start() 耗时)。
|
|
913
|
+
watchdog = this.startIdleWatchdog({
|
|
914
|
+
runId,
|
|
915
|
+
session: cardSession,
|
|
916
|
+
runner,
|
|
917
|
+
pendingApprovals: () => this.approvalCoordinators.get(runId)?.pendingCount() ?? 0,
|
|
918
|
+
});
|
|
908
919
|
let contextLength;
|
|
909
920
|
getLogger().info(`[lark-remote] runner.run() begin runId=${runId} message=${message.slice(0, 100)}`);
|
|
910
921
|
// Extract agent-specific options from config
|
|
@@ -995,7 +1006,7 @@ export class Bridge {
|
|
|
995
1006
|
if (preInitResult && usageAuthority !== 'live') {
|
|
996
1007
|
getLogger().info(`[lark-remote] pre-init result ignored (resume replay) runId=${runId}`);
|
|
997
1008
|
// Still reset idle timer — the CLI is alive and producing events.
|
|
998
|
-
|
|
1009
|
+
watchdog?.touch();
|
|
999
1010
|
// Do NOT push to cardSession: the run-state reducer would also
|
|
1000
1011
|
// skip it (sessionId === undefined), but skipping at the bridge
|
|
1001
1012
|
// layer avoids the push + render overhead for a no-op event.
|
|
@@ -1107,17 +1118,14 @@ export class Bridge {
|
|
|
1107
1118
|
// lastEventTs 重新武装看门狗。result 后 CLI 进程做 jsonl flush/清理(finalizing
|
|
1108
1119
|
// 过渡)期间,宽限期恢复为完整的 idleTimeoutMs,避免已产出结果的 turn 被误标
|
|
1109
1120
|
// idle_timeout。旧实现 `else { resetIdle(); }` 漏掉 result 分支。
|
|
1110
|
-
|
|
1121
|
+
watchdog?.touch();
|
|
1111
1122
|
await cardSession.push(event);
|
|
1112
1123
|
}
|
|
1113
1124
|
getLogger().info(`[lark-remote] runner stream end runId=${runId} sawResult=${sawResult}`);
|
|
1114
1125
|
// §9.12 红线(§P1-2 方案 A 第 2 点):for-await 自然结束 = CLI 已退出,
|
|
1115
1126
|
// 看门狗使命结束,立即摘除 interval(不再等 finally),堵掉「stream 已结束、
|
|
1116
1127
|
// 收尾处理跨过死线被误标 idle_timeout」的同源小窗口(P2-4 随本修复一并关闭)。
|
|
1117
|
-
|
|
1118
|
-
clearInterval(idleInterval);
|
|
1119
|
-
idleInterval = null;
|
|
1120
|
-
}
|
|
1128
|
+
watchdog?.stop();
|
|
1121
1129
|
// stream 结束 = CLI 退出 = jsonl 已落盘。读 jsonl 拿准确的 contextLength + compactCount:
|
|
1122
1130
|
// live stream-json 不发 compact_boundary,live contextLength 会被 result 的 input+output 兜底失真。
|
|
1123
1131
|
// 但 result=error 时跳过: 进程可能立即退出未写新 usage (如 kimi 0.26+ 拒绝 -p+--auto
|
|
@@ -1222,8 +1230,7 @@ export class Bridge {
|
|
|
1222
1230
|
}
|
|
1223
1231
|
}
|
|
1224
1232
|
finally {
|
|
1225
|
-
|
|
1226
|
-
clearInterval(idleInterval);
|
|
1233
|
+
watchdog?.stop();
|
|
1227
1234
|
const coordinator = this.approvalCoordinators.get(runId);
|
|
1228
1235
|
if (coordinator) {
|
|
1229
1236
|
coordinator.onTurnEnded();
|
|
@@ -1231,6 +1238,24 @@ export class Bridge {
|
|
|
1231
1238
|
}
|
|
1232
1239
|
}
|
|
1233
1240
|
}
|
|
1241
|
+
/**
|
|
1242
|
+
* 终态 reaction 收尾:撤掉 Typing、打上下方终态表情。
|
|
1243
|
+
*
|
|
1244
|
+
* 逐条挂 Typing(`src/index.ts` 对每条入站消息都加),但 ctx.messageId 只是
|
|
1245
|
+
* 装配窗口里的最后一条——只对它收尾会让先到的那几条永远停在「正在输入」。
|
|
1246
|
+
* connector 的两个 reaction 方法自带 try/catch(缺失 reaction 是 no-op),
|
|
1247
|
+
* 所以这里 fire-and-forget 即可,不额外兜错。
|
|
1248
|
+
*/
|
|
1249
|
+
finishTurnReactions(ctx, emoji) {
|
|
1250
|
+
const ids = ctx.turnMessageIds?.length
|
|
1251
|
+
? [...new Set([...ctx.turnMessageIds, ctx.messageId])]
|
|
1252
|
+
: [ctx.messageId];
|
|
1253
|
+
// 两类 emoji 互不相干(撤 Typing / 打终态),并发发出没有顺序依赖。
|
|
1254
|
+
for (const id of ids) {
|
|
1255
|
+
void this.connector.removeReactionByEmoji(id, 'Typing');
|
|
1256
|
+
void this.connector.addReaction(id, emoji);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1234
1259
|
/**
|
|
1235
1260
|
* Step 3 of forwardToClaude: Finalize the run.
|
|
1236
1261
|
* Handles fallback card sending, completion notification, and cleanup.
|
|
@@ -1264,7 +1289,7 @@ export class Bridge {
|
|
|
1264
1289
|
await this.sendCompletionNotificationCard(sessionId, cwd, ctx, activeRun.agentKind);
|
|
1265
1290
|
}
|
|
1266
1291
|
// Add a terminal-state emoji reaction to the user's original message
|
|
1267
|
-
|
|
1292
|
+
this.finishTurnReactions(ctx, terminalReactionEmoji(cardSession.currentState.terminal));
|
|
1268
1293
|
}
|
|
1269
1294
|
finally {
|
|
1270
1295
|
// P1-13: cleanup must survive mid-finalize errors. The only unguarded
|
|
@@ -1548,6 +1573,14 @@ export class Bridge {
|
|
|
1548
1573
|
await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
|
|
1549
1574
|
return;
|
|
1550
1575
|
}
|
|
1576
|
+
// activeRuns 以 cwd 为键:注册前先确认槽位空闲,否则压缩会覆盖在跑的 run 的
|
|
1577
|
+
// 记录,令 /stop 只停到最后注册的那个。
|
|
1578
|
+
const busyWith = this.activeRuns.get(cwd);
|
|
1579
|
+
if (busyWith) {
|
|
1580
|
+
log.info(`[lark-remote] compact refused: workspace busy cwd=${cwd} runId=${busyWith.runId}`);
|
|
1581
|
+
await this.sendResult({ text: '⚠️ 该工作区正在运行任务,请等待结束或先 /stop' }, ctx);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1551
1584
|
await this.sendResult({ text: '🗜 Compact 已触发,正在压缩会话…' }, ctx);
|
|
1552
1585
|
const compactRunId = randomUUID();
|
|
1553
1586
|
const cardSession = new RunCardSession({
|
|
@@ -1559,8 +1592,28 @@ export class Bridge {
|
|
|
1559
1592
|
agentKind,
|
|
1560
1593
|
},
|
|
1561
1594
|
});
|
|
1595
|
+
// A6:压缩与普通 run 共用同一套生命周期——注册进 activeRuns(/stop 有对象可停、
|
|
1596
|
+
// /active 看得见)、挂 §9.12 空闲看门狗、终态后补 reaction。缺任何一环,卡住的
|
|
1597
|
+
// 压缩都只能等 runner 级超时,用户既停不掉也看不出它还在跑。
|
|
1598
|
+
const activeRun = {
|
|
1599
|
+
runId: compactRunId,
|
|
1600
|
+
userId: ctx.userId,
|
|
1601
|
+
chatId: ctx.chatId,
|
|
1602
|
+
session: cardSession,
|
|
1603
|
+
cwd,
|
|
1604
|
+
runner,
|
|
1605
|
+
agentKind,
|
|
1606
|
+
};
|
|
1607
|
+
this.activeRuns.set(cwd, activeRun);
|
|
1608
|
+
getLogger().info(`[lark-remote] activeRuns.set cwd=${cwd} runId=${compactRunId} compact=true`);
|
|
1609
|
+
let watchdog = null;
|
|
1562
1610
|
try {
|
|
1563
1611
|
await cardSession.start();
|
|
1612
|
+
watchdog = this.startIdleWatchdog({
|
|
1613
|
+
runId: compactRunId,
|
|
1614
|
+
session: cardSession,
|
|
1615
|
+
runner,
|
|
1616
|
+
});
|
|
1564
1617
|
// operationKind=compaction:卡片不渲染 Compact 按钮(防递归 Compact)。
|
|
1565
1618
|
await cardSession.push({
|
|
1566
1619
|
type: 'turn_started',
|
|
@@ -1572,11 +1625,21 @@ export class Bridge {
|
|
|
1572
1625
|
let resultSubtype;
|
|
1573
1626
|
let resultError;
|
|
1574
1627
|
for await (const event of runner.runCompact('', { cwd, sessionId })) {
|
|
1628
|
+
// 已终态(/stop、空闲看门狗)后不再消费事件:与 runAgentStreamToEnd 同口径,
|
|
1629
|
+
// 否则迟到的 result 会把终态卡改写成「已完成」。
|
|
1630
|
+
const cardTerminal = cardSession.currentState.terminal;
|
|
1631
|
+
if (cardTerminal === 'done' ||
|
|
1632
|
+
cardTerminal === 'error' ||
|
|
1633
|
+
cardTerminal === 'interrupted' ||
|
|
1634
|
+
cardTerminal === 'idle_timeout') {
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1575
1637
|
if (event.type === 'result') {
|
|
1576
1638
|
sawResult = true;
|
|
1577
1639
|
resultSubtype = event.subtype;
|
|
1578
1640
|
resultError = event.errorMessage;
|
|
1579
1641
|
}
|
|
1642
|
+
watchdog?.touch();
|
|
1580
1643
|
await cardSession.push(event);
|
|
1581
1644
|
}
|
|
1582
1645
|
// §5.3: 卡片终态必须等于引擎真实终态。旧实现只看 sawResult 布尔——error
|
|
@@ -1616,7 +1679,26 @@ export class Bridge {
|
|
|
1616
1679
|
}
|
|
1617
1680
|
catch (err) {
|
|
1618
1681
|
log.error(`[lark-remote] streamCodexCompact failed: ${errorMessage(err)}`);
|
|
1619
|
-
|
|
1682
|
+
// 终态守卫(与 runAgentStreamToEnd 同口径):running/finalizing 才转 error,
|
|
1683
|
+
// 已终态(interrupted/idle_timeout)保留首终态,不被迟到的异常改写。
|
|
1684
|
+
const catchTerminal = cardSession.currentState.terminal;
|
|
1685
|
+
if (catchTerminal === 'running' || catchTerminal === 'finalizing') {
|
|
1686
|
+
await cardSession.finish('error', { errorMsg: errorMessage(err) });
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
log.info(`[lark-remote] skip compact error finish: state already terminal (${catchTerminal}) runId=${compactRunId}`);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
finally {
|
|
1693
|
+
watchdog?.stop();
|
|
1694
|
+
// 只摘自己的槽位:/stop 可能已删掉本条并让排队的下一条占住同一 cwd。
|
|
1695
|
+
if (this.activeRuns.get(cwd) === activeRun) {
|
|
1696
|
+
this.activeRuns.delete(cwd);
|
|
1697
|
+
getLogger().info(`[lark-remote] activeRuns.delete cwd=${cwd} runId=${compactRunId}`);
|
|
1698
|
+
}
|
|
1699
|
+
// 终态 reaction:与普通 run 同一口径(done→Done / error→ERROR /
|
|
1700
|
+
// interrupted→SHHH / idle_timeout→Alarm)。
|
|
1701
|
+
this.finishTurnReactions(ctx, terminalReactionEmoji(cardSession.currentState.terminal));
|
|
1620
1702
|
}
|
|
1621
1703
|
}
|
|
1622
1704
|
/**
|
|
@@ -1808,7 +1890,7 @@ export class Bridge {
|
|
|
1808
1890
|
getLogger().info(`[lark-remote] executeBash done runId=${runId} exitCode=${exitCode}`);
|
|
1809
1891
|
}
|
|
1810
1892
|
// Add a Done emoji reaction to the user's original message to signal completion
|
|
1811
|
-
|
|
1893
|
+
this.finishTurnReactions(ctx, 'Done');
|
|
1812
1894
|
}
|
|
1813
1895
|
}
|
|
1814
1896
|
}
|
package/dist/card/card-budget.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { truncateUtf8,
|
|
1
|
+
import { truncateUtf8, dropOldestMarkdownTables, countMarkdownTables, FEISHU_MAX_TABLES, CARD_BUDGET_BYTES, } from './text-truncate.js';
|
|
2
2
|
/**
|
|
3
3
|
* 静态卡片体积保护
|
|
4
4
|
*
|
|
@@ -12,16 +12,14 @@ export function enforceCardBudget(card, opts = {}) {
|
|
|
12
12
|
// 小体积可以含很多 table,大体积可以没有 table。必须在字节检查之前独立判断。
|
|
13
13
|
const cardStr = JSON.stringify(card);
|
|
14
14
|
const bytesBefore = Buffer.byteLength(cardStr, 'utf8');
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
truncateCardMarkdownTables(fixed);
|
|
20
|
-
const fixedStr = JSON.stringify(fixed);
|
|
15
|
+
const parsed = JSON.parse(cardStr);
|
|
16
|
+
const droppedTables = capCardMarkdownTables(parsed, FEISHU_MAX_TABLES);
|
|
17
|
+
if (droppedTables > 0) {
|
|
18
|
+
const fixedStr = JSON.stringify(parsed);
|
|
21
19
|
// table 截断后可能仍超字节预算,继续走后续阶段
|
|
22
20
|
if (Buffer.byteLength(fixedStr, 'utf8') <= CARD_BUDGET_BYTES) {
|
|
23
21
|
return {
|
|
24
|
-
card:
|
|
22
|
+
card: parsed,
|
|
25
23
|
wasTruncated: true,
|
|
26
24
|
reason: 'table_count_limited',
|
|
27
25
|
bytesBefore,
|
|
@@ -29,7 +27,7 @@ export function enforceCardBudget(card, opts = {}) {
|
|
|
29
27
|
};
|
|
30
28
|
}
|
|
31
29
|
// table 修好了但字节仍超,用 fixed 作为后续处理的起点
|
|
32
|
-
return enforceByteBudget(
|
|
30
|
+
return enforceByteBudget(parsed, opts, bytesBefore);
|
|
33
31
|
}
|
|
34
32
|
if (bytesBefore <= CARD_BUDGET_BYTES) {
|
|
35
33
|
return { card, wasTruncated: false, bytesBefore, bytesAfter: bytesBefore };
|
|
@@ -189,7 +187,7 @@ function getPanelIndices(elements) {
|
|
|
189
187
|
/**
|
|
190
188
|
* 遍历卡片元素树(含 collapsible_panel.elements 与 column_set.columns[].elements)。
|
|
191
189
|
*
|
|
192
|
-
*
|
|
190
|
+
* 计数与裁剪两条路径共用同一遍历,避免两套递归漂移(G5 Duplication)。
|
|
193
191
|
*/
|
|
194
192
|
function walkCardElements(elements, visit) {
|
|
195
193
|
for (const el of elements) {
|
|
@@ -205,29 +203,44 @@ function walkCardElements(elements, visit) {
|
|
|
205
203
|
}
|
|
206
204
|
}
|
|
207
205
|
/**
|
|
208
|
-
*
|
|
206
|
+
* 卡片里含 markdown table 的 lark_md 字段,按**文档顺序**(计数与裁剪共用同一
|
|
207
|
+
* 份遍历,避免两套递归漂移)。
|
|
209
208
|
*/
|
|
210
|
-
function
|
|
211
|
-
|
|
209
|
+
function tableFields(card) {
|
|
210
|
+
const fields = [];
|
|
212
211
|
walkCardElements(card?.body?.elements ?? [], (el) => {
|
|
213
212
|
if (el.text?.tag === 'lark_md' && typeof el.text.content === 'string') {
|
|
214
|
-
|
|
213
|
+
const tables = countMarkdownTables(el.text.content);
|
|
214
|
+
if (tables > 0)
|
|
215
|
+
fields.push({ el, tables });
|
|
215
216
|
}
|
|
216
217
|
});
|
|
217
|
-
return
|
|
218
|
+
return fields;
|
|
218
219
|
}
|
|
219
220
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
221
|
+
* 按**整卡**预算删除 markdown table:`maxTables` 是整张卡的上限,不是每个字段的。
|
|
222
|
+
*
|
|
223
|
+
* 逐字段各限 5 张是空操作(11310 红线失效):table 分散在多个字段时,每个字段
|
|
224
|
+
* 都「没超限」,一张都不会被删,而守卫照样上报 `table_count_limited`。
|
|
225
|
+
* 这里按文档顺序累计,超出预算的**最旧** table 整块删除、保留最新 `maxTables`
|
|
226
|
+
* 张——与 `truncateMarkdownTables()` 的「删最旧、留最新」口径一致。
|
|
227
|
+
*
|
|
228
|
+
* @returns 实际删除的 table 数(0 = 无需处理,未改动 card)
|
|
222
229
|
*/
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
function capCardMarkdownTables(card, maxTables) {
|
|
231
|
+
const fields = tableFields(card);
|
|
232
|
+
const total = fields.reduce((sum, f) => sum + f.tables, 0);
|
|
233
|
+
let dropped = 0;
|
|
234
|
+
for (const { el, tables } of fields) {
|
|
235
|
+
// 每轮按「还差几张」重算:某字段的分隔行数可能多于可删块(畸形表),
|
|
236
|
+
// 只按声明数删会漏。
|
|
237
|
+
const toDrop = total - dropped - maxTables;
|
|
238
|
+
if (toDrop <= 0)
|
|
239
|
+
break;
|
|
240
|
+
const content = el.text.content;
|
|
241
|
+
el.text.content = dropOldestMarkdownTables(content, Math.min(toDrop, tables));
|
|
242
|
+
dropped += tables - countMarkdownTables(el.text.content);
|
|
243
|
+
}
|
|
244
|
+
return dropped;
|
|
232
245
|
}
|
|
233
246
|
//# sourceMappingURL=card-budget.js.map
|
|
@@ -659,11 +659,18 @@ export function renderRunCard(state, options = {}) {
|
|
|
659
659
|
if (Buffer.byteLength(JSON.stringify(skeletonCard), 'utf8') <= CARD_BUDGET_BYTES) {
|
|
660
660
|
return skeletonCard;
|
|
661
661
|
}
|
|
662
|
-
// 理论上不可达:skeleton
|
|
662
|
+
// 理论上不可达:skeleton 已是静态最小结构。若仍超限(errorMsg 等无界文本
|
|
663
|
+
// 灌进 summary),硬截断只丢 summary——审批入口与 stop/新会话按钮是红线,
|
|
664
|
+
// 任何一级兜底都不能把它们截掉(丢了用户就再也停不下这个 run)。
|
|
663
665
|
const truncatedSkeleton = {
|
|
664
666
|
...skeletonCard,
|
|
665
667
|
body: {
|
|
666
|
-
elements: [
|
|
668
|
+
elements: [
|
|
669
|
+
statusRow(state),
|
|
670
|
+
markdownDiv('_⚠️ 输出过大_'),
|
|
671
|
+
...approvalArea(state),
|
|
672
|
+
...actionRow(state, options),
|
|
673
|
+
],
|
|
667
674
|
},
|
|
668
675
|
};
|
|
669
676
|
return truncatedSkeleton;
|