lark-remote 0.4.5 → 0.5.1

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 (127) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/bridge/approval-coordinator.d.ts +1 -1
  3. package/dist/bridge/approval-coordinator.js +3 -3
  4. package/dist/bridge/inbound-media.js +10 -7
  5. package/dist/bridge/index.d.ts +55 -17
  6. package/dist/bridge/index.js +145 -74
  7. package/dist/bridge/queue-manager.d.ts +71 -20
  8. package/dist/bridge/queue-manager.js +139 -57
  9. package/dist/card/approval-render.d.ts +1 -1
  10. package/dist/card/approval-render.js +6 -6
  11. package/dist/card/bash-card-session.d.ts +3 -3
  12. package/dist/card/bash-card-session.js +3 -3
  13. package/dist/card/bash-renderer.js +1 -1
  14. package/dist/card/card-session.d.ts +4 -4
  15. package/dist/card/card-session.js +6 -6
  16. package/dist/card/run-card-session.js +2 -2
  17. package/dist/card/run-renderer.d.ts +1 -1
  18. package/dist/card/run-renderer.js +14 -14
  19. package/dist/card/run-state.d.ts +2 -2
  20. package/dist/card/run-state.js +3 -3
  21. package/dist/card/tool-render.d.ts +1 -1
  22. package/dist/card/tool-render.js +6 -6
  23. package/dist/cli.js +2 -1
  24. package/dist/clone.d.ts +5 -3
  25. package/dist/clone.js +8 -4
  26. package/dist/common/truncate.d.ts +1 -1
  27. package/dist/common/truncate.js +2 -2
  28. package/dist/common/ttl-cache.d.ts +2 -2
  29. package/dist/common/ttl-cache.js +2 -2
  30. package/dist/config/codex-config.d.ts +9 -9
  31. package/dist/config/codex-config.js +18 -18
  32. package/dist/config/dir.js +5 -2
  33. package/dist/config/index.d.ts +3 -3
  34. package/dist/config/index.js +6 -6
  35. package/dist/config/opencode-config.js +3 -3
  36. package/dist/connector/index.d.ts +5 -5
  37. package/dist/connector/index.js +24 -11
  38. package/dist/inbound/event.d.ts +1 -1
  39. package/dist/inbound/event.js +1 -1
  40. package/dist/inbound/placeholder.d.ts +2 -2
  41. package/dist/inbound/placeholder.js +2 -2
  42. package/dist/index.js +23 -12
  43. package/dist/instance-lock.js +4 -3
  44. package/dist/logger/index.d.ts +1 -1
  45. package/dist/logger/index.js +4 -4
  46. package/dist/persistence/atomic-write.d.ts +1 -1
  47. package/dist/persistence/atomic-write.js +6 -2
  48. package/dist/platform/agent-stopper.d.ts +1 -1
  49. package/dist/platform/identity.d.ts +7 -1
  50. package/dist/platform/identity.js +2 -2
  51. package/dist/platform/path.d.ts +6 -0
  52. package/dist/platform/path.js +14 -8
  53. package/dist/platform/select.d.ts +1 -1
  54. package/dist/platform/select.js +1 -1
  55. package/dist/platform/shell.d.ts +2 -3
  56. package/dist/platform/spawn.d.ts +2 -2
  57. package/dist/platform/spawn.js +2 -2
  58. package/dist/platform/terminator-posix.js +1 -1
  59. package/dist/platform/terminator-win32.js +2 -2
  60. package/dist/platform/terminator.d.ts +3 -3
  61. package/dist/platform/terminator.js +2 -2
  62. package/dist/platform/types.d.ts +1 -1
  63. package/dist/restart.d.ts +1 -1
  64. package/dist/restart.js +5 -3
  65. package/dist/router/config/codex.d.ts +1 -1
  66. package/dist/router/config/codex.js +2 -2
  67. package/dist/router/config/opencode.js +1 -1
  68. package/dist/router/index.d.ts +65 -16
  69. package/dist/router/index.js +141 -47
  70. package/dist/router/order-exec-dispatch.js +2 -2
  71. package/dist/runner/bash/runner.d.ts +2 -2
  72. package/dist/runner/bash/runner.js +6 -6
  73. package/dist/runner/claude/runner.js +1 -1
  74. package/dist/runner/claude/session.d.ts +2 -2
  75. package/dist/runner/claude/session.js +14 -14
  76. package/dist/runner/codex/app-server/runner.d.ts +28 -0
  77. package/dist/runner/codex/app-server/runner.js +57 -4
  78. package/dist/runner/codex/app-server/translator.d.ts +2 -2
  79. package/dist/runner/codex/app-server/translator.js +5 -5
  80. package/dist/runner/common/acp/acp-approval.d.ts +2 -2
  81. package/dist/runner/common/acp/acp-approval.js +3 -3
  82. package/dist/runner/common/acp/base-acp-runner.js +1 -1
  83. package/dist/runner/common/acp/base-translator.d.ts +3 -3
  84. package/dist/runner/common/acp/base-translator.js +4 -4
  85. package/dist/runner/common/acp/protocol-types.js +1 -1
  86. package/dist/runner/common/connection-based-runner.d.ts +1 -1
  87. package/dist/runner/common/connection-based-runner.js +2 -2
  88. package/dist/runner/common/jsonl-stream.d.ts +2 -2
  89. package/dist/runner/common/jsonl-stream.js +12 -12
  90. package/dist/runner/common/jsonrpc/client.d.ts +1 -1
  91. package/dist/runner/common/jsonrpc/client.js +1 -1
  92. package/dist/runner/common/jsonrpc/connection-manager.d.ts +1 -1
  93. package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
  94. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  95. package/dist/runner/common/jsonrpc/transport.js +2 -2
  96. package/dist/runner/common/spawning-runner.d.ts +21 -8
  97. package/dist/runner/common/spawning-runner.js +41 -15
  98. package/dist/runner/dsh/runner.js +2 -2
  99. package/dist/runner/kimi/acp/runner.d.ts +3 -3
  100. package/dist/runner/kimi/acp/runner.js +9 -9
  101. package/dist/runner/kimi/acp/translator.d.ts +2 -2
  102. package/dist/runner/kimi/acp/translator.js +4 -4
  103. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  104. package/dist/runner/opencode/acp/runner.js +3 -3
  105. package/dist/runner/opencode/acp/translator.js +1 -1
  106. package/dist/runner/pi/rpc/runner.js +2 -2
  107. package/dist/runner/pi/rpc/translator.d.ts +1 -1
  108. package/dist/runner/pi/rpc/translator.js +4 -4
  109. package/dist/runner/types.d.ts +11 -2
  110. package/dist/runner/types.js +12 -1
  111. package/dist/session/claude/session-index.d.ts +12 -0
  112. package/dist/session/claude/session-index.js +20 -5
  113. package/dist/session/claude/sessions.js +10 -10
  114. package/dist/session/codex/rollout-reader.d.ts +4 -4
  115. package/dist/session/codex/rollout-reader.js +12 -12
  116. package/dist/session/common/two-pass.js +2 -2
  117. package/dist/session/common/usage-accumulator.d.ts +2 -2
  118. package/dist/session/common/usage-accumulator.js +2 -2
  119. package/dist/session/dsh/sessions.js +2 -2
  120. package/dist/session/kimi/sessions.d.ts +2 -2
  121. package/dist/session/kimi/sessions.js +11 -11
  122. package/dist/session/opencode/sessions.d.ts +3 -3
  123. package/dist/session/opencode/sessions.js +12 -10
  124. package/dist/session/pi/sessions.js +13 -12
  125. package/dist/session/session-store.d.ts +1 -1
  126. package/dist/session/session-store.js +1 -1
  127. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  import type { AgentKind } from '../runner/types.js';
