lark-remote 0.5.10 → 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 (126) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.en.md +1 -14
  3. package/README.md +1 -14
  4. package/dist/binder.js +7 -1
  5. package/dist/bridge/approval-coordinator.d.ts +1 -0
  6. package/dist/bridge/approval-coordinator.js +12 -24
  7. package/dist/bridge/index.d.ts +7 -0
  8. package/dist/bridge/index.js +89 -230
  9. package/dist/bridge/run-approval.d.ts +15 -0
  10. package/dist/bridge/run-approval.js +41 -0
  11. package/dist/bridge/run-usage.d.ts +46 -0
  12. package/dist/bridge/run-usage.js +96 -0
  13. package/dist/bridge/session-writeback.d.ts +18 -0
  14. package/dist/bridge/session-writeback.js +36 -0
  15. package/dist/card/approval-render.d.ts +14 -0
  16. package/dist/card/approval-render.js +155 -29
  17. package/dist/card/bash-card-session.js +10 -5
  18. package/dist/card/bash-renderer.js +11 -23
  19. package/dist/card/card-budget.js +24 -4
  20. package/dist/card/card-session.d.ts +2 -0
  21. package/dist/card/card-session.js +20 -22
  22. package/dist/card/card-shared.d.ts +4 -4
  23. package/dist/card/card-shared.js +3 -9
  24. package/dist/card/collapsible.js +3 -2
  25. package/dist/card/final-answer-overflow.d.ts +7 -0
  26. package/dist/card/final-answer-overflow.js +46 -0
  27. package/dist/card/run-card-session.js +2 -7
  28. package/dist/card/run-renderer.d.ts +14 -0
  29. package/dist/card/run-renderer.js +150 -10
  30. package/dist/card/run-state.d.ts +12 -1
  31. package/dist/card/run-state.js +73 -34
  32. package/dist/common/text-width.js +5 -0
  33. package/dist/common/tool-summary.js +4 -2
  34. package/dist/config/codex-config.js +5 -3
  35. package/dist/config/index.d.ts +4 -2
  36. package/dist/config/index.js +19 -5
  37. package/dist/inbound/turn-assembler.js +6 -0
  38. package/dist/index.js +15 -391
  39. package/dist/instance-lock.d.ts +9 -1
  40. package/dist/instance-lock.js +42 -7
  41. package/dist/message-handlers.d.ts +26 -0
  42. package/dist/message-handlers.js +275 -0
  43. package/dist/platform/identity.js +4 -3
  44. package/dist/platform/path.js +3 -1
  45. package/dist/platform/terminator-win32.d.ts +13 -1
  46. package/dist/platform/terminator-win32.js +4 -14
  47. package/dist/router/card-action-payload.d.ts +1 -1
  48. package/dist/router/card-action-payload.js +1 -1
  49. package/dist/router/commands/ls.d.ts +9 -0
  50. package/dist/router/commands/ls.js +243 -0
  51. package/dist/router/commands/order.d.ts +12 -0
  52. package/dist/router/commands/order.js +178 -0
  53. package/dist/router/commands/resume.d.ts +47 -0
  54. package/dist/router/commands/resume.js +246 -0
  55. package/dist/router/commands/ws.d.ts +15 -0
  56. package/dist/router/commands/ws.js +189 -0
  57. package/dist/router/config/claude.js +3 -3
  58. package/dist/router/config/codex.js +2 -6
  59. package/dist/router/config/common/turn-idle.d.ts +2 -0
  60. package/dist/router/config/common/turn-idle.js +9 -0
  61. package/dist/router/config/kimi.js +2 -6
  62. package/dist/router/config/opencode.js +2 -6
  63. package/dist/router/index.d.ts +14 -22
  64. package/dist/router/index.js +93 -1133
  65. package/dist/router/paging.d.ts +13 -0
  66. package/dist/router/paging.js +20 -0
  67. package/dist/router/path-validation.d.ts +27 -0
  68. package/dist/router/path-validation.js +45 -0
  69. package/dist/runner/claude/runner.d.ts +3 -4
  70. package/dist/runner/claude/runner.js +4 -5
  71. package/dist/runner/claude/session.js +1 -1
  72. package/dist/runner/codex/app-server/runner.js +12 -0
  73. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  74. package/dist/runner/codex/app-server/translator.js +65 -9
  75. package/dist/runner/common/acp/base-translator.d.ts +15 -8
  76. package/dist/runner/common/acp/base-translator.js +38 -7
  77. package/dist/runner/common/jsonl-stream.js +12 -1
  78. package/dist/runner/common/jsonrpc/connection-manager.js +11 -1
  79. package/dist/runner/common/jsonrpc/transport.d.ts +2 -1
  80. package/dist/runner/common/jsonrpc/transport.js +7 -1
  81. package/dist/runner/common/spawning-runner.js +10 -4
  82. package/dist/runner/common/turn-started.d.ts +2 -0
  83. package/dist/runner/common/turn-started.js +10 -0
  84. package/dist/runner/dsh/approval.js +16 -3
  85. package/dist/runner/dsh/auth.js +2 -3
  86. package/dist/runner/dsh/client.d.ts +32 -7
  87. package/dist/runner/dsh/client.js +115 -149
  88. package/dist/runner/dsh/daemon.d.ts +0 -2
  89. package/dist/runner/dsh/daemon.js +1 -6
  90. package/dist/runner/dsh/errors.d.ts +5 -0
  91. package/dist/runner/dsh/errors.js +11 -0
  92. package/dist/runner/dsh/exit-cancel.d.ts +0 -2
  93. package/dist/runner/dsh/exit-cancel.js +63 -24
  94. package/dist/runner/dsh/home.d.ts +2 -0
  95. package/dist/runner/dsh/home.js +8 -0
  96. package/dist/runner/dsh/inflight.d.ts +0 -2
  97. package/dist/runner/dsh/inflight.js +1 -1
  98. package/dist/runner/dsh/mux-stream.d.ts +26 -0
  99. package/dist/runner/dsh/mux-stream.js +157 -0
  100. package/dist/runner/dsh/runner.d.ts +7 -0
  101. package/dist/runner/dsh/runner.js +42 -5
  102. package/dist/runner/dsh/translator.d.ts +0 -10
  103. package/dist/runner/dsh/translator.js +1 -1
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.js +42 -5
  106. package/dist/runner/kimi/acp/translator.d.ts +8 -0
  107. package/dist/runner/kimi/acp/translator.js +11 -12
  108. package/dist/runner/opencode/acp/translator.js +1 -12
  109. package/dist/runner/pi/rpc/runner.js +33 -5
  110. package/dist/runner/pi/rpc/translator.d.ts +2 -8
  111. package/dist/runner/pi/rpc/translator.js +2 -7
  112. package/dist/runner/registry.d.ts +0 -19
  113. package/dist/runner/registry.js +0 -32
  114. package/dist/runner/resolve-agent-choices.js +5 -1
  115. package/dist/runner/sync-agent-choices.js +4 -1
  116. package/dist/runner/types.d.ts +17 -1
  117. package/dist/session/claude/session-index.js +13 -4
  118. package/dist/session/codex/rollout-reader.js +51 -26
  119. package/dist/session/common/usage-accumulator.d.ts +10 -1
  120. package/dist/session/common/usage-accumulator.js +10 -2
  121. package/dist/session/dsh/sessions.js +29 -9
  122. package/dist/session/kimi/sessions.js +11 -7
  123. package/dist/session/opencode/sessions.js +15 -7
  124. package/dist/session/pi/sessions.js +3 -2
  125. package/dist/session/session-store.js +4 -3
  126. 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 且进程已停」的死缓存,防止随访问过的工作区数量
