lark-remote 0.4.5 → 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 (124) hide show
  1. package/CHANGELOG.md +18 -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 +9 -9
  6. package/dist/bridge/index.js +41 -41
  7. package/dist/bridge/queue-manager.d.ts +12 -12
  8. package/dist/bridge/queue-manager.js +19 -19
  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/clone.d.ts +5 -3
  24. package/dist/clone.js +8 -4
  25. package/dist/common/truncate.d.ts +1 -1
  26. package/dist/common/truncate.js +2 -2
  27. package/dist/common/ttl-cache.d.ts +2 -2
  28. package/dist/common/ttl-cache.js +2 -2
  29. package/dist/config/codex-config.d.ts +9 -9
  30. package/dist/config/codex-config.js +18 -18
  31. package/dist/config/dir.js +5 -2
  32. package/dist/config/index.d.ts +3 -3
  33. package/dist/config/index.js +6 -6
  34. package/dist/config/opencode-config.js +3 -3
  35. package/dist/connector/index.d.ts +5 -5
  36. package/dist/connector/index.js +24 -11
  37. package/dist/inbound/event.d.ts +1 -1
  38. package/dist/inbound/event.js +1 -1
  39. package/dist/inbound/placeholder.d.ts +2 -2
  40. package/dist/inbound/placeholder.js +2 -2
  41. package/dist/index.js +9 -9
  42. package/dist/instance-lock.js +4 -3
  43. package/dist/logger/index.d.ts +1 -1
  44. package/dist/logger/index.js +4 -4
  45. package/dist/persistence/atomic-write.d.ts +1 -1
  46. package/dist/persistence/atomic-write.js +6 -2
  47. package/dist/platform/agent-stopper.d.ts +1 -1
  48. package/dist/platform/identity.d.ts +7 -1
  49. package/dist/platform/identity.js +2 -2
  50. package/dist/platform/path.d.ts +6 -0
  51. package/dist/platform/path.js +14 -8
  52. package/dist/platform/select.d.ts +1 -1
  53. package/dist/platform/select.js +1 -1
  54. package/dist/platform/shell.d.ts +2 -3
  55. package/dist/platform/spawn.d.ts +2 -2
  56. package/dist/platform/spawn.js +2 -2
  57. package/dist/platform/terminator-posix.js +1 -1
  58. package/dist/platform/terminator-win32.js +2 -2
  59. package/dist/platform/terminator.d.ts +3 -3
  60. package/dist/platform/terminator.js +2 -2
  61. package/dist/platform/types.d.ts +1 -1
  62. package/dist/restart.d.ts +1 -1
  63. package/dist/restart.js +5 -3
  64. package/dist/router/config/codex.d.ts +1 -1
  65. package/dist/router/config/codex.js +2 -2
  66. package/dist/router/config/opencode.js +1 -1
  67. package/dist/router/index.d.ts +9 -9
  68. package/dist/router/index.js +34 -34
  69. package/dist/router/order-exec-dispatch.js +2 -2
  70. package/dist/runner/bash/runner.d.ts +2 -2
  71. package/dist/runner/bash/runner.js +6 -6
  72. package/dist/runner/claude/runner.js +1 -1
  73. package/dist/runner/claude/session.d.ts +2 -2
  74. package/dist/runner/claude/session.js +14 -14
  75. package/dist/runner/codex/app-server/runner.js +3 -3
  76. package/dist/runner/codex/app-server/translator.d.ts +2 -2
  77. package/dist/runner/codex/app-server/translator.js +5 -5
  78. package/dist/runner/common/acp/acp-approval.d.ts +2 -2
  79. package/dist/runner/common/acp/acp-approval.js +3 -3
  80. package/dist/runner/common/acp/base-acp-runner.js +1 -1
  81. package/dist/runner/common/acp/base-translator.d.ts +3 -3
  82. package/dist/runner/common/acp/base-translator.js +4 -4
  83. package/dist/runner/common/acp/protocol-types.js +1 -1
  84. package/dist/runner/common/connection-based-runner.d.ts +1 -1
  85. package/dist/runner/common/connection-based-runner.js +2 -2
  86. package/dist/runner/common/jsonl-stream.d.ts +2 -2
  87. package/dist/runner/common/jsonl-stream.js +12 -12
  88. package/dist/runner/common/jsonrpc/client.d.ts +1 -1
  89. package/dist/runner/common/jsonrpc/client.js +1 -1
  90. package/dist/runner/common/jsonrpc/connection-manager.d.ts +1 -1
  91. package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
  92. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  93. package/dist/runner/common/jsonrpc/transport.js +2 -2
  94. package/dist/runner/common/spawning-runner.d.ts +21 -8
  95. package/dist/runner/common/spawning-runner.js +41 -15
  96. package/dist/runner/dsh/runner.js +2 -2
  97. package/dist/runner/kimi/acp/runner.d.ts +3 -3
  98. package/dist/runner/kimi/acp/runner.js +9 -9
  99. package/dist/runner/kimi/acp/translator.d.ts +2 -2
  100. package/dist/runner/kimi/acp/translator.js +4 -4
  101. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  102. package/dist/runner/opencode/acp/runner.js +3 -3
  103. package/dist/runner/opencode/acp/translator.js +1 -1
  104. package/dist/runner/pi/rpc/runner.js +2 -2
  105. package/dist/runner/pi/rpc/translator.d.ts +1 -1
  106. package/dist/runner/pi/rpc/translator.js +4 -4
  107. package/dist/runner/types.d.ts +1 -1
  108. package/dist/session/claude/session-index.d.ts +12 -0
  109. package/dist/session/claude/session-index.js +20 -5
  110. package/dist/session/claude/sessions.js +10 -10
  111. package/dist/session/codex/rollout-reader.d.ts +4 -4
  112. package/dist/session/codex/rollout-reader.js +12 -12
  113. package/dist/session/common/two-pass.js +2 -2
  114. package/dist/session/common/usage-accumulator.d.ts +2 -2
  115. package/dist/session/common/usage-accumulator.js +2 -2
  116. package/dist/session/dsh/sessions.js +2 -2
  117. package/dist/session/kimi/sessions.d.ts +2 -2
  118. package/dist/session/kimi/sessions.js +11 -11
  119. package/dist/session/opencode/sessions.d.ts +3 -3
  120. package/dist/session/opencode/sessions.js +12 -10
  121. package/dist/session/pi/sessions.js +13 -12
  122. package/dist/session/session-store.d.ts +1 -1
  123. package/dist/session/session-store.js +1 -1
  124. package/package.json +1 -1
