lark-remote 0.5.8 → 0.5.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.
- package/CHANGELOG.md +21 -2
- package/dist/bridge/index.d.ts +32 -7
- package/dist/bridge/index.js +239 -129
- package/dist/bridge/queue-manager.d.ts +6 -0
- package/dist/bridge/queue-manager.js +20 -5
- package/dist/card/card-shared.js +4 -0
- package/dist/card/run-renderer.d.ts +8 -1
- package/dist/card/run-renderer.js +100 -9
- package/dist/card/run-state.d.ts +29 -2
- package/dist/card/run-state.js +23 -4
- package/dist/index.js +8 -7
- package/dist/router/index.js +14 -14
- package/dist/router/usage-format.d.ts +5 -0
- package/dist/router/usage-format.js +1 -1
- package/dist/runner/dsh/client.d.ts +15 -0
- package/dist/runner/dsh/client.js +27 -0
- package/dist/runner/dsh/goal-view.d.ts +36 -0
- package/dist/runner/dsh/goal-view.js +106 -0
- package/dist/runner/dsh/runner.js +51 -2
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/types.d.ts +40 -1
- package/dist/session/dsh/sessions.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,12 +5,31 @@ 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.9] - 2026-10-22
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- **运行卡片看得出长期目标跑到哪了**:DSH 会话可以带一个跨回合自动续跑的目标(objective / phase / 轮次)。此前卡片只看单个回合,目标还在续跑时你从卡上看不出来。现在状态行下方直接显示 `🎯 目标进行中 · 第 r/N 轮` 与目标摘要,长目标收进折叠面板并保留全文,回合开始就能看见而不是等收尾;摘要与轮次来自宿主投影,卡片不自行推断
|
|
13
|
+
- **`goal_continuing` 终态**:本轮结束但目标仍 active 时,卡片标题写「本轮结束(目标继续)」,摘要首行同调,并附一句「宿主会自动开始第 r+1 轮」。它与「已完成」是两件事:任务还在跑
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
|
|
17
|
+
- **本轮结束不再被当成任务完成**:目标仍 active 时宿主会立刻起下一轮,会话永远不空闲。旧实现按回合收尾,卡片写着「已完成」而任务还在跑,你随后点压缩会被服务端拒。现在终态与按钮都以目标为准,这类卡片不给压缩按钮
|
|
18
|
+
- **中断后的卡片上压缩按钮不再是死按钮**:可压缩记录改在运行收尾的 `finally` 里写,且在「这个工作区的槽位还归不归本轮」守卫之外。停止(`/stop`、卡片上的停止按钮、抢占式立即执行)都会先摘掉槽位,等收尾走到守卫时已经不成立——记录跟着守卫走就永远不落,中断后的终态卡点压缩一律回「该任务已结束或不属于当前会话」。渲染侧共用同一判据:本轮没有会话、开跑前也没有会话时不渲染按钮,不给一个注定失败的点击
|
|
19
|
+
- **`/stop` 在多个工作区并行时不再假报已停止**:槽位按工作区为键,旧实现停在第一条命中就返回,只停一个却回「已停止」,另一个无声继续。现在遍历所有命中的运行与后台任务,并把实际停止数量回报出来(多条时提示已停止 N 个);按 runId 停某一条仍然只停那一条
|
|
20
|
+
- **停止不会把配置变更多拖一轮**:中断时不再立即回收常驻的 runner 实例——队列链可能在等待窗口里推进下一条消息并拿走同一实例,立即回收会让那条回合并无人托管长驻连接,进程残留到空闲超时且占着写锁。改为只标记待替换,由下一次即将开跑、且该工作区确实空闲时换新实例;只看一眼的调用(探压缩能力、杀后台任务)不会被顺手换实例
|
|
21
|
+
- **排队卡不再停在旧状态**:移除排队条目与「已撤销」卡更新收口到同一处,调用方不再需要手工配对(漏一处就永久停在「等待中」,而映射已被消费、之后再无修正机会);卡片的更新请求失败会重试一次,一次网络抖动不再让卡停在旧视图
|
|
22
|
+
|
|
23
|
+
### 变更
|
|
24
|
+
|
|
25
|
+
- **英文架构文档补齐本轮叙述**:停止范围与中断交接、可压缩记录与槽位所有权解耦、按钮可见性判据、队列移除与撤销卡收口,以及目标终态与目标视图,中英两侧一致,不再一边落后
|
|
26
|
+
|
|
8
27
|
## [0.5.8] - 2026-10-21
|
|
9
28
|
|
|
10
29
|
### 修复
|
|
11
30
|
|
|
12
|
-
- **DSH 的 `/resume` 列表卡看得出会话内容**:此前 DSH 会话的列表卡只有 session id 和时间,必须逐条点进详情才知道哪一条是要找的会话。DSH 0.2.x 的列表响应里已带着会话标题与每轮的输入/输出,现在把它们映射成「标题 / 最近输入 /
|
|
13
|
-
-
|
|
31
|
+
- **DSH 的 `/resume` 列表卡看得出会话内容**:此前 DSH 会话的列表卡只有 session id 和时间,必须逐条点进详情才知道哪一条是要找的会话。DSH 0.2.x 的列表响应里已带着会话标题与每轮的输入/输出,现在把它们映射成「标题 / 最近输入 / 最后输出」直接渲染,行上有预览就不再逐条去拉分页详情(那一次会按上限把整段历史拉回来);字段缺失或标题只是占位符时回退原有行为。详情卡头部同样优先用服务端的末轮输入作「最近输入」,并在它与会话标题不同时分成两段展示,不再把用户自己说的话挂在「AI 标题」下面
|
|
32
|
+
- **后台任务卡里的命令不再被截断**:活过本轮的后台任务(终端里起的后台进程)原先按固定字数截断,用户没法核对卡上那条命令到底是要停哪个。现在短命令内联展示、中等长度用代码块整段展示、只有确实很长的脚本或链式命令才收进折叠面板,且面板里保留完整原文——命令正是你要核对、要复制的对象,任何情况下不再 truncate
|
|
14
33
|
- **停止按钮不再被挤成「🟥…」**:命令与停止按钮原先并排同一行(命令列 `grow`、按钮列 `auto`),长命令的最小内容宽度会把按钮压成一个色块。现在命令独占一行,停止按钮在下一行独占一列
|
|
15
34
|
- **运行卡片收尾后保留状态行**:终态原先会把「⏱ 耗时 · 🧰 步数 · 📊 上下文」整行去掉,回看卡片时看不出这一跑花了多久、做了多少步。现在保留:耗时定格,步数与上下文显示该回合最后一次投影的快照
|
|
16
35
|
|
package/dist/bridge/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AgentRegistry } from '../runner/registry.js';
|
|
|
3
3
|
import type { SessionReaderRegistry, SessionStore } from '../session/index.js';
|
|
4
4
|
import type { AppConfig } from '../config/index.js';
|
|
5
5
|
import { type RunCardChannel } from '../card/run-card-session.js';
|
|
6
|
-
import type
|
|
6
|
+
import { type RunTerminal } from '../card/run-state.js';
|
|
7
7
|
import { type AgentKind, type StopOutcome } from '../runner/types.js';
|
|
8
8
|
import { QueueManager, type QueuedTask, type EnqueueOptions, type AgentBinding, type ExecutingInfo } from './queue-manager.js';
|
|
9
9
|
import type { InboundMediaPayload } from '../connector/index.js';
|
|
@@ -49,6 +49,8 @@ export interface CommandResult {
|
|
|
49
49
|
/** Detailed result of one interrupt, for callers that must not over-promise. */
|
|
50
50
|
export interface InterruptOutcome {
|
|
51
51
|
stopped: boolean;
|
|
52
|
+
/** 本次实际停掉的 run 数(activeRuns + bash)。多个 workspace 并行时可能 >1。 */
|
|
53
|
+
stoppedCount?: number;
|
|
52
54
|
agentKind?: AgentKind;
|
|
53
55
|
/** Present for DSH: what the remote stop actually achieved. */
|
|
54
56
|
stop?: StopOutcome;
|
|
@@ -159,12 +161,11 @@ export declare class Bridge {
|
|
|
159
161
|
*/
|
|
160
162
|
private compactableRuns;
|
|
161
163
|
/**
|
|
162
|
-
* 终态卡片的渲染快照(runId → state + options
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* 受 MAX_BLOCKS 约束(几十 KB 以内),用户只会点刚收到的卡。
|
|
164
|
+
* 终态卡片的渲染快照(runId → state + options):run 结束后 activeRuns 已
|
|
165
|
+
* 清空,而卡片上的「⏹ 停止」后台任务按钮(job.kill)还活着,需要按 runId
|
|
166
|
+
* 反查该 run 的 sessionId/cwd 才能停对任务。只保留最近若干条:单条快照受
|
|
167
|
+
* MAX_BLOCKS 约束(几十 KB 以内),用户只会点刚收到的卡;超出窗口的旧卡
|
|
168
|
+
* 会得到明确的「记录已回收」提示,不假装任务仍在。
|
|
168
169
|
*/
|
|
169
170
|
private runCardSnapshots;
|
|
170
171
|
private static readonly RUN_CARD_SNAPSHOT_MAX;
|
|
@@ -349,6 +350,14 @@ export declare class Bridge {
|
|
|
349
350
|
runId?: string;
|
|
350
351
|
cwd?: string;
|
|
351
352
|
}): Promise<InterruptOutcome>;
|
|
353
|
+
/**
|
|
354
|
+
* 停掉一条已注册的 active run:置终态、停 runner、摘槽位、标 stale、
|
|
355
|
+
* 归零该 cwd 的队列执行计数。返回 runner 的停止结果(DSH 才有)。
|
|
356
|
+
*
|
|
357
|
+
* 只做「这一条 run」的事——跨多个 workspace 的遍历在调用方,避免再次
|
|
358
|
+
* 出现「停第一条就 return」的漏停。
|
|
359
|
+
*/
|
|
360
|
+
private interruptActiveRun;
|
|
352
361
|
/** Send a result message to the user (§7). Returns true on success, false on failure. */
|
|
353
362
|
sendResult(result: CommandResult, ctx: CommandContext): Promise<boolean>;
|
|
354
363
|
/**
|
|
@@ -556,6 +565,22 @@ export declare class Bridge {
|
|
|
556
565
|
* 先 delete 再 set 以刷新位置)。
|
|
557
566
|
*/
|
|
558
567
|
private recordCompactableRun;
|
|
568
|
+
/**
|
|
569
|
+
* 终态 Compact 记账:把本次 run 实际使用的 sessionId 记进
|
|
570
|
+
* {@link compactableRuns},供终态卡上的 Compact 按钮按 runId 反查。
|
|
571
|
+
*
|
|
572
|
+
* 调用点固定在 `finalizeRun` 的 `finally` 里、**槽位所有权守卫之外**。
|
|
573
|
+
* 所有权守卫只回答「cwd 槽位还归不归本 run」(决定能否删槽位 / evict
|
|
574
|
+
* runner);记账回答「本次 run 结束了吗、用的哪个 sessionId」。两者在不同
|
|
575
|
+
* 维度上:外部中断(`/stop`、停止卡按钮、`queue.immediate`)会先摘槽位,
|
|
576
|
+
* 记账若跟着守卫走就永远不会落,中断卡片的按钮必然反查不到(中断
|
|
577
|
+
* 后点 Compact 一律回「该任务已结束或不属于当前会话」)。
|
|
578
|
+
*
|
|
579
|
+
* 覆盖所有终态入口(正常结束、error、interrupted、idle_timeout、
|
|
580
|
+
* goal_continuing)——这些卡片都渲染 Compact 按钮(`goal` 仍 active 时
|
|
581
|
+
* 除外)。记账失败不能影响清理,故内部兜错。
|
|
582
|
+
*/
|
|
583
|
+
private recordCompactableRunForTerminal;
|
|
559
584
|
/**
|
|
560
585
|
* 按 runId 反查可压缩 run 记录(跨 cwd 扫描)。
|
|
561
586
|
*
|
package/dist/bridge/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { RunCardSession } from '../card/run-card-session.js';
|
|
|
7
7
|
import { renderRunCard } from '../card/run-renderer.js';
|
|
8
8
|
import { renderBashCard } from '../card/bash-renderer.js';
|
|
9
9
|
import { BashCardSession, capBashOutput } from '../card/bash-card-session.js';
|
|
10
|
+
import { isRunTerminal } from '../card/run-state.js';
|
|
10
11
|
import { buildSessionHistoryCard } from '../router/card-helpers.js';
|
|
11
12
|
import { agentDisplayName, resumeUseButton } from '../card/card-shared.js';
|
|
12
13
|
import { enforceCardBudget } from '../card/card-budget.js';
|
|
@@ -152,12 +153,11 @@ export class Bridge {
|
|
|
152
153
|
*/
|
|
153
154
|
compactableRuns = new Map();
|
|
154
155
|
/**
|
|
155
|
-
* 终态卡片的渲染快照(runId → state + options
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* 受 MAX_BLOCKS 约束(几十 KB 以内),用户只会点刚收到的卡。
|
|
156
|
+
* 终态卡片的渲染快照(runId → state + options):run 结束后 activeRuns 已
|
|
157
|
+
* 清空,而卡片上的「⏹ 停止」后台任务按钮(job.kill)还活着,需要按 runId
|
|
158
|
+
* 反查该 run 的 sessionId/cwd 才能停对任务。只保留最近若干条:单条快照受
|
|
159
|
+
* MAX_BLOCKS 约束(几十 KB 以内),用户只会点刚收到的卡;超出窗口的旧卡
|
|
160
|
+
* 会得到明确的「记录已回收」提示,不假装任务仍在。
|
|
161
161
|
*/
|
|
162
162
|
runCardSnapshots = new Map();
|
|
163
163
|
static RUN_CARD_SNAPSHOT_MAX = 8;
|
|
@@ -189,11 +189,23 @@ export class Bridge {
|
|
|
189
189
|
* creates a NEW runner under a different key without evicting the old one.
|
|
190
190
|
* This eliminates the need for clearRunners() on agent switch.
|
|
191
191
|
*/
|
|
192
|
-
getRunner(cwd, kind = this.config.defaultAgent) {
|
|
192
|
+
getRunner(cwd, kind = this.config.defaultAgent, opts) {
|
|
193
193
|
// Fix 4: Check for runner under (cwd, kind) key
|
|
194
194
|
const workspaceMap = this.runners.get(cwd);
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
const slotKey = `${cwd}\u0000${kind}`;
|
|
196
|
+
const cached = workspaceMap?.get(kind);
|
|
197
|
+
// 活跃 run 期间不替换:queue 链可能在 interrupt 的 await 窗口里推进,把
|
|
198
|
+
// 正在跑的那一轮手里的实例换掉会脱管长驻连接。
|
|
199
|
+
const replaceStaleInstance = cached !== undefined &&
|
|
200
|
+
opts?.replaceStale === true &&
|
|
201
|
+
this.staleRunnerSlots.has(slotKey) &&
|
|
202
|
+
!this.activeRuns.has(cwd);
|
|
203
|
+
if (cached && !replaceStaleInstance) {
|
|
204
|
+
return cached;
|
|
205
|
+
}
|
|
206
|
+
if (replaceStaleInstance) {
|
|
207
|
+
getLogger().info(`[lark-remote] replace stale runner before run cwd=${cwd} kind=${kind}`);
|
|
208
|
+
this.evictRunnerSlot(cwd, kind);
|
|
197
209
|
}
|
|
198
210
|
// 缓存超过上限且是新工作区时才回收死槽位——刚创建未 run
|
|
199
211
|
// 的 runner 同样是 isRunning=false,小缓存下立即回收会破坏「创建即注册」
|
|
@@ -218,12 +230,16 @@ export class Bridge {
|
|
|
218
230
|
if (runner && typeof runner.registerExitHandlers === 'function') {
|
|
219
231
|
runner.registerExitHandlers();
|
|
220
232
|
}
|
|
221
|
-
// Store in nested map: Map<cwd, Map<kind, Runner
|
|
222
|
-
|
|
233
|
+
// Store in nested map: Map<cwd, Map<kind, Runner>>。必须重新取一次:
|
|
234
|
+
// replaceStale 刚把槽位 evict 掉,上面的局部 workspaceMap 可能已随空槽
|
|
235
|
+
// 从 this.runners 移除——继续写它会生成一个「缓存里查不到」的 runner,
|
|
236
|
+
// 下一次 getRunner 会再建一个(实例数翻倍)。
|
|
237
|
+
const currentWorkspaceMap = this.runners.get(cwd);
|
|
238
|
+
if (!currentWorkspaceMap) {
|
|
223
239
|
this.runners.set(cwd, new Map([[kind, runner]]));
|
|
224
240
|
}
|
|
225
241
|
else {
|
|
226
|
-
|
|
242
|
+
currentWorkspaceMap.set(kind, runner);
|
|
227
243
|
}
|
|
228
244
|
return runner;
|
|
229
245
|
}
|
|
@@ -358,7 +374,7 @@ export class Bridge {
|
|
|
358
374
|
if (evicted && typeof evicted.unregisterExitHandlers === 'function') {
|
|
359
375
|
evicted.unregisterExitHandlers();
|
|
360
376
|
}
|
|
361
|
-
//
|
|
377
|
+
// 先标记 orphan 再 dispose:evict 与「run 刚拿走本实例」竞态时
|
|
362
378
|
// (queue 链在 interruptCurrentRun await 期间推进),dispose 只能关掉当时
|
|
363
379
|
// 已存在的连接,run 后续 acquire 的新连接会挂在无人托管的实例上——orphan
|
|
364
380
|
// 标记让 executeTurn 在 run 结束时主动 dispose 这批连接,而不是挂 idle
|
|
@@ -454,7 +470,15 @@ export class Bridge {
|
|
|
454
470
|
}
|
|
455
471
|
/** Remove a task from the queue by messageId. Returns true if found and removed. */
|
|
456
472
|
removeFromQueue(cwd, messageId) {
|
|
457
|
-
|
|
473
|
+
const removed = this.queueManager.removeFromQueue(cwd, messageId);
|
|
474
|
+
// 单一收口:撤销卡片跟着移除动作一起发生,不再依赖调用方记得配对调
|
|
475
|
+
// updateQueueCardToCancelled(漏配对会让卡片永久停在「等待中」,而映射
|
|
476
|
+
// 已被消费,之后再也修不回来)。fire-and-forget 保持
|
|
477
|
+
// 「移除同步发生」的队列不变式——queue.immediate 依赖先清空再让出事件循环。
|
|
478
|
+
if (removed) {
|
|
479
|
+
void this.updateQueueCardToCancelled(cwd, messageId);
|
|
480
|
+
}
|
|
481
|
+
return removed;
|
|
458
482
|
}
|
|
459
483
|
/**
|
|
460
484
|
* One-shot: replace the execution closure of an existing queued task in
|
|
@@ -597,99 +621,146 @@ export class Bridge {
|
|
|
597
621
|
* "executing immediately" when a turn may still be running in the host.
|
|
598
622
|
*/
|
|
599
623
|
async interruptCurrentRunDetailed(input) {
|
|
600
|
-
//
|
|
624
|
+
// 先收集全部命中项再逐个停。activeRuns 以 cwd 为键、多个 workspace 可以
|
|
625
|
+
// 并行跑;旧实现停在「第一条命中」就 return —— 两个 workspace 并行时
|
|
626
|
+
// `/stop` 只停一个却回「已停止」,另一个无声继续。
|
|
627
|
+
// 限定 cwd(queue.immediate)或带 runId(停止卡按钮)时仍只有一条命中。
|
|
628
|
+
const activeTargets = [];
|
|
601
629
|
for (const [cwd, active] of this.activeRuns) {
|
|
602
630
|
if (input.cwd !== undefined && cwd !== input.cwd)
|
|
603
631
|
continue;
|
|
604
|
-
if (active.userId
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
// 卡取消(approval_cancelled)在终态数据上可区分(方案 A)。
|
|
624
|
-
active.session.finish('interrupted', { interruptedReason: 'user_stop' }),
|
|
625
|
-
runner.stop({ immediate: true }),
|
|
626
|
-
]);
|
|
627
|
-
const stop = stopResult.status === 'fulfilled' &&
|
|
628
|
-
stopResult.value !== null &&
|
|
629
|
-
typeof stopResult.value === 'object'
|
|
630
|
-
? stopResult.value
|
|
631
|
-
: undefined;
|
|
632
|
-
// The chain may have advanced while the stop was in flight: a new run
|
|
633
|
-
// (from a task that began during the stop window) may now occupy this
|
|
634
|
-
// cwd. Only delete the entry we actually stopped.
|
|
635
|
-
if (this.activeRuns.get(cwd) === active) {
|
|
636
|
-
this.activeRuns.delete(cwd);
|
|
637
|
-
}
|
|
638
|
-
// Fix 4: Clean up this specific (cwd, agentKind) runner slot
|
|
639
|
-
// (eviction must also unregister the runner from the exit dispatcher)
|
|
640
|
-
// evict by the run's captured agentKind, not the live
|
|
641
|
-
// defaultAgent (may have changed mid-run via /config).
|
|
642
|
-
//
|
|
643
|
-
// 2026-10-11:workspace-lifetime runner 不能在这里立即 evict+dispose。
|
|
644
|
-
// interruptCurrentRun 的 await 窗口内 queue 链会推进下一条消息,其
|
|
645
|
-
// getRunner 可能刚拿走本实例(缓存命中先于本行执行)——立即 evict 会
|
|
646
|
-
// 让那个 run 的长驻连接脱管:进程残留至 idle TTL 才回收,期间持有
|
|
647
|
-
// agent 侧会话写锁(codex "thread already has an active writer",
|
|
648
|
-
// 线上实测)。改为标 stale,由 finalizeRun 在下一次 run 结束时统一
|
|
649
|
-
// evict+dispose;「stop 后下一 run 换新实例」的语义
|
|
650
|
-
// 保留(stale 命中即最后一轮复用)。run-per-message runner 无长驻
|
|
651
|
-
// 连接,保持立即 evict 不变。
|
|
652
|
-
if (active.runner.lifetime === 'workspace') {
|
|
653
|
-
this.staleRunnerSlots.add(`${cwd}\u0000${active.agentKind}`);
|
|
654
|
-
}
|
|
655
|
-
else {
|
|
656
|
-
this.evictRunnerSlot(cwd, active.agentKind);
|
|
657
|
-
}
|
|
658
|
-
// Reset queue executing count: the task was interrupted externally, so
|
|
659
|
-
// its promise-chain settle (.then/.catch) may lag behind the process
|
|
660
|
-
// kill. Without this reset, subsequent messages would incorrectly show
|
|
661
|
-
// as "排队中". resetExecutingCount also grants a skip-credit so the
|
|
662
|
-
// stale settle does not double-decrement the count. Centralized here so
|
|
663
|
-
// ALL interrupt paths (/stop, /t, card stop, queue.immediate) benefit.
|
|
664
|
-
// When stoppedSlot is undefined (no active run matched), skip the reset —
|
|
665
|
-
// there is no queue slot to reconcile. All production interrupt paths
|
|
666
|
-
// (/stop, /t, card stop, queue.immediate) provide a defined stoppedSlot.
|
|
667
|
-
if (stoppedSlot !== undefined) {
|
|
668
|
-
this.queueManager.resetExecutingCount(cwd, stoppedSlot);
|
|
669
|
-
}
|
|
670
|
-
getLogger().info(`[lark-remote] interrupt stop done runId=${active.runId}`);
|
|
671
|
-
return { stopped: true, agentKind: active.agentKind, ...(stop ? { stop } : {}) };
|
|
632
|
+
if (active.userId !== input.userId || active.chatId !== input.chatId)
|
|
633
|
+
continue;
|
|
634
|
+
if (input.runId && active.runId !== input.runId)
|
|
635
|
+
continue;
|
|
636
|
+
activeTargets.push([cwd, active]);
|
|
637
|
+
}
|
|
638
|
+
let stoppedCount = 0;
|
|
639
|
+
let agentKind;
|
|
640
|
+
let stop;
|
|
641
|
+
for (const [cwd, active] of activeTargets) {
|
|
642
|
+
const stopResult = await this.interruptActiveRun(cwd, active);
|
|
643
|
+
stoppedCount++;
|
|
644
|
+
agentKind ??= active.agentKind;
|
|
645
|
+
// 聚合口径:只要有一次 remote stop 没能确认,就按「可能仍在跑」上报,
|
|
646
|
+
// 不让后面的已确认结果盖掉它(调用方据此决定是否承诺"已停止")。
|
|
647
|
+
if (stopResult &&
|
|
648
|
+
(stop === undefined ||
|
|
649
|
+
(stop.status !== 'unconfirmed' && stopResult.status === 'unconfirmed'))) {
|
|
650
|
+
stop = stopResult;
|
|
672
651
|
}
|
|
673
652
|
}
|
|
674
653
|
// Then check active bash runs (! commands). These bypass the serial queue
|
|
675
654
|
// and are tracked separately; stop the per-run BashRunner directly.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
655
|
+
// `/stop`(无 runId、无 cwd)语义是「停掉这个会话的一切」,所以并行跑的
|
|
656
|
+
// `!` 命令也要停——旧实现命中 claude run 就 return,这些 bash 会被漏掉。
|
|
657
|
+
// 带范围的中断(停止卡 runId、queue.immediate 的 cwd)保持旧语义:只有
|
|
658
|
+
// 没有 claude run 命中时才回退到 bash,避免误杀用户并行跑的 `!` 任务。
|
|
659
|
+
const includeBashRuns = (input.runId === undefined && input.cwd === undefined) || activeTargets.length === 0;
|
|
660
|
+
const bashTargets = includeBashRuns
|
|
661
|
+
? [...this.activeBashRuns.entries()].filter(([runId, b]) => {
|
|
662
|
+
if (input.cwd !== undefined && b.cwd !== input.cwd)
|
|
663
|
+
return false;
|
|
664
|
+
if (b.userId !== input.userId || b.chatId !== input.chatId)
|
|
665
|
+
return false;
|
|
680
666
|
if (input.runId && runId !== input.runId)
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
}
|
|
667
|
+
return false;
|
|
668
|
+
return true;
|
|
669
|
+
})
|
|
670
|
+
: [];
|
|
671
|
+
for (const [runId, b] of bashTargets) {
|
|
672
|
+
getLogger().info(`[lark-remote] interrupt hit bash runId=${runId} cwd=${b.cwd} userId=${input.userId}`);
|
|
673
|
+
await b.bashRunner.stop({ immediate: true });
|
|
674
|
+
// Update card to show interrupted state
|
|
675
|
+
await b.cardSession.finish('interrupted', { exitCode: -1 });
|
|
676
|
+
this.activeBashRuns.delete(runId);
|
|
677
|
+
getLogger().info(`[lark-remote] interrupt bash stop done runId=${runId}`);
|
|
678
|
+
stoppedCount++;
|
|
679
|
+
}
|
|
680
|
+
if (stoppedCount === 0) {
|
|
681
|
+
getLogger().debug(`[lark-remote] interrupt miss userId=${input.userId}`);
|
|
682
|
+
return { stopped: false };
|
|
690
683
|
}
|
|
691
|
-
|
|
692
|
-
|
|
684
|
+
return {
|
|
685
|
+
stopped: true,
|
|
686
|
+
stoppedCount,
|
|
687
|
+
...(agentKind ? { agentKind } : {}),
|
|
688
|
+
...(stop ? { stop } : {}),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* 停掉一条已注册的 active run:置终态、停 runner、摘槽位、标 stale、
|
|
693
|
+
* 归零该 cwd 的队列执行计数。返回 runner 的停止结果(DSH 才有)。
|
|
694
|
+
*
|
|
695
|
+
* 只做「这一条 run」的事——跨多个 workspace 的遍历在调用方,避免再次
|
|
696
|
+
* 出现「停第一条就 return」的漏停。
|
|
697
|
+
*/
|
|
698
|
+
async interruptActiveRun(cwd, active) {
|
|
699
|
+
getLogger().info(`[lark-remote] interrupt hit runId=${active.runId} cwd=${cwd} userId=${active.userId}`);
|
|
700
|
+
// Stop the EXACT runner instance that is running (carried by activeRun),
|
|
701
|
+
// not whatever getRunner(cwd) would return now. clearRunners() may have
|
|
702
|
+
// emptied the cache mid-run, in which case getRunner creates a fresh empty
|
|
703
|
+
// runner whose stop() is a no-op and the real subprocess is never killed
|
|
704
|
+
// (regression 2026-07-18: queue chain blocked forever).
|
|
705
|
+
const runner = active.runner;
|
|
706
|
+
// Bind the queue reset to THIS task's execution slot: the stop window
|
|
707
|
+
// below may outlive the task's settle, letting the serial chain advance
|
|
708
|
+
// to a successor that begins executing before we resume. An
|
|
709
|
+
// unconditional reset would then zero the successor's count and mark
|
|
710
|
+
// its slot interrupted, hiding it from queue cards.
|
|
711
|
+
const stoppedSlot = this.queueManager.getExecutingSlot(cwd);
|
|
712
|
+
const [, stopResult] = await Promise.allSettled([
|
|
713
|
+
// 用户主动中断(/stop、stop 卡按钮、queue.immediate)显式打标
|
|
714
|
+
// interruptedReason,与审批超时自动取消(approval_timeout)、审批
|
|
715
|
+
// 卡取消(approval_cancelled)在终态数据上可区分(方案 A)。
|
|
716
|
+
active.session.finish('interrupted', { interruptedReason: 'user_stop' }),
|
|
717
|
+
runner.stop({ immediate: true }),
|
|
718
|
+
]);
|
|
719
|
+
const stop = stopResult.status === 'fulfilled' &&
|
|
720
|
+
stopResult.value !== null &&
|
|
721
|
+
typeof stopResult.value === 'object'
|
|
722
|
+
? stopResult.value
|
|
723
|
+
: undefined;
|
|
724
|
+
// The chain may have advanced while the stop was in flight: a new run
|
|
725
|
+
// (from a task that began during the stop window) may now occupy this
|
|
726
|
+
// cwd. Only delete the entry we actually stopped.
|
|
727
|
+
if (this.activeRuns.get(cwd) === active) {
|
|
728
|
+
this.activeRuns.delete(cwd);
|
|
729
|
+
}
|
|
730
|
+
// Fix 4: Clean up this specific (cwd, agentKind) runner slot
|
|
731
|
+
// (eviction must also unregister the runner from the exit dispatcher)
|
|
732
|
+
// evict by the run's captured agentKind, not the live
|
|
733
|
+
// defaultAgent (may have changed mid-run via /config).
|
|
734
|
+
//
|
|
735
|
+
// workspace-lifetime runner 不能在中断路径里立即 evict+dispose。
|
|
736
|
+
// interruptCurrentRun 的 await 窗口内 queue 链会推进下一条消息,其
|
|
737
|
+
// getRunner 可能刚拿走本实例(缓存命中先于本行执行)——立即 evict 会
|
|
738
|
+
// 让那个 run 的长驻连接脱管:进程残留至 idle TTL 才回收,期间持有
|
|
739
|
+
// agent 侧会话写锁(codex "thread already has an active writer",
|
|
740
|
+
// 线上实测)。改为标 stale:下一次「即将开跑」的 getRunner 在槽位空闲时
|
|
741
|
+
// 换新实例(见 getRunner 的 replaceStale),本次 run 的 finalize 收尾时
|
|
742
|
+
// 也会消费该标记;两者都保证不会有活跃 run 使用被 dispose 的实例。
|
|
743
|
+
// run-per-message runner 无长驻连接,保持立即 evict 不变。
|
|
744
|
+
if (active.runner.lifetime === 'workspace') {
|
|
745
|
+
this.staleRunnerSlots.add(`${cwd}\u0000${active.agentKind}`);
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
this.evictRunnerSlot(cwd, active.agentKind);
|
|
749
|
+
}
|
|
750
|
+
// Reset queue executing count: the task was interrupted externally, so
|
|
751
|
+
// its promise-chain settle (.then/.catch) may lag behind the process
|
|
752
|
+
// kill. Without this reset, subsequent messages would incorrectly show
|
|
753
|
+
// as "排队中". resetExecutingCount also grants a skip-credit so the
|
|
754
|
+
// stale settle does not double-decrement the count. Centralized here so
|
|
755
|
+
// ALL interrupt paths (/stop, /t, card stop, queue.immediate) benefit.
|
|
756
|
+
// When stoppedSlot is undefined (no active run matched), skip the reset —
|
|
757
|
+
// there is no queue slot to reconcile. All production interrupt paths
|
|
758
|
+
// (/stop, /t, card stop, queue.immediate) provide a defined stoppedSlot.
|
|
759
|
+
if (stoppedSlot !== undefined) {
|
|
760
|
+
this.queueManager.resetExecutingCount(cwd, stoppedSlot);
|
|
761
|
+
}
|
|
762
|
+
getLogger().info(`[lark-remote] interrupt stop done runId=${active.runId}`);
|
|
763
|
+
return stop;
|
|
693
764
|
}
|
|
694
765
|
/** Send a result message to the user (§7). Returns true on success, false on failure. */
|
|
695
766
|
async sendResult(result, ctx) {
|
|
@@ -907,7 +978,7 @@ export class Bridge {
|
|
|
907
978
|
// that will run is captured into activeRun. interruptCurrentRun stops THAT
|
|
908
979
|
// instance directly -- never a fresh runner re-resolved from a cache that
|
|
909
980
|
// clearRunners() may have emptied mid-run (regression 2026-07-18).
|
|
910
|
-
const runner = this.getRunner(cwd, agentKind);
|
|
981
|
+
const runner = this.getRunner(cwd, agentKind, { replaceStale: true });
|
|
911
982
|
// Step 1: Create run session (runId, cardSession, activeRun tracking)
|
|
912
983
|
const { runId, cardSession, activeRun } = this.createRunSession(ctx, cwd, runner, agentKind);
|
|
913
984
|
// Step 2: Consume the agent stream (runner.run loop, event processing, state updates)
|
|
@@ -1001,6 +1072,10 @@ export class Bridge {
|
|
|
1001
1072
|
// Compact 按钮能力门控与 resume 卡 / auto-resume 卡一致(hasRunCompact
|
|
1002
1073
|
// 鸭子探测):claude(stream-json,无 turn_started)也能拿到按钮。
|
|
1003
1074
|
compactSupported: this.hasRunCompact(cwd, agentKind),
|
|
1075
|
+
// 本轮 init 到达前,按钮是否仍有可压缩目标:开跑时 store 里已有会话
|
|
1076
|
+
// 才置 true。false 时按钮只在 state.sessionId 出现后渲染——否则
|
|
1077
|
+
// init 前被 /stop 的卡片会留下一个注定失败的按钮。
|
|
1078
|
+
sessionFallbackAvailable: !!this.sessionStore.getSessionId(ctx.userId, agentKind),
|
|
1004
1079
|
},
|
|
1005
1080
|
});
|
|
1006
1081
|
const activeRun = {
|
|
@@ -1171,10 +1246,7 @@ export class Bridge {
|
|
|
1171
1246
|
// 只有终态才阻止事件处理。finalizing 是非终态,应该继续处理
|
|
1172
1247
|
// 这样在 result 事件后,后台任务输出仍能被正确处理
|
|
1173
1248
|
const terminal = cardSession.currentState.terminal;
|
|
1174
|
-
if (terminal
|
|
1175
|
-
terminal === 'error' ||
|
|
1176
|
-
terminal === 'interrupted' ||
|
|
1177
|
-
terminal === 'idle_timeout') {
|
|
1249
|
+
if (isRunTerminal(terminal)) {
|
|
1178
1250
|
continue;
|
|
1179
1251
|
}
|
|
1180
1252
|
if (event.type === 'system' && event.subtype === 'init') {
|
|
@@ -1445,7 +1517,16 @@ export class Bridge {
|
|
|
1445
1517
|
// 终态,不再归因于 Agent 错误;error 保持「运行出错」;其余为 done。
|
|
1446
1518
|
const subtype = finalState.resultSubtype;
|
|
1447
1519
|
const isError = subtype === 'error' || !!finalState.errorMsg;
|
|
1448
|
-
|
|
1520
|
+
// goal 仍 active = 宿主会自动起下一轮:这不是"完成",而是"本轮结束"。
|
|
1521
|
+
// 用独立终态而不是 done,既不改 header 文案也不用谎报。
|
|
1522
|
+
const goalContinues = !isError && subtype !== 'interrupted' && finalState.goal?.phase === 'active';
|
|
1523
|
+
const terminal = isError
|
|
1524
|
+
? 'error'
|
|
1525
|
+
: subtype === 'interrupted'
|
|
1526
|
+
? 'interrupted'
|
|
1527
|
+
: goalContinues
|
|
1528
|
+
? 'goal_continuing'
|
|
1529
|
+
: 'done';
|
|
1449
1530
|
await cardSession.finish(terminal, {
|
|
1450
1531
|
resultSubtype: subtype,
|
|
1451
1532
|
errorMsg: finalState.errorMsg,
|
|
@@ -1548,6 +1629,10 @@ export class Bridge {
|
|
|
1548
1629
|
// runner 能力门控 Compact 按钮(缺省 undefined = 显示按钮,
|
|
1549
1630
|
// DSH 等无 runCompact 的 agent 会渲染出点了报错的死按钮)。
|
|
1550
1631
|
compactSupported: this.hasRunCompact(cwd, activeRun.agentKind),
|
|
1632
|
+
// 终态卡按「此刻是否真的有可压缩会话」渲染按钮,与记账用的是同一
|
|
1633
|
+
// 判据(本轮 sessionId 或 store 回退),避免按钮可见但记录不存在。
|
|
1634
|
+
sessionFallbackAvailable: !!(cardSession.currentState.sessionId ??
|
|
1635
|
+
this.sessionStore.getSessionId(ctx.userId, activeRun.agentKind)),
|
|
1551
1636
|
};
|
|
1552
1637
|
this.rememberRunCardSnapshot(runId, cardSession.currentState, finalRenderOptions, cwd, activeRun.agentKind);
|
|
1553
1638
|
let runCardSent = false;
|
|
@@ -1574,6 +1659,15 @@ export class Bridge {
|
|
|
1574
1659
|
// at the sendResult call site; if it throws, the run must still release
|
|
1575
1660
|
// its activeRuns slot — otherwise the cwd is permanently busy
|
|
1576
1661
|
// (every later message busy-dropped until a manual /stop).
|
|
1662
|
+
//
|
|
1663
|
+
// 记账与槽位所有权必须分开:/stop、停止卡按钮、
|
|
1664
|
+
// queue.immediate 都会先在 interruptCurrentRunDetailed 里删掉
|
|
1665
|
+
// activeRuns[cwd] 并把卡片置 interrupted,等收尾走到这里时下面的守卫
|
|
1666
|
+
// 已为 false。记录若跟着守卫走,中断后的终态卡上 Compact 按钮必然查不到
|
|
1667
|
+
// runId,只能回「该任务已结束或不属于当前会话」。守卫只回答「cwd 槽位还
|
|
1668
|
+
// 归不归本 run」(决定能不能删槽位/evict runner),记账回答「这次 run
|
|
1669
|
+
// 结束了吗、用的哪个 sessionId」——后者与所有权无关,放在守卫外。
|
|
1670
|
+
this.recordCompactableRunForTerminal(cardSession, activeRun, ctx);
|
|
1577
1671
|
if (this.activeRuns.get(cwd) === activeRun) {
|
|
1578
1672
|
this.activeRuns.delete(cwd);
|
|
1579
1673
|
// Workspace-lifetime runners (codex app-server) keep their persistent
|
|
@@ -1589,25 +1683,6 @@ export class Bridge {
|
|
|
1589
1683
|
this.evictRunnerSlot(cwd, activeRun.agentKind);
|
|
1590
1684
|
}
|
|
1591
1685
|
this.staleRunnerSlots.delete(slotKey);
|
|
1592
|
-
// Compact 卡片需要 runId + sessionId;record 供终态卡上的 Compact 按钮
|
|
1593
|
-
// 校验与取参(activeRuns 已清空,无法再查到)。异常退出(error/
|
|
1594
|
-
// interrupted/idle_timeout)同样记录——上下文往往更大,压缩后再续。
|
|
1595
|
-
// 写入条件从「codex 终态」放宽为「runner 有 runCompact 的终态」
|
|
1596
|
-
// (codex app-server + kimi acp 均满足)。
|
|
1597
|
-
const runTerminal = cardSession.currentState.terminal;
|
|
1598
|
-
const runnerHasCompact = runnerSupportsCompact(activeRun.runner, activeRun.agentKind);
|
|
1599
|
-
if (runnerHasCompact &&
|
|
1600
|
-
activeRun.runner.lifetime === 'workspace' &&
|
|
1601
|
-
(runTerminal === 'done' ||
|
|
1602
|
-
runTerminal === 'error' ||
|
|
1603
|
-
runTerminal === 'interrupted' ||
|
|
1604
|
-
runTerminal === 'idle_timeout')) {
|
|
1605
|
-
const compactSessionId = cardSession.currentState.sessionId ??
|
|
1606
|
-
this.sessionStore.getSessionId(ctx.userId, activeRun.agentKind);
|
|
1607
|
-
if (compactSessionId) {
|
|
1608
|
-
this.recordCompactableRun(cwd, activeRun.runId, compactSessionId, activeRun.agentKind);
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
1686
|
getLogger().info(`[lark-remote] activeRuns.delete cwd=${cwd} runId=${runId}`);
|
|
1612
1687
|
}
|
|
1613
1688
|
}
|
|
@@ -1768,6 +1843,44 @@ export class Bridge {
|
|
|
1768
1843
|
byRunId.delete(oldest);
|
|
1769
1844
|
}
|
|
1770
1845
|
}
|
|
1846
|
+
/**
|
|
1847
|
+
* 终态 Compact 记账:把本次 run 实际使用的 sessionId 记进
|
|
1848
|
+
* {@link compactableRuns},供终态卡上的 Compact 按钮按 runId 反查。
|
|
1849
|
+
*
|
|
1850
|
+
* 调用点固定在 `finalizeRun` 的 `finally` 里、**槽位所有权守卫之外**。
|
|
1851
|
+
* 所有权守卫只回答「cwd 槽位还归不归本 run」(决定能否删槽位 / evict
|
|
1852
|
+
* runner);记账回答「本次 run 结束了吗、用的哪个 sessionId」。两者在不同
|
|
1853
|
+
* 维度上:外部中断(`/stop`、停止卡按钮、`queue.immediate`)会先摘槽位,
|
|
1854
|
+
* 记账若跟着守卫走就永远不会落,中断卡片的按钮必然反查不到(中断
|
|
1855
|
+
* 后点 Compact 一律回「该任务已结束或不属于当前会话」)。
|
|
1856
|
+
*
|
|
1857
|
+
* 覆盖所有终态入口(正常结束、error、interrupted、idle_timeout、
|
|
1858
|
+
* goal_continuing)——这些卡片都渲染 Compact 按钮(`goal` 仍 active 时
|
|
1859
|
+
* 除外)。记账失败不能影响清理,故内部兜错。
|
|
1860
|
+
*/
|
|
1861
|
+
recordCompactableRunForTerminal(cardSession, activeRun, ctx) {
|
|
1862
|
+
try {
|
|
1863
|
+
if (!runnerSupportsCompact(activeRun.runner, activeRun.agentKind))
|
|
1864
|
+
return;
|
|
1865
|
+
if (activeRun.runner.lifetime !== 'workspace')
|
|
1866
|
+
return;
|
|
1867
|
+
const terminal = cardSession.currentState.terminal;
|
|
1868
|
+
// 与事件消费守卫、Compact 按钮共用同一份终态集合(run-state.ts)。
|
|
1869
|
+
if (!isRunTerminal(terminal))
|
|
1870
|
+
return;
|
|
1871
|
+
// 卡片状态优先(本轮 init/turn_started 写入);缺失时回退 store——
|
|
1872
|
+
// 启动失败但已 /stop 的 run 没有自己的 init,压缩此前会话仍是有意义
|
|
1873
|
+
// 的动作。
|
|
1874
|
+
const compactSessionId = cardSession.currentState.sessionId ??
|
|
1875
|
+
this.sessionStore.getSessionId(ctx.userId, activeRun.agentKind);
|
|
1876
|
+
if (!compactSessionId)
|
|
1877
|
+
return;
|
|
1878
|
+
this.recordCompactableRun(activeRun.cwd, activeRun.runId, compactSessionId, activeRun.agentKind);
|
|
1879
|
+
}
|
|
1880
|
+
catch (err) {
|
|
1881
|
+
getLogger().warn(`[lark-remote] compact record failed runId=${activeRun.runId}: ${errorMessage(err)}`);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1771
1884
|
/**
|
|
1772
1885
|
* 按 runId 反查可压缩 run 记录(跨 cwd 扫描)。
|
|
1773
1886
|
*
|
|
@@ -1814,7 +1927,7 @@ export class Bridge {
|
|
|
1814
1927
|
return;
|
|
1815
1928
|
}
|
|
1816
1929
|
// Check that the runner has runCompact method(声明表 && runner 实现)
|
|
1817
|
-
const runner = this.getRunner(cwd, target.agentKind);
|
|
1930
|
+
const runner = this.getRunner(cwd, target.agentKind, { replaceStale: true });
|
|
1818
1931
|
if (!runnerSupportsCompact(runner, target.agentKind)) {
|
|
1819
1932
|
await this.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
|
|
1820
1933
|
return;
|
|
@@ -1935,10 +2048,7 @@ export class Bridge {
|
|
|
1935
2048
|
// 已终态(/stop、空闲看门狗)后不再消费事件:与 runAgentStreamToEnd 同口径,
|
|
1936
2049
|
// 否则迟到的 result 会把终态卡改写成「已完成」。
|
|
1937
2050
|
const cardTerminal = cardSession.currentState.terminal;
|
|
1938
|
-
if (cardTerminal
|
|
1939
|
-
cardTerminal === 'error' ||
|
|
1940
|
-
cardTerminal === 'interrupted' ||
|
|
1941
|
-
cardTerminal === 'idle_timeout') {
|
|
2051
|
+
if (isRunTerminal(cardTerminal)) {
|
|
1942
2052
|
continue;
|
|
1943
2053
|
}
|
|
1944
2054
|
if (event.type === 'result') {
|
|
@@ -1989,7 +2099,7 @@ export class Bridge {
|
|
|
1989
2099
|
// 终态守卫(与 runAgentStreamToEnd 同口径):running/finalizing 才转 error,
|
|
1990
2100
|
// 已终态(interrupted/idle_timeout)保留首终态,不被迟到的异常改写。
|
|
1991
2101
|
const catchTerminal = cardSession.currentState.terminal;
|
|
1992
|
-
if (catchTerminal
|
|
2102
|
+
if (!isRunTerminal(catchTerminal)) {
|
|
1993
2103
|
await cardSession.finish('error', { errorMsg: errorMessage(err) });
|
|
1994
2104
|
}
|
|
1995
2105
|
else {
|
|
@@ -261,6 +261,12 @@ export declare class QueueManager {
|
|
|
261
261
|
removeFromQueue(cwd: string, messageId: string): boolean;
|
|
262
262
|
/** Update queue card to "cancelled" status when user clicks 撤销. */
|
|
263
263
|
updateQueueCardToCancelled(cwd: string, messageId: string): Promise<void>;
|
|
264
|
+
/**
|
|
265
|
+
* 卡片 PATCH 带一次重试:一次网络抖动不该让排队卡永久停在旧状态——映射
|
|
266
|
+
* 会在本次调用结束时被清理,之后没有任何自动修正机会。
|
|
267
|
+
* 第二次仍失败则向上抛,由调用方 catch 记日志(行为与改前一致)。
|
|
268
|
+
*/
|
|
269
|
+
private updateCardWithRetry;
|
|
264
270
|
/** Get task metadata from queue. */
|
|
265
271
|
getQueuedTask(cwd: string, messageId: string): QueuedTask | undefined;
|
|
266
272
|
/**
|