lark-remote 0.5.19 → 0.5.20

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/bridge/approval-actions.d.ts +1 -1
  3. package/dist/bridge/approval-actions.js +1 -1
  4. package/dist/bridge/bash-run.d.ts +71 -0
  5. package/dist/bridge/bash-run.js +217 -0
  6. package/dist/bridge/compact.d.ts +155 -0
  7. package/dist/bridge/compact.js +450 -0
  8. package/dist/bridge/final-usage.js +4 -4
  9. package/dist/bridge/inbound-media.js +1 -1
  10. package/dist/bridge/index.d.ts +97 -153
  11. package/dist/bridge/index.js +182 -809
  12. package/dist/bridge/job-watch.d.ts +68 -0
  13. package/dist/bridge/job-watch.js +146 -0
  14. package/dist/bridge/queue-manager.d.ts +3 -3
  15. package/dist/bridge/queue-manager.js +10 -29
  16. package/dist/card/approval-render.d.ts +2 -2
  17. package/dist/card/approval-render.js +109 -97
  18. package/dist/card/card-budget.js +1 -1
  19. package/dist/card/card-helpers.d.ts +3 -3
  20. package/dist/card/card-helpers.js +6 -6
  21. package/dist/card/final-answer-file.js +1 -1
  22. package/dist/card/run-renderer.d.ts +3 -3
  23. package/dist/card/run-renderer.js +34 -93
  24. package/dist/card/run-state.d.ts +1 -1
  25. package/dist/card/run-state.js +28 -90
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +1 -1
  28. package/dist/card/usage-format.d.ts +12 -0
  29. package/dist/card/usage-format.js +8 -3
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/config/codex-config.js +130 -106
  33. package/dist/config/index.d.ts +16 -2
  34. package/dist/config/index.js +23 -8
  35. package/dist/connector/index.js +6 -6
  36. package/dist/error-classification.js +3 -2
  37. package/dist/inbound/placeholder.d.ts +1 -1
  38. package/dist/inbound/placeholder.js +1 -1
  39. package/dist/index.js +203 -133
  40. package/dist/logger/index.js +6 -6
  41. package/dist/message-handlers.d.ts +0 -5
  42. package/dist/message-handlers.js +1 -1
  43. package/dist/model-catalog.d.ts +2 -2
  44. package/dist/model-catalog.js +3 -8
  45. package/dist/platform/identity.js +1 -1
  46. package/dist/platform/path.d.ts +1 -1
  47. package/dist/platform/path.js +1 -1
  48. package/dist/platform/sleep-blocker.js +1 -1
  49. package/dist/platform/terminator-win32.js +2 -1
  50. package/dist/router/commands/resume.js +2 -20
  51. package/dist/router/config/dsh.d.ts +4 -8
  52. package/dist/router/config/dsh.js +3 -10
  53. package/dist/router/config/kimi.js +1 -1
  54. package/dist/router/config/types.d.ts +1 -1
  55. package/dist/router/config-diff.js +1 -1
  56. package/dist/router/index.d.ts +56 -11
  57. package/dist/router/index.js +219 -200
  58. package/dist/runner/claude/session.d.ts +1 -1
  59. package/dist/runner/claude/session.js +6 -4
  60. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  61. package/dist/runner/codex/app-server/runner.js +30 -25
  62. package/dist/runner/codex/app-server/translator.d.ts +0 -5
  63. package/dist/runner/codex/app-server/translator.js +4 -17
  64. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  65. package/dist/runner/common/acp/acp-approval.js +1 -1
  66. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  67. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  68. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  69. package/dist/runner/common/acp/base-translator.js +31 -9
  70. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  71. package/dist/runner/common/acp/protocol-types.js +2 -2
  72. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  73. package/dist/runner/common/connection-based-runner.js +32 -2
  74. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  75. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  76. package/dist/runner/common/spawning-runner.d.ts +1 -1
  77. package/dist/runner/common/spawning-runner.js +1 -1
  78. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  79. package/dist/runner/dsh/catalog-probe.js +10 -0
  80. package/dist/runner/dsh/client.d.ts +1 -3
  81. package/dist/runner/dsh/client.js +2 -4
  82. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  83. package/dist/runner/dsh/exit-cancel.js +2 -2
  84. package/dist/runner/dsh/inflight.d.ts +1 -1
  85. package/dist/runner/dsh/inflight.js +1 -1
  86. package/dist/runner/dsh/runner.d.ts +40 -1
  87. package/dist/runner/dsh/runner.js +272 -199
  88. package/dist/runner/dsh/translator.d.ts +1 -1
  89. package/dist/runner/dsh/translator.js +1 -1
  90. package/dist/runner/index.d.ts +1 -1
  91. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  92. package/dist/runner/kimi/acp/runner.js +1 -2
  93. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  94. package/dist/runner/kimi/acp/translator.js +3 -4
  95. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  96. package/dist/runner/opencode/acp/runner.js +4 -4
  97. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  98. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  99. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  100. package/dist/runner/pi/rpc/runner.js +27 -42
  101. package/dist/runner/types.d.ts +2 -2
  102. package/dist/runner/types.js +1 -1
  103. package/dist/session/claude/sessions.d.ts +45 -0
  104. package/dist/session/claude/sessions.js +97 -71
  105. package/dist/session/codex/rollout-reader.js +4 -4
  106. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  107. package/dist/session/common/throw-session-list-error.js +16 -0
  108. package/dist/session/dsh/sessions.js +11 -5
  109. package/dist/session/kimi/sessions.d.ts +40 -1
  110. package/dist/session/kimi/sessions.js +244 -192
  111. package/dist/session/opencode/sessions.d.ts +153 -1
  112. package/dist/session/opencode/sessions.js +354 -321
  113. package/dist/session/pi/sessions.js +4 -5
  114. package/dist/session/session-store.d.ts +1 -1
  115. package/dist/session/session-store.js +1 -1
  116. package/dist/update/version-check.js +2 -1
  117. package/dist/workspace/index.d.ts +1 -1
  118. package/dist/workspace/index.js +1 -1
  119. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ 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.20] - 2026-11-04