2
2
  /**
3
3
  * 入队时刻快照:当前 defaultAgent + 该 agent 的 sessionId(无 session 则 undefined)。
4
- * 唯一捕获点:排队消息在 T0 把 agent+session 钉进 AgentBinding,随任务闭包带到 T1
5
- * 执行时刻,避免 /new、/config 在排队期间改写 live 状态导致语义漂移(方案 D4)。
4
+ * 唯一捕获点:排队消息在入队时刻把 agent+session 钉进 AgentBinding,随任务闭包带到执行时刻
5
+ * 执行时刻,避免 /new、/config 在排队期间改写 live 状态导致语义漂移。
6
6
  */
7
7
  export interface AgentBinding {
8
8
  agent: AgentKind;
@@ -51,9 +51,32 @@ export interface QueuedTask {
51
51
  * `handleQueueImmediate`).
52
52
  */
53
53
  editedMessage?: string;
54
- /** 入队时刻捕获的 agent+session 绑定(方案 D4),随任务闭包带到执行时刻。 */
54
+ /** 入队时刻捕获的 agent+session 绑定,随任务闭包带到执行时刻。 */
55
55
  binding?: AgentBinding;
56
56
  }
57
+ /**
58
+ * lane 条目的展示类型。只影响排队卡如何描述「正在执行的是什么」:
59
+ * `message` 普通消息(缺省)、`compact` 压缩会话、`command` 其他卡片动作。
60
+ */
61
+ export type LaneTaskKind = 'message' | 'compact' | 'command';
62
+ /**
63
+ * lane 当前占用者的展示信息。由 begin 路径登记、settle 路径清除,与执行
64
+ * 槽位同寿命;排队卡靠它回答「我在等谁」。
65
+ */
66
+ export interface ExecutingInfo {
67
+ /**
68
+ * 归属标记:与执行槽位共用同一个 slot id。清除时校验它,防止一条迟到的
69
+ * settle 清掉后继任务的占用者(身份不用外部 id,同 §9.24 口径)。
70
+ */
71
+ slotId: number;
72
+ kind: LaneTaskKind;
73
+ /** 展示标签,例如 '💬 消息' / '🗜 Compact' / '📋 config.save'。 */
74
+ label: string;
75
+ /** 该条目自己的排队预览(可选)。 */
76
+ preview?: string;
77
+ /** 登记时刻(epoch ms)。当前只供诊断,卡片不渲染实时时长。 */
78
+ startedAt: number;
79
+ }
57
80
  /** Options for enqueue operation */
