lark-remote 0.5.18 → 0.5.20

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 (123) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/bridge/approval-actions.d.ts +1 -1
  3. package/dist/bridge/approval-actions.js +1 -1
  4. package/dist/bridge/bash-run.d.ts +71 -0
  5. package/dist/bridge/bash-run.js +217 -0
  6. package/dist/bridge/compact.d.ts +155 -0
  7. package/dist/bridge/compact.js +450 -0
  8. package/dist/bridge/final-usage.js +4 -4
  9. package/dist/bridge/inbound-media.js +1 -1
  10. package/dist/bridge/index.d.ts +120 -119
  11. package/dist/bridge/index.js +257 -695
  12. package/dist/bridge/job-watch.d.ts +68 -0
  13. package/dist/bridge/job-watch.js +146 -0
  14. package/dist/bridge/queue-manager.d.ts +3 -3
  15. package/dist/bridge/queue-manager.js +10 -29
  16. package/dist/card/approval-render.d.ts +2 -2
  17. package/dist/card/approval-render.js +109 -97
  18. package/dist/card/bash-card-session.js +2 -0
  19. package/dist/card/card-budget.js +1 -1
  20. package/dist/card/card-helpers.d.ts +3 -3
  21. package/dist/card/card-helpers.js +6 -6
  22. package/dist/card/card-session.d.ts +17 -0
  23. package/dist/card/card-session.js +35 -0
  24. package/dist/card/final-answer-file.js +1 -1
  25. package/dist/card/run-card-session.js +2 -0
  26. package/dist/card/run-renderer.d.ts +3 -3
  27. package/dist/card/run-renderer.js +77 -102
  28. package/dist/card/run-state.d.ts +35 -8
  29. package/dist/card/run-state.js +117 -90
  30. package/dist/card/tool-render.d.ts +1 -1
  31. package/dist/card/tool-render.js +1 -1
  32. package/dist/card/usage-format.d.ts +12 -0
  33. package/dist/card/usage-format.js +8 -3
  34. package/dist/cli.js +15 -3
  35. package/dist/clone.js +2 -2
  36. package/dist/config/codex-config.js +130 -106
  37. package/dist/config/index.d.ts +16 -2
  38. package/dist/config/index.js +23 -8
  39. package/dist/connector/index.js +6 -6
  40. package/dist/error-classification.js +3 -2
  41. package/dist/inbound/placeholder.d.ts +1 -1
  42. package/dist/inbound/placeholder.js +1 -1
  43. package/dist/index.js +203 -133
  44. package/dist/logger/index.js +6 -6
  45. package/dist/message-handlers.d.ts +0 -5
  46. package/dist/message-handlers.js +4 -2
  47. package/dist/model-catalog.d.ts +2 -2
  48. package/dist/model-catalog.js +3 -8
  49. package/dist/platform/identity.js +1 -1
  50. package/dist/platform/path.d.ts +1 -1
  51. package/dist/platform/path.js +1 -1
  52. package/dist/platform/sleep-blocker.js +1 -1
  53. package/dist/platform/terminator-win32.js +2 -1
  54. package/dist/router/commands/resume.js +2 -20
  55. package/dist/router/config/dsh.d.ts +4 -8
  56. package/dist/router/config/dsh.js +3 -10
  57. package/dist/router/config/kimi.js +1 -1
  58. package/dist/router/config/types.d.ts +1 -1
  59. package/dist/router/config-diff.js +1 -1
  60. package/dist/router/index.d.ts +56 -11
  61. package/dist/router/index.js +222 -202
  62. package/dist/runner/claude/session.d.ts +1 -1
  63. package/dist/runner/claude/session.js +6 -4
  64. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  65. package/dist/runner/codex/app-server/runner.js +30 -25
  66. package/dist/runner/codex/app-server/translator.d.ts +0 -5
  67. package/dist/runner/codex/app-server/translator.js +4 -17
  68. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  69. package/dist/runner/common/acp/acp-approval.js +1 -1
  70. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  71. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  72. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  73. package/dist/runner/common/acp/base-translator.js +31 -9
  74. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  75. package/dist/runner/common/acp/protocol-types.js +2 -2
  76. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  77. package/dist/runner/common/connection-based-runner.js +32 -2
  78. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  79. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  80. package/dist/runner/common/spawning-runner.d.ts +1 -1
  81. package/dist/runner/common/spawning-runner.js +1 -1
  82. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  83. package/dist/runner/dsh/catalog-probe.js +10 -0
  84. package/dist/runner/dsh/client.d.ts +13 -8
  85. package/dist/runner/dsh/client.js +23 -15
  86. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  87. package/dist/runner/dsh/exit-cancel.js +2 -2
  88. package/dist/runner/dsh/inflight.d.ts +1 -1
  89. package/dist/runner/dsh/inflight.js +1 -1
  90. package/dist/runner/dsh/runner.d.ts +48 -2
  91. package/dist/runner/dsh/runner.js +288 -199
  92. package/dist/runner/dsh/translator.d.ts +1 -1
  93. package/dist/runner/dsh/translator.js +1 -1
  94. package/dist/runner/index.d.ts +1 -1
  95. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  96. package/dist/runner/kimi/acp/runner.js +1 -2
  97. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  98. package/dist/runner/kimi/acp/translator.js +3 -4
  99. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  100. package/dist/runner/opencode/acp/runner.js +4 -4
  101. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  102. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  103. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  104. package/dist/runner/pi/rpc/runner.js +27 -42
  105. package/dist/runner/types.d.ts +25 -8
  106. package/dist/runner/types.js +1 -1
  107. package/dist/session/claude/sessions.d.ts +45 -0
  108. package/dist/session/claude/sessions.js +97 -71
  109. package/dist/session/codex/rollout-reader.js +4 -4
  110. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  111. package/dist/session/common/throw-session-list-error.js +16 -0
  112. package/dist/session/dsh/sessions.js +11 -5
  113. package/dist/session/kimi/sessions.d.ts +40 -1
  114. package/dist/session/kimi/sessions.js +244 -192
  115. package/dist/session/opencode/sessions.d.ts +153 -1
  116. package/dist/session/opencode/sessions.js +354 -321
  117. package/dist/session/pi/sessions.js +4 -5
  118. package/dist/session/session-store.d.ts +1 -1
  119. package/dist/session/session-store.js +1 -1
  120. package/dist/update/version-check.js +2 -1
  121. package/dist/workspace/index.d.ts +1 -1
  122. package/dist/workspace/index.js +1 -1
  123. package/package.json +1 -1