9
+
10
+ ### 修复
11
+
12
+ - **超长回答的 Markdown 文件不再在发送失败时一起丢掉**:正文装不进一张卡时,完整回答会逐字写成临时 `.md` 随卡发出,卡上只留一句「完整内容见随附的 Markdown 文件」。此前发送失败也在收尾无条件删文件,于是卡上是被占位替代的半截正文、文件又已经没了,用户两头都拿不到。现在发送失败保留文件,并在错误日志里给出路径便于事后找回
13
+ - **`/stop` 没真的停下来不再被记成已停止**:停止动作同时做「终结卡片」和「终止进程」两件事,此前只看其中一个的结果——前者被飞书拒绝时卡片永久停在运行态,后者失败时进程可能还在跑而工作目录已被释放,日志照样写「停止完成」。现在两个结果分别判定:卡片终结失败与进程终止失败各自记错误日志,进程未确认终止时按「未确认」上报,不承诺已停止(这类运行仍会继续消耗额度,日志里能直接看出是哪一半失败)
14
+ - **DSH 会话列表读取失败不再显示成「没有会话」**:列表读取出错时此前静默返回空列表,`/resume` 于是画出「无会话」。远端根本没有「空」这个状态——守护进程没起、登录凭据失效、请求超时全都是故障,用户却只能把它读成「历史会话没了」。现在读取失败直接报错并说明是哪个 agent 与原因,与 kimi / opencode 的口径一致;目录确实不存在(真的还没会话)仍按空态返回,两者分得开
15
+ - **切换 agent 后的切换语义说明重新随卡可见**:`/config` 保存并切换 agent 时会发一张恢复卡,而文档承诺的那句切换语义(当前会话已清空、下一条消息开新对话)此前只挂在一条生产走不到的兜底卡上,实际一个字都没显示过。现在它作为卡片标题的尾行随卡显示;卡片刷新失败也不再吞掉这条通知——保存确实成功了,只记日志,后续消息照常切换
16
+ - **启动失败不再零输出退出**:安装损坏或依赖缺失时,此前表现为没有任何说明地直接退出码 1,用户无从知道是哪里坏了。现在把根因打到标准错误;用 Bun 拉起、回落到 Node 的那条路径同样留痕,「为什么跑在了 Node 上」可查
17
+
18
+ ### 维护
19
+
20
+ - **超大模块按职责拆分**:消息处理主模块拆出压缩、后台任务监视、命令执行三块独立实现;三个 agent 的会话目录扫描器把重复的遍历与解析逻辑收进共享实现;超长的事件循环按段拆分,原有的顺序约束注释随迁。无行为变化
21
+ - **重复定义收敛为单一来源**:agent 默认配置、token 计数格式化、卡片图标与按钮构造等多处副本各自合并成一份;agent 类型改由统一常量派生,新增 agent 时不再需要多处同步改。无行为变化
22
+ - **删除走不到的代码**:生产不可达的回退分支、恒假的判空与淘汰守卫、纯转发的委托方法、被参数收窄取代的入口校验一并删除,配套测试桩从假接口迁到真实调用路径。无行为变化
23
+ - **日志与错误口径对齐**:若干处失败被静默吞掉或级别低于兄弟模块的问题按统一口径修正(克隆兜底、Windows 进程终止、审批拒绝、媒体发送、更新缓存、凭据重签等)。无行为变化
24
+ - **文档与注释去陈旧**:删掉指向已不存在文件的引用,修正过时的测试规模与卡片元素上限说法,把「排查日期 + 现场反馈 + 时长读数」式的事故叙述改成自描述注释——保留故障形态、机制、阈值与约束,去掉读者无法复核的时间性细节。无行为变化
25
+ - **会话列表故障用例不再向操作系统要错误码**:pi 的列表失败契约此前靠「sessions 这个名字被文件占用」制造 `ENOTDIR`,而同一个形态在 Windows 上并不报这个码,用例只在单一系统成立。改为直接注入给定错误码:`EPERM` 钉住上抛的契约文案与原因链,并新增「目录不存在=优雅空态」这条此前无人覆盖的出口;原真实文件系统形态保留,按系统门控并写明分歧原因。无行为变化
26
+
8
27
  ## [0.5.19] - 2026-11-03