@@ -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
@@ -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);
@@ -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;
@@ -21,7 +21,7 @@ function todayStr(d) {
21
21
  return `${y}-${m}-${day}`;
22
22
  }
23
23
  function timestampStr(d) {
24
- // P2-19②: local ISO 8601 with milliseconds and explicit offset, e.g.
24
+ // local ISO 8601 with milliseconds and explicit offset, e.g.
25
25
  // 2026-08-03T01:30:00.000+08:00. Previously this used toISOString() (UTC)
26
26
  // while the daily directory boundary (todayStr) used local time — after
27
27
  // local midnight in a +08:00 zone the new day's directory held lines
@@ -83,7 +83,7 @@ export class Logger {
83
83
  fs.mkdirSync(dayDir, { recursive: true });
84
84
  this.currentFile = path.join(dayDir, `${this.prefix}-${this.pid}.log`);
85
85
  this.currentDate = dateStr;
86
- // P2-19①: prune daily log directories older than the retention window so
86
+ // prune daily log directories older than the retention window so
87
87
  // the logs directory does not grow without bound (single-day size cap is
88
88
  // out of scope; bounded retention by day is the requested guard). Wrapped
89
89
  // defensively so cleanup failure never blocks logging.
@@ -91,7 +91,7 @@ export class Logger {
91
91
  return this.currentFile;
92
92
  }
93
93
  /**
94
- * P2-19①: remove daily subdirectories older than the retention window.
94
+ * remove daily subdirectories older than the retention window.
95
95
  * Best-effort — any error is swallowed (logging must never throw).
96
96
  */
97
97
  pruneOldDirs(currentDateStr) {
@@ -125,7 +125,7 @@ export class Logger {
125
125
  const message = format(...args);
126
126
  const line = `${ts} [${LEVEL_UPPER[level]}] ${message}\n`;
127
127
  const file = this.ensureFile(date);
128
- // P2-19③: never let a disk error (EACCES / ENOSPC / EROFS) escape write().
128
+ // never let a disk error (EACCES / ENOSPC / EROFS) escape write().
129
129
  // If logging itself throws — especially inside an uncaughtException handler
130
130
  // that calls logger.error(...) — a re-throw would abort Node and skip
131
131
  // instanceLock.release(), leaking the singleton lock. Logging must never
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Atomically write content (string or binary Buffer) to a file via tmp + rename.
3
3
  * Falls back to copy+unlink when rename fails with EXDEV (cross-device).
4
- * rename 目标被占用(win32 EPERM/EBUSY)时短暂退避重试(§6);重试耗尽后
4
+ * rename 目标被占用(win32 EPERM/EBUSY)时短暂退避重试;重试耗尽后
5
5
  * 保留 tmp 现场并告警,再抛出原错误。
6
6
  */
7
7
  export declare function atomicWrite(filePath: string, content: string | Buffer, encoding?: BufferEncoding): void;
@@ -5,13 +5,17 @@ import { getLogger } from '../logger/index.js';
5
5
  /**
6
6
  * Atomically write content (string or binary Buffer) to a file via tmp + rename.
7
7
  * Falls back to copy+unlink when rename fails with EXDEV (cross-device).
8
- * rename 目标被占用(win32 EPERM/EBUSY)时短暂退避重试(§6);重试耗尽后
8
+ * rename 目标被占用(win32 EPERM/EBUSY)时短暂退避重试;重试耗尽后
9
9
  * 保留 tmp 现场并告警,再抛出原错误。
10
10
  */
11
11
  export function atomicWrite(filePath, content, encoding = 'utf-8') {
12
12
  const dir = path.dirname(filePath);
13
13
  fs.mkdirSync(dir, { recursive: true });
14
- const tmpPath = filePath + '.tmp';
14
+ // tmp 命名:pid-ts-rand 后缀,避免固定
15
+ // `.tmp` 名在异常残留后被下一次写入复用/误判;必须与目标同目录(rename 原子性)
16
+ const tmpPath = path.join(dir, `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random()
17
+ .toString(36)
18
+ .slice(2, 10)}`);
15
19
  // Buffer 内容忽略 encoding(writeFileSync 对 Buffer 自动以二进制写出)。
16
20
  fs.writeFileSync(tmpPath, content, encoding);
17
21
  // fsync the tmp file before rename so its data is durable on disk;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 每 agent 的协议停止通道注册表(design.md §3.3)。
2
+ * 每 agent 的协议停止通道注册表。
3
3
  *
4
4
  * Windows 没有可拦截的跨进程 SIGTERM,优雅停止只能靠协议通道(claude 的 stdin
5
5
  * 控制通道、codex 的 turn interrupt、ACP 的 session/cancel…)。「无通道」是
@@ -12,7 +12,13 @@ export interface QueryIdentityOptions {
12
12
  export interface VerifyPidIdentityOptions extends QueryIdentityOptions {
13
13
  /** 期望的二进制名(basename,如 'claude'/'node');win32/darwin 大小写不敏感 */
14
14
  expectedBinary: string;
15
- /** pid 文件记录的 CreationDate;旧文件缺省该字段则退化为仅命令行匹配 */
15
+ /**
16
+ * pid 文件记录的 CreationDate;缺省则退化为仅命令行匹配。
17
+ * 现状:**无生产写入方**(pid 文件/锁文件都不记 CreationDate),仅测试
18
+ * 使用——接线需 win32 写侧在正常启动路径多一次 PowerShell 查询自身
19
+ * CreationDate,与 instance-lock「CIM 只在锁已存在路径查询」的约束冲突,
20
+ * 故保留该字段但暂不接线。
21
+ */
16
22
  expectedCreationDate?: string;
17
23
  /** 匹配强度;默认 'executable'(见 {@link IdentityMatchMode}) */
18
24
  matchMode?: IdentityMatchMode;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * pid 身份验证(design.md §3.4 / §3.5)。
2
+ * pid 身份验证。
3
3
  *
4
4
  * POSIX 现状用 `ps -o command=` 读命令行,Windows 没有 ps,等价物是 CIM:
5
5
  * PowerShell `Get-CimInstance Win32_Process` 取 CommandLine,并额外取
@@ -9,7 +9,7 @@
9
9
  * - 只在查询失败/身份不匹配时返回 null / false,绝不抛给调用方——调用方
10
10
  * (killOrphan、实例锁陈旧判定)一律「只有身份匹配才动手」;
11
11
  * - win32 每次查询要拉起 PowerShell(冷启动约 1s),批量扫 pid 文件的场景
12
- * (killOrphan)应按需调用,真机矩阵(M0)需实测整体耗时;
12
+ * (killOrphan)应按需调用,真机矩阵需实测整体耗时;
13
13
  * - platform 显式注入,win32 语义可在任意宿主上单测。
14
14
  */
15
15
  import { execFileSync, spawn } from 'node:child_process';
@@ -1,6 +1,12 @@
1
1
  export interface PathOptions {
2
2
  platform?: NodeJS.Platform;
3
3
  }
4
+ /**
5
+ * 路径的「判等键」形态:把 samePath 的平台归一规则单源化,供 Map/Set 键控
6
+ * 场景使用(按 cwd 建倒排索引时不能每次查询做 O(n) 两两比较)。不变量:
7
+ * `samePath(a, b)` 当且仅当 `pathKey(a) === pathKey(b)`。
8
+ */
9
+ export declare function pathKey(p: string, opts?: PathOptions): string;
4
10
  export declare function samePath(a: string, b: string, opts?: PathOptions): boolean;
5
11
  /**
6
12
  * cwd → 会话项目目录名(Claude/pi 的 on-disk 编码,lossy N-to-N)。
@@ -1,5 +1,5 @@
1
1
  /**
2
- * PathKit — 跨平台路径语义(design.md §5.1)。
2
+ * PathKit — 跨平台路径语义。
3
3
  *
4
4
  * 所有 cwd 相等性判断、会话目录编码、pid 文件名后缀、spawn cwd 前统一走本模块。
5
5
  * 语义映射:
@@ -14,14 +14,18 @@ import { currentPlatform, isWin32 } from './select.js';
14
14
  function resolveOptions(opts) {
15
15
  return { platform: opts?.platform ?? currentPlatform };
16
16
  }
17
- export function samePath(a, b, opts) {
17
+ /**
18
+ * 路径的「判等键」形态:把 samePath 的平台归一规则单源化,供 Map/Set 键控
19
+ * 场景使用(按 cwd 建倒排索引时不能每次查询做 O(n) 两两比较)。不变量:
20
+ * `samePath(a, b)` 当且仅当 `pathKey(a) === pathKey(b)`。
21
+ */
22
+ export function pathKey(p, opts) {
18
23
  const { platform } = resolveOptions(opts);
19
24
  if (platform === 'linux')
20
- return a === b;
25
+ return p;
21
26
  if (isWin32(platform)) {
22
27
  // win32:\ 与 / 都是分隔符,且路径大小写不敏感
23
- const norm = (p) => p.replaceAll('\\', '/').replace(/\/+$/, '');
24
- return norm(a).toLowerCase() === norm(b).toLowerCase();
28
+ return p.replaceAll('\\', '/').replace(/\/+$/, '').toLowerCase();
25
29
  }
26
30
  // darwin:默认按大小写不敏感处理(macOS 默认文件系统 APFS 不敏感);
27
31
  // 反斜杠是合法文件名字符,不动。分隔符只有 /。
@@ -29,9 +33,11 @@ export function samePath(a, b, opts) {
29
33
  // 此处会判等(cwd 守卫假阳性)——影响面小(默认卷不敏感),如需精确需
30
34
  // 逐目录 stat 探测卷大小写属性,暂不做。
31
35
  // 尾部分隔符必须与 win32 同口径忽略,否则 `/cd ~/repo/` 与 `/cd ~/repo`
32
- // 会被判成两个目录,M2 迁移后直接表现为会话目录错配。
33
- const norm = (p) => p.replace(/\/+$/, '');
34
- return norm(a).toLowerCase() === norm(b).toLowerCase();
36
+ // 会被判成两个目录,迁移后直接表现为会话目录错配。
37
+ return p.replace(/\/+$/, '').toLowerCase();
38
+ }
39
+ export function samePath(a, b, opts) {
40
+ return pathKey(a, opts) === pathKey(b, opts);
35
41
  }
36
42
  /**
37
43
  * cwd → 会话项目目录名(Claude/pi 的 on-disk 编码,lossy N-to-N)。
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 平台选择的唯一入口(design.md §2.2 纪律)。
2
+ * 平台选择的唯一入口。
3
3
  *
4
4
  * `process.platform` 的读取只允许发生在本文件;其余模块一律导入
5
5
  * `currentPlatform` / `isWin32`,或接受显式 platform 参数(便于在任意
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 平台选择的唯一入口(design.md §2.2 纪律)。
2
+ * 平台选择的唯一入口。
3
3
  *
4
4
  * `process.platform` 的读取只允许发生在本文件;其余模块一律导入
5
5
  * `currentPlatform` / `isWin32`,或接受显式 platform 参数(便于在任意
@@ -1,10 +1,9 @@
1
1
  /**
2
- * ShellBackend:把 `!` 命令与 kimi ACP terminal 的 shell 依赖收进 seam
3
- * (design.md §7.2)。
2
+ * ShellBackend:把 `!` 命令与 kimi ACP terminal 的 shell 依赖收进 seam。
4
3
  *
5
4
  * - posix:`bash -c <command>`(与现状 BashProcessRunner 同语义);
6
5
  * - win32:默认 Git Bash(`bash.exe -c <command>`),powershell / cmd 仅作为
7
- * 显式用户选择 §7.3;
6
+ * 显式用户选择;
8
7
  * - 首版不做命令翻译器:Git Bash 缺失时抛 {@link ShellUnavailableError},
9
8
  * 由调用方转成明确错误卡(提示需要 Git Bash)。
10
9
  *
@@ -14,7 +14,7 @@ export declare function spawnProcessSync(command: string, args?: readonly string
14
14
  /**
15
15
  * env 覆盖合并:先按不区分大小写删除 base 中的同名旧键,再设新值。
16
16
  * Windows env 键大小写不敏感,`{...process.env, PATH:x}` 会产生 `PATH`/`Path`
17
- * 双键(§8.3)。注意 posix 上并非普通 spread:base 中大小写不同名的键(如
17
+ * 双键。注意 posix 上并非普通 spread:base 中大小写不同名的键(如
18
18
  * `Path`)也会被删除,而非与 `PATH` 并存——可接受(posix 语义键为全大写
19
19
  * `PATH`,大小写变体本身就不是语义键),但语义与 spread 不同,勿混淆。
20
20
  */
@@ -30,7 +30,7 @@ export declare function mergeProcessEnv(base?: NodeJS.ProcessEnv, overrides?: No
30
30
  * Windows 上 `DETACHED_PROCESS` 下 cmd.exe 另建控制台并重绑标准句柄,于是
31
31
  * 子进程(npm 安装的 agent 全是 `.cmd` 垫片)的 stdout/stderr 永不抵达父进程,
32
32
  * 表现为 agent 全程静默、turn 超时。实测 `windowsHide: true` 不能改善,
33
- * 因此 v1 §3.3「保留 detached + windowsHide」的假设据此修正为「win32 不
33
+ * 因此 「保留 detached + windowsHide」的假设据此修正为「win32 不
34
34
  * detached」。调用方应同时传 `windowsHide: true`,避免 cmd.exe 闪控制台窗口。
35
35
  */
36
36
  export declare function useDetachedProcessGroup(platform?: NodeJS.Platform): boolean;
@@ -32,7 +32,7 @@ export function spawnProcessSync(command, args = [], options = {}) {
32
32
  /**
33
33
  * env 覆盖合并:先按不区分大小写删除 base 中的同名旧键,再设新值。
34
34
  * Windows env 键大小写不敏感,`{...process.env, PATH:x}` 会产生 `PATH`/`Path`
35
- * 双键(§8.3)。注意 posix 上并非普通 spread:base 中大小写不同名的键(如
35
+ * 双键。注意 posix 上并非普通 spread:base 中大小写不同名的键(如
36
36
  * `Path`)也会被删除,而非与 `PATH` 并存——可接受(posix 语义键为全大写
37
37
  * `PATH`,大小写变体本身就不是语义键),但语义与 spread 不同,勿混淆。
38
38
  */
@@ -61,7 +61,7 @@ const COMMAND_NOT_FOUND_PATTERN = /is not recognized as an internal or external
61
61
  * Windows 上 `DETACHED_PROCESS` 下 cmd.exe 另建控制台并重绑标准句柄,于是
62
62
  * 子进程(npm 安装的 agent 全是 `.cmd` 垫片)的 stdout/stderr 永不抵达父进程,
63
63
  * 表现为 agent 全程静默、turn 超时。实测 `windowsHide: true` 不能改善,
64
- * 因此 v1 §3.3「保留 detached + windowsHide」的假设据此修正为「win32 不
64
+ * 因此 「保留 detached + windowsHide」的假设据此修正为「win32 不
65
65
  * detached」。调用方应同时传 `windowsHide: true`,避免 cmd.exe 闪控制台窗口。
66
66
  */
67
67
  export function useDetachedProcessGroup(platform = currentPlatform) {
@@ -56,7 +56,7 @@ async function stopPosix(proc, opts, log) {
56
56
  }
57
57
  return { requested: true, via: 'taskkill' };
58
58
  }
59
- // 与 exit 事件竞速等 grace;用 once 避免重复 stop() 累积监听器(P2-15)
59
+ // 与 exit 事件竞速等 grace;用 once 避免重复 stop() 累积监听器
60
60
  let graceTimer;
61
61
  const exited = await Promise.race([
62
62
  new Promise((resolve) => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * win32 终止器:协议停止通道 → grace 等待 → taskkill 树杀(design.md §3.2)。
2
+ * win32 终止器:协议停止通道 → grace 等待 → taskkill 树杀。
3
3
  *
4
4
  * 状态机:
5
5
  * 1. 进程已退出 → nothing(via='already-exited')
@@ -99,7 +99,7 @@ export function createWin32Terminator(deps) {
99
99
  return { requested: true, via: 'taskkill' };
100
100
  },
101
101
  cleanupOnExit(proc) {
102
- // M0 真机验证项(§9.2 矩阵):本方法在 process.on('exit') 里调用,而
102
+ // 真机验证项(§9.2 矩阵):本方法在 process.on('exit') 里调用,而
103
103
  // killTree 依赖 libuv uv_spawn 在 exit handler 内同步发起 taskkill——
104
104
  // 大概率可用(uv_spawn 本身同步),但与 posix 路径(同步 process.kill)
105
105
  // 可靠性不等价;失败表现为 win32 上 lark-remote 退出后 agent 变孤儿。
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 平台终止器 seam 的接口层(design.md §3.2 / §3.3)。
2
+ * 平台终止器 seam 的接口层。
3
3
  *
4
4
  * Windows 没有可拦截的跨进程 SIGTERM、负 PID 无效,因此把「进程组 + 信号」
5
5
  * 换成「协议停止通道 + 进程树杀」。本文件只放接口与平台分发,实现见
@@ -14,7 +14,7 @@ export type { AgentStopper } from './agent-stopper.js';
14
14
  * 进程终止器。
15
15
  *
16
16
  * `stop` 的 `immediate` 语义与原 `process-stopper` 的 `stop({immediate})` 对齐。
17
- * 设计文档声明返回 `Promise<void>`,这里收窄为 {@link TerminateResult}:降级
17
+ * 声明返回 `Promise<void>`,这里收窄为 {@link TerminateResult}:降级
18
18
  * 必须可观测(优雅段被跳过、走了树杀、进程早已退出),否则 win32 上「等一个
19
19
  * 不会来的退出事件」这类退化无法被发现。
20
20
  */
@@ -39,6 +39,6 @@ export interface TerminatorDeps {
39
39
  }
40
40
  /**
41
41
  * 唯一的终止器选择入口:posix 走组杀,win32 走「协议通道 + taskkill 树杀」。
42
- * 其余模块一律拿这里的 Terminator,禁止散落平台 if(§2.2)。
42
+ * 其余模块一律拿这里的 Terminator,禁止散落平台 if。
43
43
  */
44
44
  export declare function createTerminator(deps: TerminatorDeps): Terminator;
@@ -17,12 +17,12 @@ function defaultTerminatorLog(level, message) {
17
17
  }
18
18
  /**
19
19
  * 唯一的终止器选择入口:posix 走组杀,win32 走「协议通道 + taskkill 树杀」。
20
- * 其余模块一律拿这里的 Terminator,禁止散落平台 if(§2.2)。
20
+ * 其余模块一律拿这里的 Terminator,禁止散落平台 if。
21
21
  */
22
22
  export function createTerminator(deps) {
23
23
  const platform = deps.platform ?? currentPlatform;
24
24
  // 默认日志出口必须真的接上:win32 的「无协议通道 → 直接树杀」「协议通道抛错
25
- // → 转树杀」以及 posix 的 grace 超时都是设计要求的可观测降级信号(§3.2),
25
+ // → 转树杀」以及 posix 的 grace 超时都是设计要求的可观测降级信号,
26
26
  // 缺省 undefined 会让这些信号全部静默消失。
27
27
  const log = deps.log ?? defaultTerminatorLog;
28
28
  if (isWin32(platform)) {
@@ -11,7 +11,7 @@ export interface LaunchSpec {
11
11
  /** 可执行文件路径(posix 绝对路径 / win32 含 .cmd 垫片路径) */
12
12
  file: string;
13
13
  }
14
- /** 终止结果(§3.2 win32 状态机产出;可观测降级用)。 */
14
+ /** 终止结果(win32 状态机产出;可观测降级用)。 */
15
15
  export interface TerminateResult {
16
16
  /** 进程是否已确认被请求终止(不代表已退出,退出由 exit 事件驱动) */
17
17
  requested: boolean;
package/dist/restart.d.ts CHANGED
@@ -16,7 +16,7 @@ export declare const RESTART_WAIT_PID_ENV = "LARK_REMOTE_RESTART_WAIT_PID";
16
16
  * dir) propagate. Caller picks throw-vs-degrade semantics.
17
17
  *
18
18
  * Two disciplines live here so every caller inherits them for free:
19
- * - windowsHide: win32 detached children would flash a console window (§3.7);
19
+ * - windowsHide: win32 detached children would flash a console window;
20
20
  * - the 'error' handler is attached BEFORE the pid check — a failed spawn
21
21
  * emits 'error' asynchronously (ENOENT/EACCES), and the synchronous pid
22
22
  * check throws/returns first, so attaching after would leave the 'error'
package/dist/restart.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawnProcess } from './platform/spawn.js';
1
+ import { mergeProcessEnv, spawnProcess } from './platform/spawn.js';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { getLogger } from './logger/index.js';
@@ -23,7 +23,7 @@ const POLL_MS = 100;
23
23
  * dir) propagate. Caller picks throw-vs-degrade semantics.
24
24
  *
25
25
  * Two disciplines live here so every caller inherits them for free:
26
- * - windowsHide: win32 detached children would flash a console window (§3.7);
26
+ * - windowsHide: win32 detached children would flash a console window;
27
27
  * - the 'error' handler is attached BEFORE the pid check — a failed spawn
28
28
  * emits 'error' asynchronously (ENOENT/EACCES), and the synchronous pid
29
29
  * check throws/returns first, so attaching after would leave the 'error'
@@ -58,7 +58,9 @@ export function spawnDetachedBridge(args, logFilePath, opts) {
58
58
  */
59
59
  export function spawnReplacementBridge(logsDir) {
60
60
  const pid = spawnDetachedBridge(process.argv.slice(1), path.join(logsDir, 'restart-child.log'), {
61
- env: { ...process.env, [RESTART_WAIT_PID_ENV]: String(process.pid) },
61
+ // env 注入统一走 mergeProcessEnv:Windows env 键大小写不敏感,
62
+ // 覆盖注入必须先按不区分大小写删旧键,避免双键
63
+ env: mergeProcessEnv(process.env, { [RESTART_WAIT_PID_ENV]: String(process.pid) }),
62
64
  });
63
65
  if (pid === null) {
64
66
  throw new Error('spawn replacement lark-remote failed: no pid');
@@ -16,7 +16,7 @@ export declare class CodexConfigBuilder implements AgentConfigCardBuilder {
16
16
  * - 否则优先 supported_reasoning_levels 中位 (len-1)/2;
17
17
  * - 列表为空才用声明 default_reasoning_level;
18
18
  * - 两者皆无 → value=undefined(清空档位 = 不传 effort;router setNestedValue
19
- * 对 undefined 做键删除,P1)。
19
+ * 对 undefined 做键删除)。
20
20
  */
21
21
  private effortPatchForModel;
22
22
  }
@@ -41,7 +41,7 @@ export class CodexConfigBuilder {
41
41
  key: 'agents.codex.reasoningEffort',
42
42
  label: '推理强度',
43
43
  type: 'select',
44
- // 档位按目录实际声明透传(P2-5):codex 支持 none/Custom,不再按标准枚举过滤
44
+ // 档位按目录实际声明透传:codex 支持 none/Custom,不再按标准枚举过滤
45
45
  options: getReasoningEffortOptions(currentCodexModel),
46
46
  currentValue: currentReasoningEffort,
47
47
  });
@@ -108,7 +108,7 @@ export class CodexConfigBuilder {
108
108
  * - 否则优先 supported_reasoning_levels 中位 (len-1)/2;
109
109
  * - 列表为空才用声明 default_reasoning_level;
110
110
  * - 两者皆无 → value=undefined(清空档位 = 不传 effort;router setNestedValue
111
- * 对 undefined 做键删除,P1)。
111
+ * 对 undefined 做键删除)。
112
112
  */
113
113
  effortPatchForModel(config, newModel) {
114
114
  const currentReasoningEffort = config.agents?.codex?.reasoningEffort;
@@ -29,7 +29,7 @@ export class OpencodeConfigBuilder {
29
29
  type: 'select',
30
30
  options: modelOptions,
31
31
  });
32
- // §P5: mode select(build/plan,默认 build)——opencode 无运行期审批档位,
32
+ // mode select(build/plan,默认 build)——opencode 无运行期审批档位,
33
33
  // mode 即审批粒度档位(build 逐项审批 / plan 规划模式)。
34
34
  const currentMode = displayConfig.agents?.opencode?.mode ?? 'build';
35
35
  fields.push({
@@ -222,7 +222,7 @@ export declare class CommandRouter {
222
222
  /**
223
223
  * Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
224
224
  *
225
- * 命令识别前置条件(2026-09-15 P0 修复,`inbound-unified-input-design.md` §5.6):
225
+ * 命令识别前置条件(2026-09-15修复,`inbound-unified-input-design.md` §5.6):
226
226
  * 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 `![image](img_v3_…)`,
227
227
  * 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
228
228
  * 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
@@ -304,7 +304,7 @@ export declare class CommandRouter {
304
304
  *
305
305
  * offset 恒重置为 0(新筛选从第 1 页开始)。路径校验与 TOCTOU 兜底照
306
306
  * handleLsPage——校验失败只回错误 toast,绝不动卡片(否则用户看到一张空卡)。
307
- * ls.browse / ls.switch 的 value 里刻意不带 q,换目录即清除筛选(设计文档 §2.3)。
307
+ * ls.browse / ls.switch 的 value 里刻意不带 q,换目录即清除筛选。
308
308
  */
309
309
  private handleLsFilter;
310
310
  /**
@@ -350,17 +350,17 @@ export declare class CommandRouter {
350
350
  internalKey: string;
351
351
  } | null;
352
352
  /**
353
- * W2.8 单源:重建 order/ws 列表卡(不投递,card 进 callback 响应体用)。
353
+ * 单源:重建 order/ws 列表卡(不投递,card 进 callback 响应体用)。
354
354
  * `q` 只服务 ws 卡的关键词筛选;order 卡无此功能,不透传。
355
355
  */
356
356
  private rebuildListCard;
357
357
  /**
358
- * W2.8 单源:「重建列表卡 → updateCardInPlace」原地刷新骨架(列表卡删除/
358
+ * 单源:「重建列表卡 → updateCardInPlace」原地刷新骨架(列表卡删除/
359
359
  * 翻页/移除/排序/筛选动作共用)。返回重建后的卡片;无卡时返回 undefined 且跳过更新。
360
360
  */
361
361
  private refreshListCard;
362
362
  /**
363
- * W2.8 单源:order 三个 input handler(aliasInput/aliasRemove/textInput)
363
+ * 单源:order 三个 input handler(aliasInput/aliasRemove/textInput)
364
364
  * 的公共骨架——校验 orderId → reload+find → mutate → 成功后 toast + raw 卡。
365
365
  * mutate 返回 error 时仅回 error toast,不重渲染。
366
366
  * 注意:本 helper 只服务「toast + card 进响应体」语义(飞书需用响应体原地
@@ -472,7 +472,7 @@ export declare class CommandRouter {
472
472
  setNestedValue(target: AppConfig, key: string, value: unknown): void;
473
473
  diffConfig(original: AppConfig, pending: AppConfig): Record<string, string | undefined>;
474
474
  /**
475
- * config 写盘后的运行时传播(P1-6:自 config.save 卡片路径抽出,卡片保存与
475
+ * config 写盘后的运行时传播(自 config.save 卡片路径抽出,卡片保存与
476
476
  * `/config <key> <value>` 文本直写两条路径共用)。
477
477
  *
478
478
  * 1. bridge 运行时值同步:setConfig + idle watchdog timeout;
@@ -480,7 +480,7 @@ export declare class CommandRouter {
480
480
  * 配置值,/status 与真实 run 自相矛盾);
481
481
  * 3. defaultAgent 切换 → 旧 agent session 存 previousSessions、新 agent 恢复
482
482
  * 或清空(等效 /new);
483
- * 4. 当前 agent 配置变更 → syncAgentChoices 并原子写盘(P1-7:禁裸同步写盘,
483
+ * 4. 当前 agent 配置变更 → syncAgentChoices 并原子写盘(禁裸同步写盘,
484
484
  * 写一半崩溃会截断 config.yaml 导致 bridge 起不来)。
485
485
  *
486
486
  * @param updates 以 config 路径形式给出的变更 key(卡片路径来自 diffConfig;
@@ -530,7 +530,7 @@ export declare class CommandRouter {
530
530
  private buildConfigSwitchCard;
531
531
  /**
532
532
  * Whether a session of the given agent kind can be compacted from a resume
533
- * card. Design doc §6.2-2: duck-typing — check if the runner has `runCompact`
533
+ * card. duck-typing — check if the runner has `runCompact`
534
534
  * (codex app-server, kimi acp), instead of hardcoding agentKind === 'codex'.
535
535
  */
536
536
  private canCompactSession;
@@ -584,7 +584,7 @@ export declare class CommandRouter {
584
584
  */
585
585
  cmdWs(args: string[], ctx: CommandContext, offset?: number, q?: string): CommandResult;
586
586
  /**
587
- * L3: degraded card when a session's export is non-empty but unparseable
587
+ * degraded card when a session's export is non-empty but unparseable
588
588
  * (truncated by opencode's pipe cap on huge sessions, or otherwise corrupt).
589
589
  * The session exists, so reuse listSessions metadata (title) instead of the
590
590
  * misleading "未找到". Reuses the same card-builder helpers (markdownDiv) as