lark-remote 0.5.9 → 0.5.12

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 (53) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/binder.js +7 -1
  3. package/dist/bridge/index.d.ts +9 -0
  4. package/dist/bridge/index.js +28 -3
  5. package/dist/bridge/queue-manager.d.ts +31 -0
  6. package/dist/bridge/queue-manager.js +78 -0
  7. package/dist/card/approval-render.d.ts +14 -0
  8. package/dist/card/approval-render.js +155 -29
  9. package/dist/card/bash-card-session.js +8 -0
  10. package/dist/card/bash-renderer.js +11 -23
  11. package/dist/card/card-budget.js +24 -4
  12. package/dist/card/run-renderer.js +31 -9
  13. package/dist/common/text-width.js +5 -0
  14. package/dist/common/tool-summary.js +4 -2
  15. package/dist/config/codex-config.js +5 -3
  16. package/dist/config/index.js +13 -1
  17. package/dist/index.js +42 -24
  18. package/dist/instance-lock.d.ts +9 -1
  19. package/dist/instance-lock.js +42 -7
  20. package/dist/platform/identity.js +4 -3
  21. package/dist/platform/path.js +3 -1
  22. package/dist/platform/terminator-win32.d.ts +13 -1
  23. package/dist/platform/terminator-win32.js +4 -14
  24. package/dist/router/index.js +179 -122
  25. package/dist/runner/codex/app-server/runner.js +12 -0
  26. package/dist/runner/common/acp/base-translator.d.ts +12 -0
  27. package/dist/runner/common/acp/base-translator.js +22 -0
  28. package/dist/runner/common/jsonl-stream.js +12 -1
  29. package/dist/runner/common/jsonrpc/connection-manager.d.ts +6 -0
  30. package/dist/runner/common/jsonrpc/connection-manager.js +34 -9
  31. package/dist/runner/common/jsonrpc/transport.d.ts +2 -1
  32. package/dist/runner/common/jsonrpc/transport.js +7 -1
  33. package/dist/runner/dsh/approval.js +16 -3
  34. package/dist/runner/dsh/client.d.ts +31 -1
  35. package/dist/runner/dsh/client.js +126 -6
  36. package/dist/runner/dsh/exit-cancel.js +62 -23
  37. package/dist/runner/dsh/runner.d.ts +7 -0
  38. package/dist/runner/dsh/runner.js +42 -5
  39. package/dist/runner/kimi/acp/runner.js +42 -5
  40. package/dist/runner/kimi/acp/translator.d.ts +8 -0
  41. package/dist/runner/kimi/acp/translator.js +10 -0
  42. package/dist/runner/pi/rpc/runner.js +33 -5
  43. package/dist/runner/resolve-agent-choices.js +5 -1
  44. package/dist/runner/sync-agent-choices.js +4 -1
  45. package/dist/session/claude/session-index.js +9 -2
  46. package/dist/session/codex/rollout-reader.js +51 -26
  47. package/dist/session/common/usage-accumulator.d.ts +10 -1
  48. package/dist/session/common/usage-accumulator.js +10 -2
  49. package/dist/session/dsh/sessions.js +29 -9
  50. package/dist/session/kimi/sessions.js +9 -5
  51. package/dist/session/opencode/sessions.js +13 -6
  52. package/dist/session/session-store.js +4 -3
  53. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -34,9 +34,17 @@ import { InboundAuthorizer } from './inbound/authorize.js';
34
34
  import { stripPlaceholders } from './inbound/placeholder.js';
35
35
  import { buildSessionHistoryCard, pickResumeCardEvents } from './router/card-helpers.js';
36
36
  import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
37
+ import { sleep } from './common/sleep.js';
37
38
  import path from 'node:path';
38
39
  import fs from 'node:fs';
39
40
  import { bestEffortUnlink } from './common/fs.js';