9
28
 
10
29
  ### 修复
@@ -82,7 +82,7 @@ export declare class ApprovalActions {
82
82
  nonce: string;
83
83
  }): Promise<void>;
84
84
  /**
85
- * Codex AskUserQuestion 补充说明(user_note,2026-08-18 live 验证模型理解
85
+ * Codex AskUserQuestion 补充说明(user_note,模型理解
86
86
  * "user_note: <text>" 条目):记录到 coordinator,随答案一起提交。
87
87
  * 路由到 ApprovalCoordinator.answerNote。
88
88
  */
@@ -93,7 +93,7 @@ export class ApprovalActions {
93
93
  return this.dispatchQuestionAction(opts, 'handleApprovalAnswerCustom', (coordinator) => coordinator.answerCustom({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
94
94
  }
95
95
  /**
96
- * Codex AskUserQuestion 补充说明(user_note,2026-08-18 live 验证模型理解
96
+ * Codex AskUserQuestion 补充说明(user_note,模型理解
97
97
  * "user_note: <text>" 条目):记录到 coordinator,随答案一起提交。
98
98
  * 路由到 ApprovalCoordinator.answerNote。
99
99
  */
@@ -0,0 +1,71 @@
1
+ import type { SessionStore } from '../session/index.js';
2
+ import type { RunTerminal } from '../card/run-state.js';
3
+ import type { BridgeChannel, CommandContext, CommandResult } from './index.js';
4
+ /**
5
+ * `!` bash 命令执行(bridge 结构治理第 3 刀):建卡、事件流消费、终态装配
6
+ * 与 runId 登记表。
7
+ *
8
+ * 红线(§9.6 / §9.18):
9
+ * - bash **绝不 re-chain 到 QueueManager**——enqueue 队列 promise 未 settle 时
10
+ * re-chain 会自等待死锁(外层 promise 等内层、内层排在外层后面)。
11
+ * - bash 用独立 runId 空间、独立登记表,不进 activeRuns,/stop 不会误杀。
12
+ * - 中断卡的 `cardSession.finish('interrupted')` 不传 exitCode(保持 null),
13
+ * bash-renderer 的「已手动终止」文案依赖它。
14
+ *
15
+ * 依赖显式注入(与 ApprovalActions / CompactLifecycle / JobWatchLifecycle
16
+ * 同一模式)。
17
+ */
18
+ export interface BashRunLifecycleDeps {
19
+ connector: BridgeChannel;
20
+ sessionStore: SessionStore;
21
+ resolveCwd(userId: string): string | undefined;
22
+ sendResult(result: CommandResult, ctx: CommandContext): Promise<boolean>;
23
+ finishTurnReactions(ctx: CommandContext, emoji: string): void;
24
+ }
25
+ export declare class BashRunLifecycle {
26
+ private readonly deps;
27
+ /** Active `!` bash runs, keyed by runId (NOT cwd)。见 {@link ActiveBashRun}。 */
28
+ private activeBashRuns;
29
+ constructor(deps: BashRunLifecycleDeps);
30
+ /**
31
+ * Execute a bash command and stream output to a card.
32
+ * `!` commands bypass the serial queue entirely (see body comment + §9.6):
33
+ * bash runs in parallel with same-workspace agent runs, tracked in
34
+ * `activeBashRuns` separately from `activeRuns`.
35
+ */
36
+ executeBash(command: string, ctx: CommandContext): Promise<void>;
37
+ /**
38
+ * Internal execution of bash command (called directly from executeBash,
39
+ * bypassing the serial queue — see executeBash).
40
+ */
41
+ private executeBashInternal;
42
+ /** Get all active bash runs with their state for /active command. */
43
+ getActiveBashRuns(): Array<{
44
+ runId: string;
45
+ cwd: string;
46
+ userId: string;
47
+ chatId: string;
48
+ terminal: RunTerminal;
49
+ command: string;
50
+ }>;
51
+ /**
52
+ * /stop 的 bash 侧(interruptCurrentRunDetailed 的后半段)。
53
+ *
54
+ * These bypass the serial queue and are tracked separately; stop the per-run
55
+ * BashRunner directly.
56
+ * `/stop`(无 runId、无 cwd)语义是「停掉这个会话的一切」,所以并行跑的
57
+ * `!` 命令也要停——旧实现命中 claude run 就 return,这些 bash 会被漏掉。
58
+ * 带范围的中断(停止卡 runId、queue.immediate 的 cwd)保持旧语义:只有
59
+ * 没有 claude run 命中时才回退到 bash,避免误杀用户并行跑的 `!` 任务。
60
+ *
61
+ * @param claudeTargetCount 本次中断已命中的 claude run 数(0 = 可回退到 bash)
62
+ * @returns 本次实际停掉的 bash run 数
63
+ */
64
+ stopMatchingBashRuns(input: {
65
+ userId: string;
66
+ chatId: string;
67
+ runId?: string;
68
+ cardMessageId?: string;
69
+ cwd?: string;
70
+ }, claudeTargetCount: number): Promise<number>;
71
+ }
@@ -0,0 +1,217 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { BashProcessRunner } from '../runner/index.js';
3
+ import { BashCardSession, capBashOutput } from '../card/bash-card-session.js';
4
+ import { renderBashCard } from '../card/bash-renderer.js';
5
+ import { getLogger } from '../logger/index.js';
6
+ import { errorMessage } from './final-usage.js';
7
+ export class BashRunLifecycle {
8
+ deps;
9
+ /** Active `!` bash runs, keyed by runId (NOT cwd)。见 {@link ActiveBashRun}。 */
10
+ activeBashRuns = new Map();
11
+ constructor(deps) {
12
+ this.deps = deps;
13
+ }
14
+ /**
15
+ * Execute a bash command and stream output to a card.
16
+ * `!` commands bypass the serial queue entirely (see body comment + §9.6):
17
+ * bash runs in parallel with same-workspace agent runs, tracked in
18
+ * `activeBashRuns` separately from `activeRuns`.
19
+ */
20
+ async executeBash(command, ctx) {
21
+ const cwd = this.deps.resolveCwd(ctx.userId);
22
+ getLogger().info(`[lark-remote] executeBash start userId=${ctx.userId} sessionCwd=${this.deps.sessionStore.get(ctx.userId)?.cwd} command="${command.slice(0, 30)}..."`);
23
+ if (!cwd) {
24
+ getLogger().info(`[lark-remote] executeBash no cwd, sending error`);
25
+ await this.deps.sendResult({ text: '请先使用 /cd <path> 设置工作目录,或保存一个 workspace (/ws save <name>)' }, ctx);
26
+ return;
27
+ }
28
+ getLogger().info(`[lark-remote] executeBash proceeding cwd=${cwd}`);
29
+ // `!` commands bypass the serial queue entirely (design.md §9.6):
30
+ // index.ts dispatches `!` to router.handle → executeBash directly, NOT via
31
+ // bridge.enqueue. bash runs in parallel with same-workspace claude runs.
32
+ // bash runs are tracked in `activeBashRuns` (keyed by runId), separate from
33
+ // claude's `activeRuns` (keyed by cwd) — they must not collide or `/stop`
34
+ // would kill the wrong task. Do NOT re-chain onto the serial queue
35
+ // (QueueManager) here: that would block on the still-pending outer promise
36
+ // and deadlock self-wait.
37
+ await this.executeBashInternal(command, ctx, cwd);
38
+ }
39
+ /**
40
+ * Internal execution of bash command (called directly from executeBash,
41
+ * bypassing the serial queue — see executeBash).
42
+ */
43
+ async executeBashInternal(command, ctx, cwd) {
44
+ getLogger().debug(`[lark-remote] executeBashInternal userId=${ctx.userId} chatId=${ctx.chatId} cwd=${cwd} command="${command.slice(0, 50)}..."`);
45
+ // Note: We don't check activeRuns here because bash bypasses the serial
46
+ // queue and runs in parallel with same-workspace agent runs (§9.6). The
47
+ // activeRuns check in forwardToClaude is for a different purpose (detecting
48
+ // concurrent agent runs that would corrupt session state). Bash commands
49
+ // don't have that issue.
50
+ const runId = randomUUID();
51
+ const bashRunner = new BashProcessRunner();
52
+ // Single streaming card across the whole run (initial → output patches →
53
+ // terminal), instead of sending many independent cards. BashCardSession is
54
+ // the bash analogue of RunCardSession.
55
+ const cardSession = new BashCardSession({
56
+ connector: this.deps.connector,
57
+ chatId: ctx.chatId,
58
+ replyTo: ctx.messageId,
59
+ runId,
60
+ command,
61
+ renderOptions: {},
62
+ });
63
+ // Track this bash run independently from claude's `activeRuns`. Bash runs
64
+ // bypass the serial queue and may run in parallel with a claude run in the
65
+ // same cwd, so they must NOT share activeRuns (which is keyed by cwd
66
+ // and would let bash/claude overwrite each other). Keyed by runId so
67
+ // multiple `!` commands can run concurrently. Tracked so /stop can reach
68
+ // the bashRunner (which is otherwise a local) via interruptCurrentRun.
69
+ this.activeBashRuns.set(runId, {
70
+ bashRunner,
71
+ cardSession,
72
+ userId: ctx.userId,
73
+ chatId: ctx.chatId,
74
+ cwd,
75
+ command,
76
+ });
77
+ let output = '';
78
+ let stderr = '';
79
+ let exitCode = null;
80
+ try {
81
+ getLogger().info(`[lark-remote] executeBashInternal about to start stream chatId=${ctx.chatId} messageId=${ctx.messageId}`);
82
+ try {
83
+ await cardSession.start();
84
+ getLogger().info(`[lark-remote] executeBash stream started runId=${runId}`);
85
+ }
86
+ catch (err) {
87
+ getLogger().warn('[lark-remote] bash card stream unavailable:', err);
88
+ }
89
+ for await (const event of bashRunner.run(command, { cwd })) {
90
+ if (event.type === 'stdout') {
91
+ // 层④:bridge 本地 output 也必须 store-time 截断——仅 session state
92
+ // 有界并不会让 `output +=` 自然有界(「store 有界则本地拼接自然有界」
93
+ // 不成立),!yes 洪峰下本地字符串仍全量驻留。capBashOutput 增量调用
94
+ // O(CAP+chunk),不会带回 O(n²) 截断成本(output 恒 ≤ CAP,拼接后
95
+ // ≤ CAP+chunk 再 slice 尾部)。
96
+ output = capBashOutput(output + event.content);
97
+ await cardSession.update({ output, stderr });
98
+ }
99
+ else if (event.type === 'stderr') {
100
+ stderr = capBashOutput(stderr + event.content);
101
+ await cardSession.update({ output, stderr });
102
+ }
103
+ else if (event.type === 'exit') {
104
+ exitCode = event.exitCode ?? 0;
105
+ }
106
+ }
107
+ await cardSession.finish(exitCode === 0 ? 'done' : 'error', { exitCode, output, stderr });
108
+ }
109
+ catch (err) {
110
+ getLogger().error(`[lark-remote] bash run error runId=${runId}:`, err);
111
+ // If for-await threw mid-stream the bash child may still be running.
112
+ // Stop it before finalizing the card — otherwise the finally block
113
+ // removes the activeBashRuns entry and /stop can no longer reach it,
114
+ // leaving an orphan process.
115
+ try {
116
+ await bashRunner.stop();
117
+ }
118
+ catch (stopErr) {
119
+ getLogger().warn(`[lark-remote] bash stop failed:`, stopErr);
120
+ }
121
+ await cardSession.finish('error', {
122
+ exitCode: 1,
123
+ output,
124
+ stderr: stderr + `\n执行错误: ${errorMessage(err)}`,
125
+ });
126
+ }
127
+ finally {
128
+ const settleResult = await cardSession.settle();
129
+ if (settleResult === 'unsent') {
130
+ // Stream never established a card — send a static terminal card as fallback.
131
+ // Use 2.0 schema to match the streaming card (avoid 2.0→1.x downgrade).
132
+ try {
133
+ await this.deps.connector.sendWithRetry(ctx.chatId, { card: renderBashCard(cardSession.currentState, {}) }, { replyTo: ctx.messageId });
134
+ }
135
+ catch (fallbackErr) {
136
+ getLogger().error(`[lark-remote] bash terminal fallback card failed runId=${runId} chatId=${ctx.chatId}:`, fallbackErr);
137
+ }
138
+ }
139
+ if (this.activeBashRuns.has(runId)) {
140
+ this.activeBashRuns.delete(runId);
141
+ getLogger().info(`[lark-remote] executeBash done runId=${runId} exitCode=${exitCode}`);
142
+ }
143
+ // Add a Done emoji reaction to the user's original message to signal completion
144
+ this.deps.finishTurnReactions(ctx, 'Done');
145
+ }
146
+ }
147
+ /** Get all active bash runs with their state for /active command. */
148
+ getActiveBashRuns() {
149
+ const result = [];
150
+ for (const [runId, bashRun] of this.activeBashRuns) {
151
+ const state = bashRun.cardSession.currentState;
152
+ // Only include non-terminal runs (bash has no finalizing state)
153
+ if (state.terminal === 'running') {
154
+ result.push({
155
+ runId,
156
+ cwd: bashRun.cwd,
157
+ userId: bashRun.userId,
158
+ chatId: bashRun.chatId,
159
+ terminal: state.terminal,
160
+ command: bashRun.command,
161
+ });
162
+ }
163
+ }
164
+ return result;
165
+ }
166
+ /**
167
+ * /stop 的 bash 侧(interruptCurrentRunDetailed 的后半段)。
168
+ *
169
+ * These bypass the serial queue and are tracked separately; stop the per-run
170
+ * BashRunner directly.
171
+ * `/stop`(无 runId、无 cwd)语义是「停掉这个会话的一切」,所以并行跑的
172
+ * `!` 命令也要停——旧实现命中 claude run 就 return,这些 bash 会被漏掉。
173
+ * 带范围的中断(停止卡 runId、queue.immediate 的 cwd)保持旧语义:只有
174
+ * 没有 claude run 命中时才回退到 bash,避免误杀用户并行跑的 `!` 任务。
175
+ *
176
+ * @param claudeTargetCount 本次中断已命中的 claude run 数(0 = 可回退到 bash)
177
+ * @returns 本次实际停掉的 bash run 数
178
+ */
179
+ async stopMatchingBashRuns(input, claudeTargetCount) {
180
+ const cardScoped = input.runId !== undefined || input.cardMessageId !== undefined;
181
+ const includeBashRuns = (input.runId === undefined && input.cardMessageId === undefined && input.cwd === undefined) ||
182
+ claudeTargetCount === 0;
183
+ const bashTargets = includeBashRuns
184
+ ? [...this.activeBashRuns.entries()].filter(([runId, b]) => {
185
+ if (input.cwd !== undefined && b.cwd !== input.cwd)
186
+ return false;
187
+ if (b.userId !== input.userId || b.chatId !== input.chatId)
188
+ return false;
189
+ if (input.runId) {
190
+ if (runId === input.runId)
191
+ return true;
192
+ // Scoped card clicks may fall back only through their own card id.
193
+ if (cardScoped)
194
+ return false;
195
+ }
196
+ if (input.cardMessageId) {
197
+ return b.cardSession.cardMessageId === input.cardMessageId;
198
+ }
199
+ return true;
200
+ })
201
+ : [];
202
+ let stoppedCount = 0;
203
+ for (const [runId, b] of bashTargets) {
204
+ getLogger().info(`[lark-remote] interrupt hit bash runId=${runId} cwd=${b.cwd} userId=${input.userId}`);
205
+ await b.bashRunner.stop({ immediate: true });
206
+ // Update card to show interrupted state. exitCode 不传(保持 null):
207
+ // bash-renderer 的「已手动终止」文案要求 exitCode === null;传 -1 会让
208
+ // 该分支不可达,中断卡退化成「退出码: -1」的失败样式。
209
+ await b.cardSession.finish('interrupted');
210
+ this.activeBashRuns.delete(runId);
211
+ getLogger().info(`[lark-remote] interrupt bash stop done runId=${runId}`);
212
+ stoppedCount++;
213
+ }
214
+ return stoppedCount;
215
+ }
216
+ }
217
+ //# sourceMappingURL=bash-run.js.map
@@ -0,0 +1,155 @@
1
+ import type { Runner } from '../runner/index.js';
2
+ import type { AppConfig } from '../config/index.js';
3
+ import type { SessionReaderRegistry, SessionStore } from '../session/index.js';
4
+ import { RunCardSession } from '../card/run-card-session.js';
5
+ import { type RunTerminal } from '../card/run-state.js';
6
+ import type { AgentKind } from '../agent-kinds.js';
7
+ import type { BridgeChannel, CommandContext, CommandResult } from './index.js';
8
+ import type { ActiveRun, IdleWatchdog } from './index.js';
9
+ /**
10
+ * Compact 能力的双重门控:声明的能力表 && runner 实际实现。
11
+ *
12
+ * 只做鸭子探测会把「漏实现」降级成静默隐藏按钮(dsh 就这样漏了),所以声明表
13
+ * 是必需项;只信声明表则可能渲染出点不动的死按钮,所以实现检查也保留。两者
14
+ * 不一致时打 error 日志,让缺陷在运行日志里可见。
15
+ */
16
+ export declare function runnerSupportsCompact(runner: Runner, kind: AgentKind): boolean;
17
+ /**
18
+ * 终态 reaction 的单源映射:普通 run 收尾(bridge/index.ts finalizeRun)与
19
+ * 压缩收尾(本文件 streamCompact)共用同一份,保证同一终态打同一表情。
20
+ */
21
+ export declare function terminalReactionEmoji(terminal: RunTerminal): string;
22
+ /**
23
+ * Compact 生命周期(bridge 结构治理第 1 刀):可压缩 run 记账、终态卡 /
24
+ * resume 卡两条 Compact 入口、kimi 在途检测与压缩流式卡。
25
+ *
26
+ * 依赖全部显式注入(与 ApprovalActions / InboundMediaHandler 同一模式):
27
+ * - `activeRuns` 是与普通 run **共用的同一张 cwd 槽位表**——压缩注册前先确认
28
+ * 槽位空闲,否则会覆盖在跑 run 的记录、`/stop` 停错对象;压缩自己不设
29
+ * per-call 超时(DSH 的 runCompact RPC 关闭 unary 预算),卡住时由
30
+ * `/stop` + §9.12 空闲看门狗兜底。
31
+ * - 记账上限 per-cwd `MAX_COMPACTABLE_RUNS_PER_CWD`,超出按插入序淘汰最旧。
32
+ */
33
+ export interface CompactLifecycleDeps {
34
+ /** 与普通 run 共用的 cwd 槽位表(Bridge.activeRuns,按引用共享)。 */
35
+ activeRuns: Map<string, ActiveRun>;
36
+ connector: BridgeChannel;
37
+ sessionStore: SessionStore;
38
+ sessionReaderRegistry: SessionReaderRegistry;
39
+ /** live 取值:`/config` 会整体替换 config 引用,不能按值快照。 */
40
+ getConfig(): AppConfig;
41
+ getRunner(cwd: string, kind?: AgentKind, opts?: {
42
+ replaceStale?: boolean;
43
+ }): Runner;
44
+ sendResult(result: CommandResult, ctx: CommandContext, opts?: {
45
+ onCardSent?: (messageId: string) => void;
46
+ }): Promise<boolean>;
47
+ startIdleWatchdog(opts: {
48
+ runId: string;
49
+ session: RunCardSession;
50
+ runner: Runner;
51
+ /** >0 = 正在等人工审批:「等审批」不是「挂死」,刷新窗口而不是终止。 */
52
+ pendingApprovals?: () => number;
53
+ }): IdleWatchdog;
54
+ finishTurnReactions(ctx: CommandContext, emoji: string): void;
55
+ resolveCwd(userId: string): string | undefined;
56
+ }
57
+ export declare class CompactLifecycle {
58
+ private readonly deps;
59
+ /**
60
+ * 最近若干次已完成的、runner 有 runCompact 的 run(cwd → runId → 记录)。
61
+ * run 结束后 activeRuns 已清空,而终态卡上的 Compact 按钮只带 runId,
62
+ * 需要它反查 sessionId + agentKind。
63
+ *
64
+ * 为什么按 runId 存而不是「每 cwd 只留最新一条」:compact 卡片动作走串行
65
+ * 队列(会被正在跑的 run 挡住),点击到执行之间可能夹着别的 run 收尾。
66
+ * 单槽会被覆盖 —— 旧卡片的压缩必然报「该任务已结束或不属于当前会话」,
67
+ * 与 session 是否还在、能否压缩无关(2026-10-08 线上缺陷)。
68
+ */
69
+ private compactableRuns;
70
+ constructor(deps: CompactLifecycleDeps);
71
+ /**
72
+ * 记下一次「可压缩 run」:终态卡上的 Compact 按钮点击时据此反查
73
+ * sessionId + agentKind。runId 是按钮自带的唯一键,同 cwd 内唯一。
74
+ * 超出上限按插入序淘汰最旧的一条(Map 保持插入序;重复写同一 runId
75
+ * 先 delete 再 set 以刷新位置)。
76
+ */
77
+ private recordCompactableRun;
78
+ /**
79
+ * 终态 Compact 记账:把本次 run 实际使用的 sessionId 记进
80
+ * {@link compactableRuns},供终态卡上的 Compact 按钮按 runId 反查。
81
+ *
82
+ * 调用点固定在 `finalizeRun` 的 `finally` 里、**槽位所有权守卫之外**。
83
+ * 所有权守卫只回答「cwd 槽位还归不归本 run」(决定能否删槽位 / evict
84
+ * runner);记账回答「本次 run 结束了吗、用的哪个 sessionId」。两者在不同
85
+ * 维度上:外部中断(`/stop`、停止卡按钮、`queue.immediate`)会先摘槽位,
86
+ * 记账若跟着守卫走就永远不会落,中断卡片的按钮必然反查不到(中断
87
+ * 后点 Compact 一律回「该任务已结束或不属于当前会话」)。
88
+ *
89
+ * 覆盖所有终态入口(正常结束、error、interrupted、idle_timeout、
90
+ * goal_continuing)——这些卡片都渲染 Compact 按钮(`goal` 仍 active 时
91
+ * 除外)。函数体全部是内存操作(查表、属性读、Map 写),无 IO 无解析,
92
+ * 不需要兜错;真正的清理容错由调用方 finally 保证。
93
+ */
94
+ recordCompactableRunForTerminal(cardSession: RunCardSession, activeRun: ActiveRun, ctx: CommandContext): void;
95
+ /**
96
+ * 按 runId 反查可压缩 run 记录(跨 cwd 扫描)。
97
+ *
98
+ * 为什么不带 cwd 过滤:按钮属于「那一次 run」,与用户此刻在哪个工作目录、
99
+ * 排队期间有没有 /cd 无关。`/stop` 的 interruptCurrentRun 同样是全局按 runId
100
+ * 找(从不查当前 cwd),compact 与它保持一致。用当前 cwd 收窄会让「切了工作区
101
+ * 再点旧卡片」和「排队期间 /cd」两种情况误报「该任务已结束或不属于当前会话」。
102
+ */
103
+ private findCompactableRun;
104
+ /**
105
+ * Handle compact card action — trigger a compaction request for any
106
+ * runCompact-capable runner(codex/kimi/opencode/pi/claude)。按钮带的是
107
+ * 那次 run 的 runId,据此反查该 run 实际使用的 sessionId + agentKind
108
+ * + cwd(compactableRuns,按 runId 记录),再调 runner 的 runCompact()。
109
+ */
110
+ handleCompactAction(value: {
111
+ runId?: string;
112
+ }, ctx: CommandContext): Promise<void>;
113
+ /**
114
+ * §5.3: kimi 压缩在途检测——reader 实现契约的 `readCompactionState` 时,若
115
+ * wire 状态机判定当前有未终结的压缩(最后一次 begin 无 terminal 记录),
116
+ * 直接回文本「已有一次 Compact 正在进行」并返回 true(调用方不开卡、不发
117
+ * session/prompt)。codex/claude 等 reader 无此方法 → 返回 false,行为不变。
118
+ */
119
+ private rejectIfCompactionInFlight;
120
+ /**
121
+ * Stream a compaction to a card: start a RunCardSession with
122
+ * operationKind='compaction' (no recursive Compact button), consume
123
+ * runner.runCompact() events, read authoritative jsonl usage, and finish
124
+ * the card. Shared by handleCompactAction (run card button) and
125
+ * handleResumeCompact (resume cards) so both flows stay in sync.
126
+ *
127
+ * Precondition: callers have already validated that `runner` implements
128
+ * runCompact and that the session exists in `cwd`.
129
+ */
130
+ private streamCompact;
131
+ /**
132
+ * 事件消费循环(streamCompact 前半段):吞 runner.runCompact() 的全部事件,
133
+ * 逐帧喂卡片。终态后不再消费(与 runAgentStreamToEnd 同口径);result 帧
134
+ * 只做记录(subype/errorMessage),终态装配在 {@link finishCompactCard}。
135
+ */
136
+ private consumeCompactEvents;
137
+ /**
138
+ * 终态装配(streamCompact 后半段):result subtype 三态映射 → 从会话 jsonl
139
+ * 读权威统计(compactCount 短重试)→ cardSession.finish。
140
+ */
141
+ private finishCompactCard;
142
+ /**
143
+ * Handle resume.compact card action — compact a session directly from a
144
+ * resume card (auto-resume / `/resume <id>`), without a runId.
145
+ *
146
+ * Unlike handleCompactAction (which validates against the last finished run's
147
+ * runId), the resume card carries the sessionId + agent it was rendered for.
148
+ * Validation: the session must exist in the current cwd (same rule as
149
+ * cmdResume / resume.use) and the runner must implement runCompact.
150
+ */
151
+ handleResumeCompact(value: {
152
+ sessionId?: string;
153
+ agent?: string;
154
+ }, ctx: CommandContext): Promise<void>;
155
+ }