lark-remote 0.4.5 → 0.5.1

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 (127) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/bridge/approval-coordinator.d.ts +1 -1
  3. package/dist/bridge/approval-coordinator.js +3 -3
  4. package/dist/bridge/inbound-media.js +10 -7
  5. package/dist/bridge/index.d.ts +55 -17
  6. package/dist/bridge/index.js +145 -74
  7. package/dist/bridge/queue-manager.d.ts +71 -20
  8. package/dist/bridge/queue-manager.js +139 -57
  9. package/dist/card/approval-render.d.ts +1 -1
  10. package/dist/card/approval-render.js +6 -6
  11. package/dist/card/bash-card-session.d.ts +3 -3
  12. package/dist/card/bash-card-session.js +3 -3
  13. package/dist/card/bash-renderer.js +1 -1
  14. package/dist/card/card-session.d.ts +4 -4
  15. package/dist/card/card-session.js +6 -6
  16. package/dist/card/run-card-session.js +2 -2
  17. package/dist/card/run-renderer.d.ts +1 -1
  18. package/dist/card/run-renderer.js +14 -14
  19. package/dist/card/run-state.d.ts +2 -2
  20. package/dist/card/run-state.js +3 -3
  21. package/dist/card/tool-render.d.ts +1 -1
  22. package/dist/card/tool-render.js +6 -6
  23. package/dist/cli.js +2 -1
  24. package/dist/clone.d.ts +5 -3
  25. package/dist/clone.js +8 -4
  26. package/dist/common/truncate.d.ts +1 -1
  27. package/dist/common/truncate.js +2 -2
  28. package/dist/common/ttl-cache.d.ts +2 -2
  29. package/dist/common/ttl-cache.js +2 -2
  30. package/dist/config/codex-config.d.ts +9 -9
  31. package/dist/config/codex-config.js +18 -18
  32. package/dist/config/dir.js +5 -2
  33. package/dist/config/index.d.ts +3 -3
  34. package/dist/config/index.js +6 -6
  35. package/dist/config/opencode-config.js +3 -3
  36. package/dist/connector/index.d.ts +5 -5
  37. package/dist/connector/index.js +24 -11
  38. package/dist/inbound/event.d.ts +1 -1
  39. package/dist/inbound/event.js +1 -1
  40. package/dist/inbound/placeholder.d.ts +2 -2
  41. package/dist/inbound/placeholder.js +2 -2
  42. package/dist/index.js +23 -12
  43. package/dist/instance-lock.js +4 -3
  44. package/dist/logger/index.d.ts +1 -1
  45. package/dist/logger/index.js +4 -4
  46. package/dist/persistence/atomic-write.d.ts +1 -1
  47. package/dist/persistence/atomic-write.js +6 -2
  48. package/dist/platform/agent-stopper.d.ts +1 -1
  49. package/dist/platform/identity.d.ts +7 -1
  50. package/dist/platform/identity.js +2 -2
  51. package/dist/platform/path.d.ts +6 -0
  52. package/dist/platform/path.js +14 -8
  53. package/dist/platform/select.d.ts +1 -1
  54. package/dist/platform/select.js +1 -1
  55. package/dist/platform/shell.d.ts +2 -3
  56. package/dist/platform/spawn.d.ts +2 -2
  57. package/dist/platform/spawn.js +2 -2
  58. package/dist/platform/terminator-posix.js +1 -1
  59. package/dist/platform/terminator-win32.js +2 -2
  60. package/dist/platform/terminator.d.ts +3 -3
  61. package/dist/platform/terminator.js +2 -2
  62. package/dist/platform/types.d.ts +1 -1
  63. package/dist/restart.d.ts +1 -1
  64. package/dist/restart.js +5 -3
  65. package/dist/router/config/codex.d.ts +1 -1
  66. package/dist/router/config/codex.js +2 -2
  67. package/dist/router/config/opencode.js +1 -1
  68. package/dist/router/index.d.ts +65 -16
  69. package/dist/router/index.js +141 -47
  70. package/dist/router/order-exec-dispatch.js +2 -2
  71. package/dist/runner/bash/runner.d.ts +2 -2
  72. package/dist/runner/bash/runner.js +6 -6
  73. package/dist/runner/claude/runner.js +1 -1
  74. package/dist/runner/claude/session.d.ts +2 -2
  75. package/dist/runner/claude/session.js +14 -14
  76. package/dist/runner/codex/app-server/runner.d.ts +28 -0
  77. package/dist/runner/codex/app-server/runner.js +57 -4
  78. package/dist/runner/codex/app-server/translator.d.ts +2 -2
  79. package/dist/runner/codex/app-server/translator.js +5 -5
  80. package/dist/runner/common/acp/acp-approval.d.ts +2 -2
  81. package/dist/runner/common/acp/acp-approval.js +3 -3
  82. package/dist/runner/common/acp/base-acp-runner.js +1 -1
  83. package/dist/runner/common/acp/base-translator.d.ts +3 -3
  84. package/dist/runner/common/acp/base-translator.js +4 -4
  85. package/dist/runner/common/acp/protocol-types.js +1 -1
  86. package/dist/runner/common/connection-based-runner.d.ts +1 -1
  87. package/dist/runner/common/connection-based-runner.js +2 -2
  88. package/dist/runner/common/jsonl-stream.d.ts +2 -2
  89. package/dist/runner/common/jsonl-stream.js +12 -12
  90. package/dist/runner/common/jsonrpc/client.d.ts +1 -1
  91. package/dist/runner/common/jsonrpc/client.js +1 -1
  92. package/dist/runner/common/jsonrpc/connection-manager.d.ts +1 -1
  93. package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
  94. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  95. package/dist/runner/common/jsonrpc/transport.js +2 -2
  96. package/dist/runner/common/spawning-runner.d.ts +21 -8
  97. package/dist/runner/common/spawning-runner.js +41 -15
  98. package/dist/runner/dsh/runner.js +2 -2
  99. package/dist/runner/kimi/acp/runner.d.ts +3 -3
  100. package/dist/runner/kimi/acp/runner.js +9 -9
  101. package/dist/runner/kimi/acp/translator.d.ts +2 -2
  102. package/dist/runner/kimi/acp/translator.js +4 -4
  103. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  104. package/dist/runner/opencode/acp/runner.js +3 -3
  105. package/dist/runner/opencode/acp/translator.js +1 -1
  106. package/dist/runner/pi/rpc/runner.js +2 -2
  107. package/dist/runner/pi/rpc/translator.d.ts +1 -1
  108. package/dist/runner/pi/rpc/translator.js +4 -4
  109. package/dist/runner/types.d.ts +11 -2
  110. package/dist/runner/types.js +12 -1
  111. package/dist/session/claude/session-index.d.ts +12 -0
  112. package/dist/session/claude/session-index.js +20 -5
  113. package/dist/session/claude/sessions.js +10 -10
  114. package/dist/session/codex/rollout-reader.d.ts +4 -4
  115. package/dist/session/codex/rollout-reader.js +12 -12
  116. package/dist/session/common/two-pass.js +2 -2
  117. package/dist/session/common/usage-accumulator.d.ts +2 -2
  118. package/dist/session/common/usage-accumulator.js +2 -2
  119. package/dist/session/dsh/sessions.js +2 -2
  120. package/dist/session/kimi/sessions.d.ts +2 -2
  121. package/dist/session/kimi/sessions.js +11 -11
  122. package/dist/session/opencode/sessions.d.ts +3 -3
  123. package/dist/session/opencode/sessions.js +12 -10
  124. package/dist/session/pi/sessions.js +13 -12
  125. package/dist/session/session-store.d.ts +1 -1
  126. package/dist/session/session-store.js +1 -1
  127. package/package.json +1 -1