41
+ /**
42
+ * 会话变更类卡片动作执行前冲刷入站装配窗口的等待预算(ms)。flush 本身无下载
43
+ * 在途时瞬时完成(顺序保证完整);预算只挡「in-flight 媒体下载卡住」的场景
44
+ * (下载超时 5 分钟/资源)——SDK 把卡片动作放进与消息共享的 per-chat 串行
45
+ * lane 并 await 到底,无界等待会让整个 chat 停摆到下载超时。
46
+ */
47
+ const SESSION_MUTATING_FLUSH_BUDGET_MS = 2_000;
40
48
  /**
41
49
  * /stop 命令别名单源:普通消息的停止分支与 clone 活跃期拦截的排除条件
42
50
  * 共用同一判定,防止将来加别名时两处漂移。
@@ -363,9 +371,12 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
363
371
  };
364
372
  let cwd = sessionStore.getCwd(turn.userId) ?? '';
365
373
  if (!cwd && workspaceStore) {
366
- const workspaces = workspaceStore.list();
367
- if (workspaces.length > 0)
368
- cwd = workspaces[0].path;
374
+ // 兜底取「最近使用」而非插入序首位:WorkspaceStore.list() 按 Map 插入序
375
+ // 返回(touch 原地改时间戳不重排),[0] 恒为最早保存的别名——触发时会把
376
+ // 消息静默发到最旧工作区,与「最近使用」直觉相反。
377
+ const recent = [...workspaceStore.list()].sort((a, b) => b.lastUsedAt - a.lastUsedAt);
378
+ if (recent.length > 0)
379
+ cwd = recent[0].path;
369
380
  }
370
381
  const binding = bridge.currentBinding(turn.userId);
371
382
  bridge.enqueue(cwd, async () => {
@@ -456,6 +467,14 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
456
467
  logger.warn(`[media] rejected inbound media from non-owner ${msg.userId}`);
457
468
  return;
458
469
  }
470
+ // clone 状态机活跃期:一切输入归 clone 流程、不落盘附件
471
+ // (inbound-unified-input-design.md §5.8;文本入口有同款闸门)。静默丢弃
472
+ // 而非发回执——装配器的「📎 已保存」回执会与 clone 引导话术冲突,诱导
473
+ // 用户「说一句话」触发 clone 的婉拒回复。
474
+ if (cloneSession.isActive()) {
475
+ logger.info(`[media] dropped inbound media during clone session ${msg.messageId}`);
476
+ return;
477
+ }
459
478
  // 读 router.config(活引用):/config 保存后 setConfigValues 返回新对象,
460
479
  // 启动时捕获的 config 参数会过期(修复)。
461
480
  if (!router.config.inboundMedia.enabled) {
@@ -599,26 +618,14 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
599
618
  // Add Typing reaction to indicate "still alive"
600
619
  void connector.addReaction(messageId, 'Typing');
601
620
  if (actionValue.cmd === 'stop') {
602
- if (!actionValue.runId) {
603
- logger.warn('[control] ignored stop card action without runId');
604
- // Card button clicks must have visible feedback (design constraint: card button
605
- // 点击必须有可见反馈,见 design.md §6.2)。
606
- void bridge
607
- .sendResult({ text: '⚠️ 无效的停止请求,缺少必要信息' }, { userId, chatId, messageId })
608
- .catch((err) => logger.error('[control] card stop feedback failed:', err));
609
- return;
610
- }
611
- void (async () => {
612
- const stopped = await bridge.interruptCurrentRun({
613
- userId,
614
- chatId,
615
- runId: actionValue.runId,
616
- });
617
- if (!stopped) {
618
- // runId mismatch or run already exited — mirror /stop text miss path.
619
- await bridge.sendResult({ text: '该任务已结束,无需终止' }, { userId, chatId, messageId });
620
- }
621
- })().catch((err) => logger.error('[control] card stop failed:', err));
621
+ // 委托 router 的 handleCardStop(单一来源):miss 反馈与 DSH unconfirmed
622
+ // 警告(「未能确认 turn 已停止,可能仍在后台运行」)都在那里。此前这里
623
+ // 内联只调 interruptCurrentRun——unconfirmed 反馈被丢,run 卡被翻成
624
+ // interrupted 却暗示干净停止;miss 文案也与 router 形成两份漂移拷贝。
625
+ // stop 绕串行队列的原则不变:本分支在队列分发之前直接 return。
626
+ void router
627
+ .handleCardAction({ ...actionValue, cmd: 'stop' }, { userId, chatId, messageId })
628
+ .catch((err) => logger.error('[control] card stop failed:', err));
622
629
  return;
623
630
  }
624
631
  const isImmediate = isImmediateAction(actionValue.cmd);
@@ -632,8 +639,19 @@ function setupMessageHandlers(connector, router, bridge, sessionStore, workspace
632
639
  // 保证窗口内先到的文本消息在本动作之前 commit(绑定旧 session/cwd/agent)。
633
640
  // 否则 700ms 静默窗内的消息会漂移到动作之后的新状态:例如输入「重新构建」
634
641
  // 后立即点「新会话」,消息会落到新会话。名单以 router 单源为准。
642
+ //
643
+ // 有界等待:flush 会等 in-flight 媒体下载落定(超时 5 分钟/
644
+ // 资源),而 SDK 的 chatQueue.cardActions 默认 'same'——卡片动作与消息共享
645
+ // 同一 per-chat 串行 lane 且被 await 到底。无界等待下,一个大视频在途 +
646
+ // 点「新会话」= 整个 chat 的所有消息与卡片动作停摆到下载超时。超预算后
647
+ // 放行本动作,flush 在后台继续:窗口内仍有未落定下载时,文本 commit 可能
648
+ // 落到本动作之后(漂移风险收窄到「卡住的下载 + 2s 内点击」的组合,可接受;
649
+ // 完整解法需要把 binding 快照进装配器,见 inbound-unified-input-design.md)。
635
650
  if (SESSION_MUTATING_ACTION_CMDS.has(actionValue.cmd)) {
636
- await assembler.flush(userId, chatId, 'flush');
651
+ const flushP = assembler
652
+ .flush(userId, chatId, 'flush')
653
+ .catch((err) => logger.error('[control] inbound flush failed:', err));
654
+ await Promise.race([flushP, sleep(SESSION_MUTATING_FLUSH_BUDGET_MS)]);
637
655
  }
638
656
  // queue.input / config.save / approval.respond / approval.toggle /
639
657
  // approval.answer 系列返回 CardActionResponse toast 给点击用户即时反馈。
@@ -33,7 +33,15 @@ export declare class InstanceLock {
33
33
  * 与 CIM CommandLine 首 token 归一后必然一致(bun/node 垫片场景亦然)。
34
34
  */
