lark-remote 0.1.7 → 0.1.9

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 (75) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/bridge/approval-coordinator.d.ts +30 -1
  3. package/dist/bridge/approval-coordinator.js +93 -5
  4. package/dist/bridge/index.d.ts +36 -6
  5. package/dist/bridge/index.js +56 -10
  6. package/dist/card/approval-render.js +287 -48
  7. package/dist/card/card-shared.d.ts +6 -4
  8. package/dist/card/card-shared.js +6 -4
  9. package/dist/card/run-renderer.d.ts +7 -0
  10. package/dist/card/run-renderer.js +22 -11
  11. package/dist/config/index.d.ts +24 -1
  12. package/dist/config/index.js +34 -3
  13. package/dist/index.js +40 -16
  14. package/dist/order/alias-resolve.d.ts +6 -2
  15. package/dist/order/index.d.ts +9 -2
  16. package/dist/order/index.js +33 -0
  17. package/dist/router/card-action-payload.d.ts +30 -0
  18. package/dist/router/card-action-payload.js +39 -0
  19. package/dist/router/config/claude.js +8 -3
  20. package/dist/router/config/dsh.d.ts +51 -0
  21. package/dist/router/config/dsh.js +179 -0
  22. package/dist/router/config/index.d.ts +1 -1
  23. package/dist/router/config/index.js +4 -1
  24. package/dist/router/config/types.d.ts +6 -0
  25. package/dist/router/index.d.ts +39 -4
  26. package/dist/router/index.js +471 -90
  27. package/dist/router/utils.js +12 -2
  28. package/dist/runner/agent-choices-common.js +3 -0
  29. package/dist/runner/claude/runner.js +22 -0
  30. package/dist/runner/claude/session.d.ts +26 -0
  31. package/dist/runner/claude/session.js +105 -9
  32. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -0
  33. package/dist/runner/codex/app-server/protocol-types.js +1 -1
  34. package/dist/runner/codex/app-server/runner.d.ts +10 -0
  35. package/dist/runner/codex/app-server/runner.js +56 -1
  36. package/dist/runner/codex/app-server/translator.d.ts +7 -0
  37. package/dist/runner/codex/app-server/translator.js +45 -0
  38. package/dist/runner/common/acp/protocol-types.d.ts +33 -0
  39. package/dist/runner/common/acp/protocol-types.js +2 -0
  40. package/dist/runner/common/connection-based-runner.js +10 -5
  41. package/dist/runner/dsh/client.d.ts +95 -0
  42. package/dist/runner/dsh/client.js +418 -0
  43. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  44. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  45. package/dist/runner/dsh/index.d.ts +3 -0
  46. package/dist/runner/dsh/index.js +4 -0
  47. package/dist/runner/dsh/runner.d.ts +57 -0
  48. package/dist/runner/dsh/runner.js +211 -0
  49. package/dist/runner/dsh/translator.d.ts +28 -0
  50. package/dist/runner/dsh/translator.js +98 -0
  51. package/dist/runner/dsh/types.d.ts +155 -0
  52. package/dist/runner/dsh/types.js +14 -0
  53. package/dist/runner/index.d.ts +1 -0
  54. package/dist/runner/index.js +1 -0
  55. package/dist/runner/kimi/acp/runner.js +84 -1
  56. package/dist/runner/kimi/acp/translator.d.ts +14 -0
  57. package/dist/runner/kimi/acp/translator.js +108 -3
  58. package/dist/runner/pi/rpc/runner.d.ts +1 -7
  59. package/dist/runner/pi/rpc/runner.js +8 -12
  60. package/dist/runner/probe.js +6 -1
  61. package/dist/runner/question-common.d.ts +26 -0
  62. package/dist/runner/question-common.js +42 -0
  63. package/dist/runner/types.d.ts +94 -10
  64. package/dist/session/claude/sessions.js +37 -15
  65. package/dist/session/dsh/index.d.ts +1 -0
  66. package/dist/session/dsh/index.js +2 -0
  67. package/dist/session/dsh/sessions.d.ts +44 -0
  68. package/dist/session/dsh/sessions.js +252 -0
  69. package/dist/session/index.d.ts +1 -0
  70. package/dist/session/index.js +1 -0
  71. package/dist/workspace/index.d.ts +12 -0
  72. package/dist/workspace/index.js +28 -2
  73. package/package.json +2 -1
  74. package/dist/order/alias-store.d.ts +0 -38
  75. package/dist/order/alias-store.js +0 -106