@@ -125,8 +125,6 @@ function reduceRunStateInner(state, event) {
125
125
  }
126
126
  /** Handle app-server turn_started — records the operation kind (turn vs compact). */
127
127
  function reduceTurnStartedEvent(state, event) {
128
- if (event.type !== 'turn_started')
129
- return state;
130
128
  return {
131
129
  ...state,
132
130
  operationKind: event.operationKind,
@@ -142,8 +140,6 @@ function reduceTurnStartedEvent(state, event) {
142
140
  * the active streaming content sits at the bottom, like the exec path.
143
141
  */
144
142
  function reduceTurnDiffEvent(state, event) {
145
- if (event.type !== 'turn_diff')
146
- return state;
147
143
  let next = state;
148
144
  const ts = event.timestamp;
149
145
  const complete = event.complete === true;
@@ -170,12 +166,7 @@ function reduceTurnDiffEvent(state, event) {
170
166
  ...(complete ? { completedAt: ts } : {}),
171
167
  };
172
168
  }, !complete);
173
- next = {
174
- ...next,
175
- blocks: keepLatestBlocks(capped),
176
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
177
- footer: 'streaming',
178
- };
169
+ next = withCappedBlocks(next, capped, 'streaming');
179
170
  }
180
171
  if (event.reasoning !== undefined) {
181
172
  const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'thinking' && b.itemId === event.itemId, () => ({
@@ -195,12 +186,7 @@ function reduceTurnDiffEvent(state, event) {
195
186
  ...(complete ? { completedAt: ts } : {}),
196
187
  };
197
188
  }, !complete);
198
- next = {
199
- ...next,
200
- blocks: keepLatestBlocks(capped),
201
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
202
- footer: 'thinking',
203
- };
189
+ next = withCappedBlocks(next, capped, 'thinking');
204
190
  }
205
191
  if (event.toolOutput !== undefined) {
206
192
  const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'tool' && b.tool.id === event.itemId, () => {
@@ -231,12 +217,7 @@ function reduceTurnDiffEvent(state, event) {
231
217
  ...(complete ? { completedAt: ts } : {}),
232
218
  },
233
219
  }), !complete);