35
35
  private lockContents;
36
- /** 原子创建锁文件;已存在返回 false,其它 IO 错误向上抛。 */
36
+ /**
37
+ * 原子创建锁文件;已存在返回 false,其它 IO 错误向上抛。
38
+ *
39
+ * 先写完整临时文件再 `link()`:`open(wx)` 与 `write` 之间存在微秒级「空文件」
40
+ * 窗口,并发进程在该窗口读到的锁是「空 → 损坏 → 陈旧」,rename 夺走后双方
41
+ * 各持一份(A 的 fd 写进已被删掉的 inode、B 建了自己的锁 → 双实例)。
42
+ * link() 创建即含全部内容,窗口不存在;文件系统不支持 hardlink 时退回 wx
43
+ * 直建(该类 FS 上保留既有行为,竞态面收窄到该类文件系统)。
44
+ */
37
45
  private tryCreate;
38
46
  /**
39
47
  * 把夺来的活锁放回原位。优先 `link`(目标已存在会失败,不会覆盖更新的锁),
@@ -177,22 +177,57 @@ export class InstanceLock {
177
177
  lockContents(pid) {
178
178
  return `${pid}\n${binaryName(process.execPath)}`;
179
179
  }
180
- /** 原子创建锁文件;已存在返回 false,其它 IO 错误向上抛。 */
180
+ /**
181
+ * 原子创建锁文件;已存在返回 false,其它 IO 错误向上抛。
182
+ *
183
+ * 先写完整临时文件再 `link()`:`open(wx)` 与 `write` 之间存在微秒级「空文件」
184
+ * 窗口,并发进程在该窗口读到的锁是「空 → 损坏 → 陈旧」,rename 夺走后双方
185
+ * 各持一份(A 的 fd 写进已被删掉的 inode、B 建了自己的锁 → 双实例)。
186
+ * link() 创建即含全部内容,窗口不存在;文件系统不支持 hardlink 时退回 wx
187
+ * 直建(该类 FS 上保留既有行为,竞态面收窄到该类文件系统)。
188
+ */
181
189
  tryCreate() {
190
+ const tmp = `${this.lockPath}.new-${process.pid}-${randomBytes(6).toString('hex')}`;
182
191
  try {
183
- const fd = fs.openSync(this.lockPath, 'wx');
192
+ const fd = fs.openSync(tmp, 'wx');
184
193
  try {
185
194
  fs.writeSync(fd, this.lockContents(process.pid));
186
195
  }
187
196
  finally {
188
197
  fs.closeSync(fd);
189
198
  }
190
- return true;
199
+ try {
200
+ fs.linkSync(tmp, this.lockPath);
201
+ return true;
202
+ }
203
+ catch (err) {
204
+ const code = err.code;
205
+ if (code === 'EEXIST')
206
+ return false;
207
+ if (code === 'EPERM' || code === 'EOPNOTSUPP' || code === 'ENOSYS' || code === 'EXDEV') {
208
+ // hardlink 不可用(网络/overlay FS):退回 wx 直建。
209
+ try {
210
+ const direct = fs.openSync(this.lockPath, 'wx');
211
+ try {
212
+ fs.writeSync(direct, this.lockContents(process.pid));
213
+ }
214
+ finally {
215
+ fs.closeSync(direct);
216
+ }
217
+ return true;
218
+ }
219
+ catch (err2) {
220
+ if (err2.code === 'EEXIST')
221
+ return false;
222
+ throw err2;
223
+ }
224
+ }
225
+ throw err;
226
+ }
191
227
  }
192
- catch (err) {
193
- if (err.code === 'EEXIST')
194
- return false;
195
- throw err;
228
+ finally {
229
+ // 成功 link 后 tmp 与 lockPath 同 inode:删 tmp 只去掉多余名字(nlink 2→1)。
230
+ bestEffortUnlink(tmp);
196
231
  }
197
232
  }
198
233
  /**
@@ -12,9 +12,9 @@
12
12
  * (killOrphan)应按需调用,真机矩阵需实测整体耗时;
13
13
  * - platform 显式注入,win32 语义可在任意宿主上单测。
14
14
  */
15
- import { execFileSync, spawn } from 'node:child_process';
16
15
  import path from 'node:path';
17
16
  import { currentPlatform, isWin32 } from './select.js';
17
+ import { spawnProcess, spawnProcessSync } from './spawn.js';
18
18
  const DEFAULT_TIMEOUT_MS = 3000;
19
19
  /** 拉起一次性查询进程并收全 stdout;超时/拉起失败/进程报错一律 null。 */
20
20
  function collect(file, args, timeoutMs) {
@@ -24,7 +24,7 @@ function collect(file, args, timeoutMs) {
24
24
  // stderr 用 ignore:查询进程的错误输出我们不需要,pipe 而不消费在极端
25
25
  // 情况下(管道缓冲满)会阻塞子进程,把一次性查询拖成挂起。
26
26
  // windowsHide:避免查询进程在桌面闪控制台窗口
27
- proc = spawn(file, args, { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
27
+ proc = spawnProcess(file, args, { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
28
28
  }
29
29
  catch {
30
30
  resolve(null);
@@ -243,12 +243,13 @@ export function verifyPidIdentityVerdictSync(pid, opts) {
243
243
  return 'unknown';
244
244
  let stdout;
245
245
  try {
246
- stdout = execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
246
+ const res = spawnProcessSync('ps', ['-o', 'command=', '-p', String(pid)], {
247
247
  encoding: 'utf-8',
248
248
  timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
249
249
  stdio: ['ignore', 'pipe', 'ignore'],
250
250
  windowsHide: true,
251
251
  });
252
+ stdout = String(res.stdout ?? '');
252
253
  }
253
254
  catch {
254
255
  // 进程不存在 / ps 不可用 / 超时:查不到身份 → unknown(不杀)
@@ -21,8 +21,10 @@ function resolveOptions(opts) {
21
21
  */
22
22
  export function pathKey(p, opts) {
23
23
  const { platform } = resolveOptions(opts);
24
+ // linux:剥尾部分隔符,与 darwin/win32 同口径(否则 `/cd ~/repo/` 与
25
+ // `/cd ~/repo` 会被判成两个目录——darwin 分支注释写明的不变量)。
24
26
  if (platform === 'linux')
25
- return p;
27
+ return p.replace(/\/+$/, '');
26
28
  if (isWin32(platform)) {
27
29
  // win32:\ 与 / 都是分隔符,且路径大小写不敏感
28
30
  return p.replaceAll('\\', '/').replace(/\/+$/, '').toLowerCase();
@@ -1,3 +1,15 @@
1
+ /**
2
+ * win32 终止器:协议停止通道 → grace 等待 → taskkill 树杀。
3
+ *
4
+ * 状态机:
5
+ * 1. 进程已退出 → nothing(via='already-exited')
6
+ * 2. immediate → taskkill /PID <pid> /T /F
7
+ * 3. 优雅 → AgentStopperRegistry 查协议通道
8
+ * - 无通道:打日志显式跳过优雅段(不等一个不会来的退出事件)→ 直接树杀
9
+ * - 有通道:调用后与 exit 事件竞速等 graceMs,仍存活 → 树杀
10
+ *
11
+ * 依赖(spawn / 等待器 / 日志)全部构造注入,win32 语义可在任意宿主上单测。
12
+ */
1
13
  import type { ChildProcess, SpawnOptions } from 'node:child_process';
2
14
  import type { Terminator } from './terminator.js';
3
15
  import { AgentStopperRegistry } from './agent-stopper.js';
@@ -11,7 +23,7 @@ export interface Win32TerminatorDeps {
11
23
  agent?: string;
12
24
  /** 协议通道注册表注入(测试隔离);默认进程级单例 */
13
25
  stoppers?: AgentStopperRegistry;
14
- /** spawn 注入(测试用);默认 node:child_process */
26
+ /** spawn 注入(测试用);默认 platform/spawn.ts 的 spawnProcess */
15
27
  spawn?: SpawnLike;
16
28
  /** grace 等待器注入(测试用);默认与 exit 事件竞速 */
17
29
  waitForExit?: WaitForExit;
@@ -1,17 +1,5 @@
1
- /**
2
- * win32 终止器:协议停止通道 → grace 等待 → taskkill 树杀。
3
- *
4
- * 状态机:
5
- * 1. 进程已退出 → nothing(via='already-exited')
6
- * 2. immediate → taskkill /PID <pid> /T /F
7
- * 3. 优雅 → AgentStopperRegistry 查协议通道
8
- * - 无通道:打日志显式跳过优雅段(不等一个不会来的退出事件)→ 直接树杀
9
- * - 有通道:调用后与 exit 事件竞速等 graceMs,仍存活 → 树杀
10
- *
11
- * 依赖(spawn / 等待器 / 日志)全部构造注入,win32 语义可在任意宿主上单测。
12
- */
13
- import { spawn as nodeSpawn } from 'node:child_process';
14
1
  import { agentStopperRegistry } from './agent-stopper.js';
2
+ import { spawnProcess } from './spawn.js';
15
3
  /** 退出判定统一口径(红线):exitCode !== null || signalCode !== null。 */
16
4
  function isAlive(proc) {
17
5
  return proc.exitCode === null && proc.signalCode === null;
@@ -36,7 +24,9 @@ async function defaultWaitForExit(proc, ms) {
36
24
  });
37
25
  }
38
26
  export function createWin32Terminator(deps) {
39
- const spawnFn = deps.spawn ?? nodeSpawn;
27
+ // 默认走 platform/spawn.ts 单点收口:taskkill 是系统二进制,
28
+ // cross-spawn 在 win32 之外直通、在 win32 垫片场景下受控执行。
29
+ const spawnFn = deps.spawn ?? spawnProcess;
40
30
  const stoppers = deps.stoppers ?? agentStopperRegistry;
41
31
  const waitForExit = deps.waitForExit ?? defaultWaitForExit;
42
32
  const log = deps.log;