package/CHANGELOG.md CHANGED
@@ -5,6 +5,50 @@ 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.1.9] - 2026-08-20
9
+
10
+ ### 新增
11
+
12
+ - ExitPlanMode 计划审批:计划全文折叠展示 + 五决策对齐 TUI(自动放行 / 附意见 / 采纳修改)
13
+ - 卡片别名管理重构:`alias-store` 并入 `alias-resolve`,`/order` 卡片新增「+别名」交互(绑定/修改/删除,已绑定指令显示 `$name` 标签 + ✕)
14
+ - DSH / connection-based runner 启动 / 配置热更 / 失败路径修复(CC-01~CC-08)
15
+
16
+ ### 变更
17
+
18
+ - 别名 API 由「触发词 → 文本」改为「指令 → 别名」:`/order alias add <orderId|序号> <别名>` 绑定、`/order alias rm` 移除;别名随指令持久化在 `orders.json`,删除指令时别名一并删除;名称不能为保留子命令、不能数字开头
19
+ - 用户文档同步别名卡片化交互,移除 `/order alias <name> <text>` 旧用法
20
+
21
+ ### 修复
22
+
23
+ - 别名卡片成功回调:携带 `card` 替换 pre-click 卡,删除文案对齐新交互
24
+ - kimi ACP runner 模型下发:迁移到纯 ACP 后 `session/set_config_option` 必须紧跟 `session/new|resume` 下发,否则实际跑的是默认模型(CC-07)
25
+
26
+ ## [0.1.8] - 2026-08-19
27
+
28
+ ### 新增
29
+
30
+ - DSH(DeepSeek Harness)Agent 接入:纯 HTTP 连接 DSH Web Host(无本地子进程),支持会话创建/续跑、`selectModel` 模型对齐、SSE mux 事件订阅、会话历史读取
31
+ - `/config` DSH 配置卡:Host 地址 / 会话 preset / 模型 / 推理强度,模型与预设目录预取动态加载(失败回退固定兜底清单)
32
+ - AskUserQuestion 公共契约(`question-common`):标准提问审批事件工厂 + 按序答案映射,claude / codex / kimi / pi 统一接入
33
+ - Codex AskUserQuestion:支持自由文本题、补充说明(user_note)、`autoResolutionMs` 透传为单请求审批超时
34
+ - Kimi elicitation 表单回编 + request_permission 兜底桥
35
+ - 工具权限审批卡(`kind === 'tool'`):ExitPlanMode 等非命令工具展示工具名 + 用途说明,计划审批语义
36
+ - 提问卡单选/多选图标区分(⚪/🔵 vs ⬜/☑️)、自由文本题输入框、自定义答案回显
37
+
38
+ ### 变更
39
+
40
+ - claude session usage 扫描对齐 codex 语义:非累计字段为末轮(本 run)scope,累计走 `cumulative*` 字段
41
+ - Compact 按钮能力门控放开到所有 `runCompact` 能力 runner(claude / codex / kimi / opencode / pi)
42
+ - claude `/config` 权限模式下拉排除 `manual`(`default` 的别名,避免等价重复项)
43
+ - workspace 保存即触达:`save` 内部调用 `touch`,新保存的工作区排在列表最前
44
+ - DSH 可用性探测跳过二进制检查(HTTP-only Agent)
45
+
46
+ ### 修复
47
+
48
+ - claude session usage 扫描:修复第三方网关(如 DeepSeek)将零 usage 写在占位行、真实 usage 只在末行导致的 per-run 统计全 0(改为按 message id 逐字段 max 聚合)
49
+ - 审批答案重复投递(重复 nonce 二次点击)改为中性提示,不再误报"提交失败"
50
+ - usage 统计一致性护栏:本 run 超出累计时显式标记 `⚠️ 累计异常`(只标记不修正,便于排查数据源问题)
51
+
8
52
  ## [0.1.7] - 2026-08-18
9
53
 
10
54
  ### 新增
@@ -175,3 +219,5 @@ Initial release.
175
219
  [0.1.4]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.4
