lark-remote 0.4.4 → 0.5.0

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 (170) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/binder.d.ts +11 -10
  3. package/dist/binder.js +17 -16
  4. package/dist/bridge/approval-coordinator.d.ts +1 -1
  5. package/dist/bridge/approval-coordinator.js +3 -3
  6. package/dist/bridge/inbound-media.d.ts +1 -27
  7. package/dist/bridge/inbound-media.js +23 -21
  8. package/dist/bridge/index.d.ts +57 -57
  9. package/dist/bridge/index.js +136 -129
  10. package/dist/bridge/queue-manager.d.ts +45 -38
  11. package/dist/bridge/queue-manager.js +136 -136
  12. package/dist/card/approval-render.d.ts +1 -1
  13. package/dist/card/approval-render.js +6 -6
  14. package/dist/card/bash-card-session.d.ts +3 -3
  15. package/dist/card/bash-card-session.js +3 -3
  16. package/dist/card/bash-renderer.js +5 -5
  17. package/dist/card/card-session.d.ts +4 -4
  18. package/dist/card/card-session.js +6 -6
  19. package/dist/card/card-shared.d.ts +1 -1
  20. package/dist/card/card-shared.js +2 -2
  21. package/dist/card/run-card-session.js +2 -2
  22. package/dist/card/run-renderer.d.ts +1 -1
  23. package/dist/card/run-renderer.js +22 -22
  24. package/dist/card/run-state.d.ts +2 -2
  25. package/dist/card/run-state.js +3 -3
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +6 -6
  28. package/dist/cli.js +5 -6
  29. package/dist/clone.d.ts +6 -9
  30. package/dist/clone.js +12 -8
  31. package/dist/common/clamp.d.ts +2 -0
  32. package/dist/common/clamp.js +5 -0
  33. package/dist/common/fs.d.ts +4 -3
  34. package/dist/common/fs.js +5 -4
  35. package/dist/common/truncate.d.ts +1 -1
  36. package/dist/common/truncate.js +2 -2
  37. package/dist/common/ttl-cache.d.ts +2 -2
  38. package/dist/common/ttl-cache.js +2 -2
  39. package/dist/config/codex-config.d.ts +9 -9
  40. package/dist/config/codex-config.js +18 -18
  41. package/dist/config/dir.d.ts +4 -5
  42. package/dist/config/dir.js +12 -10
  43. package/dist/config/index.d.ts +3 -3
  44. package/dist/config/index.js +6 -6
  45. package/dist/config/opencode-config.js +3 -3
  46. package/dist/config/wizard.js +2 -2
  47. package/dist/connector/index.d.ts +5 -7
  48. package/dist/connector/index.js +29 -16
  49. package/dist/inbound/event.d.ts +1 -1
  50. package/dist/inbound/event.js +1 -1
  51. package/dist/inbound/placeholder.d.ts +2 -2
  52. package/dist/inbound/placeholder.js +2 -2
  53. package/dist/inbound/receipt.d.ts +1 -3
  54. package/dist/inbound/receipt.js +2 -2
  55. package/dist/inbound/turn-assembler.d.ts +1 -3
  56. package/dist/inbound/turn-assembler.js +2 -2
  57. package/dist/index.js +38 -30
  58. package/dist/instance-lock.js +8 -7
  59. package/dist/logger/index.d.ts +1 -1
  60. package/dist/logger/index.js +4 -4
  61. package/dist/persistence/atomic-write.d.ts +1 -1
  62. package/dist/persistence/atomic-write.js +9 -5
  63. package/dist/platform/agent-stopper.d.ts +2 -2
  64. package/dist/platform/identity.d.ts +7 -1
  65. package/dist/platform/identity.js +2 -2
  66. package/dist/platform/path.d.ts +6 -0
  67. package/dist/platform/path.js +14 -8
  68. package/dist/platform/select.d.ts +1 -1
  69. package/dist/platform/select.js +1 -1
  70. package/dist/platform/shell.d.ts +2 -3
  71. package/dist/platform/spawn.d.ts +2 -2
  72. package/dist/platform/spawn.js +2 -2
  73. package/dist/platform/terminator-posix.js +6 -6
  74. package/dist/platform/terminator-win32.js +3 -3
  75. package/dist/platform/terminator.d.ts +3 -3
  76. package/dist/platform/terminator.js +2 -2
  77. package/dist/platform/types.d.ts +1 -1
  78. package/dist/restart.d.ts +1 -1
  79. package/dist/restart.js +5 -3
  80. package/dist/router/card-helpers.d.ts +1 -3
  81. package/dist/router/card-helpers.js +2 -2
  82. package/dist/router/config/codex.d.ts +1 -1
  83. package/dist/router/config/codex.js +3 -3
  84. package/dist/router/config/common/model-patch.d.ts +1 -2
  85. package/dist/router/config/common/model-patch.js +1 -2
  86. package/dist/router/config/opencode.js +2 -2
  87. package/dist/router/config/pi.js +1 -1
  88. package/dist/router/index.d.ts +30 -30
  89. package/dist/router/index.js +103 -84
  90. package/dist/router/order-exec-dispatch.d.ts +3 -10
  91. package/dist/router/order-exec-dispatch.js +5 -5
  92. package/dist/router/{utils.d.ts → usage-format.d.ts} +2 -4
  93. package/dist/router/{utils.js → usage-format.js} +3 -8
  94. package/dist/runner/bash/runner.d.ts +2 -2
  95. package/dist/runner/bash/runner.js +6 -6
  96. package/dist/runner/claude/runner.d.ts +1 -1
  97. package/dist/runner/claude/runner.js +2 -3
  98. package/dist/runner/claude/session.d.ts +4 -6
  99. package/dist/runner/claude/session.js +21 -23
  100. package/dist/runner/codex/app-server/runner.js +3 -3
  101. package/dist/runner/codex/app-server/translator.d.ts +2 -2
  102. package/dist/runner/codex/app-server/translator.js +5 -5
  103. package/dist/runner/common/acp/{protocol-helpers.d.ts → acp-approval.d.ts} +4 -10
  104. package/dist/runner/common/acp/{protocol-helpers.js → acp-approval.js} +7 -6
  105. package/dist/runner/common/acp/base-acp-runner.d.ts +1 -1
  106. package/dist/runner/common/acp/base-acp-runner.js +2 -2
  107. package/dist/runner/common/acp/base-translator.d.ts +3 -3
  108. package/dist/runner/common/acp/base-translator.js +4 -4
  109. package/dist/runner/common/acp/protocol-types.js +1 -1
  110. package/dist/runner/common/connection-based-runner.d.ts +5 -5
  111. package/dist/runner/common/connection-based-runner.js +5 -5
  112. package/dist/runner/common/jsonl-stream.d.ts +2 -2
  113. package/dist/runner/common/jsonl-stream.js +12 -12
  114. package/dist/runner/common/jsonrpc/client.d.ts +1 -1
  115. package/dist/runner/common/jsonrpc/client.js +1 -1
  116. package/dist/runner/common/jsonrpc/connection-manager.d.ts +13 -13
  117. package/dist/runner/common/jsonrpc/connection-manager.js +37 -37
  118. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  119. package/dist/runner/common/jsonrpc/transport.js +2 -2
  120. package/dist/runner/common/{usage.d.ts → result-usage.d.ts} +1 -1
  121. package/dist/runner/common/{usage.js → result-usage.js} +2 -2
  122. package/dist/runner/common/spawning-runner.d.ts +23 -10
  123. package/dist/runner/common/spawning-runner.js +48 -22
  124. package/dist/runner/common/{runner-utils.d.ts → synthetic-events.d.ts} +4 -4
  125. package/dist/runner/common/{runner-utils.js → synthetic-events.js} +5 -5
  126. package/dist/runner/dsh/client.d.ts +1 -1
  127. package/dist/runner/dsh/client.js +1 -1
  128. package/dist/runner/dsh/runner.js +3 -3
  129. package/dist/runner/kimi/acp/runner.d.ts +4 -4
  130. package/dist/runner/kimi/acp/runner.js +10 -10
  131. package/dist/runner/kimi/acp/translator.d.ts +2 -2
  132. package/dist/runner/kimi/acp/translator.js +5 -5
  133. package/dist/runner/opencode/acp/runner.d.ts +4 -4
  134. package/dist/runner/opencode/acp/runner.js +4 -4
  135. package/dist/runner/opencode/acp/translator.js +2 -2
  136. package/dist/runner/pi/rpc/runner.d.ts +1 -1
  137. package/dist/runner/pi/rpc/runner.js +2 -2
  138. package/dist/runner/pi/rpc/translator.d.ts +1 -2
  139. package/dist/runner/pi/rpc/translator.js +4 -6
  140. package/dist/runner/registry.d.ts +3 -3
  141. package/dist/runner/registry.js +3 -3
  142. package/dist/runner/types.d.ts +1 -1
  143. package/dist/session/claude/session-index.d.ts +13 -1
  144. package/dist/session/claude/session-index.js +21 -6
  145. package/dist/session/claude/session-reader.d.ts +1 -1
  146. package/dist/session/claude/session-reader.js +1 -1
  147. package/dist/session/claude/sessions.js +10 -10
  148. package/dist/session/codex/rollout-reader.d.ts +4 -4
  149. package/dist/session/codex/rollout-reader.js +14 -14
  150. package/dist/session/common/two-pass.js +2 -2
  151. package/dist/session/common/usage-accumulator.d.ts +2 -2
  152. package/dist/session/common/usage-accumulator.js +2 -2
  153. package/dist/session/dsh/sessions.d.ts +1 -1
  154. package/dist/session/dsh/sessions.js +3 -3
  155. package/dist/session/kimi/sessions.d.ts +2 -6
  156. package/dist/session/kimi/sessions.js +13 -19
  157. package/dist/session/opencode/sessions.d.ts +3 -3
  158. package/dist/session/opencode/sessions.js +15 -13
  159. package/dist/session/pi/sessions.js +13 -12
  160. package/dist/session/registry.d.ts +1 -1
  161. package/dist/session/registry.js +1 -1
  162. package/dist/session/session-store.d.ts +3 -4
  163. package/dist/session/session-store.js +3 -5
  164. package/dist/update/index.d.ts +1 -1
  165. package/dist/update/index.js +1 -1
  166. package/dist/update/version-check.d.ts +5 -3
  167. package/dist/update/version-check.js +10 -8
  168. package/dist/workspace/index.d.ts +4 -4
  169. package/dist/workspace/index.js +5 -5
  170. package/package.json +1 -1
