lark-remote 0.5.13 → 0.5.14

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 +15 -0
  2. package/dist/agent-kinds.d.ts +27 -0
  3. package/dist/agent-kinds.js +38 -0
  4. package/dist/bridge/approval-actions.d.ts +95 -0
  5. package/dist/bridge/approval-actions.js +104 -0
  6. package/dist/bridge/final-usage.d.ts +8 -17
  7. package/dist/bridge/final-usage.js +1 -18
  8. package/dist/bridge/index.d.ts +46 -39
  9. package/dist/bridge/index.js +201 -229
  10. package/dist/bridge/queue-manager.d.ts +25 -1
  11. package/dist/bridge/queue-manager.js +179 -160
  12. package/dist/bridge/run-approval.d.ts +2 -1
  13. package/dist/bridge/run-usage.d.ts +16 -30
  14. package/dist/bridge/run-usage.js +41 -50
  15. package/dist/bridge/session-writeback.d.ts +3 -2
  16. package/dist/card/approval-render.d.ts +3 -2
  17. package/dist/card/approval-render.js +3 -2
  18. package/dist/{router → card}/card-helpers.d.ts +7 -7
  19. package/dist/{router → card}/card-helpers.js +8 -8
  20. package/dist/card/card-session.d.ts +4 -2
  21. package/dist/card/card-session.js +8 -6
  22. package/dist/card/card-shared.d.ts +1 -1
  23. package/dist/card/card-shared.js +1 -1
  24. package/dist/card/final-answer-overflow.d.ts +0 -6
  25. package/dist/card/final-answer-overflow.js +1 -1
  26. package/dist/card/run-card-session.d.ts +2 -2
  27. package/dist/card/run-card-session.js +2 -2
  28. package/dist/card/run-renderer.js +7 -22
  29. package/dist/card/run-state.d.ts +28 -50
  30. package/dist/card/run-state.js +23 -0
  31. package/dist/card/usage-format.d.ts +30 -0
  32. package/dist/{router → card}/usage-format.js +3 -10
  33. package/dist/clone.js +6 -1
  34. package/dist/config/index.d.ts +16 -20
  35. package/dist/config/index.js +0 -16
  36. package/dist/config/pi-config.js +6 -4
  37. package/dist/connector/index.d.ts +2 -4
  38. package/dist/connector/index.js +7 -11
  39. package/dist/{runner/dsh → dsh}/auth.js +1 -1
  40. package/dist/dsh/protocol.d.ts +9 -0
  41. package/dist/dsh/protocol.js +61 -0
  42. package/dist/index.js +10 -5
  43. package/dist/model-catalog.d.ts +14 -0
  44. package/dist/model-catalog.js +25 -0
  45. package/dist/platform/path.d.ts +9 -0
  46. package/dist/platform/path.js +18 -0
  47. package/dist/router/active-run-usage.d.ts +4 -0
  48. package/dist/router/active-run-usage.js +12 -0
  49. package/dist/router/commands/ls.js +1 -1
  50. package/dist/router/commands/order.js +1 -1
  51. package/dist/router/commands/resume.d.ts +5 -4
  52. package/dist/router/commands/resume.js +4 -4
  53. package/dist/router/commands/ws.js +1 -1
  54. package/dist/router/config/claude.js +2 -1
  55. package/dist/router/config/common/render.d.ts +5 -1
  56. package/dist/router/config/common/render.js +6 -13
  57. package/dist/router/config/dsh.d.ts +1 -1
  58. package/dist/router/config/index.d.ts +1 -1
  59. package/dist/router/config/types.d.ts +11 -3
  60. package/dist/router/index.d.ts +41 -11
  61. package/dist/router/index.js +165 -226
  62. package/dist/runner/agent-choices-common.d.ts +1 -1
  63. package/dist/runner/claude/runner.d.ts +20 -2
  64. package/dist/runner/claude/runner.js +2 -15
  65. package/dist/runner/codex/app-server/runner.d.ts +3 -1
  66. package/dist/runner/codex/app-server/translator.d.ts +11 -0
  67. package/dist/runner/codex/app-server/translator.js +56 -149
  68. package/dist/runner/common/acp/base-acp-runner.d.ts +3 -1
  69. package/dist/runner/common/connection-based-runner.d.ts +3 -1
  70. package/dist/runner/common/jsonrpc/transport.js +17 -7
  71. package/dist/runner/dsh/client.d.ts +1 -4
  72. package/dist/runner/dsh/client.js +2 -55
  73. package/dist/runner/dsh/daemon.js +9 -2
  74. package/dist/runner/dsh/exit-cancel.js +1 -1
  75. package/dist/runner/dsh/mux-stream.js +21 -2
  76. package/dist/runner/dsh/runner.d.ts +21 -1
  77. package/dist/runner/dsh/runner.js +76 -73
  78. package/dist/runner/dsh/translator.d.ts +1 -1
  79. package/dist/runner/index.d.ts +1 -1
  80. package/dist/runner/kimi/acp/runner.d.ts +22 -1
  81. package/dist/runner/kimi/acp/runner.js +83 -60
  82. package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
  83. package/dist/runner/opencode/acp/error-monitor.js +31 -2
  84. package/dist/runner/opencode/acp/runner.d.ts +3 -1
  85. package/dist/runner/pi/rpc/runner.d.ts +2 -1
  86. package/dist/runner/probe.d.ts +1 -1
  87. package/dist/runner/probe.js +10 -9
  88. package/dist/runner/registry.d.ts +2 -1
  89. package/dist/runner/types.d.ts +20 -154
  90. package/dist/runner/types.js +4 -24
  91. package/dist/session/claude/session-index.d.ts +11 -4
  92. package/dist/session/claude/session-index.js +30 -8
  93. package/dist/session/claude/session-reader.d.ts +1 -1
  94. package/dist/session/claude/sessions.d.ts +2 -2
  95. package/dist/session/claude/sessions.js +41 -26
  96. package/dist/session/codex/rollout-reader.d.ts +1 -1
  97. package/dist/session/codex/rollout-reader.js +14 -13
  98. package/dist/session/codex/sessions.d.ts +1 -1
  99. package/dist/session/codex/sessions.js +4 -2
  100. package/dist/session/common/jsonl.js +7 -1
  101. package/dist/session/common/pagination.d.ts +2 -0
  102. package/dist/session/common/pagination.js +1 -1
  103. package/dist/session/common/recency.d.ts +6 -0
  104. package/dist/session/common/recency.js +7 -5
  105. package/dist/session/common/two-pass.d.ts +1 -1
  106. package/dist/session/common/usage-accumulator.d.ts +1 -1
  107. package/dist/session/dsh/sessions.d.ts +1 -1
  108. package/dist/session/dsh/sessions.js +37 -12
  109. package/dist/session/kimi/sessions.d.ts +1 -1
  110. package/dist/session/kimi/sessions.js +35 -8
  111. package/dist/session/opencode/sessions.d.ts +1 -1
  112. package/dist/session/opencode/sessions.js +51 -47
  113. package/dist/session/pi/sessions.d.ts +1 -1
  114. package/dist/session/pi/sessions.js +20 -7
  115. package/dist/session/registry.d.ts +2 -1
  116. package/dist/session/session-store.d.ts +1 -2
  117. package/dist/session/session-store.js +14 -7
  118. package/dist/session-contract.d.ts +178 -0
  119. package/dist/session-contract.js +9 -0
  120. package/package.json +1 -1
  121. package/dist/router/usage-format.d.ts +0 -60
  122. /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
  123. /package/dist/{runner/dsh → dsh}/home.d.ts +0 -0
  124. /package/dist/{runner/dsh → dsh}/home.js +0 -0
  125. /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
  126. /package/dist/{runner/dsh → dsh}/types.js +0 -0