@@ -682,8 +683,10 @@ export class Bridge {
682
683
  for (const [runId, b] of bashTargets) {
683
684
  getLogger().info(`[lark-remote] interrupt hit bash runId=${runId} cwd=${b.cwd} userId=${input.userId}`);
684
685
  await b.bashRunner.stop({ immediate: true });
685
- // Update card to show interrupted state
686
- await b.cardSession.finish('interrupted', { exitCode: -1 });
686
+ // Update card to show interrupted state. exitCode 不传(保持 null):
687
+ // bash-renderer 的「已手动终止」文案要求 exitCode === null;传 -1 会让
688
+ // 该分支不可达,中断卡退化成「退出码: -1」的失败样式。
689
+ await b.cardSession.finish('interrupted');
687
690
  this.activeBashRuns.delete(runId);
688
691
  getLogger().info(`[lark-remote] interrupt bash stop done runId=${runId}`);
689
692
  stoppedCount++;
@@ -812,6 +815,29 @@ export class Bridge {
812
815
  return false;
813
816
  }
814
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
+ }
815
841
  /**
816
842
  * Update an existing card in place (原地更新) instead of sending a new one.
817
843
  *
@@ -1006,6 +1032,7 @@ export class Bridge {
1006
1032
  // 放在 finalize 之后:activeRuns 槽位已释放,续跑能正常开新卡片。
1007
1033
  await this.retryAfterContentFilter({
1008
1034
  cardSession,
1035
+ activeRun,
1009
1036
  ctx,
1010
1037
  cwd,
1011
1038
  agentKind,
@@ -1042,6 +1069,12 @@ export class Bridge {
1042
1069
  getLogger().info(`[lark-remote] content_filter auto-retry skipped: session epoch moved runId=${state.runId}`);
1043
1070
  return;
1044
1071
  }
1072
+ // 续跑必须接「本轮实际使用的 session」而非入队时刻快照(与 finalize 路径
1073
+ // 的三级优先同口径:卡片状态 → activeRun → 起点快照)。快照只在 run 未建立
1074
+ // 任何新会话时兜底。可证场景:新会话第一条消息(快照 undefined)被截断——
1075
+ // init 已建立 S1,epoch 未动,传快照会让续跑落到全新空会话,本轮上下文
1076
+ // 全部丢失;claude --resume fork 场景同理(init 上报的 id 才是权威)。
1077
+ const retrySessionId = state.sessionId ?? params.activeRun.sessionId ?? params.sessionId;
1045
1078
  getLogger().warn(`[lark-remote] content_filter detected, auto-retrying once runId=${state.runId} cwd=${params.cwd}`);
1046
1079
  await this.sendResult({
1047
1080
  text: '🔁 上一回合被服务方内容审核截断(content_filter,通常是误报),已自动重试一次。',
@@ -1052,7 +1085,7 @@ export class Bridge {
1052
1085
  cwdOverride: params.cwd,
1053
1086
  binding: {
1054
1087
  agent: params.agentKind,
1055
- sessionId: params.sessionId,
1088
+ sessionId: retrySessionId,
1056
1089
  sessionEpoch: params.sessionEpochAtStart,
1057
1090
  },
1058
1091
  autoRetry: true,
@@ -1164,60 +1197,7 @@ export class Bridge {
1164
1197
  // init the result was the only terminal signal we had. Keep it so the card
1165
1198
  // shows the real error instead of the generic "no result" fallback.
1166
1199
  let pendingPreInitTerminal;
1167
- // 记录 result 是否为 error: error 时跳过 jsonl usage 读取 (见 stream end 处说明)
1168
- let resultNotSuccess = false;
1169
- // Final usage read from jsonl after the run completes. live stream-json does
1170
- // not emit compact_boundary, so the live contextLength is unreliable.
1171
- let finalContextLength;
1172
- let finalContextLimit;
1173
- let finalCompactCount;
1174
- let finalCacheReadTokens;
1175
- let finalCacheCreationTokens;
1176
- let finalTotalTokens;
1177
- // Real input/output tokens captured from the live result event (codex/
1178
- // opencode carry them in ResultEvent.usage). Threaded to the card so the
1179
- // done card shows real values instead of the 10% estimate.
1180
- let liveInputTokens;
1181
- let liveOutputTokens;
1182
- // Real context window limit from the live result event (codex app-server
1183
- // carries it in ResultEvent.usage.context_limit). jsonl wins when present;
1184
- // live is the fallback (app-server error runs / jsonl 未落盘时).
1185
- let liveContextLimit;
1186
- // Final input/output for the done card: live value wins (codex/opencode
1187
- // carry per-run usage in the result event); jsonl is the fallback for
1188
- // agents whose live stream has no usage (kimi).
1189
- let finalInputTokens;
1190
- let finalOutputTokens;
1191
- // Session-cumulative input/output (all runs) read from jsonl; threaded to
1192
- // the done card's "累计" display. Always from jsonl (live has no cumulative).
1193
- let finalCumulativeTotalTokens;
1194
- let finalCumulativeInputTokens;
1195
- let finalCumulativeOutputTokens;
1196
- let finalCumulativeCacheReadTokens;
1197
- let finalCumulativeCacheCreationTokens;
1198
- // Reasoning tokens (pi usage.reasoning) from the live result event.
1199
- let finalReasoningTokens;
1200
- // 统一的 usage meta 构造(finalize 各分支共享,本方法内原先 5 处手写 11-15
1201
- // 字段对象;第 6 处 streamCompact 形状不同未收敛)。
1202
- // catch 路径需覆盖 flow 字段时在展开后覆写即可。catch error 路径亦复用此
1203
- // 闭包,相比旧手写对象补齐了累计 cache 字段(cumulativeCacheReadTokens/
1204
- // cumulativeCacheCreationTokens),属有意的口径对齐(向 done 路径看齐)。
1205
- const usageMeta = () => ({
1206
- contextLength: finalContextLength,
1207
- contextLimit: finalContextLimit,
1208
- compactCount: finalCompactCount,
1209
- cacheReadTokens: finalCacheReadTokens,
1210
- cacheCreationTokens: finalCacheCreationTokens,
1211
- totalTokens: finalTotalTokens,
1212
- inputTokens: finalInputTokens,
1213
- outputTokens: finalOutputTokens,
1214
- cumulativeTotalTokens: finalCumulativeTotalTokens,
1215
- cumulativeInputTokens: finalCumulativeInputTokens,
1216
- cumulativeOutputTokens: finalCumulativeOutputTokens,
1217
- cumulativeCacheReadTokens: finalCumulativeCacheReadTokens,
1218
- cumulativeCacheCreationTokens: finalCumulativeCacheCreationTokens,
1219
- reasoningTokens: finalReasoningTokens,
1220
- });
1200
+ const usage = new RunUsageCollector();
1221
1201
  try {
1222
1202
  try {
1223
1203
  // queue.immediate 的 run-start hold:目标槽位可能在 handler 发最终
@@ -1247,7 +1227,6 @@ export class Bridge {
1247
1227
  runner,
1248
1228
  pendingApprovals: () => this.approvalCoordinators.get(runId)?.pendingCount() ?? 0,
1249
1229
  });
1250
- let contextLength;
1251
1230
  getLogger().info(`[lark-remote] runner.run() begin runId=${runId} message=${message.slice(0, 100)}`);
1252
1231
  // Extract agent-specific options from config
1253
1232
  const agentOpts = this.getAgentRunOptions(agentKind);
@@ -1258,6 +1237,14 @@ export class Bridge {
1258
1237
  // ending (sawResult, usage capture, or cardSession.push would all
1259
1238
  // be wrong for the historical event).
1260
1239
  let sawInit = false;
1240
+ const writeback = {
1241
+ sessionStore: this.sessionStore,
1242
+ userId: ctx.userId,
1243
+ agentKind,
1244
+ cwd,
1245
+ sessionEpochAtStart,
1246
+ runId,
1247
+ };
1261
1248
  for await (const event of runner.run(message, runOpts)) {
1262
1249
  // 只有终态才阻止事件处理。finalizing 是非终态,应该继续处理
1263
1250
  // 这样在 result 事件后,后台任务输出仍能被正确处理
@@ -1270,34 +1257,7 @@ export class Bridge {
1270
1257
  // 本轮真实使用的 session(不可变身份):指针在 run 中被 /new 等移动时,
1271
1258
  // 结束后的 usage 读取仍以它为准。
1272
1259
  activeRun.sessionId = event.session_id;
1273
- // 代际守卫:run 在途时 /new(或 new-session 卡片、/cd、/resume、
1274
- // /config 切换)移动了 session 指针,此 init 的写回是 stale 的——
1275
- // 跳过,否则 /new 的清空会被在途 run 静默撤销(2026-08-09 事故:
1276
- // task-notification 注入触发 Claude 重发 init,已终止的 run 被复活)。
1277
- const pointerMoved = this.sessionStore.getSessionEpoch(ctx.userId, agentKind) !== sessionEpochAtStart;
1278
- if (pointerMoved) {
1279
- getLogger().info(`[lark-remote] system.init write-back skipped: session pointer moved since run start runId=${runId} sessionId=${event.session_id}`);
1280
- }
1281
- else {
1282
- // Use session's real directory from event.cwd (not runner's cwd parameter)
1283
- // guard empty string too -- `??` only catches null/undefined, but a
1284
- // translator (or older build) may emit cwd="", which must NOT overwrite
1285
- // the good runner cwd.
1286
- const hasCwd = !!this.sessionStore.getCwd(ctx.userId);
1287
- const realCwd = event.cwd && event.cwd.length > 0 ? event.cwd : cwd;
1288
- if (hasCwd && event.cwd && event.cwd !== cwd) {
1289
- // Session has its own cwd (e.g. EnterWorktree relocate). Keep the
1290
- // cwd unchanged; record the session's actual cwd in sessionCwds.
1291
- getLogger().info(`[lark-remote] system.init: cwd=${cwd}, session cwd=${event.cwd}`);
1292
- this.sessionStore.setSessionIdAndSessionCwd(ctx.userId, agentKind, event.session_id, realCwd);
1293
- }
1294
- else {
1295
- // First use (no cwd yet) or event.cwd matches runner cwd:
1296
- // set sessionId + cwd as before; on first use also bootstrap sessionCwds.
1297
- const bootstrapSessionCwd = !hasCwd && event.cwd && event.cwd.length > 0 ? event.cwd : undefined;
1298
- this.sessionStore.setSessionIdAndCwd(ctx.userId, agentKind, event.session_id, realCwd, bootstrapSessionCwd);
1299
- }
1300
- }
1260
+ writeBackInitSession(writeback, event);
1301
1261
  getLogger().info(`[lark-remote] system.init received runId=${runId} sessionId=${event.session_id} cwd=${event.cwd}`);
1302
1262
  }
1303
1263
  if (event.type === 'turn_started') {
@@ -1307,20 +1267,13 @@ export class Bridge {
1307
1267
  // 都新建线程(thread/start 而非 thread/resume),会话无法延续。
1308
1268
  // init 缺失时以 threadId 作为本轮身份兜底。
1309
1269
  activeRun.sessionId ??= event.threadId;
1310
- const pointerMoved = this.sessionStore.getSessionEpoch(ctx.userId, agentKind) !== sessionEpochAtStart;
1311
- if (!pointerMoved) {
1312
- this.sessionStore.setSessionIdAndCwd(ctx.userId, agentKind, event.threadId, cwd);
1313
- getLogger().info(`[lark-remote] turn_started session write-back runId=${runId} sessionId=${event.threadId}`);
1314
- }
1315
- else {
1316
- getLogger().info(`[lark-remote] turn_started write-back skipped: session pointer moved since run start runId=${runId} sessionId=${event.threadId}`);
1317
- }
1270
+ writeBackTurnStarted(writeback, event);
1318
1271
  }
1319
1272
  // Track context length from compaction events
1320
1273
  if (event.type === 'system' &&
1321
1274
  event.subtype === 'compact_boundary' &&
1322
1275
  event.compactMetadata) {
1323
- contextLength = event.compactMetadata.postTokens;
1276
+ usage.observeCompaction(event.compactMetadata.postTokens);
1324
1277
  }
1325
1278
  if (event.type === 'result') {
1326
1279
  // Pre-init result guard: Claude CLI --resume emits a stale result
@@ -1360,7 +1313,7 @@ export class Bridge {
1360
1313
  if (!preInitResult) {
1361
1314
  sawResult = true;
1362
1315
  }
1363
- resultNotSuccess = event.subtype !== 'success';
1316
+ usage.observeResult(event);
1364
1317
  getLogger().info(preInitResult
1365
1318
  ? `[lark-remote] pre-init result from live runner (app-server setup failure) runId=${runId}`
1366
1319
  : `[lark-remote] result event received runId=${runId}`);
@@ -1369,83 +1322,22 @@ export class Bridge {
1369
1322
  // 里兜底上报。此处仅当 runner 声明 live usage(app-server)
1370
1323
  // 且会话指针未动时写回,避免下条消息再开一个孤儿线程。其余 agent 不在
1371
1324
  // 此列(它们各自有 system.init / turn_started 写回)。
1372
- if (usageAuthority === 'live' &&
1373
- event.session_id &&
1374
- this.sessionStore.getSessionEpoch(ctx.userId, agentKind) === sessionEpochAtStart) {
1375
- this.sessionStore.setSessionIdAndCwd(ctx.userId, agentKind, event.session_id, cwd);
1376
- getLogger().info(`[lark-remote] result session write-back runId=${runId} sessionId=${event.session_id}`);
1377
- }
1378
- // result 后进入 finalizing 非终态(暂存 subtype/errorMsg),由 for-await
1379
- // 自然结束后在 finally 转 done/error。提取 usage 信息。
1380
- if (event.usage) {
1381
- const u = normalizeResultUsage(event.usage);
1382
- // input_tokens is non-cached (ccusage-aligned), so the context
1383
- // length fallback must include cached tokens. Prefer the agent's
1384
- // declared total; otherwise reconstruct from the parts.
1385
- if (!contextLength) {
1386
- contextLength = u.contextLength;
1387
- }
1388
- // Capture real input/output for the done card (avoiding the 10%
1389
- // estimate in formatUsageStats). live 值优先:有 live input/output 时
1390
- // 所有 flow 字段统一用 live scope(本 run),避免同一张卡片上
1391
- // Input/Output 是本 run、Cache/Total 是 session 累计的混 scope 问题。
1392
- liveInputTokens = u.inputTokens;
1393
- liveOutputTokens = u.outputTokens;
1394
- liveContextLimit = u.contextLimit;
1395
- if (u.cacheReadTokens !== undefined) {
1396
- finalCacheReadTokens = u.cacheReadTokens;
1397
- }
1398
- if (u.cacheCreationTokens !== undefined) {
1399
- finalCacheCreationTokens = u.cacheCreationTokens;
1400
- }
1401
- if (u.totalTokens !== undefined) {
1402
- finalTotalTokens = u.totalTokens;
1403
- }
1404
- if (u.reasoningTokens !== undefined) {
1405
- finalReasoningTokens = u.reasoningTokens;
1406
- }
1407
- }
1325
+ if (usageAuthority === 'live')
1326
+ writeBackResultSession(writeback, event.session_id);
1408
1327
  }
1409
1328
  // Approval events (Codex app-server mode)
1410
1329
  if (event.type === 'approval_requested') {
1411
1330
  // 审批请求到达:撤回 Typing 表情再重发,让飞书重新触发提醒,把用户
1412
1331
  // 注意力拉回待审批的卡片(run 在等待人工决策,Typing 应重新闪烁)。
1413
- void (async () => {
1414
- try {
1415
- await this.connector.removeReactionByEmoji(ctx.messageId, 'Typing');
1416
- }
1417
- catch (err) {
1418
- getLogger().warn(`[lark-remote] remove Typing reaction failed runId=${runId}: ${errorMessage(err)}`);
1419
- }
1420
- try {
1421
- await this.connector.addReaction(ctx.messageId, 'Typing');
1422
- }
1423
- catch (err) {
1424
- getLogger().warn(`[lark-remote] re-add Typing reaction failed runId=${runId}: ${errorMessage(err)}`);
1425
- }
1426
- })();
1332
+ remindPendingApproval(this.connector, ctx.messageId, runId);
1427
1333
  let coordinator = this.approvalCoordinators.get(runId);
1428
1334
  if (!coordinator) {
1429
- coordinator = new ApprovalCoordinator({
1430
- // claude 审批超时走 config(claude.approvalTimeoutMs,默认 5 分钟,
1431
- // 对齐 codex 红线勿改短);codex 维持模块常量。
1432
- approvalTimeoutMs: agentKind === 'claude'
1433
- ? (this.config.claude?.approvalTimeoutMs ?? APPROVAL_TIMEOUT_MS)
1434
- : APPROVAL_TIMEOUT_MS,
1435
- responder: async (requestId, response) => {
1436
- const r = runner;
1437
- if (typeof r.respondApproval === 'function') {
1438
- await r.respondApproval(requestId, response);
1439
- }
1440
- },
1441
- interruptTurn: async () => {
1442
- // DSH may return a StopOutcome; the coordinator only awaits.
1443
- await runner.stop();
1444
- },
1445
- pushToCard: async (events) => {
1446
- for (const ev of events)
1447
- await cardSession.push(ev);
1448
- },
1335
+ coordinator = createRunApprovalCoordinator({
1336
+ runner,
1337
+ cardSession,
1338
+ config: this.config,
1339
+ runId,
1340
+ agentKind,
1449
1341
  });
1450
1342
  this.approvalCoordinators.set(runId, coordinator);
1451
1343
  getLogger().info(`[lark-remote] approval coordinator created runId=${runId.slice(0, 8)} requestId=${event.requestId}`);
@@ -1484,44 +1376,11 @@ export class Bridge {
1484
1376
  // 优先级:卡片状态(本轮 init 写入)→ activeRun(本轮 init/turn_started)
1485
1377
  // → 本轮开始时捕获的 sessionId。三者都没有就跳过 usage,绝不回读全局指针。
1486
1378
  const finalSessionId = cardSession.currentState.sessionId ?? activeRun.sessionId ?? sessionId;
1487
- const finalUsage = resultNotSuccess
1379
+ const finalUsage = usage.resultNotSuccess
1488
1380
  ? undefined
1489
1381
  : resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, finalSessionId, cwd, agentKind);
1490
- finalContextLength = finalUsage?.contextLength ?? contextLength;
1491
- finalContextLimit = finalUsage?.contextLimit ?? liveContextLimit;
1492
- finalCompactCount = finalUsage?.compactCount;
1493
- finalCumulativeTotalTokens = finalUsage?.cumulativeTotalTokens;
1494
- finalCumulativeInputTokens = finalUsage?.cumulativeInputTokens;
1495
- finalCumulativeOutputTokens = finalUsage?.cumulativeOutputTokens;
1496
- finalCumulativeCacheReadTokens = finalUsage?.cumulativeCacheReadTokens;
1497
- finalCumulativeCacheCreationTokens = finalUsage?.cumulativeCacheCreationTokens;
1498
- // 统一 flow 字段 scope:live 优先,jsonl 兜底。
1499
- // 当 live result 事件带了 input/output(即 liveInputTokens/liveOutputTokens
1500
- // 非 undefined),所有 flow 字段统一用 live scope(本 run),不用 jsonl
1501
- // 的 session 累计值覆盖,避免 cache%/Total 分子分母不同源。
1502
- // 无 live usage 时(如 kimi),jsonl 值兜底。
1503
- // contextLength/compactCount 保持 jsonl 优先(它们是水位/历史计数)。
1504
- const codexJsonlFirst = usageAuthority === 'jsonl';
1505
- if (!codexJsonlFirst && liveInputTokens !== undefined && liveOutputTokens !== undefined) {
1506
- // 非 codex 且 live 有 input/output → 所有 flow 字段用 live scope
1507
- finalInputTokens = liveInputTokens;
1508
- finalOutputTokens = liveOutputTokens;
1509
- // cacheReadTokens/cacheCreationTokens/totalTokens
1510
- // 已在上面 result 事件处理中赋了 live 值;若 live 没有则为 undefined,
1511
- // 由 formatUsageStats 的 max/兜底公式处理。
1512
- }
1513
- else {
1514
- // codex(jsonl 优先)或无 live usage → jsonl 兜底所有 flow 字段。
1515
- // jsonl 的非累计字段已是末轮(本 run)scope(claude reader 修复后对齐
1516
- // codex `last_token_usage` 语义,见 scalarScan),不再是 session 累计;
1517
- // session 累计只经 cumulative* 字段透传,卡片"本 run ≤ 累计"不变量成立。
1518
- finalInputTokens = finalUsage?.inputTokens ?? liveInputTokens;
1519
- finalOutputTokens = finalUsage?.outputTokens ?? liveOutputTokens;
1520
- finalCacheReadTokens = finalUsage?.cacheReadTokens ?? finalCacheReadTokens;
1521
- finalCacheCreationTokens = finalUsage?.cacheCreationTokens ?? finalCacheCreationTokens;
1522
- finalTotalTokens = finalUsage?.totalTokens ?? finalTotalTokens;
1523
- }
1524
- 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));
1525
1384
  // for-await 自然结束 = stream 关闭 = CLI 进程退出。
1526
1385
  // 进程退出(for-await end)= 唯一终态触发源。
1527
1386
  // 1) finalizing(result 已收到)-> 转 done/error + usage meta
@@ -1546,7 +1405,7 @@ export class Bridge {
1546
1405
  await cardSession.finish(terminal, {
1547
1406
  resultSubtype: subtype,
1548
1407
  errorMsg: finalState.errorMsg,
1549
- ...usageMeta(),
1408
+ ...usage.meta(),
1550
1409
  });
1551
1410
  }
1552
1411
  else if (finalState.terminal === 'running') {
@@ -1554,14 +1413,14 @@ export class Bridge {
1554
1413
  const terminal = pendingPreInitTerminal.subtype === 'interrupted' ? 'interrupted' : 'error';
1555
1414
  getLogger().warn(`[lark-remote] stream ended without init; using pre-init ${terminal} result runId=${runId} error=${pendingPreInitTerminal.errorMessage ?? '(none)'}`);
1556
1415
  await cardSession.finish(terminal, {
1557
- ...usageMeta(),
1416
+ ...usage.meta(),
1558
1417
  errorMsg: pendingPreInitTerminal.errorMessage ??
1559
1418
  agentDisplayName(agentKind) + (terminal === 'interrupted' ? ' 已中断' : ' 启动失败'),
1560
1419
  });
1561
1420
  }
1562
1421
  else {
1563
1422
  await cardSession.finish('error', {
1564
- ...usageMeta(),
1423
+ ...usage.meta(),
1565
1424
  errorMsg: agentDisplayName(agentKind) + ' 输出流已结束,但未收到 result 事件',
1566
1425
  });
1567
1426
  }
@@ -1569,7 +1428,7 @@ export class Bridge {
1569
1428
  else if (sawResult) {
1570
1429
  // 已终态(interrupted/idle_timeout)+ result 已收到:首终态优先,
1571
1430
  // 仅补充 usage meta(finalizing 期间被 /stop 或 idle 超时也展示 token 统计)
1572
- await cardSession.finish(finalState.terminal, usageMeta());
1431
+ await cardSession.finish(finalState.terminal, usage.meta());
1573
1432
  }
1574
1433
  // else: 已终态且未收到 result(如 spawn 失败后 interrupted)-> 无 usage 可补,跳过
1575
1434
  }
@@ -1588,16 +1447,16 @@ export class Bridge {
1588
1447
  const catchTerminal = cardSession.currentState.terminal;
1589
1448
  if (catchTerminal === 'running' || catchTerminal === 'finalizing') {
1590
1449
  await cardSession.finish('error', {
1591
- ...usageMeta(),
1592
- inputTokens: liveInputTokens,
1593
- outputTokens: liveOutputTokens,
1450
+ ...usage.meta(),
1451
+ inputTokens: usage.liveInputTokens,
1452
+ outputTokens: usage.liveOutputTokens,
1594
1453
  errorMsg: errorMessage(err),
1595
1454
  });
1596
1455
  }
1597
1456
  else {
1598
1457
  getLogger().info(`[lark-remote] skip error finish: state already terminal (${catchTerminal}) runId=${runId}`);
1599
1458
  if (sawResult) {
1600
- await cardSession.finish(catchTerminal, usageMeta());
1459
+ await cardSession.finish(catchTerminal, usage.meta());
1601
1460
  }
1602
1461
  }
1603
1462
  }
@@ -1650,10 +1509,11 @@ export class Bridge {
1650
1509
  sessionFallbackAvailable: !!(cardSession.currentState.sessionId ??
1651
1510
  this.sessionStore.getSessionId(ctx.userId, activeRun.agentKind)),
1652
1511
  };
1512
+ const finalRender = renderRunCardDetailed(cardSession.currentState, finalRenderOptions);
1653
1513
  this.rememberRunCardSnapshot(runId, cardSession.currentState, finalRenderOptions, cwd, activeRun.agentKind);
1654
1514
  let runCardSent = false;
1655
1515
  if (settleResult === 'unsent') {
1656
- const sent = await this.sendResult({ card: renderRunCard(cardSession.currentState, finalRenderOptions) }, ctx);
1516
+ const sent = await this.sendResult({ card: finalRender.card }, ctx);
1657
1517
  runCardSent = sent;
1658
1518
  }
1659
1519
  else {
@@ -1666,6 +1526,9 @@ export class Bridge {
1666
1526
  if (sessionId && runCardSent) {
1667
1527
  await this.sendCompletionNotificationCard(sessionId, cwd, ctx, activeRun.agentKind);
1668
1528
  }
1529
+ if (finalRender.finalAnswerStatus === 'overflow' && finalRender.finalAnswerText) {
1530
+ await this.sendFinalAnswerOverflow(finalRender.finalAnswerText, ctx);
1531
+ }
1669
1532
  // Add a terminal-state emoji reaction to the user's original message
1670
1533
  this.finishTurnReactions(ctx, terminalReactionEmoji(cardSession.currentState.terminal));
1671
1534
  }
@@ -1806,27 +1669,21 @@ export class Bridge {
1806
1669
  * 路由到 ApprovalCoordinator.toggleAnswer。
1807
1670
  */
1808
1671
  async handleApprovalAnswer(opts) {
1809
- const log = getLogger();
1810
- log.info(`[lark-remote] handleApprovalAnswer runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1811
- 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 }));
1812
1673
  }
1813
1674
  /**
1814
1675
  * Claude AskUserQuestion 多选问题的「提交答案」按钮。
1815
1676
  * 路由到 ApprovalCoordinator.submitAnswers。
1816
1677
  */
1817
1678
  async handleApprovalAnswerSubmit(opts) {
1818
- const log = getLogger();
1819
- log.info(`[lark-remote] handleApprovalAnswerSubmit runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1820
- 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 }));
1821
1680
  }