@@ -73,7 +73,7 @@ export interface InboundMediaPayload {
73
73
  }
74
74
  /**
75
75
  * 媒体消息到达的轻量描述(未下载):供上层先做 owner/配置闸门,
76
- * 通过后再调 downloadInboundMedia —— 下载发生在认证之后(P1 review 修复)。
76
+ * 通过后再调 downloadInboundMedia —— 下载发生在认证之后(修复)。
77
77
  */
78
78
  /** SDK 给出的可下载资源种类(`resources[].type`)。 */
79
79
  export type InboundResourceKind = 'image' | 'file' | 'audio' | 'video' | 'sticker';
@@ -107,8 +107,6 @@ type CardActionHandler = (action: CardActionEvent) => void | CardActionResponse
107
107
  * 卡片 patch 的重试上限。含义:1 次原始尝试 + 最多 PATCH_MAX_RETRIES 次重试。
108
108
  */
109
109
  export declare const PATCH_MAX_RETRIES = 3;
110
- /** 重试退避基数(ms);第 n 次重试前等待 n × 基数,给对端恢复的时间。 */
111
- export declare const PATCH_RETRY_BASE_DELAY_MS = 150;
112
110
  export declare class FeishuConnector {
113
111
  /** 飞书 channel 实例(public:测试直接访问/注入 mock,替代 as unknown as)。 */
114
112
  channel: LarkChannel;
@@ -182,14 +180,14 @@ export declare class FeishuConnector {
182
180
  */
183
181
  private checkUploadable;
184
182
  /**
185
- * sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁,P2-18)
183
+ * sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁)
186
184
  * → 发送媒体消息。上传端点 / form 字段 / 响应 key / msg_type 的差异由调用方
187
185
  * 以 uploadUrl + buildForm + keyField + msgType 表达;timeout、noKeepAliveAgent
188
186
  * (Bun keep-alive 修复)与错误包装在此单源。label 是对外错误前缀与日志
189
187
  * 标签('sendFile' | 'sendImage')。
190
188
  */
191
189
  private uploadAndSendMedia;
192
- /** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁(P2-18)。 */
190
+ /** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁。 */
193
191
  private uploadWithToken;
194
192
  /**
195
193
  * Upload a file to Feishu and send it to the specified chat.
@@ -203,13 +201,13 @@ export declare class FeishuConnector {
203
201
  */
204
202
  sendImage(chatId: string, filePath: string): Promise<string>;
205
203
  /**
206
- * Cached tenant_access_token for sendFile (P2-18). The token is valid ~2h;
204
+ * Cached tenant_access_token for sendFile. The token is valid ~2h;
207
205
  * fetching it on every file send wastes a round-trip and an unguarded fetch
208
206
  * could return data.code != 0 with an undefined token. Cache with expiry and
209
207
  * validate the response code.
210
208
  *
211
209
  * 缓存的两条失效路径:到期(下面 expire)与被服务端判 token 无效
212
- * (invalidateTenantToken,见 §B13)。
210
+ * (invalidateTenantToken)。
213
211
  */
214
212
  private cachedToken;
215
213
  private cachedTokenExpireAt;
@@ -7,7 +7,7 @@ import { sleep } from '../common/sleep.js';
7
7
  import { isTransientTransportError } from '../error-classification.js';
8
8
  import axios from 'axios';
9
9
  import fs from 'node:fs';
10
- import { silentlyUnlink } from '../common/fs.js';
10
+ import { bestEffortUnlink } from '../common/fs.js';
11
11
  import { displayName } from '../platform/path.js';
12
12
  import https from 'node:https';
13
13
  import os from 'node:os';
@@ -111,7 +111,7 @@ function withTimeout(promise, ms, label, onLateSettle) {
111
111
  });
112
112
  }