58
81
  export interface EnqueueOptions {
59
82
  taskMeta?: {
@@ -70,8 +93,14 @@ export interface EnqueueOptions {
70
93
  feishuReplyTo?: string;
71
94
  /** Whether the queued task is editable (queue card ✏️ 编辑 button). Defaults to true. */
72
95
  editable?: boolean;
73
- /** 入队时刻捕获的 agent+session 绑定(方案 D4)。 */
96
+ /** 入队时刻捕获的 agent+session 绑定。 */
74
97
  binding?: AgentBinding;
98
+ /**
99
+ * lane 占用者的展示类型,缺省 'message'。
100
+ * 'compact' 时排队卡写明「正在执行: 🗜 Compact」,并把「立即执行」置灰
101
+ * ——该按钮会中断压缩(见 buildQueueActionButtons)。
102
+ */
103
+ kind?: LaneTaskKind;
75
104
  };
76
105
  }
77
106
  /** Queue info for a cwd */
@@ -90,7 +119,7 @@ export declare class QueueManager {
90
119
  /** Per-workspace queued tasks for queue display cards. Key = cwd, value = task list. */
91
120
  private queuedTasks;
92
121
  /**
93
- * P3-5: per-workspace `messageId → QueuedTask` index mirroring `queuedTasks`.
122
+ * per-workspace `messageId → QueuedTask` index mirroring `queuedTasks`.
94
123
  * O(1) lookup for the task-start removal path (replacing the old double
95
124
  * `find`+`findIndex` O(N) scan) and for `getQueuedTask`/`removeFromQueue`/
96
125
  * `updateQueuedTaskMessage`. The ordered array remains the source of truth
@@ -115,18 +144,23 @@ export declare class QueueManager {
115
144
  /**
116
145
  * Monotonic counter minting a unique slot id per enqueued task. A slot
117
146
  * identifies one task's execution period for the interrupt bookkeeping in
118
- * `executingSlot`/`interruptedSlots` — replacing the unowned skip-credit
147
+ * `executingInfo`/`interruptedSlots` — replacing the unowned skip-credit
119
148
  * counter, which any settle could consume and which therefore leaked a
120
- * re-armed count after repeated resets (see A4 anchor).
149
+ * re-armed count after repeated resets.
121
150
  */
122
151
  private slotCounter;
123
152
  /**
124
- * Per-workspace slot id of the task currently executing. Set in the begin
125
- * path right before the task runs; deleted when that task's settle fires.
126
- * `resetExecutingCount` reads it to bind the interrupt credit to exactly
127
- * the interrupted task's slot.
153
+ * Per-workspace record of the task currently executing — the lane occupant.
154
+ * Set in the begin path right before the task runs; deleted by that task's
155
+ * settle. Carries both the slot id (`resetExecutingCount` binds the
156
+ * interrupt credit to exactly the interrupted task's slot) and the display
157
+ * info the queue card needs to name what a waiting message waits for.
158
+ *
159
+ * 2026-10-09 线上:一次 Compact 占着 lane,新消息的排队卡却写「位置: 第 1
160
+ * 位 / 前面还有: 0 条消息」——开卡判据看 lane(含正在执行者),位置文案只看
161
+ * 等待队列,两个口径的差集正是这条记录。
128
162
  */
129
- private executingSlot;
163
+ private executingInfo;
130
164
  /**
131
165
  * Per-workspace set of slot ids whose task was interrupted by
132
166
  * `resetExecutingCount`. When such a slot settles, its decrement is skipped
@@ -141,7 +175,7 @@ export declare class QueueManager {
141
175
  /**
142
176
  * MessageIds of tasks that have actually begun executing (cancellation check
143
177
  * passed, task removed from the queue). Entries are sticky: they remain
144
- * after the task settles (A21). `handleQueueImmediate` step 6 uses this to
178
+ * after the task settles. `handleQueueImmediate` step 6 uses this to
145
179
  * distinguish "target was cancelled (never began)" from "target began while
146
180
  * the interrupt was in flight" so the final feedback can report the true
147
181
  * state instead of telling a running task's user that nothing was
@@ -162,11 +196,11 @@ export declare class QueueManager {
162
196
  constructor(isWorkspaceRunning: (cwd: string) => boolean, sendCard: (chatId: string, card: object, opts?: {
163
197
  replyTo?: string;
164
198
  }) => Promise<string>, updateCard: (messageId: string, card: object) => Promise<void>);
165
- /** P3-5: register a queued task in the per-workspace `messageId → task` index. */
199
+ /** register a queued task in the per-workspace `messageId → task` index. */
166
200
  private indexAdd;
167
- /** P3-5: drop a task from the per-workspace index by messageId. */
201
+ /** drop a task from the per-workspace index by messageId. */
168
202
  private indexRemove;
169
- /** P3-5: O(1) lookup of a queued task by messageId via the index. */
203
+ /** O(1) lookup of a queued task by messageId via the index. */
170
204
  private indexGet;
171
205
  /**
172
206
  * Enqueue a task into the workspace-level serial queue.
@@ -184,6 +218,12 @@ export declare class QueueManager {
184
218
  * after it). Consumed when the task's slot begins; removed if cancelled.
185
219
  */
186
220
  setTaskReplacement(cwd: string, messageId: string, task: () => Promise<void>): void;
221
+ /**
222
+ * 排队卡头部。只有前面确实还有等待中的消息时才说「排队」;前面为空时
223
+ * 用户等的是当前正在执行的任务(例如 Compact),用「等待当前任务结束」
224
+ * 表达才不会与「前面还有 0 条」自相矛盾(2026-10-09 线上实例)。
225
+ */
226
+ private queueCardHeader;
187
227
  /** Send a queue status card showing current queue position and actions. */
188
228
  private sendQueueStatusCard;
189
229
  /** Get queue info for a cwd. */
@@ -198,7 +238,11 @@ export declare class QueueManager {
198
238
  * state — pending cards enable both, executing/cancelled cards disable
199
239
  * both. Centralizing the pair here eliminates the 3-way duplication
200
240
  * between `buildQueueStatusCardElements`, `updateQueueCardToExecuting`,
201
- * and `updateQueueCardToCancelled` (Clean Code P2-1).
241
+ * and `updateQueueCardToCancelled` (Clean Code).
242
+ *
243
+ * `opts.immediateLabel`(仅 pending 卡用)在 lane 被 Compact 占用时替换
244
+ * 「⚡ 立即执行」并强制置灰:点击它会中断正在跑的压缩。「❌ 撤销」不参与,
245
+ * 它只移除等待中的消息,不触碰 Compact。
202
246
  */
203
247
  private buildQueueActionButtons;
204
248
  /**
@@ -251,7 +295,7 @@ export declare class QueueManager {
251
295
  * id minted at enqueue time. Three cases:
252
296
  *
253
297
  * 1. The slot is the workspace's current executing slot → its execution
254
- * period ends (`executingSlot` marker removed).
298
+ * period ends (`executingInfo` record removed).
255
299
  * 2. The slot is in `interruptedSlots` (its task was interrupted by
256
300
  * `resetExecutingCount`) → skip the decrement: the reset already zeroed
257
301
  * the count, so a stale decrement could wrongly zero it again while a
@@ -279,16 +323,23 @@ export declare class QueueManager {
279
323
  * `expectedSlot` binds the reset to the task that was actually interrupted:
280
324
  * the caller captures the executing slot BEFORE stopping the runner, and
281
325
  * the stop window may outlive the interrupted task's settle (the chain can
282
- * advance to a NEW task, which then owns `executingSlot`). When the current
326
+ * advance to a NEW task, which then owns `executingInfo`). When the current
283
327
  * slot no longer matches the interrupted task's slot, the interrupted task
284
328
  * already decremented normally — resetting now would zero the count of the
285
329
  * running successor and mark ITS slot interrupted, hiding it from the
286
- * queue card (A22).
330
+ * queue card.
287
331
  */
288
332
  resetExecutingCount(cwd: string, expectedSlot: number): void;
289
333
  /**
290
334
  * The slot id of the task currently executing in this cwd (if any).
291
335
  */
292
336
  getExecutingSlot(cwd: string): number | undefined;
337
+ /**
338
+ * The lane occupant's display info for this cwd (if any). Queue cards read
339
+ * it to name what a waiting message is waiting for; `handleQueueImmediate`
340
+ * reads it to refuse interrupting a running Compact. Returns a copy so
341
+ * callers cannot mutate the live record.
342
+ */
343
+ getExecutingInfo(cwd: string): ExecutingInfo | undefined;
293
344
  }
294
345
  export {};
@@ -1,7 +1,7 @@
1
1
  import { getLogger } from '../logger/index.js';
2
2
  import { displayName } from '../platform/path.js';
3
3
  /**
4
- * Per-workspace cap on the number of tasks WAITING to execute (review P2-5).
4
+ * Per-workspace cap on the number of tasks WAITING to execute.
5
5
  * Without a bound, a message flood grows `queuedTasks` / the promise chain /
6
6
  * each task's captured `msg.content` closure without limit, and every enqueued
7
7
  * task fires a queue card (amplifying outbound API calls). When the waiting
@@ -11,6 +11,18 @@ import { displayName } from '../platform/path.js';
11
11
  * backlog, not throughput. Owner is a single user, so 50 is ample headroom.
12
12
  */
13
13
  const MAX_WAITING_QUEUE = 50;
14
+ /** 排队卡「正在执行」一行用的展示标签。 */
15
+ function laneOccupantLabel(kind, preview) {
16
+ switch (kind) {
17
+ case 'compact':
18
+ return '🗜 Compact';
19
+ case 'command':
20
+ // 卡片动作的预览形如 'card action: config.save',标签里去掉前缀更短。
21
+ return `📋 ${preview.replace(/^card action:\s*/, '')}`;
22
+ default:
23
+ return '💬 消息';
24
+ }
25
+ }
14
26
  /**
15
27
  * QueueManager handles the per-workspace serial processing queue.
16
28
  * Manages task queuing, cancellation, and queue status card rendering.
@@ -21,7 +33,7 @@ export class QueueManager {
21
33
  /** Per-workspace queued tasks for queue display cards. Key = cwd, value = task list. */
22
34
  queuedTasks = new Map();
23
35
  /**
24
- * P3-5: per-workspace `messageId → QueuedTask` index mirroring `queuedTasks`.
36
+ * per-workspace `messageId → QueuedTask` index mirroring `queuedTasks`.
25
37
  * O(1) lookup for the task-start removal path (replacing the old double
26
38
  * `find`+`findIndex` O(N) scan) and for `getQueuedTask`/`removeFromQueue`/
27
39
  * `updateQueuedTaskMessage`. The ordered array remains the source of truth
@@ -46,18 +58,23 @@ export class QueueManager {
46
58
  /**
47
59
  * Monotonic counter minting a unique slot id per enqueued task. A slot
48
60
  * identifies one task's execution period for the interrupt bookkeeping in
49
- * `executingSlot`/`interruptedSlots` — replacing the unowned skip-credit
61
+ * `executingInfo`/`interruptedSlots` — replacing the unowned skip-credit
50
62
  * counter, which any settle could consume and which therefore leaked a
51
- * re-armed count after repeated resets (see A4 anchor).
63
+ * re-armed count after repeated resets.
52
64
  */
53
65
  slotCounter = 0;
54
66
  /**
55
- * Per-workspace slot id of the task currently executing. Set in the begin
56
- * path right before the task runs; deleted when that task's settle fires.
57
- * `resetExecutingCount` reads it to bind the interrupt credit to exactly
58
- * the interrupted task's slot.
67
+ * Per-workspace record of the task currently executing — the lane occupant.
68
+ * Set in the begin path right before the task runs; deleted by that task's
69
+ * settle. Carries both the slot id (`resetExecutingCount` binds the
70
+ * interrupt credit to exactly the interrupted task's slot) and the display
71
+ * info the queue card needs to name what a waiting message waits for.
72
+ *
73
+ * 2026-10-09 线上:一次 Compact 占着 lane,新消息的排队卡却写「位置: 第 1
74
+ * 位 / 前面还有: 0 条消息」——开卡判据看 lane(含正在执行者),位置文案只看
75
+ * 等待队列,两个口径的差集正是这条记录。
59
76
  */
60
- executingSlot = new Map();
77
+ executingInfo = new Map();
61
78
  /**
62
79
  * Per-workspace set of slot ids whose task was interrupted by
63
80
  * `resetExecutingCount`. When such a slot settles, its decrement is skipped
@@ -72,7 +89,7 @@ export class QueueManager {
72
89
  /**
73
90
  * MessageIds of tasks that have actually begun executing (cancellation check
74
91
  * passed, task removed from the queue). Entries are sticky: they remain
75
- * after the task settles (A21). `handleQueueImmediate` step 6 uses this to
92
+ * after the task settles. `handleQueueImmediate` step 6 uses this to
76
93
  * distinguish "target was cancelled (never began)" from "target began while
77
94
  * the interrupt was in flight" so the final feedback can report the true
78
95
  * state instead of telling a running task's user that nothing was
@@ -95,7 +112,7 @@ export class QueueManager {
95
112
  this.sendCard = sendCard;
96
113
  this.updateCard = updateCard;
97
114
  }
98
- /** P3-5: register a queued task in the per-workspace `messageId → task` index. */
115
+ /** register a queued task in the per-workspace `messageId → task` index. */
99
116
  indexAdd(cwd, task) {
100
117
  let idx = this.taskIndex.get(cwd);
101
118
  if (!idx) {
@@ -104,11 +121,11 @@ export class QueueManager {
104
121
  }
105
122
  idx.set(task.messageId, task);
106
123
  }
107
- /** P3-5: drop a task from the per-workspace index by messageId. */
124
+ /** drop a task from the per-workspace index by messageId. */
108
125
  indexRemove(cwd, messageId) {
109
126
  this.taskIndex.get(cwd)?.delete(messageId);
110
127
  }
111
- /** P3-5: O(1) lookup of a queued task by messageId via the index. */
128
+ /** O(1) lookup of a queued task by messageId via the index. */
112
129
  indexGet(cwd, messageId) {
113
130
  return this.taskIndex.get(cwd)?.get(messageId);
114
131
  }
@@ -147,7 +164,7 @@ export class QueueManager {
147
164
  const currentExecutingCount = this.pendingOrExecutingCount.get(cwd) ?? 0;
148
165
  const currentQueueLength = this.queuedTasks.get(cwd)?.length ?? 0;
149
166
  const hasWaitingTasks = currentExecutingCount > 0 || currentQueueLength > 0;
150
- // P2-5: bound the waiting backlog. When the queue is full, reject the
167
+ // bound the waiting backlog. When the queue is full, reject the
151
168
  // task with a visible card instead of appending it (which would grow
152
169
  // the promise chain + closures unboundedly under a message flood and
153
170
  // fire another queue card). Do NOT increment pendingOrExecutingCount —
@@ -183,6 +200,14 @@ export class QueueManager {
183
200
  taskList = [];
184
201
  this.queuedTasks.set(cwd, taskList);
185
202
  }
203
+ // 身份不变量 tripwire:同一 cwd 内 queue key 必须唯一(一次入队动作一个
204
+ // key,铸造点 router.mintQueueTaskKey)。撞 key 时数组(追加)与 taskIndex
205
+ // (覆盖)必然漂移,位置显示/撤销/立即执行/卡片更新会串到别的条目上——
206
+ // 2026-10-08 线上缺陷就是这么长出来的。这里不改行为,只留证据。
207
+ if (this.indexGet(cwd, queuedTask.messageId)) {
208
+ getLogger().warn(`[queue-manager] duplicate queue key cwd=${cwd} messageId=${queuedTask.messageId} — ` +
209
+ 'queue identity must be minted per enqueue action (see router.mintQueueTaskKey)');
210
+ }
186
211
  taskList.push(queuedTask);
187
212
  this.indexAdd(cwd, queuedTask);
188
213
  // Increment executing count SYNCHRONOUSLY so subsequent enqueues see it
@@ -217,7 +242,7 @@ export class QueueManager {
217
242
  let livePreview = taskMeta?.messagePreview ?? '';
218
243
  // Check if this task was cancelled before executing
219
244
  if (messageId) {
220
- // P3-5: O(1) index lookup replaces the old `find` + `findIndex`
245
+ // O(1) index lookup replaces the old `find` + `findIndex`
221
246
  // double O(N) scan. The ordered array still drives removal (splice
222
247
  // preserves queue order for position display); the index stays in
223
248
  // sync via indexRemove.
@@ -281,16 +306,24 @@ export class QueueManager {
281
306
  this.pendingOrExecutingCount.set(cwd, 1);
282
307
  getLogger().debug(`[queue-manager] re-armed pendingOrExecutingCount cwd=${cwd} (interrupt resume)`);
283
308
  }
284
- // Mark this task's slot as the current execution period before running
285
- // it. `resetExecutingCount` reads this marker to grant the interrupt
286
- // credit to exactly this task; the settle removes the marker.
287
- this.executingSlot.set(cwd, slotId);
309
+ // Mark this task as the lane occupant for its whole execution period
310
+ // before running it. `resetExecutingCount` reads the slot id to grant
311
+ // the interrupt credit to exactly this task; the settle removes the
312
+ // record. `kind`/`label` let a later message's queue card name what it
313
+ // is waiting for instead of reporting "前面还有 0 条".
314
+ this.executingInfo.set(cwd, {
315
+ slotId,
316
+ kind: taskMeta?.kind ?? 'message',
317
+ label: laneOccupantLabel(taskMeta?.kind ?? 'message', livePreview),
318
+ preview: livePreview,
319
+ startedAt: Date.now(),
320
+ });
288
321
  return replacement ? replacement() : task();
289
322
  })
290
323
  .then(() => {
291
324
  getLogger().debug(`[queue-manager] task end cwd=${cwd}`);
292
325
  // beganMessageIds entry intentionally retained: sticky by design
293
- // (A21) — the marker records "has ever begun", not "is currently
326
+ // the marker records "has ever begun", not "is currently
294
327
  // running", so a task that began and settled quickly still reports
295
328
  // "已开始执行" to queue.immediate.
296
329
  this.decrementExecutingCount(cwd, slotId);
@@ -298,7 +331,7 @@ export class QueueManager {
298
331
  .catch((err) => {
299
332
  getLogger().error('[queue-manager] queue task error:', err);
300
333
  // Same sticky rationale as the success settle: the marker survives
301
- // even when the task errors after beginning (A21).
334
+ // even when the task errors after beginning.
302
335
  this.decrementExecutingCount(cwd, slotId);
303
336
  });
304
337
  this.queues.set(cwd, newQueue);
@@ -325,6 +358,20 @@ export class QueueManager {
325
358
  workspaceReplacements.set(messageId, task);
326
359
  getLogger().debug(`[queue-manager] set task replacement cwd=${cwd} messageId=${messageId}`);
327
360
  }
361
+ /**
362
+ * 排队卡头部。只有前面确实还有等待中的消息时才说「排队」;前面为空时
363
+ * 用户等的是当前正在执行的任务(例如 Compact),用「等待当前任务结束」
364
+ * 表达才不会与「前面还有 0 条」自相矛盾(2026-10-09 线上实例)。
365
+ */
366
+ queueCardHeader(tasksAhead) {
367
+ return {
368
+ template: 'orange',
369
+ title: {
370
+ tag: 'plain_text',
371
+ content: tasksAhead > 0 ? '⏳ 消息排队中' : '⏳ 等待当前任务结束',
372
+ },
373
+ };
374
+ }
328
375
  /** Send a queue status card showing current queue position and actions. */
329
376
  async sendQueueStatusCard(cwd, chatId, replyToMessageId, messagePreview,
330
377
  /** Feishu reply target when `replyToMessageId` (the queue key) is an
@@ -338,10 +385,7 @@ export class QueueManager {
338
385
  const card = {
339
386
  schema: '2.0',
340
387
  config: { wide_screen_mode: true },
341
- header: {
342
- template: 'orange',
343
- title: { tag: 'plain_text', content: '⏳ 消息排队中' },
344
- },
388
+ header: this.queueCardHeader(tasksAhead),
345
389
  body: {
346
390
  elements: this.buildQueueStatusCardElements(cwd, actualPosition, tasksAhead, replyToMessageId, messagePreview),
347
391
  },
@@ -379,25 +423,50 @@ export class QueueManager {
379
423
  /** Build queue status card elements. */
380
424
  buildQueueStatusCardElements(cwd, actualPosition, tasksAhead, messageId, messagePreview) {
381
425
  const workspaceName = displayName(cwd);
382
- const isRunning = this.isWorkspaceRunning(cwd);
426
+ const occupant = this.executingInfo.get(cwd);
383
427
  const elements = [
384
428
  {
429
+ tag: 'div',
430
+ text: { tag: 'lark_md', content: `**当前 Workspace:** \`${workspaceName}\`` },
431
+ },
432
+ ];
433
+ // 「队列位置 / 前面还有」只在前面确实还有等待中的消息时渲染。前面为空时
434
+ // 这两个数是「第 1 位 / 0 条」,与「排队中」字面矛盾;此时挡住用户的只有
435
+ // 正在执行的任务,交给下一段如实说明。
436
+ if (tasksAhead > 0) {
437
+ elements.push({
385
438
  tag: 'div',
386
439
  text: {
387
440
  tag: 'lark_md',
388
- content: `**当前 Workspace:** \`${workspaceName}\`\n**位置:** 第 ${actualPosition} 位\n**前面还有:** ${tasksAhead} 条消息在排队`,
441
+ content: `**队列位置:** 第 ${actualPosition} 位\n**前面还有:** ${tasksAhead} 条消息`,
389
442
  },
390
- },
391
- { tag: 'hr' },
392
- {
443
+ });
444
+ }
445
+ if (occupant) {
446
+ elements.push({
447
+ tag: 'div',
448
+ text: { tag: 'lark_md', content: `**正在执行:** ${occupant.label}` },
449
+ });
450
+ elements.push({
393
451
  tag: 'div',
394
452
  text: {
395
453
  tag: 'lark_md',
396
- content: `💡 消息将按顺序执行…当前正在处理: ${isRunning ? '有任务运行中' : '空闲'}`,
454
+ content: occupant.kind === 'compact'
455
+ ? '💡 压缩结束后将按顺序执行你的消息'
456
+ : '💡 消息将按顺序执行…',
397
457
  },
398
- },
399
- { tag: 'hr' },
400
- ];
458
+ });
459
+ }
460
+ else {
461
+ // 防御分支:开卡判据是 lane 非空,所以「有排队卡但没有占用者」说明计数
462
+ // 与占用者记录漂移了。不改行为,只留证据(与 duplicate queue key 同口径)。
463
+ getLogger().warn(`[queue-manager] queue card without lane occupant cwd=${cwd} tasksAhead=${tasksAhead}`);
464
+ elements.push({
465
+ tag: 'div',
466
+ text: { tag: 'lark_md', content: '💡 消息将按顺序执行…' },
467
+ });
468
+ }
469
+ elements.push({ tag: 'hr' });
401
470
  // Show message preview with edit button (only if not executing AND the
402
471
  // task is editable). One-shot card actions like Compact enqueue with
403
472
  // editable=false: their preview is not user-editable text, so the ✏️ 编辑
@@ -423,8 +492,10 @@ export class QueueManager {
423
492
  }
424
493
  elements.push({ tag: 'hr' });
425
494
  }
426
- // Action buttons
427
- elements.push(...this.buildQueueActionButtons(cwd, messageId, false));
495
+ // Action buttons. Compact 占着 lane 时「⚡ 立即执行」会中断压缩
496
+ // (interruptCurrentRun 遍历 activeRuns,压缩也注册在里面),按钮置灰并
497
+ // 换文案说明;执行层 handleQueueImmediate 再做一次同样的守卫兜住旧卡片。
498
+ elements.push(...this.buildQueueActionButtons(cwd, messageId, false, occupant?.kind === 'compact' ? { immediateLabel: '压缩完成后按顺序执行' } : undefined));
428
499
  return elements;
429
500
  }
430
501
  /**
@@ -435,9 +506,13 @@ export class QueueManager {
435
506
  * state — pending cards enable both, executing/cancelled cards disable
436
507
  * both. Centralizing the pair here eliminates the 3-way duplication
437
508
  * between `buildQueueStatusCardElements`, `updateQueueCardToExecuting`,
438
- * and `updateQueueCardToCancelled` (Clean Code P2-1).
509
+ * and `updateQueueCardToCancelled` (Clean Code).
510
+ *
511
+ * `opts.immediateLabel`(仅 pending 卡用)在 lane 被 Compact 占用时替换
512
+ * 「⚡ 立即执行」并强制置灰:点击它会中断正在跑的压缩。「❌ 撤销」不参与,
513
+ * 它只移除等待中的消息,不触碰 Compact。
439
514
  */
440
- buildQueueActionButtons(cwd, messageId, disabled) {
515
+ buildQueueActionButtons(cwd, messageId, disabled, opts) {
441
516
  return [
442
517
  {
443
518
  tag: 'button',
@@ -448,9 +523,9 @@ export class QueueManager {
448
523
  },
449
524
  {
450
525
  tag: 'button',
451
- text: { tag: 'plain_text', content: '⚡ 立即执行' },
526
+ text: { tag: 'plain_text', content: opts?.immediateLabel ?? '⚡ 立即执行' },
452
527
  type: 'primary',
453
- disabled,
528
+ disabled: disabled || opts?.immediateLabel !== undefined,
454
529
  behaviors: [{ type: 'callback', value: { cmd: 'queue.immediate', cwd, messageId } }],
455
530
  },
456
531
  ];
@@ -476,8 +551,8 @@ export class QueueManager {
476
551
  try {
477
552
  // Prefer the live preview: an edited task's messagePreview is updated in
478
553
  // place, and the passed-in snapshot may be stale if the card send was in
479
- // flight while the user edited (A19). The begin path (started=true) already
480
- // passes the live preview captured at begin (A12), so the fallback is only
554
+ // flight while the user edited. The begin path (started=true) already
555
+ // passes the live preview captured at begin, so the fallback is only
481
556
  // exercised when the task is no longer queued.
482
557
  const liveTask = this.indexGet(cwd, messageId);
483
558
  const stillQueued = liveTask !== undefined;
@@ -527,7 +602,7 @@ export class QueueManager {
527
602
  const taskList = this.queuedTasks.get(cwd);
528
603
  if (!taskList)
529
604
  return false;
530
- // P3-5: index is the O(1) presence check; array splice keeps order.
605
+ // index is the O(1) presence check; array splice keeps order.
531
606
  if (!this.indexGet(cwd, messageId))
532
607
  return false;
533
608
  const index = taskList.findIndex((t) => t.messageId === messageId);
@@ -582,7 +657,7 @@ export class QueueManager {
582
657
  }
583
658
  /** Get task metadata from queue. */
584
659
  getQueuedTask(cwd, messageId) {
585
- // P3-5: O(1) index lookup instead of array `find`.
660
+ // O(1) index lookup instead of array `find`.
586
661
  return this.indexGet(cwd, messageId);
587
662
  }
588
663
  /**
@@ -600,7 +675,7 @@ export class QueueManager {
600
675
  }
601
676
  /** Update the messagePreview for a queued task. Returns true if found and updated. */
602
677
  updateQueuedTaskMessage(cwd, messageId, newMessage) {
603
- // P3-5: O(1) index lookup instead of array `find`.
678
+ // O(1) index lookup instead of array `find`.
604
679
  const task = this.indexGet(cwd, messageId);
605
680
  if (!task)
606
681
  return false;
@@ -633,10 +708,7 @@ export class QueueManager {
633
708
  return {
634
709
  schema: '2.0',
635
710
  config: { wide_screen_mode: true },
636
- header: {
637
- template: 'orange',
638
- title: { tag: 'plain_text', content: '⏳ 消息排队中' },
639
- },
711
+ header: this.queueCardHeader(tasksAhead),
640
712
  body: {
641
713
  elements: this.buildQueueStatusCardElements(cwd, actualPosition, tasksAhead, messageId, newMessagePreview),
642
714
  },
@@ -650,7 +722,7 @@ export class QueueManager {
650
722
  * id minted at enqueue time. Three cases:
651
723
  *
652
724
  * 1. The slot is the workspace's current executing slot → its execution
653
- * period ends (`executingSlot` marker removed).
725
+ * period ends (`executingInfo` record removed).
654
726
  * 2. The slot is in `interruptedSlots` (its task was interrupted by
655
727
  * `resetExecutingCount`) → skip the decrement: the reset already zeroed
656
728
  * the count, so a stale decrement could wrongly zero it again while a
@@ -658,8 +730,8 @@ export class QueueManager {
658
730
  * 3. Otherwise → normal decrement.
659
731
  */
660
732
  decrementExecutingCount(cwd, slotId) {
661
- if (this.executingSlot.get(cwd) === slotId) {
662
- this.executingSlot.delete(cwd);
733
+ if (this.executingInfo.get(cwd)?.slotId === slotId) {
734
+ this.executingInfo.delete(cwd);
663
735
  }
664
736
  const interrupted = this.interruptedSlots.get(cwd);
665
737
  if (interrupted?.has(slotId)) {
@@ -693,19 +765,19 @@ export class QueueManager {
693
765
  * `expectedSlot` binds the reset to the task that was actually interrupted:
694
766
  * the caller captures the executing slot BEFORE stopping the runner, and
695
767
  * the stop window may outlive the interrupted task's settle (the chain can
696
- * advance to a NEW task, which then owns `executingSlot`). When the current
768
+ * advance to a NEW task, which then owns `executingInfo`). When the current
697
769
  * slot no longer matches the interrupted task's slot, the interrupted task
698
770
  * already decremented normally — resetting now would zero the count of the
699
771
  * running successor and mark ITS slot interrupted, hiding it from the
700
- * queue card (A22).
772
+ * queue card.
701
773
  */
702
774
  resetExecutingCount(cwd, expectedSlot) {
703
- const currentSlot = this.executingSlot.get(cwd);
775
+ const currentSlot = this.executingInfo.get(cwd)?.slotId;
704
776
  if (currentSlot !== expectedSlot) {
705
777
  getLogger().debug(`[queue-manager] resetExecutingCount skip (stopped task settled, slot advanced) cwd=${cwd} expectedSlot=${expectedSlot} currentSlot=${currentSlot ?? 'none'}`);
706
778
  return;
707
779
  }
708
- // P3#9: only reset and grant an interrupt slot when there is actually a
780
+ // only reset and grant an interrupt slot when there is actually a
709
781
  // pending/executing task.
710
782
  const currentCount = this.pendingOrExecutingCount.get(cwd) ?? 0;
711
783
  if (currentCount === 0) {
@@ -733,7 +805,17 @@ export class QueueManager {
733
805
  * The slot id of the task currently executing in this cwd (if any).
734
806
  */
735
807
  getExecutingSlot(cwd) {
736
- return this.executingSlot.get(cwd);
808
+ return this.executingInfo.get(cwd)?.slotId;
809
+ }
810
+ /**
811
+ * The lane occupant's display info for this cwd (if any). Queue cards read
812
+ * it to name what a waiting message is waiting for; `handleQueueImmediate`
813
+ * reads it to refuse interrupting a running Compact. Returns a copy so
814
+ * callers cannot mutate the live record.
815
+ */
816
+ getExecutingInfo(cwd) {
817
+ const info = this.executingInfo.get(cwd);
818
+ return info ? { ...info } : undefined;
737
819
  }
738
820
  }
739
821
  //# sourceMappingURL=queue-manager.js.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * renderApprovalArea: renders the approval request area in a run card.
3
3
  *
4
- * Renders command/file/permissions approval areas with button mappings per §8.3.
4
+ * Renders command/file/permissions approval areas with button mappings.
5
5
  * Uses CardKit 2.0 (no tag:"action" container — 200861 rule).
6
6
  */
7
7
  import type { ApprovalView } from '../runner/types.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * renderApprovalArea: renders the approval request area in a run card.
3
3
  *
4
- * Renders command/file/permissions approval areas with button mappings per §8.3.
4
+ * Renders command/file/permissions approval areas with button mappings.
5
5
  * Uses CardKit 2.0 (no tag:"action" container — 200861 rule).
6
6
  */
7
7
  import { collapsibleMarkdownPanel } from './collapsible.js';
@@ -13,7 +13,7 @@ function mkNonce() {
13
13
  }
14
14
  /** 计划面板正文字节预算(折叠展示 + 截断保护 28KB 卡片预算)。 */
15
15
  const PLAN_PANEL_BYTES = 6000;
16
- /** reason div(3 处共用);反引号中和防 lark_md 解析错误(review P2-2)。 */
16
+ /** reason div(3 处共用);反引号中和防 lark_md 解析错误。 */
17
17
  function reasonDiv(approval) {
18
18
  if (!approval.reason)
19
19
  return null;
@@ -39,7 +39,7 @@ export function renderApprovalArea(approval, opts) {
39
39
  if (!approval)
40
40
  return [];
41
41
  // 终态(done/error/interrupted/idle_timeout)下 run 已结束:coordinator 已
42
- // 释放,按钮点击无处响应,隐藏整个审批区避免"点了没反应"(P2-7)。
42
+ // 释放,按钮点击无处响应,隐藏整个审批区避免"点了没反应"。
43
43
  if (opts?.terminal && opts.terminal !== 'running' && opts.terminal !== 'finalizing') {
44
44
  return [];
45
45
  }
@@ -238,7 +238,7 @@ function renderPlanFeedbackInput(requestId, runId) {
238
238
  function renderCommandApproval(approval, expired, runId) {
239
239
  const elements = [];
240
240
  // Command display
241
- // review P2-2:命令里的反引号会提前终止 lark_md 行内代码 span,奇数次
241
+ // 命令里的反引号会提前终止 lark_md 行内代码 span,奇数次
242
242
  // 反引号极易触发 11311 解析错误导致整卡失败(同 renderFileDiff 的中和策略)。
243
243
  const neutralizedCommand = (approval.command ?? '').replace(/`/g, '·');
244
244
  const cmdContent = neutralizedCommand
@@ -422,7 +422,7 @@ function renderQuestionApproval(approval, expired, runId) {
422
422
  elements.push(renderOptionRow(approval.requestId, i, option, isSelected, icon, runId));
423
423
  }
424
424
  // 自定义答案(Other)的选中态展示:选项按钮无法表示自由文本,单独显示
425
- // 已选文本,避免「点了没反应」的困惑(review P3)。
425
+ // 已选文本,避免「点了没反应」的困惑。
426
426
  const customSelected = q.selected?.find((s) => !q.options.some((o) => o.label === s));
427
427
  if (customSelected) {
428
428
  elements.push({
@@ -430,7 +430,7 @@ function renderQuestionApproval(approval, expired, runId) {
430
430
  text: { tag: 'lark_md', content: `✍️ 自定义答案:${customSelected}` },
431
431
  });
432
432
  }
433
- // review P3-4:单选问题提供自定义答案(Other)输入——输入文本后提交
433
+ // 单选问题提供自定义答案(Other)输入——输入文本后提交
434
434
  // (移动端键盘回车/完成键,桌面端输入框右侧提交图标),input_value 经
435
435
  // connector raw 事件回传。
436
436
  // 显隐按 isOther !== false:Kimi form 会丢弃非声明选项值,翻译时置