@@ -8,15 +8,16 @@ import { renderRunCard, renderRunCardDetailed, } from '../card/run-renderer.js';
8
8
  import { splitFinalAnswer } from '../card/final-answer-overflow.js';
9
9
  import { renderBashCard } from '../card/bash-renderer.js';
10
10
  import { BashCardSession, capBashOutput } from '../card/bash-card-session.js';
11
- import { isRunTerminal } from '../card/run-state.js';
12
- import { buildSessionHistoryCard } from '../router/card-helpers.js';
11
+ import { isRunActive, isRunTerminal, removeBackgroundJob, } from '../card/run-state.js';
12
+ import { buildSessionHistoryCard } from '../card/card-helpers.js';
13
13
  import { agentDisplayName, resumeUseButton } from '../card/card-shared.js';
14
14
  import { enforceCardBudget } from '../card/card-budget.js';
15
15
  import { RunUsageCollector } from './run-usage.js';
16
16
  import { BashProcessRunner } from '../runner/index.js';
17
- import { AGENT_KINDS, supportsCompact } from '../runner/types.js';
17
+ import { AGENT_KINDS } from '../agent-kinds.js';
18
+ import { supportsCompact } from '../runner/types.js';
18
19
  import { QueueManager, } from './queue-manager.js';
19
- import { decisionToApprovalAction } from './approval-coordinator.js';
20
+ import { ApprovalActions } from './approval-actions.js';
20
21
  import { createRunApprovalCoordinator, remindPendingApproval } from './run-approval.js';
21
22
  import { writeBackInitSession, writeBackResultSession, writeBackTurnStarted, } from './session-writeback.js';
22
23
  import { InboundMediaHandler } from './inbound-media.js';