113
113
  /**
114
- * 判断 send 失败是否值得重试一次(§9.5 限流重试口径,§P1-3 修正)。
114
+ * 判断 send 失败是否值得重试一次(§9.5 限流重试口径)。
115
115
  *
116
116
  * @larksuite/channel@0.7.1 的 classifyError 把飞书业务码 99991400/99991401
117
117
  * (频率控制)归类为 `code='permission_denied'`(SDK 源码实证),并保留原始
@@ -145,7 +145,7 @@ function shouldRetrySendError(err) {
145
145
  */
146
146
  export const PATCH_MAX_RETRIES = 3;
147
147
  /** 重试退避基数(ms);第 n 次重试前等待 n × 基数,给对端恢复的时间。 */
148
- export const PATCH_RETRY_BASE_DELAY_MS = 150;
148
+ const PATCH_RETRY_BASE_DELAY_MS = 150;
149
149
  /** 记日志用的错误摘要(不打印 axios 巨型循环对象)。 */
150
150
  function formatErrorForLog(err) {
151
151
  if (err instanceof Error) {
@@ -271,6 +271,19 @@ export class FeishuConnector {
271
271
  ttl: DEDUP_TTL_MS,
272
272
  maxEntries: 1000,
273
273
  },
274
+ // 关掉 SDK 的入站去抖合批(`ChatPipeline` + `mergeBatch`),delayMs=0 让每条
275
+ // 消息单独成批、保留自己的 messageId(`enabled` 仍为 true,per-chat 串行与
276
+ // 卡片动作同通道不受影响)。
277
+ //
278
+ // 背景:SDK 默认 600ms 内把同一会话的多条消息合并成一条,`mergeBatch` 只保留
279
+ // **最后一条**的 `messageId` 却把多条消息的 `resources` 求并集。飞书客户端
280
+ // "一次选多图/多文件"会背靠背秒发成多条独立消息,被合并后下载侧拿同一个
281
+ // `messageId` 下所有 `fileKey`,除最后一条外全部报错 234003(HTTP 400):
282
+ // 表现为"多图只有 1 张保存成功,其余下载失败"。合批本应由本项目自己的
283
+ // 700ms 装配器(`InboundTurnAssembler`)负责,SDK 这层是叠加的第二次合批。
284
+ batch: {
285
+ text: { delayMs: 0 },
286
+ },
274
287
  },
275
288
  });
276
289
  // 卡片 patch 的统一出口(观测 + 重试):