234
- next = {
235
- ...next,
236
- blocks: keepLatestBlocks(capped),
237
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
238
- footer: 'tool_running',
239
- };
220
+ next = withCappedBlocks(next, capped, 'tool_running');
240
221
  }
241
222
  if (event.plan !== undefined) {
242
223
  const newPlan = keepTailMarked(event.plan, MAX_REASONING_CHARS * 2);
@@ -256,12 +237,7 @@ function reduceTurnDiffEvent(state, event) {
256
237
  ...(complete ? { completedAt: ts } : {}),
257
238
  };
258
239
  }, !complete);
259
- next = {
260
- ...next,
261
- plan: newPlan,
262
- blocks: keepLatestBlocks(capped),
263
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
264
- };
240
+ next = withCappedBlocks({ ...next, plan: newPlan }, capped);
265
241
  }
266
242
  if (event.fileChanges !== undefined) {
267
243
  let blocks = next.blocks;
@@ -278,12 +254,7 @@ function reduceTurnDiffEvent(state, event) {
278
254
  // 同一 item 的同一路径原地替换(权威 diff 校正),新路径/新 item 追加。
279
255
  blocks = upsertSnapshotBlock(blocks, (b) => b.kind === 'file_change' && b.itemId === event.itemId && b.path === fc.path, () => fileBlock, () => fileBlock, !complete);
280
256
  }
281
- const capped = blocks;
282
- next = {
283
- ...next,
284
- blocks: keepLatestBlocks(capped),
285
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
286
- };
257
+ next = withCappedBlocks(next, blocks);
287
258
  }
288
259
  return next;
289
260
  }
@@ -313,8 +284,6 @@ function upsertSnapshotBlock(blocks, predicate, fallback, update, moveToEndWhenN
313
284
  }
314
285
  /** Handle system init and compact_boundary events. */