176
220
  [0.1.5]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.5
177
221
  [0.1.6]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.6
222
+ [0.1.7]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.7
223
+ [0.1.8]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.8
@@ -13,13 +13,17 @@ import type { ApprovalAction } from '../runner/types.js';
13
13
  type ApprovalState = 'pending' | 'submitting' | 'resolved' | 'failed' | 'expired';
14
14
  export interface TrackedApproval {
15
15
  requestId: number | string;
16
- kind: 'command' | 'file' | 'permissions' | 'question';
16
+ kind: 'command' | 'file' | 'permissions' | 'question' | 'tool';
17
17
  state: ApprovalState;
18
18
  view: ApprovalView;
19
19
  createdAt: number;
20
20
  timeoutTimer: ReturnType<typeof setTimeout> | null;
21
21
  /** AskUserQuestion 已答问题索引 → 答案 label[](kind === 'question')。 */
22
22
  answers?: Map<number, string[]>;
23
+ /** AskUserQuestion 补充说明(Codex user_note):问题索引 → 文本。 */
24
+ notes?: Map<number, string>;
25
+ /** 计划审批修改意见(ExitPlanMode):planFeedback 回流,供反馈类决策注入。 */
26
+ feedback?: string;
23
27
  }
24
28
  type ApprovalResponder = (requestId: number | string, response: unknown) => Promise<void>;
25
29
  export type { ApprovalAction } from '../runner/types.js';
@@ -101,6 +105,29 @@ export declare class ApprovalCoordinator {
101
105
  requestId: number | string;
102
106
  nonce?: string;
103
107
  }): Promise<void>;
108
+ /**
109
+ * AskUserQuestion 补充说明(Codex user_note):选项之外的可选文本,随答案
110
+ * 提交;不单独构成答案(仍需选项/自定义答案才能完成该问题)。note 更新
111
+ * 后回流视图(卡片回显),同一请求可多次修改(新 nonce 来自新渲染)。
112
+ */
113
+ answerNote(action: {
114
+ questionIndex: number;
115
+ text: string;
116
+ }, ctx: {
117
+ requestId: number | string;
118
+ nonce?: string;
119
+ }): Promise<void>;
120
+ /**
121
+ * 计划审批修改意见(ExitPlanMode):输入框提交 → coordinator 记录并回流
122
+ * 卡片回显,随后「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
123
+ * 同一请求可多次修改(新 nonce 来自新渲染)。
124
+ */
125
+ planFeedback(action: {
126
+ text: string;
127
+ }, ctx: {
128
+ requestId: number | string;
129
+ nonce?: string;
130
+ }): Promise<void>;
104
131
  /**
105
132
  * Update a pending approval's view (out-of-order item/started arrives late).
106
133
  * Only applies while the approval is still pending — resolved/expired
@@ -118,6 +145,8 @@ export declare class ApprovalCoordinator {
118
145
  private recordAnswer;
119
146
  private getPendingQuestion;
120
147
  private allQuestionsAnswered;
148
+ /** 推送审批卡视图更新(approval_view_updated 重渲染;失败仅记日志)。 */
149
+ private pushApprovalViewUpdate;
121
150
  /** 推送 question 卡片视图更新(选项勾选状态变化)。 */
122
151
  private pushQuestionViewUpdate;
123
152
  /** 全部问题已答:构建 answers 并提交(非空校验 + 原子状态迁移)。 */
@@ -37,9 +37,12 @@ export class ApprovalCoordinator {
37
37
  existing.view = event.view;
38
38
  return;
39
39
  }