@@ -305,7 +318,7 @@ export class FeishuConnector {
305
318
  // SDK 只为 image/file/audio/video/media/sticker 五类产出非空 resources,
306
319
  // 枚举 msg_type 必然漏(2026-09-15 事故:mp4 被当文本转发给 agent)。
307
320
  // 一条消息可以**同时**产出资源事件与文本事件(post 既有文字又有图),
308
- // 由 B3 的装配器合并成同一个 turn。
321
+ // 由的装配器合并成同一个 turn。
309
322
  const hasResources = msg.resources.length > 0;
310
323
  if (hasResources) {
311
324
  // 只上报「媒体到达」,不在此下载:owner/配置闸门在 index.ts 里先执行,
@@ -403,9 +416,9 @@ export class FeishuConnector {
403
416
  for (const res of msg.resources) {
404
417
  const tmpPath = path.join(os.tmpdir(), `lark-remote-inbound-${randomUUID()}`);
405
418
  try {
406
- const { contentType, bytesWritten } = await withTimeout(this.channel.downloadResourceToFile(msg.messageId, res.fileKey, res.type, tmpPath), timeoutMs, `downloadResource fileKey=${res.fileKey}`, () => silentlyUnlink(tmpPath));
419
+ const { contentType, bytesWritten } = await withTimeout(this.channel.downloadResourceToFile(msg.messageId, res.fileKey, res.type, tmpPath), timeoutMs, `downloadResource fileKey=${res.fileKey}`, () => bestEffortUnlink(tmpPath));
407
420
  if (bytesWritten > maxBytes) {
408
- silentlyUnlink(tmpPath);
421
+ bestEffortUnlink(tmpPath);
409
422
  const limitMb = maxBytes / (1024 * 1024);
410
423
  failures.push({
411
424
  kind: res.kind,
@@ -429,7 +442,7 @@ export class FeishuConnector {
429
442
  catch (err) {
430
443
  // 超时不在这里删文件:上面已把清理挂到底层传输的 settle 上。
431
444
  if (!(err instanceof ResourceTimeoutError))
432
- silentlyUnlink(tmpPath);
445
+ bestEffortUnlink(tmpPath);
433
446
  getLogger().warn(`[feishu] downloadResource failed fileKey=${res.fileKey} type=${res.type}:`, err.message);
434
447
  failures.push({
435
448
  kind: res.kind,
@@ -489,7 +502,7 @@ export class FeishuConnector {
489
502
  return result.messageId;
490
503
  }
491
504
  catch (retryErr) {
492
- // P2-17: throw the RETRY error, not the outer `err`. Previously
505
+ // throw the RETRY error, not the outer `err`. Previously
493
506
  // `throw err` here re-threw the first (rate-limit) failure, hiding
494
507
  // the real reason the retry attempt failed (e.g. auth invalid).
495
508
  getLogger().warn('[feishu] retry after rate limit still failed');
@@ -595,7 +608,7 @@ export class FeishuConnector {
595
608
  }
596
609
  }
597
610
  /**
598
- * sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁,P2-18)
611
+ * sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁)
599
612
  * → 发送媒体消息。上传端点 / form 字段 / 响应 key / msg_type 的差异由调用方
600
613
  * 以 uploadUrl + buildForm + keyField + msgType 表达;timeout、noKeepAliveAgent
601
614
  * (Bun keep-alive 修复)与错误包装在此单源。label 是对外错误前缀与日志
@@ -627,13 +640,13 @@ export class FeishuConnector {
627
640
  throw new Error(`${label} failed: ${errorInfo}`, { cause: err });
628
641
  }
629
642
  }
630
- /** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁(P2-18)。 */
643
+ /** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁。 */
631
644
  async uploadWithToken(accessToken, chatId, filePath, uploadUrl, buildForm, keyField, msgType) {
632
645
  let fileStream = null;
633
646
  try {
634
647
  fileStream = fs.createReadStream(filePath);
635
648
  const form = buildForm(fileStream);
636
- // P2-18: timeout 120s(大文件慢链路);httpsAgent: noKeepAliveAgent 防止
649
+ // timeout 120s(大文件慢链路);httpsAgent: noKeepAliveAgent 防止
637
650
  // Bun 复用已被服务端 RST 的 keep-alive socket(ECONNRESET after ~30s)。
638
651
  const uploadResp = await axios.post(uploadUrl, form, {
639
652
  headers: {
@@ -647,7 +660,7 @@ export class FeishuConnector {
647
660
  throw new FeishuBusinessError(`Upload failed: ${uploadResp.data.msg}`, uploadResp.data.code);
648
661
  }
649
662
  const mediaKey = uploadResp.data.data[keyField];
650
- // P2-18: timeout 30s;noKeepAlive 同上。
663
+ // timeout 30s;noKeepAlive 同上。
651
664
  const sendResp = await axios.post('https://open.feishu.cn/open-apis/im/v1/messages', {
652
665
  receive_id: chatId,
653
666
  msg_type: msgType,
@@ -667,7 +680,7 @@ export class FeishuConnector {
667
680
  return sendResp.data.data.message_id;
668
681
  }
669
682
  catch (err) {
670
- // P2-18: destroy the read stream on failure so the fd is released.
683
+ // destroy the read stream on failure so the fd is released.
671
684
  if (fileStream) {
672
685
  try {
673
686
  fileStream.destroy();
@@ -708,13 +721,13 @@ export class FeishuConnector {
708
721
  }, 'image_key', 'image');
709
722
  }
710
723
  /**
711
- * Cached tenant_access_token for sendFile (P2-18). The token is valid ~2h;
724
+ * Cached tenant_access_token for sendFile. The token is valid ~2h;
712
725
  * fetching it on every file send wastes a round-trip and an unguarded fetch
713
726
  * could return data.code != 0 with an undefined token. Cache with expiry and
714
727
  * validate the response code.
715
728
  *
716
729
  * 缓存的两条失效路径:到期(下面 expire)与被服务端判 token 无效
717
- * (invalidateTenantToken,见 §B13)。
730
+ * (invalidateTenantToken)。
718
731
  */
719
732
  cachedToken = null;
720
733
  cachedTokenExpireAt = 0;
@@ -736,7 +749,7 @@ export class FeishuConnector {
736
749
  this.cachedTokenExpireAt = 0;
737
750
  }
738
751
  async fetchTenantAccessToken() {
739
- // P2-18: timeout 30s on the token request.
752
+ // timeout 30s on the token request.
740
753
  // httpsAgent: noKeepAlive — same Bun keep-alive fix as upload above.
741
754
  const tokenResp = await axios.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
742
755
  app_id: this.appId,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 装配器事件(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
3
3
  *
4
- * 设计文档早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
4
+ * 早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
5
5
  * 下载是异步的(`index.ts` 先过 owner/enabled 闸门再下载),若等下载完成才投递事件,
6
6
  * 「先图后文」在慢下载下会先提交一个只含文本的 turn(正是要修的症状)。因此媒体事件
7
7
  * 携带一个 **outcome promise**:装配器立刻知道「本 turn 还有 in-flight 下载」,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 装配器事件(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
3
3
  *
4
- * 设计文档早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
4
+ * 早期的媒体事件形态是「下载完成后的 attachments/failures」。实际链路里
5
5
  * 下载是异步的(`index.ts` 先过 owner/enabled 闸门再下载),若等下载完成才投递事件,
6
6
  * 「先图后文」在慢下载下会先提交一个只含文本的 turn(正是要修的症状)。因此媒体事件
7
7
  * 携带一个 **outcome promise**:装配器立刻知道「本 turn 还有 in-flight 下载」,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 入站消息结构占位符识别与剥离(纯函数,无 IO,便于表驱动测试)。
3
3
  *
4
- * 背景(P0 事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
4
+ * 背景(事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
5
5
  * 富文本(`post`)里的图片是 `![image](img_v3_…)` —— 首字符恰好是 `!`,于是被
6
6
  * `src/router/index.ts` 的裸前缀判定当成 bash 命令执行了整段消息。
7
7
  *
@@ -12,7 +12,7 @@
12
12
  * 取舍(有意为之):剥离对所有消息生效,所以用户在**纯文本**消息里手打
13
13
  * `![image](…)` / `<file key="…"/>` 这类字面量也会被当成占位符剥掉。代价是极少数
14
14
  * 「讨论本工具自身语法」的消息会丢片段;换来的是任何入口都不可能把占位符当命令
15
- * (P0 事故正是 `![image](img_v3_…)` 的首字符 `!` 命中 bash 判定)。
15
+ * (事故正是 `![image](img_v3_…)` 的首字符 `!` 命中 bash 判定)。
16
16
  */
17
17
  export type PlaceholderKind = 'image' | 'file' | 'video' | 'audio' | 'sticker' | 'share' | 'location' | 'folder' | 'vote' | 'todo' | 'meeting' | 'calendar' | 'hongbao' | 'forwarded' | 'unsupported' | 'unknown';
18
18
  export interface StripResult {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 入站消息结构占位符识别与剥离(纯函数,无 IO,便于表驱动测试)。
3
3
  *
4
- * 背景(P0 事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
4
+ * 背景(事故 2026-09-15):`@larksuite/channel` 把非文本消息渲染成结构占位符,
5
5
  * 富文本(`post`)里的图片是 `![image](img_v3_…)` —— 首字符恰好是 `!`,于是被
6
6
  * `src/router/index.ts` 的裸前缀判定当成 bash 命令执行了整段消息。
7
7
  *
@@ -12,7 +12,7 @@
12
12
  * 取舍(有意为之):剥离对所有消息生效,所以用户在**纯文本**消息里手打
13
13
  * `![image](…)` / `<file key="…"/>` 这类字面量也会被当成占位符剥掉。代价是极少数
14
14
  * 「讨论本工具自身语法」的消息会丢片段;换来的是任何入口都不可能把占位符当命令
15
- * (P0 事故正是 `![image](img_v3_…)` 的首字符 `!` 命中 bash 判定)。
15
+ * (事故正是 `![image](img_v3_…)` 的首字符 `!` 命中 bash 判定)。
16
16
  */
17
17
  /**
18
18
  * 可下载资源类占位符:这些占位符对应的资源会由媒体通道下载落盘,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 入站回执文案(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
2
+ * 入站回执文案(`inbound-unified-input-design.md` §5.7)。
3
3
  *
4
4
  * 两类回执合并成一条:
5
5
  * 1. 无文本纯附件(决策 2):不自动起 turn,只回已保存路径 + 引导语;
@@ -10,8 +10,6 @@
10
10
  */
11
11
  import type { PlaceholderKind } from './placeholder.js';
12
12
  import type { RejectedItem } from './turn.js';
13
- /** 单条回执最多列出的文件数;超出折叠为 "… 等 N 个文件"(沿用旧提示口径)。 */
14
- export declare const MAX_PATHS_IN_RECEIPT = 10;
15
13
  /** 「不支持」回执句式(§5.7 表:不支持类型统一「暂不支持 X」)。 */
16
14
  export declare function unsupportedReason(kind: PlaceholderKind): string;
17
15
  export interface ReceiptInput {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 入站回执文案(设计依据见 `docs/zh/architecture/inbound-unified-input-design.md`)。
2
+ * 入站回执文案(`inbound-unified-input-design.md` §5.7)。
3
3
  *
4
4
  * 两类回执合并成一条:
5
5
  * 1. 无文本纯附件(决策 2):不自动起 turn,只回已保存路径 + 引导语;
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import path from 'node:path';
12
12
  /** 单条回执最多列出的文件数;超出折叠为 "… 等 N 个文件"(沿用旧提示口径)。 */
13
- export const MAX_PATHS_IN_RECEIPT = 10;
13
+ const MAX_PATHS_IN_RECEIPT = 10;
14
14
  /** 占位符种类 → 面向人的名称。 */
15
15
  const PLACEHOLDER_LABELS = {
16
16
  image: '图片',
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * InboundTurnAssembler:统一静默期窗口 + 合并 + 下载等待 + commit。
3
3
  *
4
- * 设计依据:`docs/zh/architecture/inbound-unified-input-design.md` §4/§5.2。
4
+ * 设计依据:`docs/zh/architecture/inbound-unified-input-design.md` §4、§5.2。
5
5
  *
6
6
  * 规则:
7
7
  * - 任何事件到达 → **重置** 700ms 定时器(按 `userId:chatId` 分桶),保证图/文任意顺序等价;
@@ -13,8 +13,6 @@
13
13
  */
14
14
  import type { InboundEvent } from './event.js';
15
15
  import { type CommitReason, type InboundTurn } from './turn.js';
16
- /** 统一静默期窗口(ms)——决策 1:不做「纯文本零延迟」快路径。 */
17
- export declare const DEFAULT_TURN_WINDOW_MS = 700;
18
16
  export interface AssemblerContext {
19
17
  userId: string;
20
18
  chatId: string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * InboundTurnAssembler:统一静默期窗口 + 合并 + 下载等待 + commit。
3
3
  *
4
- * 设计依据:`docs/zh/architecture/inbound-unified-input-design.md` §4/§5.2。
4
+ * 设计依据:`docs/zh/architecture/inbound-unified-input-design.md` §4、§5.2。
5
5
  *
6
6
  * 规则:
7
7
  * - 任何事件到达 → **重置** 700ms 定时器(按 `userId:chatId` 分桶),保证图/文任意顺序等价;
@@ -17,7 +17,7 @@ import { buildPrompt } from './prompt.js';
17
17
  import { buildReceipt, unsupportedReason } from './receipt.js';
18
18
  import { bucketKey, } from './turn.js';
19
19
  /** 统一静默期窗口(ms)——决策 1:不做「纯文本零延迟」快路径。 */
20
- export const DEFAULT_TURN_WINDOW_MS = 700;
20
+ const DEFAULT_TURN_WINDOW_MS = 700;
21
21
  export class InboundTurnAssembler {
22
22
  deps;
23
23
  buckets = new Map();
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { probeAllAgents } from './runner/probe.js';
9
9
  import { warmCodexCatalogCache } from './config/codex-config.js';
10
10
  import { SessionReaderRegistry, SessionStore } from './session/index.js';
11
11
  import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, DshSessionReader, } from './session/index.js';
12
- import { CommandRouter, isImmediateAction, DIRECT_RETURN_CMDS, } from './router/index.js';
12
+ import { CommandRouter, isImmediateAction, DIRECT_RETURN_CMDS, SESSION_MUTATING_ACTION_CMDS, } from './router/index.js';
13
13
  import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
14
14
  import { buildCardActionFullValue } from './router/card-action-payload.js';
15
15
  import { actionFeedbackText } from './router/card-action-feedback.js';
@@ -22,7 +22,7 @@ import { InstanceAlreadyRunningError, InstanceLock } from './instance-lock.js';
22
22
  import { spawnReplacementBridge, waitForPreviousInstance } from './restart.js';
23
23
  import { currentPlatform } from './platform/select.js';
24
24
  import { startSleepBlocker } from './platform/sleep-blocker.js';
25
- import { checkLatestVersion, isNewer, runInstallLatest, formatUpdateHint } from './update/index.js';
25
+ import { checkLatestVersion, compareVersions, runInstallLatest, formatUpdateHint, } from './update/index.js';
26
26
  import { classifyRejection } from './error-classification.js';
27
27
  import { WorkspaceStore } from './workspace/index.js';
28
28
  import { InboundTurnAssembler } from './inbound/turn-assembler.js';
@@ -31,7 +31,7 @@ import { buildSessionHistoryCard } from './router/card-helpers.js';
31
31
  import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
32
32
  import path from 'node:path';
33
33
  import fs from 'node:fs';
34
- import { silentlyUnlink } from './common/fs.js';
34
+ import { bestEffortUnlink } from './common/fs.js';
35
35
  /**
36
36
  * /stop 命令别名单源:普通消息的停止分支与 clone 活跃期拦截的排除条件
37
37
  * 共用同一判定,防止将来加别名时两处漂移。
@@ -133,10 +133,10 @@ async function setupInstanceLockAndHandlers(instanceLock, logger, configDir) {
133
133
  /** Initialize per-workspace runner factory + agent registry. */
134
134
  function initializeRunner(config, configDir, cliArgs) {
135
135
  const agentRegistry = new AgentRegistry();
136
- // W2.10 单点化:runner factory 取最新 config 的唯一入口(P1-15 语义保持,
136
+ // 单点化:runner factory 取最新 config 的唯一入口(语义保持,
137
137
  // container 未接线时回退启动快照)。
138
138
  const latest = () => agentRegistry.getConfigContainer()?.current ?? config;
139
- // P1-15: Claude factory reads from configContainer (not closure) so runtime
139
+ // Claude factory reads from configContainer (not closure) so runtime
140
140
  // config changes (model, effort, stopGraceMs) take effect after
141
141
  // bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
142
142
  agentRegistry.register('claude', (ws) => {
@@ -147,7 +147,7 @@ function initializeRunner(config, configDir, cliArgs) {
147
147
  stopGraceMs: claudeConfig.stopGraceMs,
148
148
  settings: cliArgs.settings,
149
149
  pidDir: configDir,
150
- workspace: ws,
150
+ cwd: ws,
151
151
  permissionMode: claudeConfig.permissionMode,
152
152
  idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
153
153
  });
@@ -228,7 +228,7 @@ function initializeRunner(config, configDir, cliArgs) {
228
228
  .split(',')
229
229
  .map((t) => t.trim())
230
230
  .filter(Boolean),
231
- workspace: ws,
231
+ cwd: ws,
232
232
  sessionReader: piSessionReader,
233
233
  });
234
234
  });
@@ -256,7 +256,7 @@ function initializeRunner(config, configDir, cliArgs) {
256
256
  // Register DshRunner (HTTP-only DSH Web Host agent) and DshSessionReader.
257
257
  const dshSessionReader = new DshSessionReader({
258
258
  // hostProvider 每次调用读最新 config:/config 修改 host 后 runner 重建为
259
- // 新 host,reader(/resume、用量、完成卡)也跟随新 host(CC-02)。
259
+ // 新 host,reader(/resume、用量、完成卡)也跟随新 host。
260
260
  hostProvider: () => getAgentConfig(latest(), 'dsh')?.host,
261
261
  });
262
262
  agentRegistry.register('dsh', (_ws) => {
@@ -308,7 +308,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
308
308
  // 附件路径统一注入 prompt;无文本纯附件只回执(决策 2)。
309
309
  const assembler = new InboundTurnAssembler({
310
310
  onCommit: (turn, prompt) => {
311
- // 入队时刻(T0)快照 workspace + agent/session 绑定,语义与旧路径一致
311
+ // 入队时刻快照 cwd + agent/session 绑定,语义与旧路径一致
312
312
  // (排队期间 /cd、/config 不再导致语义漂移)。
313
313
  const messageId = turn.messageIds[turn.messageIds.length - 1] ?? '';
314
314
  // turnMessageIds:终态收尾要覆盖本轮每一条消息(逐条挂的 Typing 得逐条撤)
@@ -318,17 +318,17 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
318
318
  messageId,
319
319
  turnMessageIds: turn.messageIds,
320
320
  };
321
- let workspace = sessionStore.getCwd(turn.userId) ?? '';
322
- if (!workspace && workspaceStore) {
321
+ let cwd = sessionStore.getCwd(turn.userId) ?? '';
322
+ if (!cwd && workspaceStore) {
323
323
  const workspaces = workspaceStore.list();
324
324
  if (workspaces.length > 0)
325
- workspace = workspaces[0].path;
325
+ cwd = workspaces[0].path;
326
326
  }
327
327
  const binding = bridge.currentBinding(turn.userId);
328
- bridge.enqueue(workspace, async () => {
328
+ bridge.enqueue(cwd, async () => {
329
329
  // allowCommandPrefix: false —— prompt 已剥离占位符,绝不再做命令分发。
330
330
  await router.handle(prompt, ctx, {
331
- cwdOverride: workspace,
331
+ cwdOverride: cwd,
332
332
  binding,
333
333
  allowCommandPrefix: false,
334
334
  });
@@ -347,7 +347,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
347
347
  },
348
348
  });
349
349
  router.setInboundFlusher(() => assembler.flushAll('flush'));
350
- // 入站媒体(图片/文件/视频/语音/表情):先认证后下载(P1 review 修复)。
350
+ // 入站媒体(图片/文件/视频/语音/表情):先认证后下载(修复)。
351
351
  // connector 只上报"媒体到达",这里先过 owner + enabled 闸门,通过后才
352
352
  // 下载——未认证/关闭配置时不会发生任何网络下载或内存/磁盘占用。
353
353
  // 下载是异步的:立刻把在途 promise 交给装配器(`kind: 'media'`),
@@ -358,9 +358,9 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
358
358
  return;
359
359
  }
360
360
  // 读 router.config(活引用):/config 保存后 setConfigValues 返回新对象,
361
- // 启动时捕获的 config 参数会过期(P2 review 修复)。
361
+ // 启动时捕获的 config 参数会过期(修复)。
362
362
  if (!router.config.inboundMedia.enabled) {
363
- // 不静默(P3 review):关闭时给 owner 明确反馈,避免发图后无任何反应。
363
+ // 不静默:关闭时给 owner 明确反馈,避免发图后无任何反应。
364
364
  // 不下载、不落盘,只进 rejected(随 turn 回执一起发出)。
365
365
  assembler.ingest({
366
366
  kind: 'rejected',
@@ -386,7 +386,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
386
386
  // save() 的正常路径会清理临时文件;这里兜底 save 进入 mkdir
387
387
  // try 之前意外抛错的情况,避免 os.tmpdir 残留。
388
388
  for (const item of payload.media) {
389
- silentlyUnlink(item.tempPath);
389
+ bestEffortUnlink(item.tempPath);
390
390
  }
391
391
  throw err;
392
392
  }
@@ -402,8 +402,8 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
402
402
  });
403
403
  });
404
404
  connector.setMessageHandler((msg) => {
405
- // 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时首条消息(任意内容)认领
406
- const decision = binder.classify(msg.userId, msg.content, msg.chatId);
405
+ // 绑定/授权闸门:仅 owner 放行;非 owner 静默丢弃;未绑定时首条消息认领
406
+ const decision = binder.authorize(msg.userId, msg.chatId);
407
407
  if (decision.kind === 'rejected')
408
408
  return;
409
409
  if (decision.kind === 'bind_success') {
@@ -562,11 +562,19 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
562
562
  return;
563
563
  }
564
564
  const isImmediate = isImmediateAction(actionValue.cmd);
565
- const workspace = sessionStore.getCwd(userId) ?? '';
565
+ const cwd = sessionStore.getCwd(userId) ?? '';
566
566
  // Build full value object — spread all fields from actionValue, then merge
567
567
  // component out-of-band fields (option/formValue/inputValue/options) where
568
568
  // present. See card-action-payload.ts for the component-type contract.
569
569
  const fullValue = buildCardActionFullValue(actionValue, action);
570
+ // 定序(2026-10-07):会话 / cwd / agent 变更类卡片动作(new-session、
571
+ // resume.use、ls.switch、ws.use、config.save)执行前先冲刷入站装配窗口,
572
+ // 保证窗口内先到的文本消息在本动作之前 commit(绑定旧 session/cwd/agent)。
573
+ // 否则 700ms 静默窗内的消息会漂移到动作之后的新状态:例如输入「重新构建」
574
+ // 后立即点「新会话」,消息会落到新会话。名单以 router 单源为准。
575
+ if (SESSION_MUTATING_ACTION_CMDS.has(actionValue.cmd)) {
576
+ await assembler.flush(userId, chatId, 'flush');
577
+ }
570
578
  // queue.input / config.save / approval.respond / approval.toggle /
571
579
  // approval.answer 系列返回 CardActionResponse toast 给点击用户即时反馈。
572
580
  // 必须直接返回值 -- enqueueImmediate / enqueueConfigAction 是
@@ -577,7 +585,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
577
585
  // order.aliasInput / order.aliasRemove / order.textInput 也返回 toast+card,
578
586
  // 需直返避免被吞(order.textInput 曾漏在列表外,编辑卡停留在编辑界面)。
579
587
  // approval.planFeedback 同 answer 系列:附意见的 toast 需直返 + 不落串行队列。
580
- // W2.1:直返名单以 router 的 DIRECT_RETURN_CMDS 为单一来源(历史注释:
588
+ // 直返名单以 router 的 DIRECT_RETURN_CMDS 为单一来源(历史注释:
581
589
  // order.textInput 曾漏在列表外、answer 家族曾漏在直返列表外——两份拷贝漂移)。
582
590
  if (DIRECT_RETURN_CMDS.has(actionValue.cmd)) {
583
591
  return router.handleCardAction(fullValue, { userId, chatId, messageId });
@@ -591,13 +599,13 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
591
599
  void dispatchOrderExecForQueue({
592
600
  router,
593
601
  bridge,
594
- workspace,
602
+ cwd,
595
603
  orderId: actionValue.orderId,
596
604
  ctx: { userId, chatId, messageId },
597
605
  }).catch((err) => logger.error('[control] order.exec dispatch failed:', err));
598
606
  return;
599
607
  }
600
- // B7:非直返命令到这里已经没有回调响应可回(enqueue* 是 fire-and-forget),
608
+ // 非直返命令到这里已经没有回调响应可回(enqueue* 是 fire-and-forget),
601
609
  // handler 返回的 toast 必须显式转成持久文本,否则失败毫无反馈。
602
610
  const forwardActionFeedback = (res) => {
603
611
  const text = actionFeedbackText(res);
@@ -608,13 +616,13 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
608
616
  .catch((err) => logger.error('[control] card action feedback failed:', err));
609
617
  };
610
618
  if (isImmediate) {
611
- bridge.enqueueImmediate(workspace, async () => {
619
+ bridge.enqueueImmediate(cwd, async () => {
612
620
  const res = await router.handleCardAction(fullValue, { userId, chatId, messageId });
613
621
  forwardActionFeedback(res);
614
622
  });
615
623
  }
616
624
  else {
617
- bridge.enqueue(workspace, async () => {
625
+ bridge.enqueue(cwd, async () => {
618
626
  const res = await router.handleCardAction(fullValue, { userId, chatId, messageId });
619
627
  forwardActionFeedback(res);
620
628
  }, {
@@ -624,7 +632,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
624
632
  messageId,
625
633
  messagePreview: `card action: ${actionValue.cmd}`,
626
634
  // Compact 是单向操作,排队卡不允许编辑(编辑预览无意义)。
627
- editable: actionValue.cmd !== 'codex.compact' && actionValue.cmd !== 'resume.compact',
635
+ editable: actionValue.cmd !== 'compact' && actionValue.cmd !== 'resume.compact',
628
636
  },
629
637
  });
630
638
  }
@@ -651,7 +659,7 @@ async function main() {
651
659
  initLogger({ dir: path.join(updateConfigDir, 'logs') });
652
660
  try {
653
661
  const { current, latest } = await checkLatestVersion();
654
- if (!isNewer(current, latest)) {
662
+ if (compareVersions(current, latest) !== 1) {
655
663
  console.log(`Already up to date: ${current}`);
656
664
  process.exit(0);
657
665
  }
@@ -699,9 +707,9 @@ async function main() {
699
707
  logger.info('[binder] awaiting first binding (any first private message binds)');
700
708
  }
701
709
  else {
702
- logger.info(`[binder] bound to owner openId=${binder.boundOpenId()}`);
710
+ logger.info(`[binder] bound to owner userId=${binder.boundUserId()}`);
703
711
  }
704
- const sessionStore = new SessionStore(path.join(configDir, 'last-session.json'), config.defaultAgent);
712
+ const sessionStore = new SessionStore(path.join(configDir, 'last-session.json'));
705
713
  const workspaceStore = new WorkspaceStore(path.join(configDir, 'workspace.json'));
706
714
  const bridge = new Bridge({
707
715
  connector,
@@ -17,7 +17,8 @@
17
17
  */
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
- import { silentlyUnlink } from './common/fs.js';
20
+ import { bestEffortUnlink } from './common/fs.js';
21
+ import { isWin32 } from './platform/select.js';
21
22
  import { binaryName, verifyPidIdentityVerdict } from './platform/identity.js';
22
23
  export class InstanceAlreadyRunningError extends Error {
23
24
  pid;
@@ -29,7 +30,7 @@ export class InstanceAlreadyRunningError extends Error {
29
30
  }
30
31
  }
31
32
  /**
32
- * Probe whether a pid is alive. P2-20:
33
+ * Probe whether a pid is alive.
33
34
  * - process.kill(pid, 0) returning normally → alive.
34
35
  * - ESRCH (no such process) → dead.
35
36
  * - EPERM (operation not permitted) → the process EXISTS but is owned by
@@ -86,7 +87,7 @@ export class InstanceLock {
86
87
  throw new InstanceAlreadyRunningError(pid, this.lockPath);
87
88
  }
88
89
  // Stale lock: remove and retry once
89
- silentlyUnlink(this.lockPath);
90
+ bestEffortUnlink(this.lockPath);
90
91
  try {
91
92
  const fd = fs.openSync(this.lockPath, 'wx');
92
93
  fs.writeSync(fd, this.lockContents(process.pid));
@@ -114,7 +115,7 @@ export class InstanceLock {
114
115
  release() {
115
116
  const { pid } = this.readPidAndName();
116
117
  if (pid === process.pid) {
117
- silentlyUnlink(this.lockPath);
118
+ bestEffortUnlink(this.lockPath);
118
119
  }
119
120
  }
120
121
  registerExitHandlers() {
@@ -127,14 +128,14 @@ export class InstanceLock {
127
128
  this.release();
128
129
  process.exit(143);
129
130
  });
130
- if (process.platform !== 'win32') {
131
+ if (!isWin32()) {
131
132
  process.on('SIGHUP', () => {
132
133
  this.release();
133
134
  process.exit(129);
134
135
  });
135
136
  }
136
137
  else {
137
- // win32 无 SIGHUP;CTRL_BREAK 退出前也要释放锁(§3.5,对照 cli.ts 信号转发)
138
+ // win32 无 SIGHUP;CTRL_BREAK 退出前也要释放锁(对照 cli.ts 信号转发)
138
139
  process.on('SIGBREAK', () => {
139
140
  this.release();
140
141
  process.exit(149);
@@ -160,7 +161,7 @@ export class InstanceLock {
160
161
  if (Number.isInteger(pid) && pid > 0 && name)
161
162
  return { pid, name };
162
163
  // Malformed or missing name — treat as corrupt, unlink and return undefined
163
- silentlyUnlink(this.lockPath);
164
+ bestEffortUnlink(this.lockPath);
164
165
  return { pid: undefined, name: undefined };
165
166
  }
166
167
  /**
@@ -35,7 +35,7 @@ export declare class Logger {
35
35
  getCurrentLogFile(): string;
36
36
  private ensureFile;
37
37
  /**
38
- * P2-19①: remove daily subdirectories older than the retention window.
38
+ * remove daily subdirectories older than the retention window.
39
39
  * Best-effort — any error is swallowed (logging must never throw).
40
40
  */
41
41
  private pruneOldDirs;