315
286
  function reduceSystemEvent(state, event) {
316
- if (event.type !== 'system')
317
- return state;
318
287
  if (event.subtype === 'init') {
319
288
  return { ...state, sessionId: event.session_id, model: event.model, footer: 'thinking' };
320
289
  }
@@ -336,8 +305,6 @@ function reduceSystemEvent(state, event) {
336
305
  * - errorMsg: 首个非 undefined 胜出(agent 的更具体)
337
306
  */
338
307
  function reduceResultEvent(state, event) {
339
- if (event.type !== 'result')
340
- return state;
341
308
  // Pre-init result guard: Claude CLI --resume emits a historical result (from
342
309
  // the previous turn) before sending system.init for the new run. Without this
343
310
  // guard, that stale result would prematurely transition to 'finalizing',
@@ -375,8 +342,6 @@ function reduceResultEvent(state, event) {
375
342
  }
376
343
  /** Record an authoritative protocol final answer and remove its progress copy. */
377
344
  function reduceFinalAnswerEvent(state, event) {
378
- if (event.type !== 'final_answer')
379
- return state;
380
345
  if (!event.text.trim() && !state.finalAnswer)
381
346
  return state;
382
347
  const blocks = event.itemId
@@ -443,8 +408,6 @@ function promoteFinalAnswer(state, source, authoritativeText) {
443
408
  * 不再走事件(那时已经没有 run 在消费流了)。
444
409
  */
445
410
  function reduceBackgroundJobsEvent(state, event) {
446
- if (event.type !== 'background_jobs')
447
- return state;
448
411
  return { ...state, backgroundJobs: event.jobs };
449
412
  }
450
413
  /**
@@ -460,6 +423,95 @@ export function removeBackgroundJob(state, jobId) {
460
423
  backgroundJobs: (state.backgroundJobs ?? []).filter((job) => job.id !== jobId),
461
424
  };
462
425
  }
426
+ /** 宿主仍在执行的任务状态词。状态缺失也算在跑——它还在名单上。 */
427
+ const RUNNING_JOB_STATUSES = new Set(['running', 'stopping']);
428
+ /**
429
+ * 该任务是否仍在跑(`ended` / `unknown` 都不算)。
430
+ *
431
+ * 卡片据此决定要不要渲染「⏹ 停止」:给已经结束的任务留一个点了必然失败的
432
+ * 按钮,等于逼用户拿「停止」当查询用。
433
+ */
434
+ export function isLiveBackgroundJob(job) {
435
+ return job.status === undefined || RUNNING_JOB_STATUSES.has(job.status);
436
+ }
437
+ /** 宿主状态 → 桥侧状态:仍在跑的保留原词,其余已知终态归一成 `ended`。 */
438
+ function bridgeJobStatus(status) {
439
+ return RUNNING_JOB_STATUSES.has(status) ? status : 'ended';
440
+ }
441
+ /**
442
+ * 收尾后把宿主推来的整份任务名单同步进终态卡状态。
443
+ *
444
+ * 与 reducer 的分工:reducer 处理的是"收尾那一刻的名单",只在 run 还在消费
445
+ * 事件时有效;终态之后 `reduceRunState` 直接早退(终态幂等),名单只能由
446
+ * 这里更新。
447
+ *
448
+ * 三条规则:
449
+ * - 名单里消失 = 宿主已结算(`job/list` 是整份替换,摘掉即结束)→ `ended`;
450
+ * - 名单里还在、宿主却给了终态词 → `ended`(有些宿主会把已结束的行留在名单里);
451
+ * - `ended` 是终态,不回退成 running —— 同一个 job id 不会重新开始跑。
452
+ *
453
+ * 无变化时返回**同一个 state 引用**:bridge 靠它跳过无谓的卡片 patch。
454
+ */
455
+ export function syncBackgroundJobs(state, roster) {
456
+ const tracked = state.backgroundJobs ?? [];
457
+ const byId = new Map(roster.map((job) => [job.id, job]));
458
+ const next = tracked.map((job) => {
459
+ const row = byId.get(job.id);
460
+ if (!row)
461
+ return job.status === 'ended' ? job : { ...job, status: 'ended' };
462
+ if (job.status === 'ended')
463
+ return job;
464
+ const status = row.status === undefined ? job.status : bridgeJobStatus(row.status);
465
+ const label = row.label.trim() === '' ? job.label : row.label;
466
+ const progress = row.progress ?? job.progress;
467
+ if (status === job.status && label === job.label && progress === job.progress)
468
+ return job;
469
+ return {
470
+ ...job,
471
+ label,
472
+ ...(status === undefined ? {} : { status }),
473
+ ...(progress === undefined ? {} : { progress }),
474
+ };
475
+ });
476
+ // 新出现的任务只收仍在跑的:与收尾采集同一口径(列出已结束的任务只会被
477
+ // 读成"还在跑")。
478
+ const seen = new Set(tracked.map((job) => job.id));
479
+ for (const row of roster) {
480
+ if (seen.has(row.id))
481
+ continue;
482
+ if (row.status !== undefined && !RUNNING_JOB_STATUSES.has(row.status))
483
+ continue;
484
+ next.push({
485
+ id: row.id,
486
+ label: row.label,
487
+ ...(row.status === undefined ? {} : { status: row.status }),
488
+ ...(row.progress === undefined ? {} : { progress: row.progress }),
489
+ });
490
+ }
491
+ if (next.length === tracked.length && next.every((job, index) => job === tracked[index])) {
492
+ return state;
493
+ }
494
+ return { ...state, backgroundJobs: next };
495
+ }
496
+ /**
497
+ * 订阅断了但任务仍未结算:把在跑的任务标成「状态未知」。
498
+ *
499
+ * 监控中断不等于任务结束——不标出来,卡片又会退化成"最后一次观测当现状",
500
+ * 正是这次要修的那个缺陷。未知态仍保留停止按钮(用户还能尝试停掉)。
501
+ */
502
+ export function markBackgroundJobsUnknown(state) {
503
+ const jobs = state.backgroundJobs;
504
+ if (!jobs || jobs.length === 0)
505
+ return state;
506
+ let changed = false;
507
+ const next = jobs.map((job) => {
508
+ if (!isLiveBackgroundJob(job))
509
+ return job;
510
+ changed = true;
511
+ return { ...job, status: 'unknown' };
512
+ });
513
+ return changed ? { ...state, backgroundJobs: next } : state;
514
+ }
463
515
  /**
464
516
  * 运行期状态投影 → 状态行。
465
517
  *
@@ -467,8 +519,6 @@ export function removeBackgroundJob(state, jobId) {
467
519
  * 维度(上一步算出的上下文水位)擦掉。
468
520
  */
469
521
  function reduceRunStatusEvent(state, event) {
470
- if (event.type !== 'run_status')
471
- return state;
472
522
  const prev = state.liveStatus ?? {};
473
523
  const live = {
474
524
  ...prev,
@@ -486,8 +536,6 @@ function reduceRunStatusEvent(state, event) {
486
536
  }
487
537
  /** Handle assistant thinking/text/tool_use content blocks. */
488
538
  function reduceAssistantEvent(state, event) {
489
- if (event.type !== 'assistant')
490
- return state;
491
539
  let next = state;
492
540
  for (const content of event.message.content) {
493
541
  if (content.type === 'thinking') {
@@ -516,12 +564,7 @@ function reduceAssistantEvent(state, event) {
516
564
  timestamp: event.timestamp,
517
565
  },
518
566
  ];
519
- next = {
520
- ...next,
521
- blocks: keepLatestBlocks(newBlocks),
522
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
523
- footer: 'thinking',
524
- };
567
+ next = withCappedBlocks(next, newBlocks, 'thinking');
525
568
  }
526
569
  else if (content.type === 'text') {
527
570
  const blocks = markThinkingInactive(next.blocks);
@@ -545,12 +588,7 @@ function reduceAssistantEvent(state, event) {
545
588
  timestamp: event.timestamp,
546
589
  },
547
590
  ];
548
- next = {
549
- ...next,
550
- blocks: keepLatestBlocks(newBlocks),
551
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
552
- footer: 'streaming',
553
- };
591
+ next = withCappedBlocks(next, newBlocks, 'streaming');
554
592
  }
555
593
  else {
556
594
  // parse the truncated stored string ONCE here (not on every render)
@@ -585,12 +623,7 @@ function reduceAssistantEvent(state, event) {
585
623
  }
586
624
  else {
587
625
  const capped = [...blocks, { kind: 'tool', tool: incomingTool }];
588
- next = {
589
- ...next,
590
- blocks: keepLatestBlocks(capped),
591
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
592
- footer: 'tool_running',
593
- };
626
+ next = withCappedBlocks(next, capped, 'tool_running');
594
627
  }
595
628
  }
596
629
  }
@@ -627,8 +660,6 @@ function mergeToolUse(existing, incoming) {
627
660
  * advances to 'streaming' on every content block (parity with old behavior);
628
661
  * only the blocks array is preserved by reference when nothing matches. */
629
662
  function reduceToolResultEvent(state, event) {
630
- if (event.type !== 'user')
631
- return state;
632
663
  let next = state;
633
664
  for (const content of event.message.content) {
634
665
  const targetId = content.tool_use_id;
@@ -656,8 +687,6 @@ function reduceToolResultEvent(state, event) {
656
687
  }
657
688
  /** Handle notice event — append a runtime notice (warning / reroute / compaction). */
658
689
  function reduceNoticeEvent(state, event) {
659
- if (event.type !== 'notice')
660
- return state;
661
690
  return {
662
691
  ...state,
663
692
  notices: [
@@ -673,8 +702,6 @@ function reduceNoticeEvent(state, event) {
673
702
  }
674
703
  /** Handle session_info event — update sessionTitle / mode only when present. */
675
704
  function reduceSessionInfoEvent(state, event) {
676
- if (event.type !== 'session_info')
677
- return state;
678
705
  return {
679
706
  ...state,
680
707
  ...(event.title !== undefined ? { sessionTitle: event.title } : {}),
@@ -683,8 +710,6 @@ function reduceSessionInfoEvent(state, event) {
683
710
  }
684
711
  /** Handle plan event — accumulate plan text for Codex-style agents. */
685
712
  function reducePlanEvent(state, event) {
686
- if (event.type !== 'plan')
687
- return state;
688
713
  const currentPlan = state.plan ?? '';
689
714
  const newPlan = keepTailMarked(currentPlan + '\n' + event.plan, MAX_REASONING_CHARS * 2);
690
715
  // Plan is a single evolving document — replace the existing plan block
@@ -694,17 +719,10 @@ function reducePlanEvent(state, event) {
694
719
  ...withoutOldPlan,
695
720
  { kind: 'plan', content: newPlan, active: true, timestamp: event.timestamp },
696
721
  ];
697
- return {
698
- ...state,
699
- plan: newPlan,
700
- blocks: keepLatestBlocks(newBlocks),
701
- omittedBlocks: (state.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
702
- };
722
+ return withCappedBlocks({ ...state, plan: newPlan }, newBlocks);
703
723
  }
704
724
  /** Handle file_change event — emit a file_change block for rendering (Codex-style agents). */
705
725
  function reduceFileChangeEvent(state, event) {
706
- if (event.type !== 'file_change')
707
- return state;
708
726
  // Add file_change block for rendering (collapsed by default)
709
727
  const newBlocks = [
710
728
  ...state.blocks,
@@ -716,11 +734,7 @@ function reduceFileChangeEvent(state, event) {
716
734
  timestamp: event.timestamp,
717
735
  },
718
736
  ];
719
- return {
720
- ...state,
721
- blocks: keepLatestBlocks(newBlocks),
722
- omittedBlocks: (state.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
723
- };
737
+ return withCappedBlocks(state, newBlocks);
724
738
  }
725
739
  export function finishRun(state, terminal, meta = {}) {
726
740
  const withFinalAnswer = promoteFinalAnswer(state, 'inferred');
@@ -773,14 +787,27 @@ function stringifyUnknown(value) {
773
787
  try {
774
788
  // JSON.stringify(undefined) returns the value undefined, which would
775
789
  // violate this function's string contract and crash callers that take
776
- // .length on the result (2026-08-17 kimi ACP tool_call without rawInput
777
- // → truncateDetail TypeError). Normalize to ''.
790
+ // .length on the result. Normalize to ''.
778
791
  return JSON.stringify(value) ?? '';
779
792
  }
780
793
  catch {
781
794
  return String(value);
782
795
  }
783
796
  }
797
+ /**
798
+ * 尾块统一收口:keepLatestBlocks 截断 + omittedBlocks 计数 + footer 更新一步
799
+ * 完成。所有「追加/更新块后只保留最新 MAX_BLOCKS 块」的事件归约共用(此前
800
+ * 多处逐字重复);`blocks` 必须传截断前的数组,被丢出的最旧块才会计入
801
+ * omittedBlocks。`footer` 不传时保持原值(plan/file_change 归约不改 footer)。
802
+ */
803
+ function withCappedBlocks(next, blocks, footer) {
804
+ return {
805
+ ...next,
806
+ blocks: keepLatestBlocks(blocks),
807
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, blocks.length - MAX_BLOCKS),
808
+ ...(footer === undefined ? {} : { footer }),
809
+ };
810
+ }
784
811
  function keepLatestBlocks(blocks) {
785
812
  return blocks.length <= MAX_BLOCKS ? blocks : blocks.slice(-MAX_BLOCKS);
786
813
  }
@@ -790,8 +817,8 @@ const TRUNCATION_MARKER_RE = /^…(前 (\d+) 字符已省略)\n/;
790
817
  * 给「已经不再是最新」的 text 块补上 `MAX_TEXT_CHARS` 存储窗口。
791
818
  *
792
819
  * 为什么最新一条不截:它就是卡片上的**输出**,终态还会被 `promoteFinalAnswer`
793
- * 提成最终回答。提前按窗口切掉头部,渲染层再怎么回答优先也补不回来(2026-11-01
794
- * 输出块被截断缺陷:16074 字符的回答只进到渲染层 12000 字符,且是从中间开始的)。
820
+ * 提成最终回答。提前按窗口切掉头部,渲染层再怎么回答优先也补不回来
821
+ * (16074 字符的回答只进到渲染层 12000 字符,且是从中间开始的)。
795
822
  *
796
823
  * 为什么其余 text 块仍然截:它们只是进度信息,卡片本来就只会展示最新几步;
797
824
  * 存储层必须保持有界(`MAX_BLOCKS` × 窗口),否则长 run 的内存随历史线性增长。
@@ -826,7 +853,7 @@ function capStaleTextBlocks(blocks) {
826
853
  * keepTail + 截断标记:被去头的存储内容以前缀如实标注(信息保真红线:禁止静默丢弃)。
827
854
  * 标记计入 maxChars 预算(存储总量仍 ≤ maxChars,保持既有长度不变量)。
828
855
  *
829
- * 计数口径(2026-10 修正):只统计**真实丢弃的正文**,不含标记自身开销。
856
+ * 计数口径:只统计**真实丢弃的正文**,不含标记自身开销。
830
857
  * 关键点是增量流下 `value` 往往已带上一次的标记(`marker + kept`);若直接
831
858
  * 用 `value.length - kept.length` 计费,会把上一帧的标记文本也算进「已省略」,
832
859
  * 造成每帧显示十几字符的虚高。这里先剥离旧标记,把 N 定义为**从该块起始累计
@@ -17,7 +17,7 @@ export { displayWidth, truncateMiddleToDisplayWidth, truncateToDisplayWidth, } f
17
17
  * 手机(~390px 内容宽)上折叠面板的标题只有**一行**可用:减去图标与内边距约
18
18
  * 320px,按 ~7.5px/半角算 ≈ 42 个半角宽度(iPhone SE 更窄,超出时按 `·`/空格
19
19
  * 折行仍可读,但不能整段换行)。超过就折行,断点常常落在 URL 或命令
20
- * 中间(2026-10-19 截图反馈)。完整参数在展开后的正文里,标题只负责"哪个工具、
20
+ * 中间。完整参数在展开后的正文里,标题只负责"哪个工具、
21
21
  * 动了什么",并带上 `HH:mm` 时间戳(一起计入预算)。
22
22
  */
23
23
  export declare const HEADER_DISPLAY_MAX = 42;
@@ -20,7 +20,7 @@ export { displayWidth, truncateMiddleToDisplayWidth, truncateToDisplayWidth, } f
20
20
  * 手机(~390px 内容宽)上折叠面板的标题只有**一行**可用:减去图标与内边距约
21
21
  * 320px,按 ~7.5px/半角算 ≈ 42 个半角宽度(iPhone SE 更窄,超出时按 `·`/空格
22
22
  * 折行仍可读,但不能整段换行)。超过就折行,断点常常落在 URL 或命令
23
- * 中间(2026-10-19 截图反馈)。完整参数在展开后的正文里,标题只负责"哪个工具、
23
+ * 中间。完整参数在展开后的正文里,标题只负责"哪个工具、
24
24
  * 动了什么",并带上 `HH:mm` 时间戳(一起计入预算)。
25
25
  */
26
26
  export const HEADER_DISPLAY_MAX = 42;
@@ -8,6 +8,18 @@
8
8
  import type { UsageSnapshot } from '../session-contract.js';
9
9
  /** `formatUsageStats` 只读这几项:会话/进度卡片上的紧凑用量行。 */
10
10
  export type SessionDisplayUsage = Pick<UsageSnapshot, 'contextLength' | 'contextLimit' | 'compactCount' | 'cacheReadTokens' | 'cacheCreationTokens'>;
11
+ /**
12
+ * Format number to human-readable unit string.
13
+ * - >= 999_500: M with at most one decimal (e.g., 999_600 → 1M, 1200000 → 1.2M,
14
+ * 25000000 → 25M)
15
+ * - >= 1K: rounded to nearest integer K (e.g., 1500 → 2K, 120000 → 120K)
16
+ * - < 1K: as-is (e.g., 500 → "500")
17
+ *
18
+ * 千位边界取 M 档:999_500 起按 K 四舍五入会得到 "1000K",直接按 M 档展示。
19
+ * 此前 run-renderer 另有一份 formatTokens 只在这边带此边界护栏,两处已收敛
20
+ * 到本函数(run-renderer 的状态行 `📊 N` 也走这里)。
21
+ */
22
+ export declare function formatTokenK(n: number): string;
11
23
  /**
12
24
  * Format usage stats (context length, compact count, cache tokens) into a display string.
13
25
  * Each stat on its own line with blank lines between sections.
@@ -7,12 +7,17 @@
7
7
  */
8
8
  /**
9
9
  * Format number to human-readable unit string.
10
- * - >= 1M: one decimal, M suffix (e.g., 1200000 → 1.2M, 25000000 → 25M)
10
+ * - >= 999_500: M with at most one decimal (e.g., 999_600 → 1M, 1200000 → 1.2M,
11
+ * 25000000 → 25M)
11
12
  * - >= 1K: rounded to nearest integer K (e.g., 1500 → 2K, 120000 → 120K)
12
13
  * - < 1K: as-is (e.g., 500 → "500")
14
+ *
15
+ * 千位边界取 M 档:999_500 起按 K 四舍五入会得到 "1000K",直接按 M 档展示。
16
+ * 此前 run-renderer 另有一份 formatTokens 只在这边带此边界护栏,两处已收敛
17
+ * 到本函数(run-renderer 的状态行 `📊 N` 也走这里)。
13
18
  */
14
- function formatTokenK(n) {
15
- if (n >= 1_000_000) {
19
+ export function formatTokenK(n) {
20
+ if (n >= 999_500) {
16
21
  const m = n / 1_000_000;
17
22
  const rounded = Math.round(m * 10) / 10;
18
23
  // Drop unnecessary ".0" for whole numbers (2.0M → 2M)
package/dist/cli.js CHANGED
@@ -92,10 +92,17 @@ export async function bootstrap(deps) {
92
92
  forwarders.set(sig, forwarder);
93
93
  onSignal(sig, forwarder);
94
94
  }
95
- child.on('error', () => {
95
+ child.on('error', (err) => {
96
+ // bun 拉起失败(ENOENT/EACCES)会静默切 node 兜底,但错误本身必须留痕,
97
+ // 否则「为什么跑在了 node 上」无从排查。logger 此时尚未初始化,console
98
+ // 是规则允许的启动期例外。
99
+ console.error('[lark-remote] bun spawn failed, falling back to node:', err);
96
100
  for (const [sig, forwarder] of forwarders)
97
101
  offSignal(sig, forwarder);
98
- void importEntry().catch(() => exit(1));
102
+ void importEntry().catch((importErr) => {
103
+ console.error('[lark-remote] node fallback import failed:', importErr);
104
+ exit(1);
105
+ });
99
106
  });
100
107
  child.on('exit', (code, signal) => {
101
108
  if (signal) {
@@ -146,7 +153,12 @@ function main() {
146
153
  killSelf: (sig) => process.kill(process.pid, sig),
147
154
  exit: (code) => process.exit(code),
148
155
  };
149
- void bootstrap(deps).catch(() => process.exit(1));
156
+ // bootstrap 是第一接触面:静默 exit(1) 会让安装损坏/依赖缺失表现为零输出
157
+ // 退出,必须把根因打到 stderr(logger 未初始化,console 为规则允许的例外)。
158
+ void bootstrap(deps).catch((err) => {
159
+ console.error('[lark-remote] bootstrap failed:', err);
160
+ process.exit(1);
161
+ });
150
162
  }
151
163
  if (isDirectEntry()) {
152
164
  main();
package/dist/clone.js CHANGED
@@ -237,7 +237,7 @@ export class CloneSession {
237
237
  // 图片发送失败不阻断流程:给出浏览器打开兜底。
238
238
  await this.connector
239
239
  .sendWithRetry(ctx.chatId, { text: `⚠️ 二维码图片发送失败,请在浏览器打开创建:${info.url}` }, { replyTo: ctx.messageId })
240
- .catch(() => { });
240
+ .catch((err) => getLogger().warn('[clone] fallback notice send failed:', err));
241
241
  return;
242
242
  }
243
243
  finally {
@@ -336,7 +336,7 @@ export class CloneSession {
336
336
  `新应用凭证(请妥善保管):App ID ${result.client_id}\n` +
337
337
  '可修复本机问题后用 /clone 重新走一遍流程(会创建新应用)',
338
338
  }, { replyTo: ctx.messageId })
339
- .catch(() => { });
339
+ .catch((sendErr) => getLogger().warn('[clone] finalize failure notice send failed:', sendErr));
340
340
  }
341
341
  finally {
342
342
  this.state = 'idle';