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.
Files changed (95) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/README.en.md +1 -14
  3. package/README.md +1 -14
  4. package/dist/bridge/approval-coordinator.d.ts +1 -0
  5. package/dist/bridge/approval-coordinator.js +12 -24
  6. package/dist/bridge/index.d.ts +7 -0
  7. package/dist/bridge/index.js +77 -227
  8. package/dist/bridge/run-approval.d.ts +15 -0
  9. package/dist/bridge/run-approval.js +41 -0
  10. package/dist/bridge/run-usage.d.ts +46 -0
  11. package/dist/bridge/run-usage.js +96 -0
  12. package/dist/bridge/session-writeback.d.ts +18 -0
  13. package/dist/bridge/session-writeback.js +36 -0
  14. package/dist/card/bash-card-session.js +2 -5
  15. package/dist/card/card-session.d.ts +2 -0
  16. package/dist/card/card-session.js +20 -22
  17. package/dist/card/card-shared.d.ts +4 -4
  18. package/dist/card/card-shared.js +3 -9
  19. package/dist/card/collapsible.js +3 -2
  20. package/dist/card/final-answer-overflow.d.ts +7 -0
  21. package/dist/card/final-answer-overflow.js +46 -0
  22. package/dist/card/run-card-session.js +2 -7
  23. package/dist/card/run-renderer.d.ts +14 -0
  24. package/dist/card/run-renderer.js +119 -1
  25. package/dist/card/run-state.d.ts +12 -1
  26. package/dist/card/run-state.js +73 -34
  27. package/dist/config/codex-config.js +4 -4
  28. package/dist/config/index.d.ts +4 -2
  29. package/dist/config/index.js +6 -4
  30. package/dist/inbound/turn-assembler.js +6 -0
  31. package/dist/index.js +15 -409
  32. package/dist/message-handlers.d.ts +26 -0
  33. package/dist/message-handlers.js +275 -0
  34. package/dist/router/card-action-payload.d.ts +1 -1
  35. package/dist/router/card-action-payload.js +1 -1
  36. package/dist/router/commands/ls.d.ts +9 -0
  37. package/dist/router/commands/ls.js +243 -0
  38. package/dist/router/commands/order.d.ts +12 -0
  39. package/dist/router/commands/order.js +178 -0
  40. package/dist/router/commands/resume.d.ts +47 -0
  41. package/dist/router/commands/resume.js +246 -0
  42. package/dist/router/commands/ws.d.ts +15 -0
  43. package/dist/router/commands/ws.js +189 -0
  44. package/dist/router/config/claude.js +3 -3
  45. package/dist/router/config/codex.js +2 -6
  46. package/dist/router/config/common/turn-idle.d.ts +2 -0
  47. package/dist/router/config/common/turn-idle.js +9 -0
  48. package/dist/router/config/kimi.js +2 -6
  49. package/dist/router/config/opencode.js +2 -6
  50. package/dist/router/index.d.ts +14 -22
  51. package/dist/router/index.js +70 -1152
  52. package/dist/router/paging.d.ts +13 -0
  53. package/dist/router/paging.js +20 -0
  54. package/dist/router/path-validation.d.ts +27 -0
  55. package/dist/router/path-validation.js +45 -0
  56. package/dist/runner/claude/runner.d.ts +3 -4
  57. package/dist/runner/claude/runner.js +4 -5
  58. package/dist/runner/claude/session.js +1 -1
  59. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  60. package/dist/runner/codex/app-server/translator.js +65 -9
  61. package/dist/runner/common/acp/base-translator.d.ts +3 -8
  62. package/dist/runner/common/acp/base-translator.js +16 -7
  63. package/dist/runner/common/spawning-runner.js +10 -4
  64. package/dist/runner/common/turn-started.d.ts +2 -0
  65. package/dist/runner/common/turn-started.js +10 -0
  66. package/dist/runner/dsh/auth.js +2 -3
  67. package/dist/runner/dsh/client.d.ts +1 -6
  68. package/dist/runner/dsh/client.js +11 -165
  69. package/dist/runner/dsh/daemon.d.ts +0 -2
  70. package/dist/runner/dsh/daemon.js +1 -6
  71. package/dist/runner/dsh/errors.d.ts +5 -0
  72. package/dist/runner/dsh/errors.js +11 -0
  73. package/dist/runner/dsh/exit-cancel.d.ts +0 -2
  74. package/dist/runner/dsh/exit-cancel.js +1 -1
  75. package/dist/runner/dsh/home.d.ts +2 -0
  76. package/dist/runner/dsh/home.js +8 -0
  77. package/dist/runner/dsh/inflight.d.ts +0 -2
  78. package/dist/runner/dsh/inflight.js +1 -1
  79. package/dist/runner/dsh/mux-stream.d.ts +26 -0
  80. package/dist/runner/dsh/mux-stream.js +157 -0
  81. package/dist/runner/dsh/translator.d.ts +0 -10
  82. package/dist/runner/dsh/translator.js +1 -1
  83. package/dist/runner/index.d.ts +1 -1
  84. package/dist/runner/kimi/acp/translator.js +1 -12
  85. package/dist/runner/opencode/acp/translator.js +1 -12
  86. package/dist/runner/pi/rpc/translator.d.ts +2 -8
  87. package/dist/runner/pi/rpc/translator.js +2 -7
  88. package/dist/runner/registry.d.ts +0 -19
  89. package/dist/runner/registry.js +0 -32
  90. package/dist/runner/types.d.ts +17 -1
  91. package/dist/session/claude/session-index.js +4 -2
  92. package/dist/session/kimi/sessions.js +2 -2
  93. package/dist/session/opencode/sessions.js +2 -1
  94. package/dist/session/pi/sessions.js +3 -2
  95. package/package.json +1 -1
@@ -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 { normalizeResultUsage } from '../runner/common/result-usage.js';
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 { ApprovalCoordinator, decisionToApprovalAction } from './approval-coordinator.js';
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
- // 记录 result 是否为 error: error 时跳过 jsonl usage 读取 (见 stream end 处说明)
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
- // 代际守卫:run 在途时 /new(或 new-session 卡片、/cd、/resume、
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
- const pointerMoved = this.sessionStore.getSessionEpoch(ctx.userId, agentKind) !== sessionEpochAtStart;
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
- contextLength = event.compactMetadata.postTokens;
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
- resultNotSuccess = event.subtype !== 'success';
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
- void (async () => {
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 = new ApprovalCoordinator({
1439
- // claude 审批超时走 config(claude.approvalTimeoutMs,默认 5 分钟,
1440
- // 对齐 codex 红线勿改短);codex 维持模块常量。
1441
- approvalTimeoutMs: agentKind === 'claude'
1442
- ? (this.config.claude?.approvalTimeoutMs ?? APPROVAL_TIMEOUT_MS)
1443
- : APPROVAL_TIMEOUT_MS,
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
- finalContextLength = finalUsage?.contextLength ?? contextLength;
1500
- finalContextLimit = finalUsage?.contextLimit ?? liveContextLimit;
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
- ...usageMeta(),
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
- ...usageMeta(),
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
- ...usageMeta(),
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, usageMeta());
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
- ...usageMeta(),
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, usageMeta());
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: renderRunCard(cardSession.currentState, finalRenderOptions) }, ctx);
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
- const log = getLogger();
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
- const log = getLogger();
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
- const log = getLogger();
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
- const log = getLogger();
1847
- log.info(`[lark-remote] handleApprovalAnswerNote runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1848
- return this.withCoordinator(opts.runId, (coordinator) => coordinator.answerNote({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
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
- /* ignore fallback errors */
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
+ }