@@ -48,7 +48,7 @@ const FALLBACK_MODELS = ['o3', 'o4-mini', 'gpt-4o', 'gpt-4.1', 'gpt-4.1-mini', '
48
48
  const ANTHROPIC_FALLBACK_MODELS = ['claude-sonnet-4-20250514', 'claude-opus-4-20250115'];
49
49
  /** Bundled models from codex binary, 1h TTL is enough (only change on binary upgrade) */
50
50
  const BUNDLED_CACHE_TTL_MS = 60 * 60 * 1000;
51
- /** 失败/空结果负缓存 TTL:避免每次卡片构建同步阻塞最长 8s(P3-5) */
51
+ /** 失败/空结果负缓存 TTL:避免每次卡片构建同步阻塞最长 8s */
52
52
  const NEGATIVE_CACHE_TTL_MS = 30 * 1000;
53
53
  let catalogCache = null;
54
54
  /** In-flight async catalog loads, keyed by cache key — dedup concurrent warm ups. */
@@ -58,7 +58,7 @@ const catalogInFlight = new Map();
58
58
  * 返回语义:
59
59
  * - undefined:config.toml 不存在或**未声明**该键 → 非 catalog 模式;
60
60
  * - ''(空串哨兵):已声明但不可用(值为空串/非字符串)→ 按"catalog 声明退化"处理
61
- * (P2-1,绝不回退 bundled/FALLBACK);
61
+ * (绝不回退 bundled/FALLBACK);
62
62
  * - TOML_PARSE_FAILED_SENTINEL:config.toml 存在但解析失败 → 无法得知是否声明,
63
63
  * 按非 catalog 处理(用户未声明 model_catalog_json 时的默认行为);
64
64
  * - 具体路径:声明有效(含文件缺失,文件缺失由调用方按 active 命令失败处理)。
@@ -87,7 +87,7 @@ function readModelCatalogJsonPath(codexHome) {
87
87
  }
88
88
  catch {
89
89
  // config.toml 解析失败:codex 对非法配置是硬错误;此处无法得知是否声明了
90
- // model_catalog_json,按非 catalog 处理(P3-5),不冒充 catalog 声明退化
90
+ // model_catalog_json,按非 catalog 处理,不冒充 catalog 声明退化
91
91
  return TOML_PARSE_FAILED_SENTINEL;
92
92
  }
93
93
  }
