lark-remote 0.5.12 → 0.5.13
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 +13 -2
- package/README.en.md +1 -14
- package/README.md +1 -14
- package/dist/bridge/approval-coordinator.d.ts +1 -0
- package/dist/bridge/approval-coordinator.js +12 -24
- package/dist/bridge/index.d.ts +7 -0
- package/dist/bridge/index.js +77 -227
- package/dist/bridge/run-approval.d.ts +15 -0
- package/dist/bridge/run-approval.js +41 -0
- package/dist/bridge/run-usage.d.ts +46 -0
- package/dist/bridge/run-usage.js +96 -0
- package/dist/bridge/session-writeback.d.ts +18 -0
- package/dist/bridge/session-writeback.js +36 -0
- package/dist/card/bash-card-session.js +2 -5
- package/dist/card/card-session.d.ts +2 -0
- package/dist/card/card-session.js +20 -22
- package/dist/card/card-shared.d.ts +4 -4
- package/dist/card/card-shared.js +3 -9
- package/dist/card/collapsible.js +3 -2
- package/dist/card/final-answer-overflow.d.ts +7 -0
- package/dist/card/final-answer-overflow.js +46 -0
- package/dist/card/run-card-session.js +2 -7
- package/dist/card/run-renderer.d.ts +14 -0
- package/dist/card/run-renderer.js +119 -1
- package/dist/card/run-state.d.ts +12 -1
- package/dist/card/run-state.js +73 -34
- package/dist/config/codex-config.js +4 -4
- package/dist/config/index.d.ts +4 -2
- package/dist/config/index.js +6 -4
- package/dist/inbound/turn-assembler.js +6 -0
- package/dist/index.js +15 -409
- package/dist/message-handlers.d.ts +26 -0
- package/dist/message-handlers.js +275 -0
- package/dist/router/card-action-payload.d.ts +1 -1
- package/dist/router/card-action-payload.js +1 -1
- package/dist/router/commands/ls.d.ts +9 -0
- package/dist/router/commands/ls.js +243 -0
- package/dist/router/commands/order.d.ts +12 -0
- package/dist/router/commands/order.js +178 -0
- package/dist/router/commands/resume.d.ts +47 -0
- package/dist/router/commands/resume.js +246 -0
- package/dist/router/commands/ws.d.ts +15 -0
- package/dist/router/commands/ws.js +189 -0
- package/dist/router/config/claude.js +3 -3
- package/dist/router/config/codex.js +2 -6
- package/dist/router/config/common/turn-idle.d.ts +2 -0
- package/dist/router/config/common/turn-idle.js +9 -0
- package/dist/router/config/kimi.js +2 -6
- package/dist/router/config/opencode.js +2 -6
- package/dist/router/index.d.ts +14 -22
- package/dist/router/index.js +70 -1152
- package/dist/router/paging.d.ts +13 -0
- package/dist/router/paging.js +20 -0
- package/dist/router/path-validation.d.ts +27 -0
- package/dist/router/path-validation.js +45 -0
- package/dist/runner/claude/runner.d.ts +3 -4
- package/dist/runner/claude/runner.js +4 -5
- package/dist/runner/claude/session.js +1 -1
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +65 -9
- package/dist/runner/common/acp/base-translator.d.ts +3 -8
- package/dist/runner/common/acp/base-translator.js +16 -7
- package/dist/runner/common/spawning-runner.js +10 -4
- package/dist/runner/common/turn-started.d.ts +2 -0
- package/dist/runner/common/turn-started.js +10 -0
- package/dist/runner/dsh/auth.js +2 -3
- package/dist/runner/dsh/client.d.ts +1 -6
- package/dist/runner/dsh/client.js +11 -165
- package/dist/runner/dsh/daemon.d.ts +0 -2
- package/dist/runner/dsh/daemon.js +1 -6
- package/dist/runner/dsh/errors.d.ts +5 -0
- package/dist/runner/dsh/errors.js +11 -0
- package/dist/runner/dsh/exit-cancel.d.ts +0 -2
- package/dist/runner/dsh/exit-cancel.js +1 -1
- package/dist/runner/dsh/home.d.ts +2 -0
- package/dist/runner/dsh/home.js +8 -0
- package/dist/runner/dsh/inflight.d.ts +0 -2
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.d.ts +26 -0
- package/dist/runner/dsh/mux-stream.js +157 -0
- package/dist/runner/dsh/translator.d.ts +0 -10
- package/dist/runner/dsh/translator.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +1 -12
- package/dist/runner/opencode/acp/translator.js +1 -12
- package/dist/runner/pi/rpc/translator.d.ts +2 -8
- package/dist/runner/pi/rpc/translator.js +2 -7
- package/dist/runner/registry.d.ts +0 -19
- package/dist/runner/registry.js +0 -32
- package/dist/runner/types.d.ts +17 -1
- package/dist/session/claude/session-index.js +4 -2
- package/dist/session/kimi/sessions.js +2 -2
- package/dist/session/opencode/sessions.js +2 -1
- package/dist/session/pi/sessions.js +3 -2
- package/package.json +1 -1
package/dist/bridge/index.js
CHANGED
|
@@ -4,18 +4,21 @@ import { isContentFilterFailure } from '../common/turn-failure.js';
|
|
|
4
4
|
import { resolveFinalUsage, errorMessage } from './final-usage.js';
|
|
5
5
|
import { getLogger } from '../logger/index.js';
|
|
6
6
|
import { RunCardSession } from '../card/run-card-session.js';
|
|
7
|
-
import { renderRunCard } from '../card/run-renderer.js';
|
|
7
|
+
import { renderRunCard, renderRunCardDetailed, } from '../card/run-renderer.js';
|
|
8
|
+
import { splitFinalAnswer } from '../card/final-answer-overflow.js';
|
|
8
9
|
import { renderBashCard } from '../card/bash-renderer.js';
|
|
9
10
|
import { BashCardSession, capBashOutput } from '../card/bash-card-session.js';
|
|
10
11
|
import { isRunTerminal } from '../card/run-state.js';
|
|
11
12
|
import { buildSessionHistoryCard } from '../router/card-helpers.js';
|
|
12
13
|
import { agentDisplayName, resumeUseButton } from '../card/card-shared.js';
|
|
13
14
|
import { enforceCardBudget } from '../card/card-budget.js';
|
|
14
|
-
import {
|
|
15
|
+
import { RunUsageCollector } from './run-usage.js';
|
|
15
16
|
import { BashProcessRunner } from '../runner/index.js';
|
|
16
17
|
import { AGENT_KINDS, supportsCompact } from '../runner/types.js';
|
|
17
18
|
import { QueueManager, } from './queue-manager.js';
|
|
18
|
-
import {
|
|
19
|
+
import { decisionToApprovalAction } from './approval-coordinator.js';
|
|
20
|
+
import { createRunApprovalCoordinator, remindPendingApproval } from './run-approval.js';
|
|
21
|
+
import { writeBackInitSession, writeBackResultSession, writeBackTurnStarted, } from './session-writeback.js';
|
|
19
22
|
import { InboundMediaHandler } from './inbound-media.js';
|
|
20
23
|
/**
|
|
21
24
|
* Max events to read for the completion notification card (mirror router's
|
|
@@ -24,8 +27,6 @@ import { InboundMediaHandler } from './inbound-media.js';
|
|
|
24
27
|
* budget.
|
|
25
28
|
*/
|
|
26
29
|
const COMPLETION_NOTIFICATION_MAX_EVENTS = 5;
|
|
27
|
-
/** Approval request timeout (ms) — pending approval expires after this. */
|
|
28
|
-
const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
29
30
|
/**
|
|
30
31
|
* runner 缓存的工作区数量上限:超过后创建新工作区槽位时
|
|
31
32
|
* 顺带回收「非 active 且进程已停」的死缓存,防止随访问过的工作区数量
|
|
@@ -814,6 +815,29 @@ export class Bridge {
|
|
|
814
815
|
return false;
|
|
815
816
|
}
|
|
816
817
|
}
|
|
818
|
+
/**
|
|
819
|
+
* Deliver a final answer that cannot fit in the run card. Plain text avoids
|
|
820
|
+
* markdown parser limits and preserves every character; sequence labels make
|
|
821
|
+
* multi-message delivery unambiguous.
|
|
822
|
+
*/
|
|
823
|
+
async sendFinalAnswerOverflow(text, ctx) {
|
|
824
|
+
const chunks = splitFinalAnswer(text);
|
|
825
|
+
try {
|
|
826
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
827
|
+
const prefix = chunks.length > 1 ? `完整回答(${i + 1}/${chunks.length})\n\n` : '完整回答\n\n';
|
|
828
|
+
await this.connector.sendWithRetry(ctx.chatId, { text: prefix + chunks[i] }, { replyTo: ctx.messageId });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
catch (err) {
|
|
832
|
+
getLogger().error('[lark-remote] failed to deliver complete final answer:', err);
|
|
833
|
+
try {
|
|
834
|
+
await this.connector.sendWithRetry(ctx.chatId, { text: `⚠️ 完整回答补发失败:${errorMessage(err)}` }, { replyTo: ctx.messageId });
|
|
835
|
+
}
|
|
836
|
+
catch (fallbackErr) {
|
|
837
|
+
getLogger().error('[lark-remote] final-answer failure notice also failed:', fallbackErr);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
817
841
|
/**
|
|
818
842
|
* Update an existing card in place (原地更新) instead of sending a new one.
|
|
819
843
|
*
|
|
@@ -1173,60 +1197,7 @@ export class Bridge {
|
|
|
1173
1197
|
// init the result was the only terminal signal we had. Keep it so the card
|
|
1174
1198
|
// shows the real error instead of the generic "no result" fallback.
|
|
1175
1199
|
let pendingPreInitTerminal;
|
|
1176
|
-
|
|
1177
|
-
let resultNotSuccess = false;
|
|
1178
|
-
// Final usage read from jsonl after the run completes. live stream-json does
|
|
1179
|
-
// not emit compact_boundary, so the live contextLength is unreliable.
|
|
1180
|
-
let finalContextLength;
|
|
1181
|
-
let finalContextLimit;
|
|
1182
|
-
let finalCompactCount;
|
|
1183
|
-
let finalCacheReadTokens;
|
|
1184
|
-
let finalCacheCreationTokens;
|
|
1185
|
-
let finalTotalTokens;
|
|
1186
|
-
// Real input/output tokens captured from the live result event (codex/
|
|
1187
|
-
// opencode carry them in ResultEvent.usage). Threaded to the card so the
|
|
1188
|
-
// done card shows real values instead of the 10% estimate.
|
|
1189
|
-
let liveInputTokens;
|
|
1190
|
-
let liveOutputTokens;
|
|
1191
|
-
// Real context window limit from the live result event (codex app-server
|
|
1192
|
-
// carries it in ResultEvent.usage.context_limit). jsonl wins when present;
|
|
1193
|
-
// live is the fallback (app-server error runs / jsonl 未落盘时).
|
|
1194
|
-
let liveContextLimit;
|
|
1195
|
-
// Final input/output for the done card: live value wins (codex/opencode
|
|
1196
|
-
// carry per-run usage in the result event); jsonl is the fallback for
|
|
1197
|
-
// agents whose live stream has no usage (kimi).
|
|
1198
|
-
let finalInputTokens;
|
|
1199
|
-
let finalOutputTokens;
|
|
1200
|
-
// Session-cumulative input/output (all runs) read from jsonl; threaded to
|
|
1201
|
-
// the done card's "累计" display. Always from jsonl (live has no cumulative).
|
|
1202
|
-
let finalCumulativeTotalTokens;
|
|
1203
|
-
let finalCumulativeInputTokens;
|
|
1204
|
-
let finalCumulativeOutputTokens;
|
|
1205
|
-
let finalCumulativeCacheReadTokens;
|
|
1206
|
-
let finalCumulativeCacheCreationTokens;
|
|
1207
|
-
// Reasoning tokens (pi usage.reasoning) from the live result event.
|
|
1208
|
-
let finalReasoningTokens;
|
|
1209
|
-
// 统一的 usage meta 构造(finalize 各分支共享,本方法内原先 5 处手写 11-15
|
|
1210
|
-
// 字段对象;第 6 处 streamCompact 形状不同未收敛)。
|
|
1211
|
-
// catch 路径需覆盖 flow 字段时在展开后覆写即可。catch error 路径亦复用此
|
|
1212
|
-
// 闭包,相比旧手写对象补齐了累计 cache 字段(cumulativeCacheReadTokens/
|
|
1213
|
-
// cumulativeCacheCreationTokens),属有意的口径对齐(向 done 路径看齐)。
|
|
1214
|
-
const usageMeta = () => ({
|
|
1215
|
-
contextLength: finalContextLength,
|
|
1216
|
-
contextLimit: finalContextLimit,
|
|
1217
|
-
compactCount: finalCompactCount,
|
|
1218
|
-
cacheReadTokens: finalCacheReadTokens,
|
|
1219
|
-
cacheCreationTokens: finalCacheCreationTokens,
|
|
1220
|
-
totalTokens: finalTotalTokens,
|
|
1221
|
-
inputTokens: finalInputTokens,
|
|
1222
|
-
outputTokens: finalOutputTokens,
|
|
1223
|
-
cumulativeTotalTokens: finalCumulativeTotalTokens,
|
|
1224
|
-
cumulativeInputTokens: finalCumulativeInputTokens,
|
|
1225
|
-
cumulativeOutputTokens: finalCumulativeOutputTokens,
|
|
1226
|
-
cumulativeCacheReadTokens: finalCumulativeCacheReadTokens,
|
|
1227
|
-
cumulativeCacheCreationTokens: finalCumulativeCacheCreationTokens,
|
|
1228
|
-
reasoningTokens: finalReasoningTokens,
|
|
1229
|
-
});
|
|
1200
|
+
const usage = new RunUsageCollector();
|
|
1230
1201
|
try {
|
|
1231
1202
|
try {
|
|
1232
1203
|
// queue.immediate 的 run-start hold:目标槽位可能在 handler 发最终
|
|
@@ -1256,7 +1227,6 @@ export class Bridge {
|
|
|
1256
1227
|
runner,
|
|
1257
1228
|
pendingApprovals: () => this.approvalCoordinators.get(runId)?.pendingCount() ?? 0,
|
|
1258
1229
|
});
|
|
1259
|
-
let contextLength;
|
|
1260
1230
|
getLogger().info(`[lark-remote] runner.run() begin runId=${runId} message=${message.slice(0, 100)}`);
|
|
1261
1231
|
// Extract agent-specific options from config
|
|
1262
1232
|
const agentOpts = this.getAgentRunOptions(agentKind);
|
|
@@ -1267,6 +1237,14 @@ export class Bridge {
|
|
|
1267
1237
|
// ending (sawResult, usage capture, or cardSession.push would all
|
|
1268
1238
|
// be wrong for the historical event).
|
|
1269
1239
|
let sawInit = false;
|
|
1240
|
+
const writeback = {
|
|
1241
|
+
sessionStore: this.sessionStore,
|
|
1242
|
+
userId: ctx.userId,
|
|
1243
|
+
agentKind,
|
|
1244
|
+
cwd,
|
|
1245
|
+
sessionEpochAtStart,
|
|
1246
|
+
runId,
|
|
1247
|
+
};
|
|
1270
1248
|
for await (const event of runner.run(message, runOpts)) {
|
|
1271
1249
|
// 只有终态才阻止事件处理。finalizing 是非终态,应该继续处理
|
|
1272
1250
|
// 这样在 result 事件后,后台任务输出仍能被正确处理
|
|
@@ -1279,34 +1257,7 @@ export class Bridge {
|
|
|
1279
1257
|
// 本轮真实使用的 session(不可变身份):指针在 run 中被 /new 等移动时,
|
|
1280
1258
|
// 结束后的 usage 读取仍以它为准。
|
|
1281
1259
|
activeRun.sessionId = event.session_id;
|
|
1282
|
-
|
|
1283
|
-
// /config 切换)移动了 session 指针,此 init 的写回是 stale 的——
|
|
1284
|
-
// 跳过,否则 /new 的清空会被在途 run 静默撤销(2026-08-09 事故:
|
|
1285
|
-
// task-notification 注入触发 Claude 重发 init,已终止的 run 被复活)。
|
|
1286
|
-
const pointerMoved = this.sessionStore.getSessionEpoch(ctx.userId, agentKind) !== sessionEpochAtStart;
|
|
1287
|
-
if (pointerMoved) {
|
|
1288
|
-
getLogger().info(`[lark-remote] system.init write-back skipped: session pointer moved since run start runId=${runId} sessionId=${event.session_id}`);
|
|
1289
|
-
}
|
|
1290
|
-
else {
|
|
1291
|
-
// Use session's real directory from event.cwd (not runner's cwd parameter)
|
|
1292
|
-
// guard empty string too -- `??` only catches null/undefined, but a
|
|
1293
|
-
// translator (or older build) may emit cwd="", which must NOT overwrite
|
|
1294
|
-
// the good runner cwd.
|
|
1295
|
-
const hasCwd = !!this.sessionStore.getCwd(ctx.userId);
|
|
1296
|
-
const realCwd = event.cwd && event.cwd.length > 0 ? event.cwd : cwd;
|
|
1297
|
-
if (hasCwd && event.cwd && event.cwd !== cwd) {
|
|
1298
|
-
// Session has its own cwd (e.g. EnterWorktree relocate). Keep the
|
|
1299
|
-
// cwd unchanged; record the session's actual cwd in sessionCwds.
|
|
1300
|
-
getLogger().info(`[lark-remote] system.init: cwd=${cwd}, session cwd=${event.cwd}`);
|
|
1301
|
-
this.sessionStore.setSessionIdAndSessionCwd(ctx.userId, agentKind, event.session_id, realCwd);
|
|
1302
|
-
}
|
|
1303
|
-
else {
|
|
1304
|
-
// First use (no cwd yet) or event.cwd matches runner cwd:
|
|
1305
|
-
// set sessionId + cwd as before; on first use also bootstrap sessionCwds.
|
|
1306
|
-
const bootstrapSessionCwd = !hasCwd && event.cwd && event.cwd.length > 0 ? event.cwd : undefined;
|
|
1307
|
-
this.sessionStore.setSessionIdAndCwd(ctx.userId, agentKind, event.session_id, realCwd, bootstrapSessionCwd);
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1260
|
+
writeBackInitSession(writeback, event);
|
|
1310
1261
|
getLogger().info(`[lark-remote] system.init received runId=${runId} sessionId=${event.session_id} cwd=${event.cwd}`);
|
|
1311
1262
|
}
|
|
1312
1263
|
if (event.type === 'turn_started') {
|
|
@@ -1316,20 +1267,13 @@ export class Bridge {
|
|
|
1316
1267
|
// 都新建线程(thread/start 而非 thread/resume),会话无法延续。
|
|
1317
1268
|
// init 缺失时以 threadId 作为本轮身份兜底。
|
|
1318
1269
|
activeRun.sessionId ??= event.threadId;
|
|
1319
|
-
|
|
1320
|
-
if (!pointerMoved) {
|
|
1321
|
-
this.sessionStore.setSessionIdAndCwd(ctx.userId, agentKind, event.threadId, cwd);
|
|
1322
|
-
getLogger().info(`[lark-remote] turn_started session write-back runId=${runId} sessionId=${event.threadId}`);
|
|
1323
|
-
}
|
|
1324
|
-
else {
|
|
1325
|
-
getLogger().info(`[lark-remote] turn_started write-back skipped: session pointer moved since run start runId=${runId} sessionId=${event.threadId}`);
|
|
1326
|
-
}
|
|
1270
|
+
writeBackTurnStarted(writeback, event);
|
|
1327
1271
|
}
|
|
1328
1272
|
// Track context length from compaction events
|
|
1329
1273
|
if (event.type === 'system' &&
|
|
1330
1274
|
event.subtype === 'compact_boundary' &&
|
|
1331
1275
|
event.compactMetadata) {
|
|
1332
|
-
|
|
1276
|
+
usage.observeCompaction(event.compactMetadata.postTokens);
|
|
1333
1277
|
}
|
|
1334
1278
|
if (event.type === 'result') {
|
|
1335
1279
|
// Pre-init result guard: Claude CLI --resume emits a stale result
|
|
@@ -1369,7 +1313,7 @@ export class Bridge {
|
|
|
1369
1313
|
if (!preInitResult) {
|
|
1370
1314
|
sawResult = true;
|
|
1371
1315
|
}
|
|
1372
|
-
|
|
1316
|
+
usage.observeResult(event);
|
|
1373
1317
|
getLogger().info(preInitResult
|
|
1374
1318
|
? `[lark-remote] pre-init result from live runner (app-server setup failure) runId=${runId}`
|
|
1375
1319
|
: `[lark-remote] result event received runId=${runId}`);
|
|
@@ -1378,83 +1322,22 @@ export class Bridge {
|
|
|
1378
1322
|
// 里兜底上报。此处仅当 runner 声明 live usage(app-server)
|
|
1379
1323
|
// 且会话指针未动时写回,避免下条消息再开一个孤儿线程。其余 agent 不在
|
|
1380
1324
|
// 此列(它们各自有 system.init / turn_started 写回)。
|
|
1381
|
-
if (usageAuthority === 'live'
|
|
1382
|
-
event.session_id
|
|
1383
|
-
this.sessionStore.getSessionEpoch(ctx.userId, agentKind) === sessionEpochAtStart) {
|
|
1384
|
-
this.sessionStore.setSessionIdAndCwd(ctx.userId, agentKind, event.session_id, cwd);
|
|
1385
|
-
getLogger().info(`[lark-remote] result session write-back runId=${runId} sessionId=${event.session_id}`);
|
|
1386
|
-
}
|
|
1387
|
-
// result 后进入 finalizing 非终态(暂存 subtype/errorMsg),由 for-await
|
|
1388
|
-
// 自然结束后在 finally 转 done/error。提取 usage 信息。
|
|
1389
|
-
if (event.usage) {
|
|
1390
|
-
const u = normalizeResultUsage(event.usage);
|
|
1391
|
-
// input_tokens is non-cached (ccusage-aligned), so the context
|
|
1392
|
-
// length fallback must include cached tokens. Prefer the agent's
|
|
1393
|
-
// declared total; otherwise reconstruct from the parts.
|
|
1394
|
-
if (!contextLength) {
|
|
1395
|
-
contextLength = u.contextLength;
|
|
1396
|
-
}
|
|
1397
|
-
// Capture real input/output for the done card (avoiding the 10%
|
|
1398
|
-
// estimate in formatUsageStats). live 值优先:有 live input/output 时
|
|
1399
|
-
// 所有 flow 字段统一用 live scope(本 run),避免同一张卡片上
|
|
1400
|
-
// Input/Output 是本 run、Cache/Total 是 session 累计的混 scope 问题。
|
|
1401
|
-
liveInputTokens = u.inputTokens;
|
|
1402
|
-
liveOutputTokens = u.outputTokens;
|
|
1403
|
-
liveContextLimit = u.contextLimit;
|
|
1404
|
-
if (u.cacheReadTokens !== undefined) {
|
|
1405
|
-
finalCacheReadTokens = u.cacheReadTokens;
|
|
1406
|
-
}
|
|
1407
|
-
if (u.cacheCreationTokens !== undefined) {
|
|
1408
|
-
finalCacheCreationTokens = u.cacheCreationTokens;
|
|
1409
|
-
}
|
|
1410
|
-
if (u.totalTokens !== undefined) {
|
|
1411
|
-
finalTotalTokens = u.totalTokens;
|
|
1412
|
-
}
|
|
1413
|
-
if (u.reasoningTokens !== undefined) {
|
|
1414
|
-
finalReasoningTokens = u.reasoningTokens;
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1325
|
+
if (usageAuthority === 'live')
|
|
1326
|
+
writeBackResultSession(writeback, event.session_id);
|
|
1417
1327
|
}
|
|
1418
1328
|
// Approval events (Codex app-server mode)
|
|
1419
1329
|
if (event.type === 'approval_requested') {
|
|
1420
1330
|
// 审批请求到达:撤回 Typing 表情再重发,让飞书重新触发提醒,把用户
|
|
1421
1331
|
// 注意力拉回待审批的卡片(run 在等待人工决策,Typing 应重新闪烁)。
|
|
1422
|
-
|
|
1423
|
-
try {
|
|
1424
|
-
await this.connector.removeReactionByEmoji(ctx.messageId, 'Typing');
|
|
1425
|
-
}
|
|
1426
|
-
catch (err) {
|
|
1427
|
-
getLogger().warn(`[lark-remote] remove Typing reaction failed runId=${runId}: ${errorMessage(err)}`);
|
|
1428
|
-
}
|
|
1429
|
-
try {
|
|
1430
|
-
await this.connector.addReaction(ctx.messageId, 'Typing');
|
|
1431
|
-
}
|
|
1432
|
-
catch (err) {
|
|
1433
|
-
getLogger().warn(`[lark-remote] re-add Typing reaction failed runId=${runId}: ${errorMessage(err)}`);
|
|
1434
|
-
}
|
|
1435
|
-
})();
|
|
1332
|
+
remindPendingApproval(this.connector, ctx.messageId, runId);
|
|
1436
1333
|
let coordinator = this.approvalCoordinators.get(runId);
|
|
1437
1334
|
if (!coordinator) {
|
|
1438
|
-
coordinator =
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
responder: async (requestId, response) => {
|
|
1445
|
-
const r = runner;
|
|
1446
|
-
if (typeof r.respondApproval === 'function') {
|
|
1447
|
-
await r.respondApproval(requestId, response);
|
|
1448
|
-
}
|
|
1449
|
-
},
|
|
1450
|
-
interruptTurn: async () => {
|
|
1451
|
-
// DSH may return a StopOutcome; the coordinator only awaits.
|
|
1452
|
-
await runner.stop();
|
|
1453
|
-
},
|
|
1454
|
-
pushToCard: async (events) => {
|
|
1455
|
-
for (const ev of events)
|
|
1456
|
-
await cardSession.push(ev);
|
|
1457
|
-
},
|
|
1335
|
+
coordinator = createRunApprovalCoordinator({
|
|
1336
|
+
runner,
|
|
1337
|
+
cardSession,
|
|
1338
|
+
config: this.config,
|
|
1339
|
+
runId,
|
|
1340
|
+
agentKind,
|
|
1458
1341
|
});
|
|
1459
1342
|
this.approvalCoordinators.set(runId, coordinator);
|
|
1460
1343
|
getLogger().info(`[lark-remote] approval coordinator created runId=${runId.slice(0, 8)} requestId=${event.requestId}`);
|
|
@@ -1493,44 +1376,11 @@ export class Bridge {
|
|
|
1493
1376
|
// 优先级:卡片状态(本轮 init 写入)→ activeRun(本轮 init/turn_started)
|
|
1494
1377
|
// → 本轮开始时捕获的 sessionId。三者都没有就跳过 usage,绝不回读全局指针。
|
|
1495
1378
|
const finalSessionId = cardSession.currentState.sessionId ?? activeRun.sessionId ?? sessionId;
|
|
1496
|
-
const finalUsage = resultNotSuccess
|
|
1379
|
+
const finalUsage = usage.resultNotSuccess
|
|
1497
1380
|
? undefined
|
|
1498
1381
|
: resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, finalSessionId, cwd, agentKind);
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
finalCompactCount = finalUsage?.compactCount;
|
|
1502
|
-
finalCumulativeTotalTokens = finalUsage?.cumulativeTotalTokens;
|
|
1503
|
-
finalCumulativeInputTokens = finalUsage?.cumulativeInputTokens;
|
|
1504
|
-
finalCumulativeOutputTokens = finalUsage?.cumulativeOutputTokens;
|
|
1505
|
-
finalCumulativeCacheReadTokens = finalUsage?.cumulativeCacheReadTokens;
|
|
1506
|
-
finalCumulativeCacheCreationTokens = finalUsage?.cumulativeCacheCreationTokens;
|
|
1507
|
-
// 统一 flow 字段 scope:live 优先,jsonl 兜底。
|
|
1508
|
-
// 当 live result 事件带了 input/output(即 liveInputTokens/liveOutputTokens
|
|
1509
|
-
// 非 undefined),所有 flow 字段统一用 live scope(本 run),不用 jsonl
|
|
1510
|
-
// 的 session 累计值覆盖,避免 cache%/Total 分子分母不同源。
|
|
1511
|
-
// 无 live usage 时(如 kimi),jsonl 值兜底。
|
|
1512
|
-
// contextLength/compactCount 保持 jsonl 优先(它们是水位/历史计数)。
|
|
1513
|
-
const codexJsonlFirst = usageAuthority === 'jsonl';
|
|
1514
|
-
if (!codexJsonlFirst && liveInputTokens !== undefined && liveOutputTokens !== undefined) {
|
|
1515
|
-
// 非 codex 且 live 有 input/output → 所有 flow 字段用 live scope
|
|
1516
|
-
finalInputTokens = liveInputTokens;
|
|
1517
|
-
finalOutputTokens = liveOutputTokens;
|
|
1518
|
-
// cacheReadTokens/cacheCreationTokens/totalTokens
|
|
1519
|
-
// 已在上面 result 事件处理中赋了 live 值;若 live 没有则为 undefined,
|
|
1520
|
-
// 由 formatUsageStats 的 max/兜底公式处理。
|
|
1521
|
-
}
|
|
1522
|
-
else {
|
|
1523
|
-
// codex(jsonl 优先)或无 live usage → jsonl 兜底所有 flow 字段。
|
|
1524
|
-
// jsonl 的非累计字段已是末轮(本 run)scope(claude reader 修复后对齐
|
|
1525
|
-
// codex `last_token_usage` 语义,见 scalarScan),不再是 session 累计;
|
|
1526
|
-
// session 累计只经 cumulative* 字段透传,卡片"本 run ≤ 累计"不变量成立。
|
|
1527
|
-
finalInputTokens = finalUsage?.inputTokens ?? liveInputTokens;
|
|
1528
|
-
finalOutputTokens = finalUsage?.outputTokens ?? liveOutputTokens;
|
|
1529
|
-
finalCacheReadTokens = finalUsage?.cacheReadTokens ?? finalCacheReadTokens;
|
|
1530
|
-
finalCacheCreationTokens = finalUsage?.cacheCreationTokens ?? finalCacheCreationTokens;
|
|
1531
|
-
finalTotalTokens = finalUsage?.totalTokens ?? finalTotalTokens;
|
|
1532
|
-
}
|
|
1533
|
-
getLogger().info(`[lark-remote] final usage runId=${runId} contextLength=${finalContextLength} contextLimit=${finalContextLimit ?? 'undefined'} compactCount=${finalCompactCount ?? 'undefined'} cacheRead=${finalCacheReadTokens ?? 'undefined'} cacheCreate=${finalCacheCreationTokens ?? 'undefined'}`);
|
|
1382
|
+
usage.finalize(finalUsage, usageAuthority);
|
|
1383
|
+
getLogger().info(usage.logSummary(runId));
|
|
1534
1384
|
// for-await 自然结束 = stream 关闭 = CLI 进程退出。
|
|
1535
1385
|
// 进程退出(for-await end)= 唯一终态触发源。
|
|
1536
1386
|
// 1) finalizing(result 已收到)-> 转 done/error + usage meta
|
|
@@ -1555,7 +1405,7 @@ export class Bridge {
|
|
|
1555
1405
|
await cardSession.finish(terminal, {
|
|
1556
1406
|
resultSubtype: subtype,
|
|
1557
1407
|
errorMsg: finalState.errorMsg,
|
|
1558
|
-
...
|
|
1408
|
+
...usage.meta(),
|
|
1559
1409
|
});
|
|
1560
1410
|
}
|
|
1561
1411
|
else if (finalState.terminal === 'running') {
|
|
@@ -1563,14 +1413,14 @@ export class Bridge {
|
|
|
1563
1413
|
const terminal = pendingPreInitTerminal.subtype === 'interrupted' ? 'interrupted' : 'error';
|
|
1564
1414
|
getLogger().warn(`[lark-remote] stream ended without init; using pre-init ${terminal} result runId=${runId} error=${pendingPreInitTerminal.errorMessage ?? '(none)'}`);
|
|
1565
1415
|
await cardSession.finish(terminal, {
|
|
1566
|
-
...
|
|
1416
|
+
...usage.meta(),
|
|
1567
1417
|
errorMsg: pendingPreInitTerminal.errorMessage ??
|
|
1568
1418
|
agentDisplayName(agentKind) + (terminal === 'interrupted' ? ' 已中断' : ' 启动失败'),
|
|
1569
1419
|
});
|
|
1570
1420
|
}
|
|
1571
1421
|
else {
|
|
1572
1422
|
await cardSession.finish('error', {
|
|
1573
|
-
...
|
|
1423
|
+
...usage.meta(),
|
|
1574
1424
|
errorMsg: agentDisplayName(agentKind) + ' 输出流已结束,但未收到 result 事件',
|
|
1575
1425
|
});
|
|
1576
1426
|
}
|
|
@@ -1578,7 +1428,7 @@ export class Bridge {
|
|
|
1578
1428
|
else if (sawResult) {
|
|
1579
1429
|
// 已终态(interrupted/idle_timeout)+ result 已收到:首终态优先,
|
|
1580
1430
|
// 仅补充 usage meta(finalizing 期间被 /stop 或 idle 超时也展示 token 统计)
|
|
1581
|
-
await cardSession.finish(finalState.terminal,
|
|
1431
|
+
await cardSession.finish(finalState.terminal, usage.meta());
|
|
1582
1432
|
}
|
|
1583
1433
|
// else: 已终态且未收到 result(如 spawn 失败后 interrupted)-> 无 usage 可补,跳过
|
|
1584
1434
|
}
|
|
@@ -1597,16 +1447,16 @@ export class Bridge {
|
|
|
1597
1447
|
const catchTerminal = cardSession.currentState.terminal;
|
|
1598
1448
|
if (catchTerminal === 'running' || catchTerminal === 'finalizing') {
|
|
1599
1449
|
await cardSession.finish('error', {
|
|
1600
|
-
...
|
|
1601
|
-
inputTokens: liveInputTokens,
|
|
1602
|
-
outputTokens: liveOutputTokens,
|
|
1450
|
+
...usage.meta(),
|
|
1451
|
+
inputTokens: usage.liveInputTokens,
|
|
1452
|
+
outputTokens: usage.liveOutputTokens,
|
|
1603
1453
|
errorMsg: errorMessage(err),
|
|
1604
1454
|
});
|
|
1605
1455
|
}
|
|
1606
1456
|
else {
|
|
1607
1457
|
getLogger().info(`[lark-remote] skip error finish: state already terminal (${catchTerminal}) runId=${runId}`);
|
|
1608
1458
|
if (sawResult) {
|
|
1609
|
-
await cardSession.finish(catchTerminal,
|
|
1459
|
+
await cardSession.finish(catchTerminal, usage.meta());
|
|
1610
1460
|
}
|
|
1611
1461
|
}
|
|
1612
1462
|
}
|
|
@@ -1659,10 +1509,11 @@ export class Bridge {
|
|
|
1659
1509
|
sessionFallbackAvailable: !!(cardSession.currentState.sessionId ??
|
|
1660
1510
|
this.sessionStore.getSessionId(ctx.userId, activeRun.agentKind)),
|
|
1661
1511
|
};
|
|
1512
|
+
const finalRender = renderRunCardDetailed(cardSession.currentState, finalRenderOptions);
|
|
1662
1513
|
this.rememberRunCardSnapshot(runId, cardSession.currentState, finalRenderOptions, cwd, activeRun.agentKind);
|
|
1663
1514
|
let runCardSent = false;
|
|
1664
1515
|
if (settleResult === 'unsent') {
|
|
1665
|
-
const sent = await this.sendResult({ card:
|
|
1516
|
+
const sent = await this.sendResult({ card: finalRender.card }, ctx);
|
|
1666
1517
|
runCardSent = sent;
|
|
1667
1518
|
}
|
|
1668
1519
|
else {
|
|
@@ -1675,6 +1526,9 @@ export class Bridge {
|
|
|
1675
1526
|
if (sessionId && runCardSent) {
|
|
1676
1527
|
await this.sendCompletionNotificationCard(sessionId, cwd, ctx, activeRun.agentKind);
|
|
1677
1528
|
}
|
|
1529
|
+
if (finalRender.finalAnswerStatus === 'overflow' && finalRender.finalAnswerText) {
|
|
1530
|
+
await this.sendFinalAnswerOverflow(finalRender.finalAnswerText, ctx);
|
|
1531
|
+
}
|
|
1678
1532
|
// Add a terminal-state emoji reaction to the user's original message
|
|
1679
1533
|
this.finishTurnReactions(ctx, terminalReactionEmoji(cardSession.currentState.terminal));
|
|
1680
1534
|
}
|
|
@@ -1815,27 +1669,21 @@ export class Bridge {
|
|
|
1815
1669
|
* 路由到 ApprovalCoordinator.toggleAnswer。
|
|
1816
1670
|
*/
|
|
1817
1671
|
async handleApprovalAnswer(opts) {
|
|
1818
|
-
|
|
1819
|
-
log.info(`[lark-remote] handleApprovalAnswer runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
|
|
1820
|
-
return this.withCoordinator(opts.runId, (coordinator) => coordinator.toggleAnswer({ questionIndex: opts.questionIndex, option: opts.option }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
1672
|
+
return this.dispatchQuestionAction(opts, 'handleApprovalAnswer', (coordinator) => coordinator.toggleAnswer({ questionIndex: opts.questionIndex, option: opts.option }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
1821
1673
|
}
|
|
1822
1674
|
/**
|
|
1823
1675
|
* Claude AskUserQuestion 多选问题的「提交答案」按钮。
|
|
1824
1676
|
* 路由到 ApprovalCoordinator.submitAnswers。
|
|
1825
1677
|
*/
|
|
1826
1678
|
async handleApprovalAnswerSubmit(opts) {
|
|
1827
|
-
|
|
1828
|
-
log.info(`[lark-remote] handleApprovalAnswerSubmit runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
|
|
1829
|
-
return this.withCoordinator(opts.runId, (coordinator) => coordinator.submitAnswers({ questionIndex: opts.questionIndex }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
1679
|
+
return this.dispatchQuestionAction(opts, 'handleApprovalAnswerSubmit', (coordinator) => coordinator.submitAnswers({ questionIndex: opts.questionIndex }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
1830
1680
|
}
|
|
1831
1681
|
/**
|
|
1832
1682
|
* Claude AskUserQuestion 自定义答案(Other):自由文本直接
|
|
1833
1683
|
* 作为该单选问题的答案提交。路由到 ApprovalCoordinator.answerCustom。
|
|
1834
1684
|
*/
|
|
1835
1685
|
async handleApprovalAnswerCustom(opts) {
|
|
1836
|
-
|
|
1837
|
-
log.info(`[lark-remote] handleApprovalAnswerCustom runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
|
|
1838
|
-
return this.withCoordinator(opts.runId, (coordinator) => coordinator.answerCustom({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
1686
|
+
return this.dispatchQuestionAction(opts, 'handleApprovalAnswerCustom', (coordinator) => coordinator.answerCustom({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
1839
1687
|
}
|
|
1840
1688
|
/**
|
|
1841
1689
|
* Codex AskUserQuestion 补充说明(user_note,2026-08-18 live 验证模型理解
|
|
@@ -1843,9 +1691,11 @@ export class Bridge {
|
|
|
1843
1691
|
* 路由到 ApprovalCoordinator.answerNote。
|
|
1844
1692
|
*/
|
|
1845
1693
|
async handleApprovalAnswerNote(opts) {
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1694
|
+
return this.dispatchQuestionAction(opts, 'handleApprovalAnswerNote', (coordinator) => coordinator.answerNote({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
|
|
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);
|
|
1849
1699
|
}
|
|
1850
1700
|
/**
|
|
1851
1701
|
* 记下一次「可压缩 run」:终态卡上的 Compact 按钮点击时据此反查
|
|
@@ -2326,8 +2176,8 @@ export class Bridge {
|
|
|
2326
2176
|
try {
|
|
2327
2177
|
await this.connector.sendWithRetry(ctx.chatId, { card: renderBashCard(cardSession.currentState, {}) }, { replyTo: ctx.messageId });
|
|
2328
2178
|
}
|
|
2329
|
-
catch {
|
|
2330
|
-
|
|
2179
|
+
catch (fallbackErr) {
|
|
2180
|
+
getLogger().error(`[lark-remote] bash terminal fallback card failed runId=${runId} chatId=${ctx.chatId}:`, fallbackErr);
|
|
2331
2181
|
}
|
|
2332
2182
|
}
|
|
2333
2183
|
if (this.activeBashRuns.has(runId)) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AppConfig } from '../config/index.js';
|
|
2
|
+
import type { AgentKind, Runner } from '../runner/index.js';
|
|
3
|
+
import type { RunCardSession } from '../card/run-card-session.js';
|
|
4
|
+
import { ApprovalCoordinator } from './approval-coordinator.js';
|
|
5
|
+
export declare function remindPendingApproval(connector: {
|
|
6
|
+
removeReactionByEmoji(messageId: string, emoji: string): Promise<void>;
|
|
7
|
+
addReaction(messageId: string, emoji: string): Promise<void>;
|
|
8
|
+
}, messageId: string, runId: string): void;
|
|
9
|
+
export declare function createRunApprovalCoordinator(opts: {
|
|
10
|
+
runner: Runner;
|
|
11
|
+
cardSession: RunCardSession;
|
|
12
|
+
config: AppConfig;
|
|
13
|
+
runId: string;
|
|
14
|
+
agentKind: AgentKind;
|
|
15
|
+
}): ApprovalCoordinator;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DEFAULT_APPROVAL_TIMEOUT_MS } from '../config/index.js';
|
|
2
|
+
import { ApprovalCoordinator } from './approval-coordinator.js';
|
|
3
|
+
import { getLogger } from '../logger/index.js';
|
|
4
|
+
import { errorMessage } from './final-usage.js';
|
|
5
|
+
export function remindPendingApproval(connector, messageId, runId) {
|
|
6
|
+
void (async () => {
|
|
7
|
+
try {
|
|
8
|
+
await connector.removeReactionByEmoji(messageId, 'Typing');
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
getLogger().warn(`[lark-remote] remove Typing reaction failed runId=${runId}: ${errorMessage(err)}`);
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
await connector.addReaction(messageId, 'Typing');
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
getLogger().warn(`[lark-remote] re-add Typing reaction failed runId=${runId}: ${errorMessage(err)}`);
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
20
|
+
}
|
|
21
|
+
export function createRunApprovalCoordinator(opts) {
|
|
22
|
+
return new ApprovalCoordinator({
|
|
23
|
+
approvalTimeoutMs: opts.agentKind === 'claude'
|
|
24
|
+
? (opts.config.claude?.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS)
|
|
25
|
+
: DEFAULT_APPROVAL_TIMEOUT_MS,
|
|
26
|
+
responder: async (requestId, response) => {
|
|
27
|
+
const runner = opts.runner;
|
|
28
|
+
if (typeof runner.respondApproval === 'function') {
|
|
29
|
+
await runner.respondApproval(requestId, response);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
interruptTurn: async () => {
|
|
33
|
+
await opts.runner.stop();
|
|
34
|
+
},
|
|
35
|
+
pushToCard: async (events) => {
|
|
36
|
+
for (const event of events)
|
|
37
|
+
await opts.cardSession.push(event);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=run-approval.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ResultEvent } from '../runner/types.js';
|
|
2
|
+
import type { FinalUsageSnapshot } from './final-usage.js';
|
|
3
|
+
export type UsageAuthority = 'live' | 'jsonl';
|
|
4
|
+
export declare class RunUsageCollector {
|
|
5
|
+
private contextLength?;
|
|
6
|
+
private finalContextLength?;
|
|
7
|
+
private finalContextLimit?;
|
|
8
|
+
private finalCompactCount?;
|
|
9
|
+
private finalCacheReadTokens?;
|
|
10
|
+
private finalCacheCreationTokens?;
|
|
11
|
+
private finalTotalTokens?;
|
|
12
|
+
private liveInput?;
|
|
13
|
+
private liveOutput?;
|
|
14
|
+
private liveContextLimit?;
|
|
15
|
+
private finalInputTokens?;
|
|
16
|
+
private finalOutputTokens?;
|
|
17
|
+
private finalCumulativeTotalTokens?;
|
|
18
|
+
private finalCumulativeInputTokens?;
|
|
19
|
+
private finalCumulativeOutputTokens?;
|
|
20
|
+
private finalCumulativeCacheReadTokens?;
|
|
21
|
+
private finalCumulativeCacheCreationTokens?;
|
|
22
|
+
private finalReasoningTokens?;
|
|
23
|
+
resultNotSuccess: boolean;
|
|
24
|
+
get liveInputTokens(): number | undefined;
|
|
25
|
+
get liveOutputTokens(): number | undefined;
|
|
26
|
+
observeCompaction(postTokens: number): void;
|
|
27
|
+
observeResult(event: ResultEvent): void;
|
|
28
|
+
finalize(finalUsage: FinalUsageSnapshot | undefined, authority: UsageAuthority | undefined): void;
|
|
29
|
+
meta(): {
|
|
30
|
+
contextLength?: number;
|
|
31
|
+
contextLimit?: number;
|
|
32
|
+
compactCount?: number;
|
|
33
|
+
cacheReadTokens?: number;
|
|
34
|
+
cacheCreationTokens?: number;
|
|
35
|
+
totalTokens?: number;
|
|
36
|
+
inputTokens?: number;
|
|
37
|
+
outputTokens?: number;
|
|
38
|
+
cumulativeTotalTokens?: number;
|
|
39
|
+
cumulativeInputTokens?: number;
|
|
40
|
+
cumulativeOutputTokens?: number;
|
|
41
|
+
cumulativeCacheReadTokens?: number;
|
|
42
|
+
cumulativeCacheCreationTokens?: number;
|
|
43
|
+
reasoningTokens?: number;
|
|
44
|
+
};
|
|
45
|
+
logSummary(runId: string): string;
|
|
46
|
+
}
|