1822
1681
  /**
1823
1682
  * Claude AskUserQuestion 自定义答案(Other):自由文本直接
1824
1683
  * 作为该单选问题的答案提交。路由到 ApprovalCoordinator.answerCustom。
1825
1684
  */
1826
1685
  async handleApprovalAnswerCustom(opts) {
1827
- const log = getLogger();
1828
- log.info(`[lark-remote] handleApprovalAnswerCustom runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1829
- 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 }));
1830
1687
  }
1831
1688
  /**
1832
1689
  * Codex AskUserQuestion 补充说明(user_note,2026-08-18 live 验证模型理解
@@ -1834,9 +1691,11 @@ export class Bridge {
1834
1691
  * 路由到 ApprovalCoordinator.answerNote。
1835
1692
  */
1836
1693
  async handleApprovalAnswerNote(opts) {
1837
- const log = getLogger();
1838
- log.info(`[lark-remote] handleApprovalAnswerNote runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1839
- 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);
1840
1699
  }
1841
1700
  /**
1842
1701
  * 记下一次「可压缩 run」:终态卡上的 Compact 按钮点击时据此反查
@@ -2317,8 +2176,8 @@ export class Bridge {
2317
2176
  try {
2318
2177
  await this.connector.sendWithRetry(ctx.chatId, { card: renderBashCard(cardSession.currentState, {}) }, { replyTo: ctx.messageId });
2319
2178
  }
2320
- catch {
2321
- /* ignore fallback errors */
2179
+ catch (fallbackErr) {
2180
+ getLogger().error(`[lark-remote] bash terminal fallback card failed runId=${runId} chatId=${ctx.chatId}:`, fallbackErr);
2322
2181
  }
2323
2182
  }
2324
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
+ }