lark-remote 0.5.13 → 0.5.15
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 +31 -0
- package/dist/agent-kinds.d.ts +27 -0
- package/dist/agent-kinds.js +38 -0
- package/dist/bridge/approval-actions.d.ts +95 -0
- package/dist/bridge/approval-actions.js +104 -0
- package/dist/bridge/approval-coordinator.d.ts +13 -0
- package/dist/bridge/approval-coordinator.js +18 -0
- package/dist/bridge/final-usage.d.ts +8 -17
- package/dist/bridge/final-usage.js +1 -18
- package/dist/bridge/index.d.ts +46 -39
- package/dist/bridge/index.js +269 -240
- package/dist/bridge/queue-manager.d.ts +25 -1
- package/dist/bridge/queue-manager.js +179 -160
- package/dist/bridge/run-approval.d.ts +2 -1
- package/dist/bridge/run-usage.d.ts +21 -30
- package/dist/bridge/run-usage.js +47 -51
- package/dist/bridge/session-writeback.d.ts +3 -2
- package/dist/card/approval-render.d.ts +3 -2
- package/dist/card/approval-render.js +3 -2
- package/dist/{router → card}/card-helpers.d.ts +7 -7
- package/dist/{router → card}/card-helpers.js +8 -8
- package/dist/card/card-session.d.ts +4 -2
- package/dist/card/card-session.js +8 -6
- package/dist/card/card-shared.d.ts +1 -1
- package/dist/card/card-shared.js +1 -1
- package/dist/card/final-answer-overflow.d.ts +0 -6
- package/dist/card/final-answer-overflow.js +1 -1
- package/dist/card/run-card-session.d.ts +2 -2
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.js +29 -30
- package/dist/card/run-state.d.ts +28 -50
- package/dist/card/run-state.js +23 -0
- package/dist/card/usage-format.d.ts +30 -0
- package/dist/{router → card}/usage-format.js +3 -10
- package/dist/clone.js +6 -1
- package/dist/config/index.d.ts +16 -20
- package/dist/config/index.js +0 -16
- package/dist/config/pi-config.js +6 -4
- package/dist/connector/index.d.ts +2 -4
- package/dist/connector/index.js +7 -11
- package/dist/{runner/dsh → dsh}/auth.js +1 -1
- package/dist/dsh/protocol.d.ts +9 -0
- package/dist/dsh/protocol.js +61 -0
- package/dist/index.js +10 -5
- package/dist/model-catalog.d.ts +14 -0
- package/dist/model-catalog.js +25 -0
- package/dist/platform/path.d.ts +9 -0
- package/dist/platform/path.js +18 -0
- package/dist/router/active-run-usage.d.ts +4 -0
- package/dist/router/active-run-usage.js +12 -0
- package/dist/router/commands/ls.js +1 -1
- package/dist/router/commands/order.js +1 -1
- package/dist/router/commands/resume.d.ts +5 -4
- package/dist/router/commands/resume.js +4 -4
- package/dist/router/commands/ws.js +1 -1
- package/dist/router/config/claude.js +2 -1
- package/dist/router/config/common/render.d.ts +5 -1
- package/dist/router/config/common/render.js +6 -13
- package/dist/router/config/dsh.d.ts +1 -1
- package/dist/router/config/index.d.ts +1 -1
- package/dist/router/config/types.d.ts +11 -3
- package/dist/router/index.d.ts +41 -11
- package/dist/router/index.js +169 -233
- package/dist/runner/agent-choices-common.d.ts +1 -1
- package/dist/runner/claude/runner.d.ts +20 -2
- package/dist/runner/claude/runner.js +2 -15
- package/dist/runner/codex/app-server/runner.d.ts +3 -1
- package/dist/runner/codex/app-server/translator.d.ts +11 -0
- package/dist/runner/codex/app-server/translator.js +56 -149
- package/dist/runner/common/acp/base-acp-runner.d.ts +19 -1
- package/dist/runner/common/acp/base-acp-runner.js +37 -3
- package/dist/runner/common/connection-based-runner.d.ts +3 -1
- package/dist/runner/common/jsonrpc/transport.js +17 -7
- package/dist/runner/dsh/client.d.ts +1 -4
- package/dist/runner/dsh/client.js +7 -55
- package/dist/runner/dsh/daemon.js +9 -2
- package/dist/runner/dsh/exit-cancel.js +1 -1
- package/dist/runner/dsh/mux-stream.js +21 -2
- package/dist/runner/dsh/runner.d.ts +21 -1
- package/dist/runner/dsh/runner.js +97 -78
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +22 -1
- package/dist/runner/kimi/acp/runner.js +83 -60
- package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
- package/dist/runner/opencode/acp/error-monitor.js +31 -2
- package/dist/runner/opencode/acp/runner.d.ts +14 -1
- package/dist/runner/opencode/acp/runner.js +13 -2
- package/dist/runner/pi/rpc/runner.d.ts +2 -1
- package/dist/runner/probe.d.ts +1 -1
- package/dist/runner/probe.js +10 -9
- package/dist/runner/registry.d.ts +2 -1
- package/dist/runner/types.d.ts +20 -154
- package/dist/runner/types.js +4 -24
- package/dist/session/claude/session-index.d.ts +11 -4
- package/dist/session/claude/session-index.js +30 -8
- package/dist/session/claude/session-reader.d.ts +1 -1
- package/dist/session/claude/sessions.d.ts +2 -2
- package/dist/session/claude/sessions.js +41 -26
- package/dist/session/codex/rollout-reader.d.ts +1 -1
- package/dist/session/codex/rollout-reader.js +14 -13
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +4 -2
- package/dist/session/common/jsonl.js +7 -1
- package/dist/session/common/pagination.d.ts +2 -0
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +6 -0
- package/dist/session/common/recency.js +7 -5
- package/dist/session/common/two-pass.d.ts +1 -1
- package/dist/session/common/usage-accumulator.d.ts +1 -1
- package/dist/session/dsh/sessions.d.ts +1 -1
- package/dist/session/dsh/sessions.js +37 -12
- package/dist/session/kimi/sessions.d.ts +1 -1
- package/dist/session/kimi/sessions.js +72 -27
- package/dist/session/opencode/sessions.d.ts +1 -1
- package/dist/session/opencode/sessions.js +164 -81
- package/dist/session/pi/sessions.d.ts +1 -1
- package/dist/session/pi/sessions.js +20 -7
- package/dist/session/registry.d.ts +2 -1
- package/dist/session/session-store.d.ts +1 -2
- package/dist/session/session-store.js +14 -7
- package/dist/session-contract.d.ts +178 -0
- package/dist/session-contract.js +9 -0
- package/package.json +1 -1
- package/dist/router/usage-format.d.ts +0 -60
- /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/home.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/home.js +0 -0
- /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/types.js +0 -0
package/dist/bridge/index.js
CHANGED
|
@@ -8,15 +8,16 @@ import { renderRunCard, renderRunCardDetailed, } from '../card/run-renderer.js';
|
|
|
8
8
|
import { splitFinalAnswer } from '../card/final-answer-overflow.js';
|
|
9
9
|
import { renderBashCard } from '../card/bash-renderer.js';
|
|
10
10
|
import { BashCardSession, capBashOutput } from '../card/bash-card-session.js';
|
|
11
|
-
import { isRunTerminal } from '../card/run-state.js';
|
|
12
|
-
import { buildSessionHistoryCard } from '../
|
|
11
|
+
import { isRunActive, isRunTerminal, removeBackgroundJob, } from '../card/run-state.js';
|
|
12
|
+
import { buildSessionHistoryCard } from '../card/card-helpers.js';
|
|
13
13
|
import { agentDisplayName, resumeUseButton } from '../card/card-shared.js';
|
|
14
14
|
import { enforceCardBudget } from '../card/card-budget.js';
|
|
15
15
|
import { RunUsageCollector } from './run-usage.js';
|
|
16
16
|
import { BashProcessRunner } from '../runner/index.js';
|
|
17
|
-
import { AGENT_KINDS
|
|
17
|
+
import { AGENT_KINDS } from '../agent-kinds.js';
|
|
18
|
+
import { supportsCompact } from '../runner/types.js';
|
|
18
19
|
import { QueueManager, } from './queue-manager.js';
|
|
19
|
-
import {
|
|
20
|
+
import { ApprovalActions } from './approval-actions.js';
|
|
20
21
|
import { createRunApprovalCoordinator, remindPendingApproval } from './run-approval.js';
|
|
21
22
|
import { writeBackInitSession, writeBackResultSession, writeBackTurnStarted, } from './session-writeback.js';
|
|
22
23
|
import { InboundMediaHandler } from './inbound-media.js';
|
|
@@ -49,10 +50,36 @@ const TERMINAL_REACTION_EMOJI = {
|
|
|
49
50
|
error: 'ERROR',
|
|
50
51
|
idle_timeout: 'Alarm',
|
|
51
52
|
interrupted: 'SHHH',
|
|
53
|
+
// 本轮结束但目标继续:任务还没完,不能打 ✓(Done)。
|
|
54
|
+
goal_continuing: 'OnIt',
|
|
52
55
|
};
|
|
53
56
|
function terminalReactionEmoji(terminal) {
|
|
54
57
|
return TERMINAL_REACTION_EMOJI[terminal] ?? 'Done';
|
|
55
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* 完成通知卡的标题 / 配色 / 结果行。
|
|
61
|
+
*
|
|
62
|
+
* 这张卡只在流式卡失败(settle==='unsent')时补发,但终态必须如实反映:
|
|
63
|
+
* 写死「✅ 会话已完成」会把出错/中断的 run 报成成功。
|
|
64
|
+
*/
|
|
65
|
+
function completionNotificationPresentation(terminal) {
|
|
66
|
+
switch (terminal) {
|
|
67
|
+
case 'error':
|
|
68
|
+
return { title: '⚠️ 会话出错', template: 'red' };
|
|
69
|
+
case 'interrupted':
|
|
70
|
+
return { title: '⏹ 会话已中断', template: 'grey' };
|
|
71
|
+
case 'idle_timeout':
|
|
72
|
+
return { title: '⏱ 会话已超时', template: 'orange' };
|
|
73
|
+
case 'goal_continuing':
|
|
74
|
+
return { title: '⏳ 本轮结束,目标继续', template: 'orange' };
|
|
75
|
+
case 'finalizing':
|
|
76
|
+
return { title: '⏳ 会话仍在收尾', template: 'orange' };
|
|
77
|
+
case 'running':
|
|
78
|
+
return { title: '🔴 会话运行中', template: 'blue' };
|
|
79
|
+
case 'done':
|
|
80
|
+
return { title: '✅ 会话已完成', template: 'green', result: 'completed' };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
56
83
|
/**
|
|
57
84
|
* 单点化:`'runCompact' in runner` 鸭子探测
|
|
58
85
|
* codex app-server + kimi/opencode ACP runner 满足。
|
|
@@ -142,6 +169,10 @@ export class Bridge {
|
|
|
142
169
|
activeRuns = new Map();
|
|
143
170
|
/** Approval coordinators keyed by runId. */
|
|
144
171
|
approvalCoordinators = new Map();
|
|
172
|
+
/** 审批卡动作的实现(只读上面那张表;见 approval-actions.ts)。 */
|
|
173
|
+
approvalActions = new ApprovalActions({
|
|
174
|
+
coordinatorFor: (runId) => this.approvalCoordinators.get(runId),
|
|
175
|
+
});
|
|
145
176
|
/**
|
|
146
177
|
* 最近若干次已完成的、runner 有 runCompact 的 run(cwd → runId → 记录)。
|
|
147
178
|
* run 结束后 activeRuns 已清空,而终态卡上的 Compact 按钮只带 runId,
|
|
@@ -224,13 +255,8 @@ export class Bridge {
|
|
|
224
255
|
}
|
|
225
256
|
// Create new runner for this (cwd, kind) slot
|
|
226
257
|
const runner = this.agentRegistry.get(kind, cwd);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
runner.killOrphan();
|
|
230
|
-
}
|
|
231
|
-
if (runner && typeof runner.registerExitHandlers === 'function') {
|
|
232
|
-
runner.registerExitHandlers();
|
|
233
|
-
}
|
|
258
|
+
runner.killOrphan();
|
|
259
|
+
runner.registerExitHandlers();
|
|
234
260
|
// Store in nested map: Map<cwd, Map<kind, Runner>>。必须重新取一次:
|
|
235
261
|
// replaceStale 刚把槽位 evict 掉,上面的局部 workspaceMap 可能已随空槽
|
|
236
262
|
// 从 this.runners 移除——继续写它会生成一个「缓存里查不到」的 runner,
|
|
@@ -589,7 +615,7 @@ export class Bridge {
|
|
|
589
615
|
for (const [_cwd, active] of this.activeRuns) {
|
|
590
616
|
const state = active.session.currentState;
|
|
591
617
|
// Only include non-terminal runs
|
|
592
|
-
if (state.terminal
|
|
618
|
+
if (isRunActive(state.terminal)) {
|
|
593
619
|
result.push({
|
|
594
620
|
runId: active.runId,
|
|
595
621
|
sessionId: state.sessionId ?? '',
|
|
@@ -841,7 +867,7 @@ export class Bridge {
|
|
|
841
867
|
/**
|
|
842
868
|
* Update an existing card in place (原地更新) instead of sending a new one.
|
|
843
869
|
*
|
|
844
|
-
* Used by /config cardAction handlers (config.
|
|
870
|
+
* Used by /config cardAction handlers (config.set/input/save) so the
|
|
845
871
|
* user sees the card refresh on the same message bubble rather than getting
|
|
846
872
|
* a new card per click — same-message-bubble update pattern.
|
|
847
873
|
*
|
|
@@ -887,7 +913,8 @@ export class Bridge {
|
|
|
887
913
|
return true;
|
|
888
914
|
}
|
|
889
915
|
catch (err) {
|
|
890
|
-
|
|
916
|
+
// 调用方会据此把卡片从集合里永久剔除——这是用户可见的退化,warn 起步。
|
|
917
|
+
getLogger().warn(`[lark-remote] patchCard failed messageId=${messageId}: ${errorMessage(err)}`);
|
|
891
918
|
return false;
|
|
892
919
|
}
|
|
893
920
|
}
|
|
@@ -925,10 +952,7 @@ export class Bridge {
|
|
|
925
952
|
const killed = await runner.killJob(sessionId, jobId);
|
|
926
953
|
if (!killed)
|
|
927
954
|
return false;
|
|
928
|
-
snapshot.state =
|
|
929
|
-
...snapshot.state,
|
|
930
|
-
backgroundJobs: (snapshot.state.backgroundJobs ?? []).filter((job) => job.id !== jobId),
|
|
931
|
-
};
|
|
955
|
+
snapshot.state = removeBackgroundJob(snapshot.state, jobId);
|
|
932
956
|
await this.updateCardInPlace(renderRunCard(snapshot.state, snapshot.options), ctx);
|
|
933
957
|
return true;
|
|
934
958
|
}
|
|
@@ -992,7 +1016,7 @@ export class Bridge {
|
|
|
992
1016
|
}
|
|
993
1017
|
const entry = this.sessionStore.get(ctx.userId);
|
|
994
1018
|
getLogger().debug(`[lark-remote] forward entry userId=${ctx.userId} chatId=${ctx.chatId} cwd=${cwd} ` +
|
|
995
|
-
`agent=${agentKind} sessionId=${opts?.binding?.sessionId ?? entry?.sessions
|
|
1019
|
+
`agent=${agentKind} sessionId=${opts?.binding?.sessionId ?? entry?.sessions.get(agentKind) ?? '(none)'} message=${message.slice(0, 100)}`);
|
|
996
1020
|
// Check if current cwd already has a run in progress
|
|
997
1021
|
if (this.activeRuns.has(cwd)) {
|
|
998
1022
|
getLogger().warn(`[lark-remote] cwd busy, dropping message userId=${ctx.userId} cwd=${cwd}`);
|
|
@@ -1180,6 +1204,150 @@ export class Bridge {
|
|
|
1180
1204
|
stop,
|
|
1181
1205
|
};
|
|
1182
1206
|
}
|
|
1207
|
+
/**
|
|
1208
|
+
* result 事件的 pre-init 守卫(§9.22)。
|
|
1209
|
+
*
|
|
1210
|
+
* Claude CLI `--resume` 会在 system.init 之前先吐一条**上一轮**的陈旧 result。
|
|
1211
|
+
* 认了它就会过早置 sawResult / 抓错 usage / 让 run-state reducer 提前进入
|
|
1212
|
+
* finalizing(reducer 用 sessionId===undefined 守卫,同一条红线)。本轮真正的
|
|
1213
|
+
* result 在 system.init 之后才到。
|
|
1214
|
+
*
|
|
1215
|
+
* 例外:app-server runner(usageAuthority==='live')的 init 是 runner 自己补发
|
|
1216
|
+
* 的 synthetic init(2026-08-13 修复)。此分支保留为防御性兜底:live runner 若
|
|
1217
|
+
* 在无 init 时发出 result(旧构建等),session 写回照常执行,但 sawResult 不置
|
|
1218
|
+
* 真——卡片不会把半初始化的 run 当作已收尾。
|
|
1219
|
+
*
|
|
1220
|
+
* @param dropFromCard 该 result 是陈旧重放,不得进卡片(调用方 continue 跳过 push)
|
|
1221
|
+
*/
|
|
1222
|
+
observeResultEvent(event, opts) {
|
|
1223
|
+
const { sawInit, usageAuthority, usage, writeback, runId } = opts;
|
|
1224
|
+
const preInitResult = !sawInit;
|
|
1225
|
+
const preInitTerminal = preInitResult && (event.subtype === 'error' || event.subtype === 'interrupted')
|
|
1226
|
+
? {
|
|
1227
|
+
subtype: event.subtype,
|
|
1228
|
+
errorMessage: event.errorMessage,
|
|
1229
|
+
}
|
|
1230
|
+
: undefined;
|
|
1231
|
+
if (preInitResult && usageAuthority !== 'live') {
|
|
1232
|
+
getLogger().info(`[lark-remote] pre-init result ignored (resume replay) runId=${runId}`);
|
|
1233
|
+
return { sawResult: false, dropFromCard: true, preInitTerminal };
|
|
1234
|
+
}
|
|
1235
|
+
usage.observeResult(event);
|
|
1236
|
+
getLogger().info(preInitResult
|
|
1237
|
+
? `[lark-remote] pre-init result from live runner (app-server setup failure) runId=${runId}`
|
|
1238
|
+
: `[lark-remote] result event received runId=${runId}`);
|
|
1239
|
+
// app-server 模式 setup 失败路径(thread/start 成功后 turn/start 失败)不会有
|
|
1240
|
+
// turn_started 事件,store 不会写回新线程 id;runner 已在 result 里兜底上报。
|
|
1241
|
+
// 仅当 runner 声明 live usage(app-server)且会话指针未动时写回,避免下条消息
|
|
1242
|
+
// 再开一个孤儿线程。其余 agent 不在此列(各自有 system.init / turn_started 写回)。
|
|
1243
|
+
if (usageAuthority === 'live')
|
|
1244
|
+
writeBackResultSession(writeback, event.session_id);
|
|
1245
|
+
return { sawResult: !preInitResult, dropFromCard: false, preInitTerminal };
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* 审批三事件的派发(requested / resolved / view_updated)。
|
|
1249
|
+
*
|
|
1250
|
+
* coordinator 一 run 一个,首次 requested 时惰性建好并登记,`finally` 里统一
|
|
1251
|
+
* onTurnEnded + 摘除。
|
|
1252
|
+
*/
|
|
1253
|
+
dispatchApprovalEvent(event, opts) {
|
|
1254
|
+
const { runId, runner, cardSession, ctx, agentKind } = opts;
|
|
1255
|
+
if (event.type === 'approval_requested') {
|
|
1256
|
+
// 审批请求到达:撤回 Typing 表情再重发,让飞书重新触发提醒,把用户
|
|
1257
|
+
// 注意力拉回待审批的卡片(run 在等待人工决策,Typing 应重新闪烁)。
|
|
1258
|
+
remindPendingApproval(this.connector, ctx.messageId, runId);
|
|
1259
|
+
let coordinator = this.approvalCoordinators.get(runId);
|
|
1260
|
+
if (!coordinator) {
|
|
1261
|
+
coordinator = createRunApprovalCoordinator({
|
|
1262
|
+
runner,
|
|
1263
|
+
cardSession,
|
|
1264
|
+
config: this.config,
|
|
1265
|
+
runId,
|
|
1266
|
+
agentKind,
|
|
1267
|
+
});
|
|
1268
|
+
this.approvalCoordinators.set(runId, coordinator);
|
|
1269
|
+
getLogger().info(`[lark-remote] approval coordinator created runId=${runId.slice(0, 8)} requestId=${event.requestId}`);
|
|
1270
|
+
}
|
|
1271
|
+
coordinator.onRequested(event);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const coordinator = this.approvalCoordinators.get(runId);
|
|
1275
|
+
if (event.type === 'approval_resolved') {
|
|
1276
|
+
coordinator?.onResolved(event.requestId);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
// 乱序流:审批先到、item/started 后到,补全审批卡内容。coordinator
|
|
1280
|
+
// 仅在 pending 时更新(已响应/过期不复活)。
|
|
1281
|
+
coordinator?.updateView(event.requestId, event.view);
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* 进程退出(for-await 自然结束)后的终态矩阵——它是本轮的唯一终态触发源。
|
|
1285
|
+
*
|
|
1286
|
+
* 1) finalizing(result 已收到)→ 三态映射 + usage meta
|
|
1287
|
+
* 2) running(未收到 result)→ 错误路径;pre-init 终态优先当唯一线索
|
|
1288
|
+
* 3) 已终态 + sawResult → 首终态不动,仅补 usage meta
|
|
1289
|
+
* 4) 已终态且未收到 result(如 spawn 失败后 interrupted)→ 无 usage 可补,跳过
|
|
1290
|
+
*/
|
|
1291
|
+
async finishRunAfterStream(opts) {
|
|
1292
|
+
const { cardSession, usage, sawResult, sawAnyResult, pendingPreInitTerminal, agentKind, runId, } = opts;
|
|
1293
|
+
const finalState = cardSession.currentState;
|
|
1294
|
+
if (finalState.terminal === 'finalizing') {
|
|
1295
|
+
// 三态终态映射(2026-08-14):interrupted(审批超时/取消、/stop)是独立
|
|
1296
|
+
// 终态,不再归因于 Agent 错误;error 保持「运行出错」;其余为 done。
|
|
1297
|
+
const subtype = finalState.resultSubtype;
|
|
1298
|
+
const isError = subtype === 'error' || !!finalState.errorMsg;
|
|
1299
|
+
// goal 仍 active = 宿主会自动起下一轮:这不是"完成",而是"本轮结束"。
|
|
1300
|
+
// 用独立终态而不是 done,既不改 header 文案也不用谎报。
|
|
1301
|
+
const goalContinues = !isError && subtype !== 'interrupted' && finalState.goal?.phase === 'active';
|
|
1302
|
+
const terminal = isError
|
|
1303
|
+
? 'error'
|
|
1304
|
+
: subtype === 'interrupted'
|
|
1305
|
+
? 'interrupted'
|
|
1306
|
+
: goalContinues
|
|
1307
|
+
? 'goal_continuing'
|
|
1308
|
+
: 'done';
|
|
1309
|
+
// 中断归因:用户主动取消审批(cancel)优先于"未归类"。用户 /stop 的
|
|
1310
|
+
// 路径先置了终态(interrupted + user_stop),不会走到这里。
|
|
1311
|
+
const approvalCancelled = terminal === 'interrupted' &&
|
|
1312
|
+
this.approvalCoordinators.get(runId)?.cancelledByUser === true;
|
|
1313
|
+
await cardSession.finish(terminal, {
|
|
1314
|
+
resultSubtype: subtype,
|
|
1315
|
+
errorMsg: finalState.errorMsg,
|
|
1316
|
+
...(approvalCancelled ? { interruptedReason: 'approval_cancelled' } : {}),
|
|
1317
|
+
...usage.meta(),
|
|
1318
|
+
});
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (finalState.terminal === 'running') {
|
|
1322
|
+
if (pendingPreInitTerminal) {
|
|
1323
|
+
const terminal = pendingPreInitTerminal.subtype === 'interrupted' ? 'interrupted' : 'error';
|
|
1324
|
+
getLogger().warn(`[lark-remote] stream ended without init; using pre-init ${terminal} result runId=${runId} error=${pendingPreInitTerminal.errorMessage ?? '(none)'}`);
|
|
1325
|
+
await cardSession.finish(terminal, {
|
|
1326
|
+
...usage.meta(),
|
|
1327
|
+
errorMsg: pendingPreInitTerminal.errorMessage ??
|
|
1328
|
+
agentDisplayName(agentKind) + (terminal === 'interrupted' ? ' 已中断' : ' 启动失败'),
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
await cardSession.finish('error', {
|
|
1333
|
+
...usage.meta(),
|
|
1334
|
+
// sawAnyResult=true 说明 result 收到了、只是没能落到卡片(reducer 的
|
|
1335
|
+
// sessionId 守卫或 pre-init 守卫挡下)。报文不能再说"未收到 result
|
|
1336
|
+
// 事件",否则排障会去查输出流而不是查守卫。
|
|
1337
|
+
errorMsg: agentDisplayName(agentKind) +
|
|
1338
|
+
(sawAnyResult
|
|
1339
|
+
? ' 输出流已结束,但本轮结果未能落到卡片上'
|
|
1340
|
+
: ' 输出流已结束,但未收到 result 事件'),
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (sawResult) {
|
|
1346
|
+
// 已终态(interrupted/idle_timeout)+ result 已收到:首终态优先,
|
|
1347
|
+
// 仅补充 usage meta(finalizing 期间被 /stop 或 idle 超时也展示 token 统计)
|
|
1348
|
+
await cardSession.finish(finalState.terminal, usage.meta());
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1183
1351
|
/**
|
|
1184
1352
|
* Step 2 of forwardToClaude: Run the agent and consume its event stream.
|
|
1185
1353
|
* Handles idle watchdog, event processing, session sync, and state transitions.
|
|
@@ -1191,6 +1359,8 @@ export class Bridge {
|
|
|
1191
1359
|
// §9.12 空闲看门狗:判定口径见 startIdleWatchdog;武装点在 cardSession.start() 之后。
|
|
1192
1360
|
let watchdog = null;
|
|
1193
1361
|
let sawResult = false;
|
|
1362
|
+
/** 收到过任何 result(含被守卫丢弃/未进卡片的):收尾报文据此措辞。 */
|
|
1363
|
+
let sawAnyResult = false;
|
|
1194
1364
|
// A runner can fail before emitting its synthetic system/init (e.g. DSH
|
|
1195
1365
|
// session/create -> 401). The pre-init guard below drops that result to
|
|
1196
1366
|
// protect Claude resume replay, but if the stream ends without ever seeing
|
|
@@ -1214,9 +1384,14 @@ export class Bridge {
|
|
|
1214
1384
|
// Card stream failed - continue with static fallback below, but ensure
|
|
1215
1385
|
// we don't silently fail if the fallback also fails (covered in catch below).
|
|
1216
1386
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1387
|
+
// /stop(或队列「立即执行」的中断)可能落在建卡窗口内:卡片已定格为
|
|
1388
|
+
// 终态,但 runner 还没开始跑,runner.stop() 也没有进程可杀——spawn 型
|
|
1389
|
+
// 与连接型 runner 的 stop() 在 run() 之前都是空操作,且 run() 入口会
|
|
1390
|
+
// 重置停止标记。必须在这里收口,否则用户看到「已中断」,agent 仍会把
|
|
1391
|
+
// 这一轮跑完并改写工作区。finalizeRun 仍会照常收尾(settle / reaction)。
|
|
1392
|
+
if (isRunTerminal(cardSession.currentState.terminal)) {
|
|
1393
|
+
getLogger().info(`[lark-remote] card terminal before run start, skip runner runId=${runId} terminal=${cardSession.currentState.terminal}`);
|
|
1394
|
+
return;
|
|
1220
1395
|
}
|
|
1221
1396
|
// 单 interval 看门狗。事件到来时只刷新时间戳(无 timer 操作);
|
|
1222
1397
|
// interval 周期检查静默时长,超时才 finish('idle_timeout') + stop()。
|
|
@@ -1276,83 +1451,31 @@ export class Bridge {
|
|
|
1276
1451
|
usage.observeCompaction(event.compactMetadata.postTokens);
|
|
1277
1452
|
}
|
|
1278
1453
|
if (event.type === 'result') {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
pendingPreInitTerminal ??= {
|
|
1296
|
-
subtype: event.subtype,
|
|
1297
|
-
errorMessage: event.errorMessage,
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
// Still reset idle timer — the CLI is alive and producing events.
|
|
1454
|
+
const outcome = this.observeResultEvent(event, {
|
|
1455
|
+
sawInit,
|
|
1456
|
+
usageAuthority,
|
|
1457
|
+
usage,
|
|
1458
|
+
writeback,
|
|
1459
|
+
runId,
|
|
1460
|
+
});
|
|
1461
|
+
if (outcome.preInitTerminal)
|
|
1462
|
+
pendingPreInitTerminal ??= outcome.preInitTerminal;
|
|
1463
|
+
if (outcome.sawResult)
|
|
1464
|
+
sawResult = true;
|
|
1465
|
+
sawAnyResult = true;
|
|
1466
|
+
// 陈旧 result 不进卡片:run-state reducer 本来也会因 sessionId 未定而
|
|
1467
|
+
// 跳过,在 bridge 层就拦掉可以省下一次无谓的 push + render。
|
|
1468
|
+
if (outcome.dropFromCard) {
|
|
1469
|
+
// 仍然重置 idle 计时——CLI 活着并且在产事件。
|
|
1301
1470
|
watchdog?.touch();
|
|
1302
|
-
// Do NOT push to cardSession: the run-state reducer would also
|
|
1303
|
-
// skip it (sessionId === undefined), but skipping at the bridge
|
|
1304
|
-
// layer avoids the push + render overhead for a no-op event.
|
|
1305
1471
|
continue;
|
|
1306
1472
|
}
|
|
1307
|
-
if (preInitResult && (event.subtype === 'error' || event.subtype === 'interrupted')) {
|
|
1308
|
-
pendingPreInitTerminal ??= {
|
|
1309
|
-
subtype: event.subtype,
|
|
1310
|
-
errorMessage: event.errorMessage,
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
if (!preInitResult) {
|
|
1314
|
-
sawResult = true;
|
|
1315
|
-
}
|
|
1316
|
-
usage.observeResult(event);
|
|
1317
|
-
getLogger().info(preInitResult
|
|
1318
|
-
? `[lark-remote] pre-init result from live runner (app-server setup failure) runId=${runId}`
|
|
1319
|
-
: `[lark-remote] result event received runId=${runId}`);
|
|
1320
|
-
// app-server 模式 setup 失败路径(thread/start 成功后 turn/start 失败)
|
|
1321
|
-
// 不会有 turn_started 事件,store 不会写回新线程 id;runner 已在 result
|
|
1322
|
-
// 里兜底上报。此处仅当 runner 声明 live usage(app-server)
|
|
1323
|
-
// 且会话指针未动时写回,避免下条消息再开一个孤儿线程。其余 agent 不在
|
|
1324
|
-
// 此列(它们各自有 system.init / turn_started 写回)。
|
|
1325
|
-
if (usageAuthority === 'live')
|
|
1326
|
-
writeBackResultSession(writeback, event.session_id);
|
|
1327
1473
|
}
|
|
1328
1474
|
// Approval events (Codex app-server mode)
|
|
1329
|
-
if (event.type === 'approval_requested'
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
let coordinator = this.approvalCoordinators.get(runId);
|
|
1334
|
-
if (!coordinator) {
|
|
1335
|
-
coordinator = createRunApprovalCoordinator({
|
|
1336
|
-
runner,
|
|
1337
|
-
cardSession,
|
|
1338
|
-
config: this.config,
|
|
1339
|
-
runId,
|
|
1340
|
-
agentKind,
|
|
1341
|
-
});
|
|
1342
|
-
this.approvalCoordinators.set(runId, coordinator);
|
|
1343
|
-
getLogger().info(`[lark-remote] approval coordinator created runId=${runId.slice(0, 8)} requestId=${event.requestId}`);
|
|
1344
|
-
}
|
|
1345
|
-
coordinator.onRequested(event);
|
|
1346
|
-
}
|
|
1347
|
-
else if (event.type === 'approval_resolved') {
|
|
1348
|
-
const coordinator = this.approvalCoordinators.get(runId);
|
|
1349
|
-
coordinator?.onResolved(event.requestId);
|
|
1350
|
-
}
|
|
1351
|
-
else if (event.type === 'approval_view_updated') {
|
|
1352
|
-
// 乱序流:审批先到、item/started 后到,补全审批卡内容。coordinator
|
|
1353
|
-
// 仅在 pending 时更新(已响应/过期不复活)。
|
|
1354
|
-
const coordinator = this.approvalCoordinators.get(runId);
|
|
1355
|
-
coordinator?.updateView(event.requestId, event.view);
|
|
1475
|
+
if (event.type === 'approval_requested' ||
|
|
1476
|
+
event.type === 'approval_resolved' ||
|
|
1477
|
+
event.type === 'approval_view_updated') {
|
|
1478
|
+
this.dispatchApprovalEvent(event, { runId, runner, cardSession, ctx, agentKind });
|
|
1356
1479
|
}
|
|
1357
1480
|
// §9.12 红线:每个事件(含 result)无条件刷新
|
|
1358
1481
|
// lastEventTs 重新武装看门狗。result 后 CLI 进程做 jsonl flush/清理(finalizing
|
|
@@ -1381,56 +1504,16 @@ export class Bridge {
|
|
|
1381
1504
|
: resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, finalSessionId, cwd, agentKind);
|
|
1382
1505
|
usage.finalize(finalUsage, usageAuthority);
|
|
1383
1506
|
getLogger().info(usage.logSummary(runId));
|
|
1384
|
-
// for-await 自然结束 = stream 关闭 = CLI
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const isError = subtype === 'error' || !!finalState.errorMsg;
|
|
1395
|
-
// goal 仍 active = 宿主会自动起下一轮:这不是"完成",而是"本轮结束"。
|
|
1396
|
-
// 用独立终态而不是 done,既不改 header 文案也不用谎报。
|
|
1397
|
-
const goalContinues = !isError && subtype !== 'interrupted' && finalState.goal?.phase === 'active';
|
|
1398
|
-
const terminal = isError
|
|
1399
|
-
? 'error'
|
|
1400
|
-
: subtype === 'interrupted'
|
|
1401
|
-
? 'interrupted'
|
|
1402
|
-
: goalContinues
|
|
1403
|
-
? 'goal_continuing'
|
|
1404
|
-
: 'done';
|
|
1405
|
-
await cardSession.finish(terminal, {
|
|
1406
|
-
resultSubtype: subtype,
|
|
1407
|
-
errorMsg: finalState.errorMsg,
|
|
1408
|
-
...usage.meta(),
|
|
1409
|
-
});
|
|
1410
|
-
}
|
|
1411
|
-
else if (finalState.terminal === 'running') {
|
|
1412
|
-
if (pendingPreInitTerminal) {
|
|
1413
|
-
const terminal = pendingPreInitTerminal.subtype === 'interrupted' ? 'interrupted' : 'error';
|
|
1414
|
-
getLogger().warn(`[lark-remote] stream ended without init; using pre-init ${terminal} result runId=${runId} error=${pendingPreInitTerminal.errorMessage ?? '(none)'}`);
|
|
1415
|
-
await cardSession.finish(terminal, {
|
|
1416
|
-
...usage.meta(),
|
|
1417
|
-
errorMsg: pendingPreInitTerminal.errorMessage ??
|
|
1418
|
-
agentDisplayName(agentKind) + (terminal === 'interrupted' ? ' 已中断' : ' 启动失败'),
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
else {
|
|
1422
|
-
await cardSession.finish('error', {
|
|
1423
|
-
...usage.meta(),
|
|
1424
|
-
errorMsg: agentDisplayName(agentKind) + ' 输出流已结束,但未收到 result 事件',
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
else if (sawResult) {
|
|
1429
|
-
// 已终态(interrupted/idle_timeout)+ result 已收到:首终态优先,
|
|
1430
|
-
// 仅补充 usage meta(finalizing 期间被 /stop 或 idle 超时也展示 token 统计)
|
|
1431
|
-
await cardSession.finish(finalState.terminal, usage.meta());
|
|
1432
|
-
}
|
|
1433
|
-
// else: 已终态且未收到 result(如 spawn 失败后 interrupted)-> 无 usage 可补,跳过
|
|
1507
|
+
// for-await 自然结束 = stream 关闭 = CLI 进程退出 → 终态矩阵
|
|
1508
|
+
await this.finishRunAfterStream({
|
|
1509
|
+
cardSession,
|
|
1510
|
+
usage,
|
|
1511
|
+
sawResult,
|
|
1512
|
+
sawAnyResult,
|
|
1513
|
+
pendingPreInitTerminal,
|
|
1514
|
+
agentKind,
|
|
1515
|
+
runId,
|
|
1516
|
+
});
|
|
1434
1517
|
}
|
|
1435
1518
|
catch (err) {
|
|
1436
1519
|
getLogger().error(`[lark-remote] claude run error runId=${runId}:`, err);
|
|
@@ -1445,7 +1528,7 @@ export class Bridge {
|
|
|
1445
1528
|
// 终态守卫:running 或 finalizing(非终态)-> 转 error;
|
|
1446
1529
|
// 已终态(interrupted/idle_timeout/done/error)-> 保留首终态,仅补充 usage。
|
|
1447
1530
|
const catchTerminal = cardSession.currentState.terminal;
|
|
1448
|
-
if (catchTerminal
|
|
1531
|
+
if (isRunActive(catchTerminal)) {
|
|
1449
1532
|
await cardSession.finish('error', {
|
|
1450
1533
|
...usage.meta(),
|
|
1451
1534
|
inputTokens: usage.liveInputTokens,
|
|
@@ -1524,7 +1607,7 @@ export class Bridge {
|
|
|
1524
1607
|
// but NOT when streaming succeeded (card already shows completion).
|
|
1525
1608
|
const sessionId = cardSession.currentState.sessionId;
|
|
1526
1609
|
if (sessionId && runCardSent) {
|
|
1527
|
-
await this.sendCompletionNotificationCard(sessionId, cwd, ctx, activeRun.agentKind);
|
|
1610
|
+
await this.sendCompletionNotificationCard(sessionId, cwd, ctx, activeRun.agentKind, cardSession.currentState.terminal);
|
|
1528
1611
|
}
|
|
1529
1612
|
if (finalRender.finalAnswerStatus === 'overflow' && finalRender.finalAnswerText) {
|
|
1530
1613
|
await this.sendFinalAnswerOverflow(finalRender.finalAnswerText, ctx);
|
|
@@ -1572,7 +1655,8 @@ export class Bridge {
|
|
|
1572
1655
|
* It's sent as a new message (not updating the run card) so users can easily
|
|
1573
1656
|
* find and return to this session without scrolling up.
|
|
1574
1657
|
*/
|
|
1575
|
-
async sendCompletionNotificationCard(sessionId, cwd, ctx, agentKind = this.config.defaultAgent) {
|
|
1658
|
+
async sendCompletionNotificationCard(sessionId, cwd, ctx, agentKind = this.config.defaultAgent, terminal = 'done') {
|
|
1659
|
+
const presentation = completionNotificationPresentation(terminal);
|
|
1576
1660
|
try {
|
|
1577
1661
|
// cap the events read for the notification card — without
|
|
1578
1662
|
// maxEvents claude returns every event after the last user message and
|
|
@@ -1589,10 +1673,10 @@ export class Bridge {
|
|
|
1589
1673
|
}, {
|
|
1590
1674
|
agentKind,
|
|
1591
1675
|
headerText: `📂 \`${cwd}\`\n会话: **${sessionId}**`,
|
|
1592
|
-
title:
|
|
1593
|
-
usageResult:
|
|
1676
|
+
title: presentation.title,
|
|
1677
|
+
...(presentation.result ? { usageResult: presentation.result } : {}),
|
|
1594
1678
|
actions: [resumeUseButton(sessionId, agentKind)],
|
|
1595
|
-
headerTemplate:
|
|
1679
|
+
headerTemplate: presentation.template,
|
|
1596
1680
|
});
|
|
1597
1681
|
// send through sendResult so enforceCardBudget guards the card
|
|
1598
1682
|
// (previously sent via connector directly, bypassing the budget — long
|
|
@@ -1612,90 +1696,36 @@ export class Bridge {
|
|
|
1612
1696
|
// =============================================================================
|
|
1613
1697
|
// Approval action handlers
|
|
1614
1698
|
// =============================================================================
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
withCoordinator(runId, fn) {
|
|
1620
|
-
const coordinator = this.approvalCoordinators.get(runId);
|
|
1621
|
-
if (!coordinator) {
|
|
1622
|
-
// run 已结束(coordinator 已释放)时点击审批按钮必须给用户明确反馈,
|
|
1623
|
-
// 不能静默。router 会把该异常转成 error toast。
|
|
1624
|
-
getLogger().warn(`[lark-remote] no approval coordinator for runId=${runId.slice(0, 8)}`);
|
|
1625
|
-
throw new Error('任务已结束,审批无法响应');
|
|
1626
|
-
}
|
|
1627
|
-
return fn(coordinator);
|
|
1628
|
-
}
|
|
1629
|
-
/**
|
|
1630
|
-
* Handle an approval response from a card button click.
|
|
1631
|
-
* Routes to the ApprovalCoordinator for the given run.
|
|
1632
|
-
*/
|
|
1699
|
+
// 实现住在 approval-actions.ts(审计 #32):这 7 个入口只碰
|
|
1700
|
+
// approvalCoordinators 一张表,不碰其余 Bridge 状态。方法名保持 Bridge 的
|
|
1701
|
+
// 公开 API 不变——router 的 APPROVAL_ACTION_SPECS 按名字分发。
|
|
1702
|
+
/** 审批响应(卡片按钮)→ ApprovalCoordinator.submit。 */
|
|
1633
1703
|
async handleApprovalRespond(opts) {
|
|
1634
|
-
|
|
1635
|
-
log.info(`[lark-remote] handleApprovalRespond runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} decision=${opts.decision}`);
|
|
1636
|
-
return this.withCoordinator(opts.runId, async (coordinator) => {
|
|
1637
|
-
// Build ApprovalAction from the decision string
|
|
1638
|
-
const action = decisionToApprovalAction(opts.decision);
|
|
1639
|
-
await coordinator.submit(action, { requestId: opts.requestId, nonce: opts.nonce });
|
|
1640
|
-
});
|
|
1704
|
+
return this.approvalActions.handleApprovalRespond(opts);
|
|
1641
1705
|
}
|
|
1642
|
-
/**
|
|
1643
|
-
* 计划审批修改意见(ExitPlanMode 输入框):记录到 coordinator 并回流卡片。
|
|
1644
|
-
* 「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
|
|
1645
|
-
*/
|
|
1706
|
+
/** 计划审批修改意见(ExitPlanMode 输入框)→ coordinator.planFeedback。 */
|
|
1646
1707
|
async handleApprovalPlanFeedback(opts) {
|
|
1647
|
-
return this.
|
|
1648
|
-
await coordinator.planFeedback({ text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce });
|
|
1649
|
-
});
|
|
1708
|
+
return this.approvalActions.handleApprovalPlanFeedback(opts);
|
|
1650
1709
|
}
|
|
1651
|
-
/**
|
|
1652
|
-
* Handle a permission toggle from a card button click.
|
|
1653
|
-
* Routes to the ApprovalCoordinator for the given run.
|
|
1654
|
-
*/
|
|
1710
|
+
/** 权限条目勾选(卡片按钮)→ coordinator.togglePerm。 */
|
|
1655
1711
|
async handleApprovalToggle(opts) {
|
|
1656
|
-
|
|
1657
|
-
log.info(`[lark-remote] handleApprovalToggle runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} permId=${opts.permId}`);
|
|
1658
|
-
return this.withCoordinator(opts.runId, async (coordinator) => {
|
|
1659
|
-
// selected 由卡片根据当前渲染状态传回(取反),不再硬编码只授不撤。
|
|
1660
|
-
const toggleAction = {
|
|
1661
|
-
permId: opts.permId,
|
|
1662
|
-
selected: opts.selected ?? true,
|
|
1663
|
-
};
|
|
1664
|
-
await coordinator.togglePerm(toggleAction, { requestId: opts.requestId });
|
|
1665
|
-
});
|
|
1712
|
+
return this.approvalActions.handleApprovalToggle(opts);
|
|
1666
1713
|
}
|
|
1667
|
-
/**
|
|
1668
|
-
* Claude AskUserQuestion 选项点击(单选即时提交 / 多选切换勾选)。
|
|
1669
|
-
* 路由到 ApprovalCoordinator.toggleAnswer。
|
|
1670
|
-
*/
|
|
1714
|
+
/** AskUserQuestion 选项点击 → coordinator.toggleAnswer。 */
|
|
1671
1715
|
async handleApprovalAnswer(opts) {
|
|
1672
|
-
return this.
|
|
1716
|
+
return this.approvalActions.handleApprovalAnswer(opts);
|
|
1673
1717
|
}
|
|
1674
|
-
/**
|
|
1675
|
-
* Claude AskUserQuestion 多选问题的「提交答案」按钮。
|
|
1676
|
-
* 路由到 ApprovalCoordinator.submitAnswers。
|
|
1677
|
-
*/
|
|
1718
|
+
/** AskUserQuestion 多选「提交答案」→ coordinator.submitAnswers。 */
|
|
1678
1719
|
async handleApprovalAnswerSubmit(opts) {
|
|
1679
|
-
return this.
|
|
1720
|
+
return this.approvalActions.handleApprovalAnswerSubmit(opts);
|
|
1680
1721
|
}
|
|
1681
|
-
/**
|
|
1682
|
-
* Claude AskUserQuestion 自定义答案(Other):自由文本直接
|
|
1683
|
-
* 作为该单选问题的答案提交。路由到 ApprovalCoordinator.answerCustom。
|
|
1684
|
-
*/
|
|
1722
|
+
/** AskUserQuestion 自定义答案(Other)→ coordinator.answerCustom。 */
|
|
1685
1723
|
async handleApprovalAnswerCustom(opts) {
|
|
1686
|
-
return this.
|
|
1724
|
+
return this.approvalActions.handleApprovalAnswerCustom(opts);
|
|
1687
1725
|
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Codex AskUserQuestion 补充说明(user_note,2026-08-18 live 验证模型理解
|
|
1690
|
-
* "user_note: <text>" 条目):记录到 coordinator,随答案一起提交。
|
|
1691
|
-
* 路由到 ApprovalCoordinator.answerNote。
|
|
1692
|
-
*/
|
|
1726
|
+
/** AskUserQuestion 补充说明(user_note)→ coordinator.answerNote。 */
|
|
1693
1727
|
async handleApprovalAnswerNote(opts) {
|
|
1694
|
-
return this.
|
|
1695
|
-
}
|
|
1696
|
-
dispatchQuestionAction(opts, action, invoke) {
|
|
1697
|
-
getLogger().info(`[lark-remote] ${action} runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
|
|
1698
|
-
return this.withCoordinator(opts.runId, invoke);
|
|
1728
|
+
return this.approvalActions.handleApprovalAnswerNote(opts);
|
|
1699
1729
|
}
|
|
1700
1730
|
/**
|
|
1701
1731
|
* 记下一次「可压缩 run」:终态卡上的 Compact 按钮点击时据此反查
|
|
@@ -1817,14 +1847,14 @@ export class Bridge {
|
|
|
1817
1847
|
});
|
|
1818
1848
|
}
|
|
1819
1849
|
/**
|
|
1820
|
-
* §5.3: kimi 压缩在途检测——reader
|
|
1850
|
+
* §5.3: kimi 压缩在途检测——reader 实现契约的 `readCompactionState` 时,若
|
|
1821
1851
|
* wire 状态机判定当前有未终结的压缩(最后一次 begin 无 terminal 记录),
|
|
1822
1852
|
* 直接回文本「已有一次 Compact 正在进行」并返回 true(调用方不开卡、不发
|
|
1823
1853
|
* session/prompt)。codex/claude 等 reader 无此方法 → 返回 false,行为不变。
|
|
1824
1854
|
*/
|
|
1825
1855
|
async rejectIfCompactionInFlight(sessionId, cwd, agentKind, ctx) {
|
|
1826
1856
|
const reader = this.sessionReaderRegistry.get(agentKind);
|
|
1827
|
-
if (!reader
|
|
1857
|
+
if (!reader?.readCompactionState)
|
|
1828
1858
|
return false;
|
|
1829
1859
|
try {
|
|
1830
1860
|
const state = reader.readCompactionState(sessionId, cwd);
|
|
@@ -1878,14 +1908,15 @@ export class Bridge {
|
|
|
1878
1908
|
}
|
|
1879
1909
|
await this.sendResult({ text: '🗜 Compact 已触发,正在压缩会话…' }, ctx);
|
|
1880
1910
|
const compactRunId = randomUUID();
|
|
1911
|
+
// 渲染选项同时用于流式卡与「卡片流没建起来」时的静态兜底卡——两者必须
|
|
1912
|
+
// 同门控,否则兜底卡会渲染出与本次 run 不一致的按钮。
|
|
1913
|
+
const renderOptions = { agentKind };
|
|
1881
1914
|
const cardSession = new RunCardSession({
|
|
1882
1915
|
connector: this.connector,
|
|
1883
1916
|
chatId: ctx.chatId,
|
|
1884
1917
|
replyTo: ctx.messageId,
|
|
1885
1918
|
runId: compactRunId,
|
|
1886
|
-
renderOptions
|
|
1887
|
-
agentKind,
|
|
1888
|
-
},
|
|
1919
|
+
renderOptions,
|
|
1889
1920
|
});
|
|
1890
1921
|
// 压缩与普通 run 共用同一套生命周期——注册进 activeRuns(/stop 有对象可停、
|
|
1891
1922
|
// /active 看得见)、挂 §9.12 空闲看门狗、终态后补 reaction。缺任何一环,卡住的
|
|
@@ -1952,20 +1983,9 @@ export class Bridge {
|
|
|
1952
1983
|
await cardSession.finish(terminal, {
|
|
1953
1984
|
resultSubtype: resultSubtype ?? 'error',
|
|
1954
1985
|
errorMsg: terminal === 'error' ? (resultError ?? '未收到压缩结果') : undefined,
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
compactCount: finalUsage?.compactCount,
|
|
1959
|
-
cacheReadTokens: finalUsage?.cacheReadTokens,
|
|
1960
|
-
cacheCreationTokens: finalUsage?.cacheCreationTokens,
|
|
1961
|
-
totalTokens: finalUsage?.totalTokens,
|
|
1962
|
-
inputTokens: finalUsage?.inputTokens,
|
|
1963
|
-
outputTokens: finalUsage?.outputTokens,
|
|
1964
|
-
cumulativeTotalTokens: finalUsage?.cumulativeTotalTokens,
|
|
1965
|
-
cumulativeInputTokens: finalUsage?.cumulativeInputTokens,
|
|
1966
|
-
cumulativeOutputTokens: finalUsage?.cumulativeOutputTokens,
|
|
1967
|
-
cumulativeCacheReadTokens: finalUsage?.cumulativeCacheReadTokens,
|
|
1968
|
-
cumulativeCacheCreationTokens: finalUsage?.cumulativeCacheCreationTokens,
|
|
1986
|
+
// finalUsage 的字段形态就是 FinishMeta 的用量部分(同为 UsageSnapshot),
|
|
1987
|
+
// 逐字段抄的那 14 行随单源一起去掉。
|
|
1988
|
+
...(finalUsage ?? {}),
|
|
1969
1989
|
});
|
|
1970
1990
|
log.info(`[lark-remote] streamCompact finished sessionId=${sessionId} sawResult=${sawResult} subtype=${resultSubtype ?? 'none'}`);
|
|
1971
1991
|
}
|
|
@@ -1983,6 +2003,15 @@ export class Bridge {
|
|
|
1983
2003
|
}
|
|
1984
2004
|
finally {
|
|
1985
2005
|
watchdog?.stop();
|
|
2006
|
+
// 与 run 路径同口径的降级:卡片流没建起来(start 失败 / stream 未收尾)时,
|
|
2007
|
+
// 终态只存在于内存里。settle() 释放 producer 并等 stream 收尾;'unsent'
|
|
2008
|
+
// 表示用户手里没有卡,必须补一张静态终态卡,否则只剩「Compact 已触发」
|
|
2009
|
+
// 和一条 reaction,用户既不知道失败也不知道原因。
|
|
2010
|
+
const settled = await cardSession.settle();
|
|
2011
|
+
if (settled === 'unsent') {
|
|
2012
|
+
const finalRender = renderRunCardDetailed(cardSession.currentState, renderOptions);
|
|
2013
|
+
await this.sendResult({ card: finalRender.card }, ctx);
|
|
2014
|
+
}
|
|
1986
2015
|
// 只摘自己的槽位:/stop 可能已删掉本条并让排队的下一条占住同一 cwd。
|
|
1987
2016
|
if (this.activeRuns.get(cwd) === activeRun) {
|
|
1988
2017
|
this.activeRuns.delete(cwd);
|