40
+ // 事件级 timeoutMs 优先(Codex autoResolutionMs / Pi extension timeout),
41
+ // 缺省回落 run 级默认超时。放在事件上而非桥按 agentKind 分支,公共层
42
+ // 保持无 agent 分支。
40
43
  const timer = setTimeout(() => {
41
44
  this.expireApproval(event.requestId);
42
- }, this.approvalTimeoutMs);
45
+ }, event.timeoutMs ?? this.approvalTimeoutMs);
43
46
  const tracked = {
44
47
  requestId: event.requestId,
45
48
  kind: event.kind,
@@ -94,12 +97,33 @@ export class ApprovalCoordinator {
94
97
  if (!tracked.view.availableDecisions.includes(decision) && !safetyOverride) {
95
98
  throw new Error(`Decision "${decision}" not in available decisions: ${tracked.view.availableDecisions.join(', ')}`);
96
99
  }
100
+ // 反馈类决策:注入用户已填写的修改意见(卡片输入框经 planFeedback 回流到
101
+ // coordinator)。decisionToApprovalAction 只带决策名,payload 在此补齐;
102
+ // 调用方直接传完整 payload 时以显式值为准。
103
+ let effectiveAction = action;
104
+ if (action.action === 'decline_with_feedback' || action.action === 'accept_with_feedback') {
105
+ const explicit = action.action === 'decline_with_feedback' ? action.message : action.plan;
106
+ const text = explicit.trim() || tracked.feedback?.trim() || '';
107
+ if (!text) {
108
+ throw new Error('请先填写修改意见');
109
+ }
110
+ effectiveAction =
111
+ action.action === 'decline_with_feedback'
112
+ ? { action: 'decline_with_feedback', message: text }
113
+ : {
114
+ action: 'accept_with_feedback',
115
+ // 批准并采纳修改:新计划 = 原计划 + 用户意见(无原计划时意见即计划)。
116
+ plan: tracked.view.plan?.trim()
117
+ ? `${tracked.view.plan.trim()}\n\n## 用户修改意见\n${text}`
118
+ : `# 用户修改意见\n\n${text}`,
119
+ };
120
+ }
97
121
  // Atomic state transition: pending → submitting(同步,无 await)
98
122
  tracked.state = 'submitting';
99
123
  this.clearTimer(tracked);
100
124
  // Send response via the responder
101
125
  try {
102
- await this.responder(ctx.requestId, action);
126
+ await this.responder(ctx.requestId, effectiveAction);
103
127
  tracked.state = 'resolved';
104
128
  // 提交成功后立即从卡片移除审批区(agent 无关):claude 控制协议没有
105
129
  // resolved 回发,不推就会「已点允许按钮仍在」残留 UI;codex 的 server
@@ -224,6 +248,50 @@ export class ApprovalCoordinator {
224
248
  }
225
249
  this.pushQuestionViewUpdate(ctx.requestId, tracked);
226
250
  }
251
+ /**
252
+ * AskUserQuestion 补充说明(Codex user_note):选项之外的可选文本,随答案
253
+ * 提交;不单独构成答案(仍需选项/自定义答案才能完成该问题)。note 更新
254
+ * 后回流视图(卡片回显),同一请求可多次修改(新 nonce 来自新渲染)。
255
+ */
256
+ async answerNote(action, ctx) {
257
+ const tracked = this.getPendingQuestion(ctx.requestId);
258
+ const questions = tracked.view.questions;
259
+ const q = questions[action.questionIndex];
260
+ if (!q) {
261
+ throw new Error(`Question ${action.questionIndex} not found in request ${ctx.requestId}`);
262
+ }
263
+ const text = action.text.trim();
264
+ if (!text) {
265
+ throw new Error('请输入补充说明');
266
+ }
267
+ this.assertFreshNonce(ctx.requestId, ctx.nonce);
268
+ tracked.notes ??= new Map();
269
+ tracked.notes.set(action.questionIndex, text);
270
+ q.note = text;
271
+ this.pushQuestionViewUpdate(ctx.requestId, tracked);
272
+ }
273
+ /**
274
+ * 计划审批修改意见(ExitPlanMode):输入框提交 → coordinator 记录并回流
275
+ * 卡片回显,随后「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
276
+ * 同一请求可多次修改(新 nonce 来自新渲染)。
277
+ */
278
+ async planFeedback(action, ctx) {
279
+ const tracked = this.approvals.get(ctx.requestId);
280
+ if (!tracked) {
281
+ throw new Error(`Approval request ${ctx.requestId} not found`);
282
+ }
283
+ if (tracked.state !== 'pending') {
284
+ throw new Error(`Approval request ${ctx.requestId} is no longer pending`);
285
+ }
286
+ const text = action.text.trim();
287
+ if (!text) {
288
+ throw new Error('请输入修改意见');
289
+ }
290
+ this.assertFreshNonce(ctx.requestId, ctx.nonce);
291
+ tracked.feedback = text;
292
+ tracked.view.planFeedback = text;
293
+ this.pushApprovalViewUpdate(ctx.requestId, tracked);
294
+ }
227
295
  /**
228
296
  * Update a pending approval's view (out-of-order item/started arrives late).
229
297
  * Only applies while the approval is still pending — resolved/expired
@@ -295,8 +363,8 @@ export class ApprovalCoordinator {
295
363
  }
296
364
  return true;
297
365
  }
298
- /** 推送 question 卡片视图更新(选项勾选状态变化)。 */
299
- pushQuestionViewUpdate(requestId, tracked) {
366
+ /** 推送审批卡视图更新(approval_view_updated 重渲染;失败仅记日志)。 */
367
+ pushApprovalViewUpdate(requestId, tracked) {
300
368
  void this.pushToCard([
301
369
  {
302
370
  type: 'approval_view_updated',
@@ -308,6 +376,10 @@ export class ApprovalCoordinator {
308
376
  getLogger().warn(`[approval-coordinator] question view update failed requestId=${requestId}: ${err instanceof Error ? err.message : String(err)}`);
309
377
  });
310
378
  }
379
+ /** 推送 question 卡片视图更新(选项勾选状态变化)。 */
380
+ pushQuestionViewUpdate(requestId, tracked) {
381
+ this.pushApprovalViewUpdate(requestId, tracked);
382
+ }
311
383
  /** 全部问题已答:构建 answers 并提交(非空校验 + 原子状态迁移)。 */
312
384
  async respondQuestionAnswers(tracked, ctx) {
313
385
  this.assertFreshNonce(ctx.requestId, ctx.nonce);
@@ -316,14 +388,22 @@ export class ApprovalCoordinator {
316
388
  }
317
389
  const questions = tracked.view.questions ?? [];
318
390
  const answers = {};
391
+ const notes = {};
319
392
  for (let i = 0; i < questions.length; i++) {
320
393
  const selected = tracked.answers?.get(i) ?? [];
321
394
  answers[questions[i].question] = questions[i].multiSelect ? selected : selected[0];
395
+ const note = tracked.notes?.get(i);
396
+ if (note)
397
+ notes[questions[i].question] = note;
322
398
  }
323
399
  tracked.state = 'submitting';
324
400
  this.clearTimer(tracked);
325
401
  try {
326
- await this.responder(ctx.requestId, { action: 'answer', answers });
402
+ await this.responder(ctx.requestId, {
403
+ action: 'answer',
404
+ answers,
405
+ ...(Object.keys(notes).length > 0 ? { notes } : {}),
406
+ });
327
407
  tracked.state = 'resolved';
328
408
  this.pushResolved(ctx.requestId);
329
409
  }
@@ -380,6 +460,10 @@ export class ApprovalCoordinator {
380
460
  return 'acceptAll';
381
461
  case 'answer':
382
462
  return 'answer';
463
+ case 'decline_with_feedback':
464
+ return 'declineWithFeedback';
465
+ case 'accept_with_feedback':
466
+ return 'acceptWithFeedback';
383
467
  case 'decline':
384
468
  return 'decline';
385
469
  case 'cancel':
@@ -398,6 +482,10 @@ export function decisionToApprovalAction(decision) {
398
482
  return { action: 'accept_with_execpolicy_amendment' };
399
483
  case 'acceptAll':
400
484
  return { action: 'accept_all' };
485
+ case 'declineWithFeedback':
486
+ return { action: 'decline_with_feedback', message: '' };
487
+ case 'acceptWithFeedback':
488
+ return { action: 'accept_with_feedback', plan: '' };
401
489
  case 'decline':
402
490
  return { action: 'decline' };
403
491
  case 'cancel':
@@ -121,6 +121,13 @@ export declare class Bridge {
121
121
  * blocked the queue forever.
122
122
  */
123
123
  private runners;
124
+ /**
125
+ * Runner 槽位在「活跃运行期间配置变更」时被标 stale(CC-06/P1):clearRunners() 对
126
+ * 活跃 workspace 只标 stale、不立即 evict(避免误杀长驻连接),当前 run 结束后由
127
+ * finalizeRun 安全 evict+dispose,下一轮 getRunner 创建新配置的 runner。
128
+ * key 为 `${cwd}\u0000${agentKind}`。
129
+ */
130
+ private staleRunnerSlots;
124
131
  /** Queue manager for per-workspace serial processing queue. */
125
132
  /** 普通消息队列管理器(public:测试注入 queue-card 状态用)。 */
126
133
  queueManager: QueueManager;
@@ -129,8 +136,8 @@ export declare class Bridge {
129
136
  /** Approval coordinators keyed by runId. */
130
137
  private approvalCoordinators;
131
138
  /**
132
- * 最近一次已完成的 codex app-server run(按 cwd)。run 结束后 activeRuns
133
- * 已清空,Compact 需要它来校验 runId 并提供 sessionId。
139
+ * 最近一次已完成的、runner 有 runCompact 的 run(按 cwd)。run 结束后
140
+ * activeRuns 已清空,Compact 需要它来校验 runId 并提供 sessionId。
134
141
  */
135
142
  private lastCompactableCodexRun;
136
143
  /**
@@ -383,6 +390,16 @@ export declare class Bridge {
383
390
  decision: string;
384
391
  nonce: string;
385
392
  }): Promise<void>;
393
+ /**
394
+ * 计划审批修改意见(ExitPlanMode 输入框):记录到 coordinator 并回流卡片。
395
+ * 「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
396
+ */
397
+ handleApprovalPlanFeedback(opts: {
398
+ runId: string;
399
+ requestId: number | string;
400
+ text: string;
401
+ nonce: string;
402
+ }): Promise<void>;
386
403
  /**
387
404
  * Handle a permission toggle from a card button click.
388
405
  * Routes to the ApprovalCoordinator for the given run.
@@ -426,15 +443,28 @@ export declare class Bridge {
426
443
  nonce: string;
427
444
  }): Promise<void>;
428
445
  /**
429
- * Handle codex.compact card action — trigger a Codex compaction request.
430
- * Validates the run exists and is in a terminal state, then calls
431
- * the app-server runner's runCompact().
446
+ * Codex AskUserQuestion 补充说明(user_note,2026-08-18 live 验证模型理解
447
+ * "user_note: <text>" 条目):记录到 coordinator,随答案一起提交。
448
+ * 路由到 ApprovalCoordinator.answerNote。
449
+ */
450
+ handleApprovalAnswerNote(opts: {
451
+ runId: string;
452
+ requestId: number | string;
453
+ questionIndex: number;
454
+ text: string;
455
+ nonce: string;
456
+ }): Promise<void>;
457
+ /**
458
+ * Handle codex.compact card action — trigger a compaction request for any
459
+ * runCompact-capable runner(codex/kimi/opencode/pi/claude,按
460
+ * lastCompactableCodexRun 记录的 agentKind 路由)。Validates the run exists
461
+ * and is in a terminal state, then calls the runner's runCompact().
432
462
  */
433
463
  handleCodexCompact(value: {
434
464
  runId?: string;
435
465
  }, ctx: BridgeContext): Promise<void>;
436
466
  /**
437
- * Stream a Codex compaction to a card: start a RunCardSession with
467
+ * Stream a compaction to a card: start a RunCardSession with
438
468
  * operationKind='compaction' (no recursive Compact button), consume
439
469
  * runner.runCompact() events, read authoritative jsonl usage, and finish
440
470
  * the card. Shared by handleCodexCompact (run card button) and
@@ -89,6 +89,13 @@ export class Bridge {
89
89
  * blocked the queue forever.
90
90
  */
91
91
  runners = new Map();
92
+ /**
93
+ * Runner 槽位在「活跃运行期间配置变更」时被标 stale(CC-06/P1):clearRunners() 对
94
+ * 活跃 workspace 只标 stale、不立即 evict(避免误杀长驻连接),当前 run 结束后由
95
+ * finalizeRun 安全 evict+dispose,下一轮 getRunner 创建新配置的 runner。
96
+ * key 为 `${cwd}\u0000${agentKind}`。
97
+ */
98
+ staleRunnerSlots = new Set();
92
99
  /** Queue manager for per-workspace serial processing queue. */
93
100
  /** 普通消息队列管理器(public:测试注入 queue-card 状态用)。 */
94
101
  queueManager;
@@ -97,8 +104,8 @@ export class Bridge {
97
104
  /** Approval coordinators keyed by runId. */
98
105
  approvalCoordinators = new Map();
99
106
  /**
100
- * 最近一次已完成的 codex app-server run(按 cwd)。run 结束后 activeRuns
101
- * 已清空,Compact 需要它来校验 runId 并提供 sessionId。
107
+ * 最近一次已完成的、runner 有 runCompact 的 run(按 cwd)。run 结束后
108
+ * activeRuns 已清空,Compact 需要它来校验 runId 并提供 sessionId。
102
109
  */
103
110
  lastCompactableCodexRun = new Map();
104
111
  /**
@@ -259,10 +266,18 @@ export class Bridge {
259
266
  // keeping it is correct; finalizeRun deletes the cache entry when the run
260
267
  // ends, and the next run picks up the new config/agent via getRunner.
261
268
  for (const cwd of [...this.runners.keys()]) {
262
- if (this.activeRuns.has(cwd))
269
+ if (this.activeRuns.has(cwd)) {
270
+ // CC-06/P1: 活跃 workspace 不能立即 evict(会误杀长驻连接),但必须标记 stale,
271
+ // 否则 finalizeRun 对 workspace-lifetime runner 也不 evict → 下一轮复用旧配置实例。
272
+ // 这里只标待淘汰,当前 run 结束后由 finalizeRun 安全 evict。
273
+ for (const kind of [...this.runners.get(cwd).keys()]) {
274
+ this.staleRunnerSlots.add(`${cwd}\u0000${kind}`);
275
+ }
263
276
  continue;
277
+ }
264
278
  for (const kind of [...this.runners.get(cwd).keys()]) {
265
279
  this.evictRunnerSlot(cwd, kind);
280
+ this.staleRunnerSlots.delete(`${cwd}\u0000${kind}`);
266
281
  }
267
282
  }
268
283
  }
@@ -287,6 +302,7 @@ export class Bridge {
287
302
  void evicted.dispose();
288
303
  }
289
304
  workspaceMap.delete(kind);
305
+ this.staleRunnerSlots.delete(`${cwd}\u0000${kind}`);
290
306
  if (workspaceMap.size === 0) {
291
307
  this.runners.delete(cwd);
292
308
  }
@@ -753,6 +769,9 @@ export class Bridge {
753
769
  showToolResult: this.config.output.showToolResult,
754
770
  // Run card header shows the current agent name (e.g. "Claude · 思考中")
755
771
  agentKind,
772
+ // Compact 按钮能力门控与 resume 卡 / auto-resume 卡一致(hasRunCompact
773
+ // 鸭子探测):claude(stream-json,无 turn_started)也能拿到按钮。
774
+ compactSupported: this.hasRunCompact(cwd, agentKind),
756
775
  },
757
776
  });
758
777
  const activeRun = {
@@ -1111,7 +1130,10 @@ export class Bridge {
1111
1130
  // 由 formatUsageStats 的 max/兜底公式处理。
1112
1131
  }
1113
1132
  else {
1114
- // codex(jsonl 优先)或无 live usage → jsonl 兜底所有 flow 字段
1133
+ // codex(jsonl 优先)或无 live usage → jsonl 兜底所有 flow 字段。
1134
+ // jsonl 的非累计字段已是末轮(本 run)scope(claude reader 修复后对齐
1135
+ // codex `last_token_usage` 语义,见 scalarScan),不再是 session 累计;
1136
+ // session 累计只经 cumulative* 字段透传,卡片"本 run ≤ 累计"不变量成立。
1115
1137
  finalInputTokens = finalUsage?.inputTokens ?? liveInputTokens;
1116
1138
  finalOutputTokens = finalUsage?.outputTokens ?? liveOutputTokens;
1117
1139
  finalCacheReadTokens = finalUsage?.cacheReadTokens ?? finalCacheReadTokens;
@@ -1296,9 +1318,13 @@ export class Bridge {
1296
1318
  // evicted+disposed by clearRunners (/config) and interruptCurrentRun
1297
1319
  // (/stop). Spawn-per-message runners are evicted as before to prevent
1298
1320
  // stale process references accumulating.
1299
- if (activeRun.runner.lifetime !== 'workspace') {
1321
+ // CC-06/P1: 配置变更期间(clearRunners 标了 stale)的 workspace-lifetime
1322
+ // runner 也必须在 run 结束后 evict,否则下一轮复用旧配置实例。
1323
+ const slotKey = `${cwd}\u0000${activeRun.agentKind}`;
1324
+ if (activeRun.runner.lifetime !== 'workspace' || this.staleRunnerSlots.has(slotKey)) {
1300
1325
  this.evictRunnerSlot(cwd, activeRun.agentKind);
1301
1326
  }
1327
+ this.staleRunnerSlots.delete(slotKey);
1302
1328
  // Compact 卡片需要 runId + sessionId;record 供终态卡上的 Compact 按钮
1303
1329
  // 校验与取参(activeRuns 已清空,无法再查到)。异常退出(error/
1304
1330
  // interrupted/idle_timeout)同样记录——上下文往往更大,压缩后再续。
@@ -1441,6 +1467,15 @@ export class Bridge {
1441
1467
  await coordinator.submit(action, { requestId: opts.requestId, nonce: opts.nonce });
1442
1468
  });
1443
1469
  }
1470
+ /**
1471
+ * 计划审批修改意见(ExitPlanMode 输入框):记录到 coordinator 并回流卡片。
1472
+ * 「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
1473
+ */
1474
+ async handleApprovalPlanFeedback(opts) {
1475
+ return this.withCoordinator(opts.runId, async (coordinator) => {
1476
+ await coordinator.planFeedback({ text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce });
1477
+ });
1478
+ }
1444
1479
  /**
1445
1480
  * Handle a permission toggle from a card button click.
1446
1481
  * Routes to the ApprovalCoordinator for the given run.
@@ -1485,9 +1520,20 @@ export class Bridge {
1485
1520
  return this.withCoordinator(opts.runId, (coordinator) => coordinator.answerCustom({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
1486
1521
  }
1487
1522
  /**
1488
- * Handle codex.compact card action — trigger a Codex compaction request.
1489
- * Validates the run exists and is in a terminal state, then calls
1490
- * the app-server runner's runCompact().
1523
+ * Codex AskUserQuestion 补充说明(user_note,2026-08-18 live 验证模型理解
1524
+ * "user_note: <text>" 条目):记录到 coordinator,随答案一起提交。
1525
+ * 路由到 ApprovalCoordinator.answerNote。
1526
+ */
1527
+ async handleApprovalAnswerNote(opts) {
1528
+ const log = getLogger();
1529
+ log.info(`[bridge] handleApprovalAnswerNote runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1530
+ return this.withCoordinator(opts.runId, (coordinator) => coordinator.answerNote({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
1531
+ }
1532
+ /**
1533
+ * Handle codex.compact card action — trigger a compaction request for any
1534
+ * runCompact-capable runner(codex/kimi/opencode/pi/claude,按
1535
+ * lastCompactableCodexRun 记录的 agentKind 路由)。Validates the run exists
1536
+ * and is in a terminal state, then calls the runner's runCompact().
1491
1537
  */
1492
1538
  async handleCodexCompact(value, ctx) {
1493
1539
  const log = getLogger();
@@ -1529,7 +1575,7 @@ export class Bridge {
1529
1575
  });
1530
1576
  }
1531
1577
  /**
1532
- * Stream a Codex compaction to a card: start a RunCardSession with
1578
+ * Stream a compaction to a card: start a RunCardSession with
1533
1579
  * operationKind='compaction' (no recursive Compact button), consume
1534
1580
  * runner.runCompact() events, read authoritative jsonl usage, and finish
1535
1581
  * the card. Shared by handleCodexCompact (run card button) and
@@ -1659,7 +1705,7 @@ export class Bridge {
1659
1705
  await this.sendResult({ text: `⚠️ 未找到 session ${sessionId}(当前目录: ${cwd})` }, ctx);
1660
1706
  return;
1661
1707
  }
1662
- // Check that the runner has runCompact (codex app-server only).
1708
+ // Check that the runner has runCompact(codex/kimi/opencode/pi/claude 鸭子探测)。
1663
1709
  const runner = this.getRunner(cwd, agentKind);
1664
1710
  if (!('runCompact' in runner) ||
1665
1711
  typeof runner.runCompact !== 'function') {