@@ -96,7 +96,7 @@ function readModelCatalogJsonPath(codexHome) {
96
96
  * 依据:codex 运行时对"已配置但文件缺失/不可解析"的目录是硬错误
97
97
  * (core/src/config/mod.rs load_model_catalog ?),绝不会回退内置目录;因此键存在即
98
98
  * catalog 模式,文件缺失自然落入 `codex debug models` 失败路径 → 模型列表回退
99
- * [currentModel](P1-1)。
99
+ * [currentModel]。
100
100
  */
101
101
  export function isCodexCatalogMode(codexHome) {
102
102
  const home = resolveCodexHome(codexHome);
@@ -105,15 +105,15 @@ export function isCodexCatalogMode(codexHome) {
105
105
  }
106
106
  /**
107
107
  * 单一解析器:把 `codex debug models`(活动目录)或 `--bundled`(内置目录)的原始 JSON
108
- * 输出解析为模型列表(P2-4 去重)。过滤/排序镜像 codex:
108
+ * 输出解析为模型列表(去重)。过滤/排序镜像 codex:
109
109
  * - show_in_picker = visibility == List(openai_models.rs ModelPreset::from);
110
110
  * - API 模式(lark 用 API key 认证)下 codex filter_by_auth 过滤 supported_in_api=false;
111
111
  * - 按 priority 升序(build_available_models)。
112
- * 档位按目录声明**原样透传**(含 custom/none,P2-5),不按标准枚举过滤、不虚构兜底:
112
+ * 档位按目录声明**原样透传**(含 custom/none),不按标准枚举过滤、不虚构兜底:
113
113
  * 未声明档位 → 空列表(codex with_model 语义:len=0 时用声明 default,都没有则不发
114
- * effort;P1)。空串档位/空串 default 被过滤(P3-9:codex ReasoningEffort::from_str
114
+ * effort;)。空串档位/空串 default 被过滤(codex ReasoningEffort::from_str
115
115
  * 对 "" 硬错误)。supported_in_api===true 过滤等价于 codex 的 chatgpt_mode ||
116
- * supported_in_api:本项目以 API-key 模式运行 codex,chatgpt_mode 恒 false(P3-3)。
116
+ * supported_in_api:本项目以 API-key 模式运行 codex,chatgpt_mode 恒 false。
117
117
  */
118
118
  export function parseCodexModelsOutput(stdout) {
119
119
  const parsed = JSON.parse(stdout);
@@ -133,7 +133,7 @@ export function parseCodexModelsOutput(stdout) {
133
133
  const declared = m.supported_reasoning_levels
134
134
  .filter((r) => r !== null && typeof r === 'object' && 'effort' in r)
135
135
  .map((r) => String(r.effort))
136
- // P3-9:codex 拒绝空串档位(openai_models.rs from_str "" → Err)
136
+ // codex 拒绝空串档位(openai_models.rs from_str "" → Err)
137
137
  .filter((effort) => effort.length > 0);
138
138
  supportedReasoningLevels.push(...declared);
139
139
  }
@@ -153,13 +153,13 @@ export function parseCodexModelsOutput(stdout) {
153
153
  /**
154
154
  * 统一目录入口:catalog 模式(model_catalog_json 已声明,含声明退化 '')跑
155
155
  * `debug models`(无 --bundled),否则(未声明或 config.toml 解析失败)跑
156
- * `debug models --bundled`。注(P3-4):codex 无参 `debug models` 在
156
+ * `debug models --bundled`。注:codex 无参 `debug models` 在
157
157
  * uses_codex_backend()/has_command_auth() 时会请求远端 /models(models-manager
158
158
  * manager.rs / models_endpoint.rs);本项目以 API-key 模式运行(无 ChatGPT/命令
159
159
  * 认证),结果与 bundled 一致,故非 catalog 用 --bundled(已文档化)。
160
160
  * 缓存:键含 binary/home/mode/models.json mtime:size + config.toml mtime:size
161
- * (P3-4/P3-15/P3-5);成功结果 TTL 1h,失败/空结果短 TTL 负缓存;stat 异常时
162
- * 指纹为空(P1-1/P3-10)。
161
+ * 成功结果 TTL 1h,失败/空结果短 TTL 负缓存;stat 异常时
162
+ * 指纹为空。
163
163
  *
164
164
  * 同步路径(卡片构建用):缓存命中直接返回;若有一个后台异步加载
165
165
  * (`loadCodexCatalogModelsAsync` / 启动 warm)在途,则不阻塞事件循环、不重复
@@ -372,13 +372,13 @@ export function _clearCodexCatalogInFlightForTest() {
372
372
  export function getReasoningEffortOptions(model, codexHome) {
373
373
  const models = getCodexCatalogModels(codexHome);
374
374
  const found = models.find((m) => m.slug === model);
375
- // 未命中(含未知模型,codex fallback 元数据 supported 为空)→ [],不虚构档位(P1)
375
+ // 未命中(含未知模型,codex fallback 元数据 supported 为空)→ [],不虚构档位
376
376
  return found?.supportedReasoningLevels ?? [];
377
377
  }
378
378
  /**
379
379
  * Get default reasoning effort for a model.
380
380
  * 未声明/未知模型 → undefined(codex 语义:ModelPreset.default_reasoning_effort
381
- * unwrap_or(ReasoningEffort::None),即不传 effort;P3-12)。
381
+ * unwrap_or(ReasoningEffort::None),即不传 effort;)。
382
382
  */
383
383
  export function getDefaultReasoningEffort(model, codexHome) {
384
384
  const models = getCodexCatalogModels(codexHome);
@@ -459,7 +459,7 @@ export function loadCodexConfig(opts = {}) {
459
459
  // 活动目录全局:codex 运行时 provider 与模型无绑定(StaticModelsManager),
460
460
  // 每个 provider 下拉都展示目录全部 slug
461
461
  // 活动目录不可用(命令失败/空)时回退 [currentModel]——无目录元数据时档位/能力
462
- // 不可信;禁止泄漏 FALLBACK_MODELS/内置目录。model 键缺失时不得虚构 'o3'(P1-2)。
462
+ // 不可信;禁止泄漏 FALLBACK_MODELS/内置目录。model 键缺失时不得虚构 'o3'。
463
463
  const catalogCurrentModel = modelKeyRaw ?? catalogSlugs[0] ?? '';
464
464
  const providerModelList = catalogSlugs.length > 0 ? catalogSlugs : modelKeyRaw ? [modelKeyRaw] : [];
465
465
  providerModels = {};
@@ -492,7 +492,7 @@ export function loadCodexConfig(opts = {}) {
492
492
  providerEnvKeys[name] = config.env_key;
493
493
  }
494
494
  }
495
- // anthropic 显式配置但未写 env_key 时保留默认键(P3-9:恢复旧版无条件映射的
495
+ // anthropic 显式配置但未写 env_key 时保留默认键(恢复旧版无条件映射的
496
496
  // 配置场景行为;未配置时不再虚构)
497
497
  if (rawProviderNames.includes('anthropic') &&
498
498
  !modelProviders['anthropic']?.env_key &&
@@ -519,7 +519,7 @@ export function loadCodexConfig(opts = {}) {
519
519
  }
520
520
  catch {
521
521
  // config.toml 解析失败(readModelCatalogJsonPath 已把此场景标记为 catalog 声明退化 ''):
522
- // catalogMode 为 true 时绝不回退 bundled/FALLBACK(P2-1),返回空 provider/模型列表;
522
+ // catalogMode 为 true 时绝不回退 bundled/FALLBACK,返回空 provider/模型列表;
523
523
  // 非 catalog 才走 FALLBACK 兜底。
524
524
  if (catalogMode) {
525
525
  return {
@@ -553,7 +553,7 @@ export function loadCodexConfig(opts = {}) {
553
553
  * - If no provider: return all models from all providers (with currentModel prepended if not in any list)
554
554
  */
555
555
  function buildModelOptions(currentModel, currentProvider, provider, providerModels) {
556
- // 不为空 currentModel 虚构默认值(catalog 退化场景由上层显式传入,P1-2);
556
+ // 不为空 currentModel 虚构默认值(catalog 退化场景由上层显式传入);
557
557
  // 空值时直接返回原始列表
558
558
  const cm = currentModel;
559
559
  if (provider && providerModels[provider]) {
@@ -11,6 +11,9 @@ export function resolveConfigDir(configDirArg) {
11
11
  p = os.homedir();
12
12
  else if (p.startsWith('~/'))
13
13
  p = path.join(os.homedir(), p.slice(2));
14
+ // cmd/PowerShell 不展开 ~;`~\` 是 Windows 侧的常见键入形态,与 `~/` 同口径
15
+ else if (p.startsWith('~\\'))
16
+ p = path.join(os.homedir(), p.slice(2));
14
17
  return path.resolve(p);
15
18
  }
16
19
  return path.join(os.homedir(), DEFAULT_DIR);
@@ -35,7 +38,7 @@ export function parseCliArgs(args = process.argv.slice(2)) {
35
38
  result.version = true;
36
39
  }
37
40
  else if (arg === '--config-dir' && i + 1 < args.length) {
38
- // P1-20: peek, don't consume — when the next arg is itself a flag,
41
+ // peek, don't consume — when the next arg is itself a flag,
39
42
  // roll back so it continues parsing (previously --settings/--help
40
43
  // right after --config-dir were silently swallowed).
41
44
  const nextArg = args[i + 1];
@@ -108,7 +111,7 @@ export function printHelp() {
108
111
  */
109
112
  export function printAdvancedHelp() {
110
113
  const configDir = resolveConfigDir(parseCliArgs().configDir);
111
- const configPath = configDir + '/config.yaml';
114
+ const configPath = path.join(configDir, 'config.yaml');
112
115
  const lines = [
113
116
  'lark-remote 高级配置参考(--advanced-help)',
114
117
  '',
@@ -17,7 +17,7 @@ export declare const PI_THINKING_LEVELS: readonly ["off", "minimal", "low", "med
17
17
  /**
18
18
  * Default SIGTERM→SIGKILL grace period (ms). Exported so runners (SpawningRunner)
19
19
  * and config-loading paths (src/index.ts) can reuse the same source of truth
20
- * instead of re-literalizing `5000` (Clean Code P3-1, G25).
20
+ * instead of re-literalizing `5000` (Clean Code G25).
21
21
  */
22
22
  export declare const DEFAULT_STOP_GRACE_MS: 5000;
23
23
  /** Codex app-server turn idle timeout default (minutes). */
@@ -299,7 +299,7 @@ export declare function loadConfig(configPath: string): AppConfig;
299
299
  */
300
300
  export declare function mapAgentKey(key: string): string;
301
301
  /**
302
- * 拒绝原型链危险 key 段(§P1-4)。
302
+ * 拒绝原型链危险 key 段。
303
303
  *
304
304
  * `/config` 直写路径的 key 是用户自由输入:`__proto__.polluted` 会通过
305
305
  * `current['__proto__']` 拿到 Object.prototype 并写入,污染整个进程且不可恢复;
@@ -324,7 +324,7 @@ export declare function setConfigValue(configPath: string, config: AppConfig, ke
324
324
  * Set multiple nested values in config, write back to YAML file once.
325
325
  *
326
326
  * 在 draft 副本上操作(structuredClone),验证通过后才写盘,避免校验失败时
327
- * 内存中的 config 已被污染(P0 atomicity)。
327
+ * 内存中的 config 已被污染(atomicity)。
328
328
  */
329
329
  export declare function setConfigValues(configPath: string, config: AppConfig, updates: Record<string, string | undefined>): AppConfig;
330
330
  /** Type helpers for agent config (used by getAgentConfig). */
@@ -45,7 +45,7 @@ const DEFAULTS = {
45
45
  /**
46
46
  * Default SIGTERM→SIGKILL grace period (ms). Exported so runners (SpawningRunner)
47
47
  * and config-loading paths (src/index.ts) can reuse the same source of truth
48
- * instead of re-literalizing `5000` (Clean Code P3-1, G25).
48
+ * instead of re-literalizing `5000` (Clean Code G25).
49
49
  */
50
50
  export const DEFAULT_STOP_GRACE_MS = DEFAULTS.STOP_GRACE_MS;
51
51
  /** Codex app-server turn idle timeout default (minutes). */
@@ -115,9 +115,9 @@ export const CodexConfigSchema = z.object({
115
115
  /**
116
116
  * Reasoning effort level。codex 标准档位 minimal/low/medium/high/xhigh/max/ultra
117
117
  * 之外还接受 none 与模型自定义值(ReasoningEffort::Custom),目录声明什么就存什么
118
- * (P2-5),故用字符串而非枚举。
118
+ * 故用字符串而非枚举。
119
119
  */
120
- // codex 拒绝空串档位(ReasoningEffort::from_str "" → Err,P3-3)
120
+ // codex 拒绝空串档位(ReasoningEffort::from_str "" → Err)
121
121
  reasoningEffort: z.string().min(1).optional(),
122
122
  /** Approval policy (Codex 官方 AskForApproval 标准值). 默认 on-request。 */
123
123
  approvalPolicy: z.enum(['untrusted', 'on-request', 'never']).default('on-request'),
@@ -363,7 +363,7 @@ function coerceValue(raw) {
363
363
  return raw;
364
364
  }
365
365
  /**
366
- * 拒绝原型链危险 key 段(§P1-4)。
366
+ * 拒绝原型链危险 key 段。
367
367
  *
368
368
  * `/config` 直写路径的 key 是用户自由输入:`__proto__.polluted` 会通过
369
369
  * `current['__proto__']` 拿到 Object.prototype 并写入,污染整个进程且不可恢复;
@@ -423,7 +423,7 @@ export function setConfigValue(configPath, config, key, value) {
423
423
  * Set multiple nested values in config, write back to YAML file once.
424
424
  *
425
425
  * 在 draft 副本上操作(structuredClone),验证通过后才写盘,避免校验失败时
426
- * 内存中的 config 已被污染(P0 atomicity)。
426
+ * 内存中的 config 已被污染(atomicity)。
427
427
  */
428
428
  export function setConfigValues(configPath, config, updates) {
429
429
  const draft = structuredClone(config);
@@ -472,7 +472,7 @@ function renderConfigPatch(configPath, updates, validated) {
472
472
  doc.deleteIn(parts);
473
473
  continue;
474
474
  }
475
- // 保存载荷里只写"校验结果中存活下来"的键:schema 不认识的键(§P2-24 的 foo.bar)
475
+ // 保存载荷里只写"校验结果中存活下来"的键:schema 不认识的键(foo.bar)
476
476
  // 会被 Zod 剥掉,跟着打在 Document 上就是把垃圾永久写进用户配置文件。
477
477
  // 磁盘上原有的未知键不受影响 —— 那是用户手写的,本函数不碰。
478
478
  const accepted = getConfigValue(validated, parts.join('.'));
@@ -22,7 +22,7 @@ const FALLBACK_PROVIDERS = Object.keys(FALLBACK_MODELS);
22
22
  const OPENCODE_CONFIG_CACHE_TTL_MS = 60_000;
23
23
  /**
24
24
  * 失败负缓存 TTL:命令失败后 30s 内直接用 fallback,避免每次 /config 卡片构建
25
- * 都同步阻塞事件循环最长 10s(§P1-8,与 codex-config 负缓存对齐)。
25
+ * 都同步阻塞事件循环最长 10s(与 codex-config 负缓存对齐)。
26
26
  */
27
27
  const OPENCODE_NEGATIVE_CACHE_TTL_MS = 30_000;
28
28
  let opencodeConfigCache = null;
@@ -57,7 +57,7 @@ function buildFallbackResult() {
57
57
  */
58
58
  export function loadOpencodeConfig() {
59
59
  const now = Date.now();
60
- // 正缓存命中:TTL 内不重复 execSync(P1-8)
60
+ // 正缓存命中:TTL 内不重复 execSync
61
61
  if (opencodeConfigCache && now - opencodeConfigCache.timestamp < OPENCODE_CONFIG_CACHE_TTL_MS) {
62
62
  return opencodeConfigCache.result;
63
63
  }
@@ -74,7 +74,7 @@ export function loadOpencodeConfig() {
74
74
  // 执行 opencode models --verbose 获取所有模型信息
75
75
  const res = spawnProcessSync(resolved.file, ['models', '--verbose'], {
76
76
  encoding: 'utf-8',
77
- // P1-8:30s → 10s,与 codex/kimi 对齐;maxBuffer 防模型清单 ENOBUFS 隐性截断
77
+ // 30s → 10s,与 codex/kimi 对齐;maxBuffer 防模型清单 ENOBUFS 隐性截断
78
78
  timeout: 10000,
79
79
  maxBuffer: 64 * 1024 * 1024,
80
80
  });
@@ -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';
@@ -180,14 +180,14 @@ export declare class FeishuConnector {
180
180
  */
181
181
  private checkUploadable;
182
182
  /**
183
- * sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁,P2-18)
183
+ * sendFile/sendImage 共用骨架:token → 上传(流在任何失败路径销毁)
184
184
  * → 发送媒体消息。上传端点 / form 字段 / 响应 key / msg_type 的差异由调用方
185
185
  * 以 uploadUrl + buildForm + keyField + msgType 表达;timeout、noKeepAliveAgent
186
186
  * (Bun keep-alive 修复)与错误包装在此单源。label 是对外错误前缀与日志
187
187
  * 标签('sendFile' | 'sendImage')。
188
188
  */
189
189
  private uploadAndSendMedia;
190
- /** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁(P2-18)。 */
190
+ /** 单次「上传 + 发媒体消息」;文件流在此拥有,任何失败路径都销毁。 */
191
191
  private uploadWithToken;
192
192
  /**
193
193
  * Upload a file to Feishu and send it to the specified chat.
@@ -201,13 +201,13 @@ export declare class FeishuConnector {
201
201
  */
202
202
  sendImage(chatId: string, filePath: string): Promise<string>;
203
203
  /**
204
- * 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;
205
205
  * fetching it on every file send wastes a round-trip and an unguarded fetch
206
206
  * could return data.code != 0 with an undefined token. Cache with expiry and
207
207
  * validate the response code.
208
208
  *
209
209
  * 缓存的两条失效路径:到期(下面 expire)与被服务端判 token 无效
210
- * (invalidateTenantToken,见 §B13)。
210
+ * (invalidateTenantToken)。
211
211
  */
212
212
  private cachedToken;
213
213
  private cachedTokenExpireAt;
@@ -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 源码实证),并保留原始
@@ -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 里先执行,
@@ -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
  * 可下载资源类占位符:这些占位符对应的资源会由媒体通道下载落盘,
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, SESSION_MUTATING_ACTION_CMDS, } from './router/index.js';
12
+ import { CommandRouter, isImmediateAction, DIRECT_RETURN_CMDS, SESSION_MUTATING_ACTION_CMDS, COMPACT_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';
@@ -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) => {
@@ -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)快照 cwd + agent/session 绑定,语义与旧路径一致
311
+ // 入队时刻快照 cwd + agent/session 绑定,语义与旧路径一致
312
312
  // (排队期间 /cd、/config 不再导致语义漂移)。
313
313
  const messageId = turn.messageIds[turn.messageIds.length - 1] ?? '';
314
314
  // turnMessageIds:终态收尾要覆盖本轮每一条消息(逐条挂的 Typing 得逐条撤)
@@ -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',
@@ -585,7 +585,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
585
585
  // order.aliasInput / order.aliasRemove / order.textInput 也返回 toast+card,
586
586
  // 需直返避免被吞(order.textInput 曾漏在列表外,编辑卡停留在编辑界面)。
587
587
  // approval.planFeedback 同 answer 系列:附意见的 toast 需直返 + 不落串行队列。
588
- // W2.1:直返名单以 router 的 DIRECT_RETURN_CMDS 为单一来源(历史注释:
588
+ // 直返名单以 router 的 DIRECT_RETURN_CMDS 为单一来源(历史注释:
589
589
  // order.textInput 曾漏在列表外、answer 家族曾漏在直返列表外——两份拷贝漂移)。
590
590
  if (DIRECT_RETURN_CMDS.has(actionValue.cmd)) {
591
591
  return router.handleCardAction(fullValue, { userId, chatId, messageId });
@@ -605,7 +605,7 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
605
605
  }).catch((err) => logger.error('[control] order.exec dispatch failed:', err));
606
606
  return;
607
607
  }
608
- // B7:非直返命令到这里已经没有回调响应可回(enqueue* 是 fire-and-forget),
608
+ // 非直返命令到这里已经没有回调响应可回(enqueue* 是 fire-and-forget),
609
609
  // handler 返回的 toast 必须显式转成持久文本,否则失败毫无反馈。
610
610
  const forwardActionFeedback = (res) => {
611
611
  const text = actionFeedbackText(res);
@@ -629,10 +629,21 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
629
629
  taskMeta: {
630
630
  userId,
631
631
  chatId,
632
- messageId,
632
+ // 队列身份 key 必须与「这一次点击」1:1:同一张卡片可点 N 次
633
+ // (Compact 尤其明显),复用卡片 messageId 会让同 key 的多条排队
634
+ // 任务在 queuedTasks/taskIndex/queueCardMessages 三处 lookup 里互相
635
+ // 顶替(2026-10-08 线上:连点 Compact 三次 → 三张卡都显示「第 1 位」、
636
+ // 两张永久停在「等待中」,只压缩一次)。
637
+ messageId: router.mintQueueTaskKey('card', actionValue.cmd),
638
+ // 卡片 messageId 仍然要留着当回复目标:排队卡要回复到被点击的卡片,
639
+ // 内部 key 不是合法飞书 id(replyTo 用它会 400)。
640
+ feishuReplyTo: messageId,
633
641
  messagePreview: `card action: ${actionValue.cmd}`,
642
+ // lane 占用者的展示类型:压缩类动作在排队卡上自报身份
643
+ // (「正在执行: 🗜 Compact」),并据此禁用「立即执行」。
644
+ kind: COMPACT_ACTION_CMDS.has(actionValue.cmd) ? 'compact' : 'command',
634
645
  // Compact 是单向操作,排队卡不允许编辑(编辑预览无意义)。
635
- editable: actionValue.cmd !== 'compact' && actionValue.cmd !== 'resume.compact',
646
+ editable: !COMPACT_ACTION_CMDS.has(actionValue.cmd),
636
647
  },
637
648
  });
638
649
  }
@@ -18,6 +18,7 @@
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
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
@@ -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);
@@ -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;