@@ -142,6 +143,10 @@ export class Bridge {
142
143
  activeRuns = new Map();
143
144
  /** Approval coordinators keyed by runId. */
144
145
  approvalCoordinators = new Map();
146
+ /** 审批卡动作的实现(只读上面那张表;见 approval-actions.ts)。 */
147
+ approvalActions = new ApprovalActions({
148
+ coordinatorFor: (runId) => this.approvalCoordinators.get(runId),
149
+ });
145
150
  /**
146
151
  * 最近若干次已完成的、runner 有 runCompact 的 run(cwd → runId → 记录)。
147
152
  * run 结束后 activeRuns 已清空,而终态卡上的 Compact 按钮只带 runId,
@@ -224,13 +229,8 @@ export class Bridge {
224
229
  }
225
230
  // Create new runner for this (cwd, kind) slot
226
231
  const runner = this.agentRegistry.get(kind, cwd);
227
- // Call lifecycle methods for agent runners created via factory
228
- if (runner && typeof runner.killOrphan === 'function') {
229
- runner.killOrphan();
230
- }
231
- if (runner && typeof runner.registerExitHandlers === 'function') {
232
- runner.registerExitHandlers();
233
- }
232
+ runner.killOrphan();
233
+ runner.registerExitHandlers();
234
234
  // Store in nested map: Map<cwd, Map<kind, Runner>>。必须重新取一次:
235
235
  // replaceStale 刚把槽位 evict 掉,上面的局部 workspaceMap 可能已随空槽
236
236
  // 从 this.runners 移除——继续写它会生成一个「缓存里查不到」的 runner,
@@ -589,7 +589,7 @@ export class Bridge {
589
589
  for (const [_cwd, active] of this.activeRuns) {
590
590
  const state = active.session.currentState;
591
591
  // Only include non-terminal runs
592
- if (state.terminal === 'running' || state.terminal === 'finalizing') {
592
+ if (isRunActive(state.terminal)) {
593
593
  result.push({
594
594
  runId: active.runId,
595
595
  sessionId: state.sessionId ?? '',
@@ -841,7 +841,7 @@ export class Bridge {
841
841
  /**
842
842
  * Update an existing card in place (原地更新) instead of sending a new one.
843
843
  *
844
- * Used by /config cardAction handlers (config.toggle/set/input/save) so the
844
+ * Used by /config cardAction handlers (config.set/input/save) so the
845
845
  * user sees the card refresh on the same message bubble rather than getting
846
846
  * a new card per click — same-message-bubble update pattern.
847
847
  *
@@ -887,7 +887,8 @@ export class Bridge {
887
887
  return true;
888
888
  }
889
889
  catch (err) {
890
- getLogger().debug(`[lark-remote] patchCard failed messageId=${messageId}: ${errorMessage(err)}`);
890
+ // 调用方会据此把卡片从集合里永久剔除——这是用户可见的退化,warn 起步。
891
+ getLogger().warn(`[lark-remote] patchCard failed messageId=${messageId}: ${errorMessage(err)}`);
891
892
  return false;
892
893
  }
893
894
  }
@@ -925,10 +926,7 @@ export class Bridge {
925
926
  const killed = await runner.killJob(sessionId, jobId);
926
927
  if (!killed)
927
928
  return false;
928
- snapshot.state = {
929
- ...snapshot.state,
930
- backgroundJobs: (snapshot.state.backgroundJobs ?? []).filter((job) => job.id !== jobId),
931
- };
929
+ snapshot.state = removeBackgroundJob(snapshot.state, jobId);
932
930
  await this.updateCardInPlace(renderRunCard(snapshot.state, snapshot.options), ctx);
933
931
  return true;
934
932
  }
@@ -992,7 +990,7 @@ export class Bridge {
992
990
  }
993
991
  const entry = this.sessionStore.get(ctx.userId);
994
992
  getLogger().debug(`[lark-remote] forward entry userId=${ctx.userId} chatId=${ctx.chatId} cwd=${cwd} ` +
995
- `agent=${agentKind} sessionId=${opts?.binding?.sessionId ?? entry?.sessions?.get(agentKind) ?? '(none)'} message=${message.slice(0, 100)}`);
993
+ `agent=${agentKind} sessionId=${opts?.binding?.sessionId ?? entry?.sessions.get(agentKind) ?? '(none)'} message=${message.slice(0, 100)}`);
996
994
  // Check if current cwd already has a run in progress
997
995
  if (this.activeRuns.has(cwd)) {
998
996
  getLogger().warn(`[lark-remote] cwd busy, dropping message userId=${ctx.userId} cwd=${cwd}`);
@@ -1180,6 +1178,139 @@ export class Bridge {
1180
1178
  stop,
1181
1179
  };
1182
1180
  }
1181
+ /**
1182
+ * result 事件的 pre-init 守卫(§9.22)。
1183
+ *
1184
+ * Claude CLI `--resume` 会在 system.init 之前先吐一条**上一轮**的陈旧 result。
1185
+ * 认了它就会过早置 sawResult / 抓错 usage / 让 run-state reducer 提前进入
1186
+ * finalizing(reducer 用 sessionId===undefined 守卫,同一条红线)。本轮真正的
1187
+ * result 在 system.init 之后才到。
1188
+ *
1189
+ * 例外:app-server runner(usageAuthority==='live')的 init 是 runner 自己补发
1190
+ * 的 synthetic init(2026-08-13 修复)。此分支保留为防御性兜底:live runner 若
1191
+ * 在无 init 时发出 result(旧构建等),session 写回照常执行,但 sawResult 不置
1192
+ * 真——卡片不会把半初始化的 run 当作已收尾。
1193
+ *
1194
+ * @param dropFromCard 该 result 是陈旧重放,不得进卡片(调用方 continue 跳过 push)
1195
+ */
1196
+ observeResultEvent(event, opts) {
1197
+ const { sawInit, usageAuthority, usage, writeback, runId } = opts;
1198
+ const preInitResult = !sawInit;
1199
+ const preInitTerminal = preInitResult && (event.subtype === 'error' || event.subtype === 'interrupted')
1200
+ ? {
1201
+ subtype: event.subtype,
1202
+ errorMessage: event.errorMessage,
1203
+ }
1204
+ : undefined;
1205
+ if (preInitResult && usageAuthority !== 'live') {
1206
+ getLogger().info(`[lark-remote] pre-init result ignored (resume replay) runId=${runId}`);
1207
+ return { sawResult: false, dropFromCard: true, preInitTerminal };
1208
+ }
1209
+ usage.observeResult(event);
1210
+ getLogger().info(preInitResult
1211
+ ? `[lark-remote] pre-init result from live runner (app-server setup failure) runId=${runId}`
1212
+ : `[lark-remote] result event received runId=${runId}`);
1213
+ // app-server 模式 setup 失败路径(thread/start 成功后 turn/start 失败)不会有
1214
+ // turn_started 事件,store 不会写回新线程 id;runner 已在 result 里兜底上报。
1215
+ // 仅当 runner 声明 live usage(app-server)且会话指针未动时写回,避免下条消息
1216
+ // 再开一个孤儿线程。其余 agent 不在此列(各自有 system.init / turn_started 写回)。
1217
+ if (usageAuthority === 'live')
1218
+ writeBackResultSession(writeback, event.session_id);
1219
+ return { sawResult: !preInitResult, dropFromCard: false, preInitTerminal };
1220
+ }
1221
+ /**
1222
+ * 审批三事件的派发(requested / resolved / view_updated)。
1223
+ *
1224
+ * coordinator 一 run 一个,首次 requested 时惰性建好并登记,`finally` 里统一
1225
+ * onTurnEnded + 摘除。
1226
+ */
1227
+ dispatchApprovalEvent(event, opts) {
1228
+ const { runId, runner, cardSession, ctx, agentKind } = opts;
1229
+ if (event.type === 'approval_requested') {
1230
+ // 审批请求到达:撤回 Typing 表情再重发,让飞书重新触发提醒,把用户
1231
+ // 注意力拉回待审批的卡片(run 在等待人工决策,Typing 应重新闪烁)。
1232
+ remindPendingApproval(this.connector, ctx.messageId, runId);
1233
+ let coordinator = this.approvalCoordinators.get(runId);
1234
+ if (!coordinator) {
1235
+ coordinator = createRunApprovalCoordinator({
1236
+ runner,
1237
+ cardSession,
1238
+ config: this.config,
1239
+ runId,
1240
+ agentKind,
1241
+ });
1242
+ this.approvalCoordinators.set(runId, coordinator);
1243
+ getLogger().info(`[lark-remote] approval coordinator created runId=${runId.slice(0, 8)} requestId=${event.requestId}`);
1244
+ }
1245
+ coordinator.onRequested(event);
1246
+ return;
1247
+ }
1248
+ const coordinator = this.approvalCoordinators.get(runId);
1249
+ if (event.type === 'approval_resolved') {
1250
+ coordinator?.onResolved(event.requestId);
1251
+ return;
1252
+ }
1253
+ // 乱序流:审批先到、item/started 后到,补全审批卡内容。coordinator
1254
+ // 仅在 pending 时更新(已响应/过期不复活)。
1255
+ coordinator?.updateView(event.requestId, event.view);
1256
+ }
1257
+ /**
1258
+ * 进程退出(for-await 自然结束)后的终态矩阵——它是本轮的唯一终态触发源。
1259
+ *
1260
+ * 1) finalizing(result 已收到)→ 三态映射 + usage meta
1261
+ * 2) running(未收到 result)→ 错误路径;pre-init 终态优先当唯一线索
1262
+ * 3) 已终态 + sawResult → 首终态不动,仅补 usage meta
1263
+ * 4) 已终态且未收到 result(如 spawn 失败后 interrupted)→ 无 usage 可补,跳过
1264
+ */
1265
+ async finishRunAfterStream(opts) {
1266
+ const { cardSession, usage, sawResult, pendingPreInitTerminal, agentKind, runId } = opts;
1267
+ const finalState = cardSession.currentState;
1268
+ if (finalState.terminal === 'finalizing') {
1269
+ // 三态终态映射(2026-08-14):interrupted(审批超时/取消、/stop)是独立
1270
+ // 终态,不再归因于 Agent 错误;error 保持「运行出错」;其余为 done。
1271
+ const subtype = finalState.resultSubtype;
1272
+ const isError = subtype === 'error' || !!finalState.errorMsg;
1273
+ // goal 仍 active = 宿主会自动起下一轮:这不是"完成",而是"本轮结束"。
1274
+ // 用独立终态而不是 done,既不改 header 文案也不用谎报。
1275
+ const goalContinues = !isError && subtype !== 'interrupted' && finalState.goal?.phase === 'active';
1276
+ const terminal = isError
1277
+ ? 'error'
1278
+ : subtype === 'interrupted'
1279
+ ? 'interrupted'
1280
+ : goalContinues
1281
+ ? 'goal_continuing'
1282
+ : 'done';
1283
+ await cardSession.finish(terminal, {
1284
+ resultSubtype: subtype,
1285
+ errorMsg: finalState.errorMsg,
1286
+ ...usage.meta(),
1287
+ });
1288
+ return;
1289
+ }
1290
+ if (finalState.terminal === 'running') {
1291
+ if (pendingPreInitTerminal) {
1292
+ const terminal = pendingPreInitTerminal.subtype === 'interrupted' ? 'interrupted' : 'error';
1293
+ getLogger().warn(`[lark-remote] stream ended without init; using pre-init ${terminal} result runId=${runId} error=${pendingPreInitTerminal.errorMessage ?? '(none)'}`);
1294
+ await cardSession.finish(terminal, {
1295
+ ...usage.meta(),
1296
+ errorMsg: pendingPreInitTerminal.errorMessage ??
1297
+ agentDisplayName(agentKind) + (terminal === 'interrupted' ? ' 已中断' : ' 启动失败'),
1298
+ });
1299
+ }
1300
+ else {
1301
+ await cardSession.finish('error', {
1302
+ ...usage.meta(),
1303
+ errorMsg: agentDisplayName(agentKind) + ' 输出流已结束,但未收到 result 事件',
1304
+ });
1305
+ }
1306
+ return;
1307
+ }
1308
+ if (sawResult) {
1309
+ // 已终态(interrupted/idle_timeout)+ result 已收到:首终态优先,
1310
+ // 仅补充 usage meta(finalizing 期间被 /stop 或 idle 超时也展示 token 统计)
1311
+ await cardSession.finish(finalState.terminal, usage.meta());
1312
+ }
1313
+ }
1183
1314
  /**
1184
1315
  * Step 2 of forwardToClaude: Run the agent and consume its event stream.
1185
1316
  * Handles idle watchdog, event processing, session sync, and state transitions.
@@ -1276,83 +1407,30 @@ export class Bridge {
1276
1407
  usage.observeCompaction(event.compactMetadata.postTokens);
1277
1408
  }
1278
1409
  if (event.type === 'result') {
1279
- // Pre-init result guard: Claude CLI --resume emits a stale result
1280
- // (from the previous turn) before system.init. Ignoring it prevents
1281
- // premature sawResult/usage capture and the run-state reducer (which
1282
- // also guards on sessionId === undefined) from transitioning to
1283
- // finalizing too early. The real result for this run arrives after
1284
- // system.init.
1285
- //
1286
- // Exception: app-server runners (usageAuthority === 'live') 的 init
1287
- // 是 runner 补发的 synthetic init(2026-08-13 修复)。此分支保留为
1288
- // 防御性兜底:任何 live runner 若在无 init 时发出 result(如旧构建、
1289
- // 其它 live runner),session 写回仍执行,但 sawResult 保持 false,
1290
- // 卡片不会把半初始化的 run 当作已收尾。
1291
- const preInitResult = !sawInit;
1292
- if (preInitResult && usageAuthority !== 'live') {
1293
- getLogger().info(`[lark-remote] pre-init result ignored (resume replay) runId=${runId}`);
1294
- if (event.subtype === 'error' || event.subtype === 'interrupted') {
1295
- pendingPreInitTerminal ??= {
1296
- subtype: event.subtype,
1297
- errorMessage: event.errorMessage,
1298
- };
1299
- }
1300
- // Still reset idle timer — the CLI is alive and producing events.
1410
+ const outcome = this.observeResultEvent(event, {
1411
+ sawInit,
1412
+ usageAuthority,
1413
+ usage,
1414
+ writeback,
1415
+ runId,
1416
+ });
1417
+ if (outcome.preInitTerminal)
1418
+ pendingPreInitTerminal ??= outcome.preInitTerminal;
1419
+ if (outcome.sawResult)
1420
+ sawResult = true;
1421
+ // 陈旧 result 不进卡片:run-state reducer 本来也会因 sessionId 未定而
1422
+ // 跳过,在 bridge 层就拦掉可以省下一次无谓的 push + render。
1423
+ if (outcome.dropFromCard) {
1424
+ // 仍然重置 idle 计时——CLI 活着并且在产事件。
1301
1425
  watchdog?.touch();
1302
- // Do NOT push to cardSession: the run-state reducer would also
1303
- // skip it (sessionId === undefined), but skipping at the bridge
1304
- // layer avoids the push + render overhead for a no-op event.
1305
1426
  continue;
1306
1427
  }
1307
- if (preInitResult && (event.subtype === 'error' || event.subtype === 'interrupted')) {
1308
- pendingPreInitTerminal ??= {
1309
- subtype: event.subtype,
1310
- errorMessage: event.errorMessage,
1311
- };
1312
- }
1313
- if (!preInitResult) {
1314
- sawResult = true;
1315
- }
1316
- usage.observeResult(event);
1317
- getLogger().info(preInitResult
1318
- ? `[lark-remote] pre-init result from live runner (app-server setup failure) runId=${runId}`
1319
- : `[lark-remote] result event received runId=${runId}`);
1320
- // app-server 模式 setup 失败路径(thread/start 成功后 turn/start 失败)
1321
- // 不会有 turn_started 事件,store 不会写回新线程 id;runner 已在 result
1322
- // 里兜底上报。此处仅当 runner 声明 live usage(app-server)
1323
- // 且会话指针未动时写回,避免下条消息再开一个孤儿线程。其余 agent 不在
1324
- // 此列(它们各自有 system.init / turn_started 写回)。
1325
- if (usageAuthority === 'live')
1326
- writeBackResultSession(writeback, event.session_id);
1327
1428
  }
1328
1429
  // Approval events (Codex app-server mode)
1329
- if (event.type === 'approval_requested') {
1330
- // 审批请求到达:撤回 Typing 表情再重发,让飞书重新触发提醒,把用户
1331
- // 注意力拉回待审批的卡片(run 在等待人工决策,Typing 应重新闪烁)。
1332
- remindPendingApproval(this.connector, ctx.messageId, runId);
1333
- let coordinator = this.approvalCoordinators.get(runId);
1334
- if (!coordinator) {
1335
- coordinator = createRunApprovalCoordinator({
1336
- runner,
1337
- cardSession,
1338
- config: this.config,
1339
- runId,
1340
- agentKind,
1341
- });
1342
- this.approvalCoordinators.set(runId, coordinator);
1343
- getLogger().info(`[lark-remote] approval coordinator created runId=${runId.slice(0, 8)} requestId=${event.requestId}`);
1344
- }
1345
- coordinator.onRequested(event);
1346
- }
1347
- else if (event.type === 'approval_resolved') {
1348
- const coordinator = this.approvalCoordinators.get(runId);
1349
- coordinator?.onResolved(event.requestId);
1350
- }
1351
- else if (event.type === 'approval_view_updated') {
1352
- // 乱序流:审批先到、item/started 后到,补全审批卡内容。coordinator
1353
- // 仅在 pending 时更新(已响应/过期不复活)。
1354
- const coordinator = this.approvalCoordinators.get(runId);
1355
- coordinator?.updateView(event.requestId, event.view);
1430
+ if (event.type === 'approval_requested' ||
1431
+ event.type === 'approval_resolved' ||
1432
+ event.type === 'approval_view_updated') {
1433
+ this.dispatchApprovalEvent(event, { runId, runner, cardSession, ctx, agentKind });
1356
1434
  }
1357
1435
  // §9.12 红线:每个事件(含 result)无条件刷新
1358
1436
  // lastEventTs 重新武装看门狗。result 后 CLI 进程做 jsonl flush/清理(finalizing
@@ -1381,56 +1459,15 @@ export class Bridge {
1381
1459
  : resolveFinalUsage(this.sessionReaderRegistry, this.config.defaultAgent, finalSessionId, cwd, agentKind);
1382
1460
  usage.finalize(finalUsage, usageAuthority);
1383
1461
  getLogger().info(usage.logSummary(runId));
1384
- // for-await 自然结束 = stream 关闭 = CLI 进程退出。
1385
- // 进程退出(for-await end)= 唯一终态触发源。
1386
- // 1) finalizing(result 已收到)-> 转 done/error + usage meta
1387
- // 2) running(未收到 result)-> 错误路径
1388
- // 3) 已终态 + sawResult -> 补充 usage meta(不改 terminal)
1389
- const finalState = cardSession.currentState;
1390
- if (finalState.terminal === 'finalizing') {
1391
- // 三态终态映射(2026-08-14):interrupted(审批超时/取消、/stop)是独立
1392
- // 终态,不再归因于 Agent 错误;error 保持「运行出错」;其余为 done。
1393
- const subtype = finalState.resultSubtype;
1394
- const isError = subtype === 'error' || !!finalState.errorMsg;
1395
- // goal 仍 active = 宿主会自动起下一轮:这不是"完成",而是"本轮结束"。
1396
- // 用独立终态而不是 done,既不改 header 文案也不用谎报。
1397
- const goalContinues = !isError && subtype !== 'interrupted' && finalState.goal?.phase === 'active';
1398
- const terminal = isError
1399
- ? 'error'
1400
- : subtype === 'interrupted'
1401
- ? 'interrupted'
1402
- : goalContinues
1403
- ? 'goal_continuing'
1404
- : 'done';
1405
- await cardSession.finish(terminal, {
1406
- resultSubtype: subtype,
1407
- errorMsg: finalState.errorMsg,
1408
- ...usage.meta(),
1409
- });
1410
- }
1411
- else if (finalState.terminal === 'running') {
1412
- if (pendingPreInitTerminal) {
1413
- const terminal = pendingPreInitTerminal.subtype === 'interrupted' ? 'interrupted' : 'error';
1414
- getLogger().warn(`[lark-remote] stream ended without init; using pre-init ${terminal} result runId=${runId} error=${pendingPreInitTerminal.errorMessage ?? '(none)'}`);
1415
- await cardSession.finish(terminal, {
1416
- ...usage.meta(),
1417
- errorMsg: pendingPreInitTerminal.errorMessage ??
1418
- agentDisplayName(agentKind) + (terminal === 'interrupted' ? ' 已中断' : ' 启动失败'),
1419
- });
1420
- }
1421
- else {
1422
- await cardSession.finish('error', {
1423
- ...usage.meta(),
1424
- errorMsg: agentDisplayName(agentKind) + ' 输出流已结束,但未收到 result 事件',
1425
- });
1426
- }
1427
- }
1428
- else if (sawResult) {
1429
- // 已终态(interrupted/idle_timeout)+ result 已收到:首终态优先,
1430
- // 仅补充 usage meta(finalizing 期间被 /stop 或 idle 超时也展示 token 统计)
1431
- await cardSession.finish(finalState.terminal, usage.meta());
1432
- }
1433
- // else: 已终态且未收到 result(如 spawn 失败后 interrupted)-> 无 usage 可补,跳过
1462
+ // for-await 自然结束 = stream 关闭 = CLI 进程退出 → 终态矩阵
1463
+ await this.finishRunAfterStream({
1464
+ cardSession,
1465
+ usage,
1466
+ sawResult,
1467
+ pendingPreInitTerminal,
1468
+ agentKind,
1469
+ runId,
1470
+ });
1434
1471
  }
1435
1472
  catch (err) {
1436
1473
  getLogger().error(`[lark-remote] claude run error runId=${runId}:`, err);
@@ -1445,7 +1482,7 @@ export class Bridge {
1445
1482
  // 终态守卫:running 或 finalizing(非终态)-> 转 error;
1446
1483
  // 已终态(interrupted/idle_timeout/done/error)-> 保留首终态,仅补充 usage。
1447
1484
  const catchTerminal = cardSession.currentState.terminal;
1448
- if (catchTerminal === 'running' || catchTerminal === 'finalizing') {
1485
+ if (isRunActive(catchTerminal)) {
1449
1486
  await cardSession.finish('error', {
1450
1487
  ...usage.meta(),
1451
1488
  inputTokens: usage.liveInputTokens,
@@ -1612,90 +1649,36 @@ export class Bridge {
1612
1649
  // =============================================================================
1613
1650
  // Approval action handlers
1614
1651
  // =============================================================================
1615
- /**
1616
- * Resolve the ApprovalCoordinator for a run, or throw the standard
1617
- * "已结束" error. Shared preamble of all approval card-action handlers.
1618
- */
1619
- withCoordinator(runId, fn) {
1620
- const coordinator = this.approvalCoordinators.get(runId);
1621
- if (!coordinator) {
1622
- // run 已结束(coordinator 已释放)时点击审批按钮必须给用户明确反馈,
1623
- // 不能静默。router 会把该异常转成 error toast。
1624
- getLogger().warn(`[lark-remote] no approval coordinator for runId=${runId.slice(0, 8)}`);
1625
- throw new Error('任务已结束,审批无法响应');
1626
- }
1627
- return fn(coordinator);
1628
- }
1629
- /**
1630
- * Handle an approval response from a card button click.
1631
- * Routes to the ApprovalCoordinator for the given run.
1632
- */
1652
+ // 实现住在 approval-actions.ts(审计 #32):这 7 个入口只碰
1653
+ // approvalCoordinators 一张表,不碰其余 Bridge 状态。方法名保持 Bridge 的
1654
+ // 公开 API 不变——router 的 APPROVAL_ACTION_SPECS 按名字分发。
1655
+ /** 审批响应(卡片按钮)→ ApprovalCoordinator.submit。 */
1633
1656
  async handleApprovalRespond(opts) {
1634
- const log = getLogger();
1635
- log.info(`[lark-remote] handleApprovalRespond runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} decision=${opts.decision}`);
1636
- return this.withCoordinator(opts.runId, async (coordinator) => {
1637
- // Build ApprovalAction from the decision string
1638
- const action = decisionToApprovalAction(opts.decision);
1639
- await coordinator.submit(action, { requestId: opts.requestId, nonce: opts.nonce });
1640
- });
1657
+ return this.approvalActions.handleApprovalRespond(opts);
1641
1658
  }
1642
- /**
1643
- * 计划审批修改意见(ExitPlanMode 输入框):记录到 coordinator 并回流卡片。
1644
- * 「拒绝并附意见」/「批准并采纳修改」按钮复用该文本。
1645
- */
1659
+ /** 计划审批修改意见(ExitPlanMode 输入框)→ coordinator.planFeedback。 */
1646
1660
  async handleApprovalPlanFeedback(opts) {
1647
- return this.withCoordinator(opts.runId, async (coordinator) => {
1648
- await coordinator.planFeedback({ text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce });
1649
- });
1661
+ return this.approvalActions.handleApprovalPlanFeedback(opts);
1650
1662
  }
1651
- /**
1652
- * Handle a permission toggle from a card button click.
1653
- * Routes to the ApprovalCoordinator for the given run.
1654
- */
1663
+ /** 权限条目勾选(卡片按钮)→ coordinator.togglePerm。 */
1655
1664
  async handleApprovalToggle(opts) {
1656
- const log = getLogger();
1657
- log.info(`[lark-remote] handleApprovalToggle runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} permId=${opts.permId}`);
1658
- return this.withCoordinator(opts.runId, async (coordinator) => {
1659
- // selected 由卡片根据当前渲染状态传回(取反),不再硬编码只授不撤。
1660
- const toggleAction = {
1661
- permId: opts.permId,
1662
- selected: opts.selected ?? true,
1663
- };
1664
- await coordinator.togglePerm(toggleAction, { requestId: opts.requestId });
1665
- });
1665
+ return this.approvalActions.handleApprovalToggle(opts);
1666
1666
  }
1667
- /**
1668
- * Claude AskUserQuestion 选项点击(单选即时提交 / 多选切换勾选)。
1669
- * 路由到 ApprovalCoordinator.toggleAnswer。
1670
- */
1667
+ /** AskUserQuestion 选项点击 → coordinator.toggleAnswer。 */
1671
1668
  async handleApprovalAnswer(opts) {
1672
- return this.dispatchQuestionAction(opts, 'handleApprovalAnswer', (coordinator) => coordinator.toggleAnswer({ questionIndex: opts.questionIndex, option: opts.option }, { requestId: opts.requestId, nonce: opts.nonce }));
1669
+ return this.approvalActions.handleApprovalAnswer(opts);
1673
1670
  }
1674
- /**
1675
- * Claude AskUserQuestion 多选问题的「提交答案」按钮。
1676
- * 路由到 ApprovalCoordinator.submitAnswers。
1677
- */
1671
+ /** AskUserQuestion 多选「提交答案」→ coordinator.submitAnswers。 */
1678
1672
  async handleApprovalAnswerSubmit(opts) {
1679
- return this.dispatchQuestionAction(opts, 'handleApprovalAnswerSubmit', (coordinator) => coordinator.submitAnswers({ questionIndex: opts.questionIndex }, { requestId: opts.requestId, nonce: opts.nonce }));
1673
+ return this.approvalActions.handleApprovalAnswerSubmit(opts);
1680
1674
  }
1681
- /**
1682
- * Claude AskUserQuestion 自定义答案(Other):自由文本直接
1683
- * 作为该单选问题的答案提交。路由到 ApprovalCoordinator.answerCustom。
1684
- */
1675
+ /** AskUserQuestion 自定义答案(Other)→ coordinator.answerCustom。 */
1685
1676
  async handleApprovalAnswerCustom(opts) {
1686
- return this.dispatchQuestionAction(opts, 'handleApprovalAnswerCustom', (coordinator) => coordinator.answerCustom({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
1677
+ return this.approvalActions.handleApprovalAnswerCustom(opts);
1687
1678
  }
1688
- /**
1689
- * Codex AskUserQuestion 补充说明(user_note,2026-08-18 live 验证模型理解
1690
- * "user_note: <text>" 条目):记录到 coordinator,随答案一起提交。
1691
- * 路由到 ApprovalCoordinator.answerNote。
1692
- */
1679
+ /** AskUserQuestion 补充说明(user_note)→ coordinator.answerNote。 */
1693
1680
  async handleApprovalAnswerNote(opts) {
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);
1681
+ return this.approvalActions.handleApprovalAnswerNote(opts);
1699
1682
  }
1700
1683
  /**
1701
1684
  * 记下一次「可压缩 run」:终态卡上的 Compact 按钮点击时据此反查
@@ -1817,14 +1800,14 @@ export class Bridge {
1817
1800
  });
1818
1801
  }
1819
1802
  /**
1820
- * §5.3: kimi 压缩在途检测——reader 鸭子实现 `readCompactionState` 时,若
1803
+ * §5.3: kimi 压缩在途检测——reader 实现契约的 `readCompactionState` 时,若
1821
1804
  * wire 状态机判定当前有未终结的压缩(最后一次 begin 无 terminal 记录),
1822
1805
  * 直接回文本「已有一次 Compact 正在进行」并返回 true(调用方不开卡、不发
1823
1806
  * session/prompt)。codex/claude 等 reader 无此方法 → 返回 false,行为不变。
1824
1807
  */
1825
1808
  async rejectIfCompactionInFlight(sessionId, cwd, agentKind, ctx) {
1826
1809
  const reader = this.sessionReaderRegistry.get(agentKind);
1827
- if (!reader || !('readCompactionState' in reader))
1810
+ if (!reader?.readCompactionState)
1828
1811
  return false;
1829
1812
  try {
1830
1813
  const state = reader.readCompactionState(sessionId, cwd);
@@ -1952,20 +1935,9 @@ export class Bridge {
1952
1935
  await cardSession.finish(terminal, {
1953
1936
  resultSubtype: resultSubtype ?? 'error',
1954
1937
  errorMsg: terminal === 'error' ? (resultError ?? '未收到压缩结果') : undefined,
1955
- contextLength: finalUsage?.contextLength,
1956
- contextLimit: finalUsage?.contextLimit,
1957
- compactPreContextLength: finalUsage?.compactPreContextLength,
1958
- compactCount: finalUsage?.compactCount,
1959
- cacheReadTokens: finalUsage?.cacheReadTokens,
1960
- cacheCreationTokens: finalUsage?.cacheCreationTokens,
1961
- totalTokens: finalUsage?.totalTokens,
1962
- inputTokens: finalUsage?.inputTokens,
1963
- outputTokens: finalUsage?.outputTokens,
1964
- cumulativeTotalTokens: finalUsage?.cumulativeTotalTokens,
1965
- cumulativeInputTokens: finalUsage?.cumulativeInputTokens,
1966
- cumulativeOutputTokens: finalUsage?.cumulativeOutputTokens,
1967
- cumulativeCacheReadTokens: finalUsage?.cumulativeCacheReadTokens,
1968
- cumulativeCacheCreationTokens: finalUsage?.cumulativeCacheCreationTokens,
1938
+ // finalUsage 的字段形态就是 FinishMeta 的用量部分(同为 UsageSnapshot),
1939
+ // 逐字段抄的那 14 行随单源一起去掉。
1940
+ ...(finalUsage ?? {}),
1969
1941
  });
1970
1942
  log.info(`[lark-remote] streamCompact finished sessionId=${sessionId} sawResult=${sawResult} subtype=${resultSubtype ?? 'none'}`);
1971
1943
  }
@@ -1,4 +1,4 @@
1
- import type { AgentKind } from '../runner/types.js';
1
+ import type { AgentKind } from '../agent-kinds.js';
2
2
  /**
3
3
  * 入队时刻快照:当前 defaultAgent + 该 agent 的 sessionId(无 session 则 undefined)。
4
4
  * 唯一捕获点:排队消息在入队时刻把 agent+session 钉进 AgentBinding,随任务闭包带到执行时刻
@@ -221,8 +221,32 @@ export declare class QueueManager {
221
221
  /**
222
222
  * Enqueue a task into the workspace-level serial queue.
223
223
  * Each cwd has its own serial queue for parallel execution across workspaces.
224
+ *
225
+ * 三段各自的职责:`registerQueuedTask`(排队元数据 + 满队拒收)、
226
+ * `beginQueuedTask`(接跑时刻的撤销检查/替换闭包/占用登记)、本方法
227
+ * (promise 链拼接与 settle 记账)。红线:链的拼接必须同步完成,
228
+ * 中途不得 await(否则两条入队会乱序)。
224
229
  */
225
230
  enqueue(cwd: string, task: () => Promise<void>, opts?: EnqueueOptions): void;
231
+ /**
232
+ * 登记排队元数据(展示卡数据源 + 计数 + 排队卡)。
233
+ *
234
+ * @returns false = 队列已满被拒收(不入队、不占执行槽位),调用方必须
235
+ * 直接把任务链跳过。背压红线:满队时拒收而不是继续 append,否则 promise
236
+ * 链与闭包在消息洪峰下无界增长,还会多打一张排队卡。
237
+ */
238
+ private registerQueuedTask;
239
+ /** 满队拒收卡(best-effort 送达,失败只记日志:任务已经被拒,不能再吞一次反馈)。 */
240
+ private sendQueueFullRejection;
241
+ /**
242
+ * 轮到本任务执行:撤销检查 → 消费一次性替换闭包 → 排队卡转「执行中」→
243
+ * 重新武装计数与 lane 占用登记 → 跑任务闭包。
244
+ *
245
+ * @returns 任务闭包的 promise;被撤销时返回 undefined(本槽位什么都不跑)。
246
+ */
247
+ private beginQueuedTask;
248
+ /** 取走并清除该任务的一次性替换闭包(消费即销毁)。 */
249
+ private takeTaskReplacement;
226
250
  /**
227
251
  * Execute a task immediately without going through the queue.
228
252
  * Used for / commands that should respond immediately.