lark-remote 0.5.18 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/bridge/approval-actions.d.ts +1 -1
- package/dist/bridge/approval-actions.js +1 -1
- package/dist/bridge/bash-run.d.ts +71 -0
- package/dist/bridge/bash-run.js +217 -0
- package/dist/bridge/compact.d.ts +155 -0
- package/dist/bridge/compact.js +450 -0
- package/dist/bridge/final-usage.js +4 -4
- package/dist/bridge/inbound-media.js +1 -1
- package/dist/bridge/index.d.ts +120 -119
- package/dist/bridge/index.js +257 -695
- package/dist/bridge/job-watch.d.ts +68 -0
- package/dist/bridge/job-watch.js +146 -0
- package/dist/bridge/queue-manager.d.ts +3 -3
- package/dist/bridge/queue-manager.js +10 -29
- package/dist/card/approval-render.d.ts +2 -2
- package/dist/card/approval-render.js +109 -97
- package/dist/card/bash-card-session.js +2 -0
- package/dist/card/card-budget.js +1 -1
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/card-session.d.ts +17 -0
- package/dist/card/card-session.js +35 -0
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-card-session.js +2 -0
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +77 -102
- package/dist/card/run-state.d.ts +35 -8
- package/dist/card/run-state.js +117 -90
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +1 -1
- package/dist/card/usage-format.d.ts +12 -0
- package/dist/card/usage-format.js +8 -3
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- package/dist/config/codex-config.js +130 -106
- package/dist/config/index.d.ts +16 -2
- package/dist/config/index.js +23 -8
- package/dist/connector/index.js +6 -6
- package/dist/error-classification.js +3 -2
- package/dist/inbound/placeholder.d.ts +1 -1
- package/dist/inbound/placeholder.js +1 -1
- package/dist/index.js +203 -133
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +4 -2
- package/dist/model-catalog.d.ts +2 -2
- package/dist/model-catalog.js +3 -8
- package/dist/platform/identity.js +1 -1
- package/dist/platform/path.d.ts +1 -1
- package/dist/platform/path.js +1 -1
- package/dist/platform/sleep-blocker.js +1 -1
- package/dist/platform/terminator-win32.js +2 -1
- package/dist/router/commands/resume.js +2 -20
- package/dist/router/config/dsh.d.ts +4 -8
- package/dist/router/config/dsh.js +3 -10
- package/dist/router/config/kimi.js +1 -1
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/config-diff.js +1 -1
- package/dist/router/index.d.ts +56 -11
- package/dist/router/index.js +222 -202
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +6 -4
- package/dist/runner/codex/app-server/runner.d.ts +9 -2
- package/dist/runner/codex/app-server/runner.js +30 -25
- package/dist/runner/codex/app-server/translator.d.ts +0 -5
- package/dist/runner/codex/app-server/translator.js +4 -17
- package/dist/runner/common/acp/acp-approval.d.ts +1 -1
- package/dist/runner/common/acp/acp-approval.js +1 -1
- package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
- package/dist/runner/common/acp/base-acp-runner.js +26 -65
- package/dist/runner/common/acp/base-translator.d.ts +12 -2
- package/dist/runner/common/acp/base-translator.js +31 -9
- package/dist/runner/common/acp/protocol-types.d.ts +1 -1
- package/dist/runner/common/acp/protocol-types.js +2 -2
- package/dist/runner/common/connection-based-runner.d.ts +32 -1
- package/dist/runner/common/connection-based-runner.js +32 -2
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
- package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
- package/dist/runner/common/spawning-runner.d.ts +1 -1
- package/dist/runner/common/spawning-runner.js +1 -1
- package/dist/runner/dsh/catalog-probe.d.ts +17 -0
- package/dist/runner/dsh/catalog-probe.js +10 -0
- package/dist/runner/dsh/client.d.ts +13 -8
- package/dist/runner/dsh/client.js +23 -15
- package/dist/runner/dsh/exit-cancel.d.ts +2 -2
- package/dist/runner/dsh/exit-cancel.js +2 -2
- package/dist/runner/dsh/inflight.d.ts +1 -1
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/runner.d.ts +48 -2
- package/dist/runner/dsh/runner.js +288 -199
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/dsh/translator.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +1 -2
- package/dist/runner/kimi/acp/runner.js +1 -2
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
- package/dist/runner/pi/rpc/protocol-types.js +1 -1
- package/dist/runner/pi/rpc/runner.d.ts +3 -0
- package/dist/runner/pi/rpc/runner.js +27 -42
- package/dist/runner/types.d.ts +25 -8
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +97 -71
- package/dist/session/codex/rollout-reader.js +4 -4
- package/dist/session/common/throw-session-list-error.d.ts +10 -0
- package/dist/session/common/throw-session-list-error.js +16 -0
- package/dist/session/dsh/sessions.js +11 -5
- package/dist/session/kimi/sessions.d.ts +40 -1
- package/dist/session/kimi/sessions.js +244 -192
- package/dist/session/opencode/sessions.d.ts +153 -1
- package/dist/session/opencode/sessions.js +354 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +1 -1
- package/dist/session/session-store.js +1 -1
- package/dist/update/version-check.js +2 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { sleep } from '../common/sleep.js';
|
|
3
|
+
import { resolveFinalUsage, errorMessage } from './final-usage.js';
|
|
4
|
+
import { getLogger } from '../logger/index.js';
|
|
5
|
+
import { RunCardSession } from '../card/run-card-session.js';
|
|
6
|
+
import { renderRunCardDetailed } from '../card/run-renderer.js';
|
|
7
|
+
import { isRunTerminal } from '../card/run-state.js';
|
|
8
|
+
import { AGENT_KINDS } from '../agent-kinds.js';
|
|
9
|
+
import { supportsCompact } from '../runner/types.js';
|
|
10
|
+
/**
|
|
11
|
+
* 单点化:`'runCompact' in runner` 鸭子探测
|
|
12
|
+
* codex app-server + kimi/opencode ACP runner 满足。
|
|
13
|
+
*/
|
|
14
|
+
function runnerHasRunCompact(runner) {
|
|
15
|
+
return ('runCompact' in runner && typeof runner.runCompact === 'function');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 已告警过的能力不一致 kind。能力表(`COMPACT_CAPABILITY`)是声明,runner 方法
|
|
19
|
+
* 是实现;两者不一致是开发期缺陷,只在首次冲突时打 error,避免每次渲染刷屏。
|
|
20
|
+
*/
|
|
21
|
+
const compactMismatchWarned = new Set();
|
|
22
|
+
/**
|
|
23
|
+
* Compact 能力的双重门控:声明的能力表 && runner 实际实现。
|
|
24
|
+
*
|
|
25
|
+
* 只做鸭子探测会把「漏实现」降级成静默隐藏按钮(dsh 就这样漏了),所以声明表
|
|
26
|
+
* 是必需项;只信声明表则可能渲染出点不动的死按钮,所以实现检查也保留。两者
|
|
27
|
+
* 不一致时打 error 日志,让缺陷在运行日志里可见。
|
|
28
|
+
*/
|
|
29
|
+
export function runnerSupportsCompact(runner, kind) {
|
|
30
|
+
const declared = supportsCompact(kind);
|
|
31
|
+
const implemented = runnerHasRunCompact(runner);
|
|
32
|
+
if (declared !== implemented && !compactMismatchWarned.has(kind)) {
|
|
33
|
+
compactMismatchWarned.add(kind);
|
|
34
|
+
getLogger().error(`[lark-remote] compact capability mismatch kind=${kind} declared=${declared} implemented=${implemented}`);
|
|
35
|
+
}
|
|
36
|
+
return declared && implemented;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Emoji reaction per run terminal state, so a glance at the user's original
|
|
40
|
+
* message distinguishes how the run ended. Unknown/non-terminal states fall
|
|
41
|
+
* back to 'Done' (default).
|
|
42
|
+
*/
|
|
43
|
+
const TERMINAL_REACTION_EMOJI = {
|
|
44
|
+
error: 'ERROR',
|
|
45
|
+
idle_timeout: 'Alarm',
|
|
46
|
+
interrupted: 'SHHH',
|
|
47
|
+
// 本轮结束但目标继续:任务还没完,不能打 ✓(Done)。
|
|
48
|
+
goal_continuing: 'OnIt',
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* 终态 reaction 的单源映射:普通 run 收尾(bridge/index.ts finalizeRun)与
|
|
52
|
+
* 压缩收尾(本文件 streamCompact)共用同一份,保证同一终态打同一表情。
|
|
53
|
+
*/
|
|
54
|
+
export function terminalReactionEmoji(terminal) {
|
|
55
|
+
return TERMINAL_REACTION_EMOJI[terminal] ?? 'Done';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 每个 cwd 保留的「可压缩 run」条数上限。Compact 按钮挂在终态 run 卡片上,
|
|
59
|
+
* 卡片会被后续消息不断顶上去;无上限会让长跑进程随 run 数无界增长,而用户
|
|
60
|
+
* 实际能点到的只有最近几张卡,20 条足以覆盖。
|
|
61
|
+
*/
|
|
62
|
+
const MAX_COMPACTABLE_RUNS_PER_CWD = 20;
|
|
63
|
+
export class CompactLifecycle {
|
|
64
|
+
deps;
|
|
65
|
+
/**
|
|
66
|
+
* 最近若干次已完成的、runner 有 runCompact 的 run(cwd → runId → 记录)。
|
|
67
|
+
* run 结束后 activeRuns 已清空,而终态卡上的 Compact 按钮只带 runId,
|
|
68
|
+
* 需要它反查 sessionId + agentKind。
|
|
69
|
+
*
|
|
70
|
+
* 为什么按 runId 存而不是「每 cwd 只留最新一条」:compact 卡片动作走串行
|
|
71
|
+
* 队列(会被正在跑的 run 挡住),点击到执行之间可能夹着别的 run 收尾。
|
|
72
|
+
* 单槽会被覆盖 —— 旧卡片的压缩必然报「该任务已结束或不属于当前会话」,
|
|
73
|
+
* 与 session 是否还在、能否压缩无关(2026-10-08 线上缺陷)。
|
|
74
|
+
*/
|
|
75
|
+
compactableRuns = new Map();
|
|
76
|
+
constructor(deps) {
|
|
77
|
+
this.deps = deps;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 记下一次「可压缩 run」:终态卡上的 Compact 按钮点击时据此反查
|
|
81
|
+
* sessionId + agentKind。runId 是按钮自带的唯一键,同 cwd 内唯一。
|
|
82
|
+
* 超出上限按插入序淘汰最旧的一条(Map 保持插入序;重复写同一 runId
|
|
83
|
+
* 先 delete 再 set 以刷新位置)。
|
|
84
|
+
*/
|
|
85
|
+
recordCompactableRun(cwd, runId, sessionId, agentKind) {
|
|
86
|
+
let byRunId = this.compactableRuns.get(cwd);
|
|
87
|
+
if (!byRunId) {
|
|
88
|
+
byRunId = new Map();
|
|
89
|
+
this.compactableRuns.set(cwd, byRunId);
|
|
90
|
+
}
|
|
91
|
+
byRunId.delete(runId);
|
|
92
|
+
byRunId.set(runId, { sessionId, agentKind, cwd });
|
|
93
|
+
while (byRunId.size > MAX_COMPACTABLE_RUNS_PER_CWD) {
|
|
94
|
+
const oldest = byRunId.keys().next().value;
|
|
95
|
+
byRunId.delete(oldest);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 终态 Compact 记账:把本次 run 实际使用的 sessionId 记进
|
|
100
|
+
* {@link compactableRuns},供终态卡上的 Compact 按钮按 runId 反查。
|
|
101
|
+
*
|
|
102
|
+
* 调用点固定在 `finalizeRun` 的 `finally` 里、**槽位所有权守卫之外**。
|
|
103
|
+
* 所有权守卫只回答「cwd 槽位还归不归本 run」(决定能否删槽位 / evict
|
|
104
|
+
* runner);记账回答「本次 run 结束了吗、用的哪个 sessionId」。两者在不同
|
|
105
|
+
* 维度上:外部中断(`/stop`、停止卡按钮、`queue.immediate`)会先摘槽位,
|
|
106
|
+
* 记账若跟着守卫走就永远不会落,中断卡片的按钮必然反查不到(中断
|
|
107
|
+
* 后点 Compact 一律回「该任务已结束或不属于当前会话」)。
|
|
108
|
+
*
|
|
109
|
+
* 覆盖所有终态入口(正常结束、error、interrupted、idle_timeout、
|
|
110
|
+
* goal_continuing)——这些卡片都渲染 Compact 按钮(`goal` 仍 active 时
|
|
111
|
+
* 除外)。函数体全部是内存操作(查表、属性读、Map 写),无 IO 无解析,
|
|
112
|
+
* 不需要兜错;真正的清理容错由调用方 finally 保证。
|
|
113
|
+
*/
|
|
114
|
+
recordCompactableRunForTerminal(cardSession, activeRun, ctx) {
|
|
115
|
+
if (!runnerSupportsCompact(activeRun.runner, activeRun.agentKind))
|
|
116
|
+
return;
|
|
117
|
+
if (activeRun.runner.lifetime !== 'workspace')
|
|
118
|
+
return;
|
|
119
|
+
const terminal = cardSession.currentState.terminal;
|
|
120
|
+
// 与事件消费守卫、Compact 按钮共用同一份终态集合(run-state.ts)。
|
|
121
|
+
if (!isRunTerminal(terminal))
|
|
122
|
+
return;
|
|
123
|
+
// 卡片状态优先(本轮 init/turn_started 写入);缺失时回退 store——
|
|
124
|
+
// 启动失败但已 /stop 的 run 没有自己的 init,压缩此前会话仍是有意义
|
|
125
|
+
// 的动作。
|
|
126
|
+
const compactSessionId = cardSession.currentState.sessionId ??
|
|
127
|
+
this.deps.sessionStore.getSessionId(ctx.userId, activeRun.agentKind);
|
|
128
|
+
if (!compactSessionId)
|
|
129
|
+
return;
|
|
130
|
+
this.recordCompactableRun(activeRun.cwd, activeRun.runId, compactSessionId, activeRun.agentKind);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 按 runId 反查可压缩 run 记录(跨 cwd 扫描)。
|
|
134
|
+
*
|
|
135
|
+
* 为什么不带 cwd 过滤:按钮属于「那一次 run」,与用户此刻在哪个工作目录、
|
|
136
|
+
* 排队期间有没有 /cd 无关。`/stop` 的 interruptCurrentRun 同样是全局按 runId
|
|
137
|
+
* 找(从不查当前 cwd),compact 与它保持一致。用当前 cwd 收窄会让「切了工作区
|
|
138
|
+
* 再点旧卡片」和「排队期间 /cd」两种情况误报「该任务已结束或不属于当前会话」。
|
|
139
|
+
*/
|
|
140
|
+
findCompactableRun(runId) {
|
|
141
|
+
for (const byRunId of this.compactableRuns.values()) {
|
|
142
|
+
const record = byRunId.get(runId);
|
|
143
|
+
if (record)
|
|
144
|
+
return record;
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Handle compact card action — trigger a compaction request for any
|
|
150
|
+
* runCompact-capable runner(codex/kimi/opencode/pi/claude)。按钮带的是
|
|
151
|
+
* 那次 run 的 runId,据此反查该 run 实际使用的 sessionId + agentKind
|
|
152
|
+
* + cwd(compactableRuns,按 runId 记录),再调 runner 的 runCompact()。
|
|
153
|
+
*/
|
|
154
|
+
async handleCompactAction(value, ctx) {
|
|
155
|
+
const log = getLogger();
|
|
156
|
+
log.info(`[lark-remote] handleCompactAction userId=${ctx.userId} runId=${value.runId?.slice(0, 8)}...`);
|
|
157
|
+
const { runId } = value;
|
|
158
|
+
if (!runId) {
|
|
159
|
+
await this.deps.sendResult({ text: '⚠️ 无效的 Compact 请求,缺少 runId' }, ctx);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// 校验:run 结束后 activeRuns 已清空,只能靠按钮自带的 runId 反查记录。
|
|
163
|
+
// 按 runId 存(不按「最新的那一条」),点击到执行之间夹了新 run 收尾也不失效;
|
|
164
|
+
// 查找也不按「当前 cwd」收窄(见 findCompactableRun)。
|
|
165
|
+
const target = this.findCompactableRun(runId);
|
|
166
|
+
if (!target) {
|
|
167
|
+
await this.deps.sendResult({ text: '⚠️ 该任务已结束或不属于当前会话' }, ctx);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// 压缩必须用那次 run 自己的 cwd(session 文件 + runner 实例都按 cwd 解析)。
|
|
171
|
+
const cwd = target.cwd;
|
|
172
|
+
// §5.3: kimi 压缩在途检测(wire 状态机)。第二次点击在本地毫秒级如实回答,
|
|
173
|
+
// 不开卡、不发 session/prompt——否则 kimi 服务端拒绝并产生「秒回」假完成。
|
|
174
|
+
if (await this.rejectIfCompactionInFlight(target.sessionId, cwd, target.agentKind, ctx)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Check that the runner has runCompact method(声明表 && runner 实现)
|
|
178
|
+
const runner = this.deps.getRunner(cwd, target.agentKind, { replaceStale: true });
|
|
179
|
+
if (!runnerSupportsCompact(runner, target.agentKind)) {
|
|
180
|
+
await this.deps.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
log.info(`[lark-remote] handleCompactAction executing runCompact for runId=${runId} cwd=${cwd}`);
|
|
184
|
+
await this.streamCompact({
|
|
185
|
+
sessionId: target.sessionId,
|
|
186
|
+
cwd,
|
|
187
|
+
agentKind: target.agentKind,
|
|
188
|
+
runner,
|
|
189
|
+
ctx,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* §5.3: kimi 压缩在途检测——reader 实现契约的 `readCompactionState` 时,若
|
|
194
|
+
* wire 状态机判定当前有未终结的压缩(最后一次 begin 无 terminal 记录),
|
|
195
|
+
* 直接回文本「已有一次 Compact 正在进行」并返回 true(调用方不开卡、不发
|
|
196
|
+
* session/prompt)。codex/claude 等 reader 无此方法 → 返回 false,行为不变。
|
|
197
|
+
*/
|
|
198
|
+
async rejectIfCompactionInFlight(sessionId, cwd, agentKind, ctx) {
|
|
199
|
+
const reader = this.deps.sessionReaderRegistry.get(agentKind);
|
|
200
|
+
if (!reader?.readCompactionState)
|
|
201
|
+
return false;
|
|
202
|
+
try {
|
|
203
|
+
const state = reader.readCompactionState(sessionId, cwd);
|
|
204
|
+
if (state?.inFlight) {
|
|
205
|
+
const since = state.inFlightSince;
|
|
206
|
+
let timeLabel = '未知时刻';
|
|
207
|
+
if (since !== undefined) {
|
|
208
|
+
const d = new Date(since);
|
|
209
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
210
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
211
|
+
timeLabel = `${hh}:${mm}`;
|
|
212
|
+
}
|
|
213
|
+
await this.deps.sendResult({ text: `⚠️ 已有一次 Compact 正在进行(开始于 ${timeLabel}),请等待完成` }, ctx);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
getLogger().warn(`[lark-remote] readCompactionState failed sessionId=${sessionId}: ${errorMessage(err)}`);
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Stream a compaction to a card: start a RunCardSession with
|
|
224
|
+
* operationKind='compaction' (no recursive Compact button), consume
|
|
225
|
+
* runner.runCompact() events, read authoritative jsonl usage, and finish
|
|
226
|
+
* the card. Shared by handleCompactAction (run card button) and
|
|
227
|
+
* handleResumeCompact (resume cards) so both flows stay in sync.
|
|
228
|
+
*
|
|
229
|
+
* Precondition: callers have already validated that `runner` implements
|
|
230
|
+
* runCompact and that the session exists in `cwd`.
|
|
231
|
+
*/
|
|
232
|
+
async streamCompact(opts) {
|
|
233
|
+
const log = getLogger();
|
|
234
|
+
const { sessionId, cwd, agentKind, runner, ctx } = opts;
|
|
235
|
+
log.info(`[lark-remote] streamCompact sessionId=${sessionId} cwd=${cwd}`);
|
|
236
|
+
// 能力收窄:调用方已校验(runId / resume 两条路径),此处仅保留 'in'
|
|
237
|
+
// 探测让 TS 收窄 runner 类型(其后的 cast 才合法)。
|
|
238
|
+
if (!('runCompact' in runner) ||
|
|
239
|
+
typeof runner.runCompact !== 'function') {
|
|
240
|
+
await this.deps.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// activeRuns 以 cwd 为键:注册前先确认槽位空闲,否则压缩会覆盖在跑的 run 的
|
|
244
|
+
// 记录,令 /stop 只停到最后注册的那个。
|
|
245
|
+
const busyWith = this.deps.activeRuns.get(cwd);
|
|
246
|
+
if (busyWith) {
|
|
247
|
+
log.info(`[lark-remote] compact refused: cwd busy cwd=${cwd} runId=${busyWith.runId}`);
|
|
248
|
+
await this.deps.sendResult({ text: '⚠️ 该工作区正在运行任务,请等待结束或先 /stop' }, ctx);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
await this.deps.sendResult({ text: '🗜 Compact 已触发,正在压缩会话…' }, ctx);
|
|
252
|
+
const compactRunId = randomUUID();
|
|
253
|
+
// 渲染选项同时用于流式卡与「卡片流没建起来」时的静态兜底卡——两者必须
|
|
254
|
+
// 同门控,否则兜底卡会渲染出与本次 run 不一致的按钮。
|
|
255
|
+
const renderOptions = { agentKind };
|
|
256
|
+
const cardSession = new RunCardSession({
|
|
257
|
+
connector: this.deps.connector,
|
|
258
|
+
chatId: ctx.chatId,
|
|
259
|
+
replyTo: ctx.messageId,
|
|
260
|
+
runId: compactRunId,
|
|
261
|
+
renderOptions,
|
|
262
|
+
});
|
|
263
|
+
// 压缩与普通 run 共用同一套生命周期——注册进 activeRuns(/stop 有对象可停、
|
|
264
|
+
// /active 看得见)、挂 §9.12 空闲看门狗、终态后补 reaction。缺任何一环,卡住的
|
|
265
|
+
// 压缩都只能等 runner 级超时,用户既停不掉也看不出它还在跑。
|
|
266
|
+
const activeRun = {
|
|
267
|
+
runId: compactRunId,
|
|
268
|
+
userId: ctx.userId,
|
|
269
|
+
chatId: ctx.chatId,
|
|
270
|
+
session: cardSession,
|
|
271
|
+
cwd,
|
|
272
|
+
runner,
|
|
273
|
+
agentKind,
|
|
274
|
+
};
|
|
275
|
+
this.deps.activeRuns.set(cwd, activeRun);
|
|
276
|
+
getLogger().info(`[lark-remote] activeRuns.set cwd=${cwd} runId=${compactRunId} compact=true`);
|
|
277
|
+
let watchdog = null;
|
|
278
|
+
try {
|
|
279
|
+
await cardSession.start();
|
|
280
|
+
watchdog = this.deps.startIdleWatchdog({
|
|
281
|
+
runId: compactRunId,
|
|
282
|
+
session: cardSession,
|
|
283
|
+
runner,
|
|
284
|
+
});
|
|
285
|
+
// operationKind=compaction:卡片不渲染 Compact 按钮(防递归 Compact)。
|
|
286
|
+
await cardSession.push({
|
|
287
|
+
type: 'turn_started',
|
|
288
|
+
threadId: sessionId,
|
|
289
|
+
turnId: '',
|
|
290
|
+
operationKind: 'compaction',
|
|
291
|
+
});
|
|
292
|
+
const outcome = await this.consumeCompactEvents(cardSession, watchdog, runner, sessionId, cwd);
|
|
293
|
+
await this.finishCompactCard(cardSession, sessionId, cwd, agentKind, outcome);
|
|
294
|
+
log.info(`[lark-remote] streamCompact finished sessionId=${sessionId} sawResult=${outcome.sawResult} subtype=${outcome.resultSubtype ?? 'none'}`);
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
log.error(`[lark-remote] streamCompact failed: ${errorMessage(err)}`);
|
|
298
|
+
// 终态守卫(与 runAgentStreamToEnd 同口径):running/finalizing 才转 error,
|
|
299
|
+
// 已终态(interrupted/idle_timeout)保留首终态,不被迟到的异常改写。
|
|
300
|
+
const catchTerminal = cardSession.currentState.terminal;
|
|
301
|
+
if (!isRunTerminal(catchTerminal)) {
|
|
302
|
+
await cardSession.finish('error', { errorMsg: errorMessage(err) });
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
log.info(`[lark-remote] skip compact error finish: state already terminal (${catchTerminal}) runId=${compactRunId}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
watchdog?.stop();
|
|
310
|
+
// 与 run 路径同口径的降级:卡片流没建起来(start 失败 / stream 未收尾)时,
|
|
311
|
+
// 终态只存在于内存里。settle() 释放 producer 并等 stream 收尾;'unsent'
|
|
312
|
+
// 表示用户手里没有卡,必须补一张静态终态卡,否则只剩「Compact 已触发」
|
|
313
|
+
// 和一条 reaction,用户既不知道失败也不知道原因。
|
|
314
|
+
const settled = await cardSession.settle();
|
|
315
|
+
if (settled === 'unsent') {
|
|
316
|
+
const finalRender = renderRunCardDetailed(cardSession.currentState, renderOptions);
|
|
317
|
+
await this.deps.sendResult({ card: finalRender.card }, ctx);
|
|
318
|
+
}
|
|
319
|
+
// 只摘自己的槽位:/stop 可能已删掉本条并让排队的下一条占住同一 cwd。
|
|
320
|
+
if (this.deps.activeRuns.get(cwd) === activeRun) {
|
|
321
|
+
this.deps.activeRuns.delete(cwd);
|
|
322
|
+
getLogger().info(`[lark-remote] activeRuns.delete cwd=${cwd} runId=${compactRunId}`);
|
|
323
|
+
}
|
|
324
|
+
// 终态 reaction:与普通 run 同一口径(done→Done / error→ERROR /
|
|
325
|
+
// interrupted→SHHH / idle_timeout→Alarm)。
|
|
326
|
+
this.deps.finishTurnReactions(ctx, terminalReactionEmoji(cardSession.currentState.terminal));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 事件消费循环(streamCompact 前半段):吞 runner.runCompact() 的全部事件,
|
|
331
|
+
* 逐帧喂卡片。终态后不再消费(与 runAgentStreamToEnd 同口径);result 帧
|
|
332
|
+
* 只做记录(subype/errorMessage),终态装配在 {@link finishCompactCard}。
|
|
333
|
+
*/
|
|
334
|
+
async consumeCompactEvents(cardSession, watchdog, runner, sessionId, cwd) {
|
|
335
|
+
let sawResult = false;
|
|
336
|
+
let resultSubtype;
|
|
337
|
+
let resultError;
|
|
338
|
+
for await (const event of runner.runCompact('', { cwd, sessionId })) {
|
|
339
|
+
// 已终态(/stop、空闲看门狗)后不再消费事件:与 runAgentStreamToEnd 同口径,
|
|
340
|
+
// 否则迟到的 result 会把终态卡改写成「已完成」。
|
|
341
|
+
const cardTerminal = cardSession.currentState.terminal;
|
|
342
|
+
if (isRunTerminal(cardTerminal)) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (event.type === 'result') {
|
|
346
|
+
sawResult = true;
|
|
347
|
+
resultSubtype = event.subtype;
|
|
348
|
+
resultError = event.errorMessage;
|
|
349
|
+
}
|
|
350
|
+
watchdog?.touch();
|
|
351
|
+
await cardSession.push(event);
|
|
352
|
+
}
|
|
353
|
+
return { sawResult, resultSubtype, resultError };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* 终态装配(streamCompact 后半段):result subtype 三态映射 → 从会话 jsonl
|
|
357
|
+
* 读权威统计(compactCount 短重试)→ cardSession.finish。
|
|
358
|
+
*/
|
|
359
|
+
async finishCompactCard(cardSession, sessionId, cwd, agentKind, outcome) {
|
|
360
|
+
const { resultSubtype, resultError } = outcome;
|
|
361
|
+
// §5.3: 卡片终态必须等于引擎真实终态。旧实现只看 sawResult 布尔——error
|
|
362
|
+
// subtype 的 result 也被 finish 'done'(放大器 bug,2026-08-31 事故)。
|
|
363
|
+
const terminal = resultSubtype === 'success'
|
|
364
|
+
? 'done'
|
|
365
|
+
: resultSubtype === 'interrupted'
|
|
366
|
+
? 'interrupted'
|
|
367
|
+
: 'error';
|
|
368
|
+
// 压缩结束后从会话 jsonl 读权威统计(压缩后上下文、压缩次数、本次压缩
|
|
369
|
+
// token 消耗与会话累计)。thread/compacted 通知与 jsonl 落盘几乎同时
|
|
370
|
+
// (实测差 ~9ms),compactCount 未读到则短重试,仍失败优雅降级(无统计)。
|
|
371
|
+
// 不支持 compactCount 的 reader(opencode/dsh)不重试——否则每次 compact
|
|
372
|
+
// 都会固定多跑两次完整 export/读取,且永远等不到该字段。
|
|
373
|
+
const compactCountSupported = this.deps.sessionReaderRegistry.tryGet(agentKind)?.supportsCompactCount !== false;
|
|
374
|
+
let finalUsage = resolveFinalUsage(this.deps.sessionReaderRegistry, this.deps.getConfig().defaultAgent, sessionId, cwd, agentKind);
|
|
375
|
+
for (let attempt = 0; compactCountSupported && !finalUsage?.compactCount && attempt < 2; attempt++) {
|
|
376
|
+
await sleep(150);
|
|
377
|
+
finalUsage = resolveFinalUsage(this.deps.sessionReaderRegistry, this.deps.getConfig().defaultAgent, sessionId, cwd, agentKind);
|
|
378
|
+
}
|
|
379
|
+
await cardSession.finish(terminal, {
|
|
380
|
+
resultSubtype: resultSubtype ?? 'error',
|
|
381
|
+
errorMsg: terminal === 'error' ? (resultError ?? '未收到压缩结果') : undefined,
|
|
382
|
+
// finalUsage 的字段形态就是 FinishMeta 的用量部分(同为 UsageSnapshot),
|
|
383
|
+
// 逐字段抄的那 14 行随单源一起去掉。
|
|
384
|
+
...(finalUsage ?? {}),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Handle resume.compact card action — compact a session directly from a
|
|
389
|
+
* resume card (auto-resume / `/resume <id>`), without a runId.
|
|
390
|
+
*
|
|
391
|
+
* Unlike handleCompactAction (which validates against the last finished run's
|
|
392
|
+
* runId), the resume card carries the sessionId + agent it was rendered for.
|
|
393
|
+
* Validation: the session must exist in the current cwd (same rule as
|
|
394
|
+
* cmdResume / resume.use) and the runner must implement runCompact.
|
|
395
|
+
*/
|
|
396
|
+
async handleResumeCompact(value, ctx) {
|
|
397
|
+
const log = getLogger();
|
|
398
|
+
log.info(`[lark-remote] handleResumeCompact userId=${ctx.userId} sessionId=${value.sessionId?.slice(0, 16)} agent=${value.agent ?? 'default'}`);
|
|
399
|
+
const { sessionId } = value;
|
|
400
|
+
if (!sessionId) {
|
|
401
|
+
await this.deps.sendResult({ text: '⚠️ 无效的 Compact 请求,缺少 sessionId' }, ctx);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const cwd = this.deps.resolveCwd(ctx.userId);
|
|
405
|
+
if (!cwd) {
|
|
406
|
+
await this.deps.sendResult({ text: '⚠️ 未设置工作目录' }, ctx);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
// Resolve agent kind the same way resume.use does: card value wins,
|
|
410
|
+
// otherwise fall back to defaultAgent. 名单来自单源 AGENT_KINDS —— 手抄一份
|
|
411
|
+
// 必然再次漂移(这份曾经漏 dsh:带 dsh 的卡片会静默退回 defaultAgent,
|
|
412
|
+
// 用错 reader 报「未找到 session」)。
|
|
413
|
+
const agentKind = value.agent && AGENT_KINDS.includes(value.agent)
|
|
414
|
+
? value.agent
|
|
415
|
+
: this.deps.getConfig().defaultAgent;
|
|
416
|
+
// Session must exist in the current cwd (same rule as cmdResume /
|
|
417
|
+
// resume.use): reject ghosts so a stale card cannot compact a session
|
|
418
|
+
// from another cwd. Read failures get visible feedback (card click
|
|
419
|
+
// 红线:不许静默失败)。
|
|
420
|
+
let content;
|
|
421
|
+
try {
|
|
422
|
+
content = this.deps.sessionReaderRegistry.get(agentKind).readSessionContent(sessionId, cwd);
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
await this.deps.sendResult({ text: `⚠️ 读取 session ${sessionId} 失败: ${errorMessage(err)}` }, ctx);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (content.events.length === 0 &&
|
|
429
|
+
!content.usage &&
|
|
430
|
+
!content.aiTitle &&
|
|
431
|
+
!content.recap &&
|
|
432
|
+
!content.displayTitle) {
|
|
433
|
+
await this.deps.sendResult({ text: `⚠️ 未找到 session ${sessionId}(当前目录: ${cwd})` }, ctx);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
// §5.3: kimi 压缩在途检测(与 handleCompactAction 同一入口)。
|
|
437
|
+
if (await this.rejectIfCompactionInFlight(sessionId, cwd, agentKind, ctx))
|
|
438
|
+
return;
|
|
439
|
+
// Check that the runner has runCompact(声明表 && runner 实现;
|
|
440
|
+
// codex/kimi/opencode/pi/claude/dsh 均已实现)。
|
|
441
|
+
const runner = this.deps.getRunner(cwd, agentKind);
|
|
442
|
+
if (!runnerSupportsCompact(runner, agentKind)) {
|
|
443
|
+
await this.deps.sendResult({ text: '⚠️ 当前运行模式不支持 Compact' }, ctx);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
log.info(`[lark-remote] handleResumeCompact executing runCompact sessionId=${sessionId} cwd=${cwd}`);
|
|
447
|
+
await this.streamCompact({ sessionId, cwd, agentKind, runner, ctx });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
//# sourceMappingURL=compact.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Final usage resolution extracted from Bridge (
|
|
2
|
+
* Final usage resolution extracted from Bridge (round-2
|
|
3
3
|
* simplification): after a run finishes, re-read the session transcript via
|
|
4
4
|
* the agent's session reader — live stream-json does not emit
|
|
5
5
|
* compact_boundary, so the live contextLength (fallback to result.usage
|
|
@@ -23,9 +23,9 @@ export function resolveFinalUsage(registry, defaultAgent, sessionId, cwd, agentK
|
|
|
23
23
|
try {
|
|
24
24
|
const content = registry.get(agentKind).readSessionContent(sessionId, cwd);
|
|
25
25
|
if (!content.usage) {
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// jsonl 可能读不到 usage(如 EnterWorktree relocate 后 transcript 换了
|
|
27
|
+
// 路径)。这里必须显式 warn:卡片会退回 per-run live 口径,静默的话
|
|
28
|
+
// 数字变化无从排查。
|
|
29
29
|
getLogger().warn(`[lark-remote] resolveFinalUsage: no usage from jsonl sessionId=${sessionId} cwd=${cwd} agent=${agentKind}, card falls back to per-run live usage`);
|
|
30
30
|
}
|
|
31
31
|
return content.usage ? { ...content.usage } : undefined;
|
|
@@ -13,7 +13,7 @@ import { truncateUtf8 } from '../common/truncate.js';
|
|
|
13
13
|
* `<cwd>/.lark-remote-temp/<YYYYMMDDHHmm>/`,不等待合批窗口;
|
|
14
14
|
* - 只负责「落盘 + 报告结果」:把落盘路径与失败原因交回装配器
|
|
15
15
|
* (`InboundTurnAssembler`),由装配器决定注入 prompt 还是发回执。
|
|
16
|
-
*
|
|
16
|
+
* 时间语义统一由装配器的静默期窗口负责(取代旧的
|
|
17
17
|
* 500ms 合批提示窗口 ——「现有合批窗口由 700ms 装配窗口取代」)。
|
|
18
18
|
*/
|
|
19
19
|
/** 文件名最长字节数(macOS/APFS 单组件上限 255 字节,留余量用 240)。 */
|