lark-remote 0.5.13 → 0.5.15

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 (130) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/agent-kinds.d.ts +27 -0
  3. package/dist/agent-kinds.js +38 -0
  4. package/dist/bridge/approval-actions.d.ts +95 -0
  5. package/dist/bridge/approval-actions.js +104 -0
  6. package/dist/bridge/approval-coordinator.d.ts +13 -0
  7. package/dist/bridge/approval-coordinator.js +18 -0
  8. package/dist/bridge/final-usage.d.ts +8 -17
  9. package/dist/bridge/final-usage.js +1 -18
  10. package/dist/bridge/index.d.ts +46 -39
  11. package/dist/bridge/index.js +269 -240
  12. package/dist/bridge/queue-manager.d.ts +25 -1
  13. package/dist/bridge/queue-manager.js +179 -160
  14. package/dist/bridge/run-approval.d.ts +2 -1
  15. package/dist/bridge/run-usage.d.ts +21 -30
  16. package/dist/bridge/run-usage.js +47 -51
  17. package/dist/bridge/session-writeback.d.ts +3 -2
  18. package/dist/card/approval-render.d.ts +3 -2
  19. package/dist/card/approval-render.js +3 -2
  20. package/dist/{router → card}/card-helpers.d.ts +7 -7
  21. package/dist/{router → card}/card-helpers.js +8 -8
  22. package/dist/card/card-session.d.ts +4 -2
  23. package/dist/card/card-session.js +8 -6
  24. package/dist/card/card-shared.d.ts +1 -1
  25. package/dist/card/card-shared.js +1 -1
  26. package/dist/card/final-answer-overflow.d.ts +0 -6
  27. package/dist/card/final-answer-overflow.js +1 -1
  28. package/dist/card/run-card-session.d.ts +2 -2
  29. package/dist/card/run-card-session.js +2 -2
  30. package/dist/card/run-renderer.js +29 -30
  31. package/dist/card/run-state.d.ts +28 -50
  32. package/dist/card/run-state.js +23 -0
  33. package/dist/card/usage-format.d.ts +30 -0
  34. package/dist/{router → card}/usage-format.js +3 -10
  35. package/dist/clone.js +6 -1
  36. package/dist/config/index.d.ts +16 -20
  37. package/dist/config/index.js +0 -16
  38. package/dist/config/pi-config.js +6 -4
  39. package/dist/connector/index.d.ts +2 -4
  40. package/dist/connector/index.js +7 -11
  41. package/dist/{runner/dsh → dsh}/auth.js +1 -1
  42. package/dist/dsh/protocol.d.ts +9 -0
  43. package/dist/dsh/protocol.js +61 -0
  44. package/dist/index.js +10 -5
  45. package/dist/model-catalog.d.ts +14 -0
  46. package/dist/model-catalog.js +25 -0
  47. package/dist/platform/path.d.ts +9 -0
  48. package/dist/platform/path.js +18 -0
  49. package/dist/router/active-run-usage.d.ts +4 -0
  50. package/dist/router/active-run-usage.js +12 -0
  51. package/dist/router/commands/ls.js +1 -1
  52. package/dist/router/commands/order.js +1 -1
  53. package/dist/router/commands/resume.d.ts +5 -4
  54. package/dist/router/commands/resume.js +4 -4
  55. package/dist/router/commands/ws.js +1 -1
  56. package/dist/router/config/claude.js +2 -1
  57. package/dist/router/config/common/render.d.ts +5 -1
  58. package/dist/router/config/common/render.js +6 -13
  59. package/dist/router/config/dsh.d.ts +1 -1
  60. package/dist/router/config/index.d.ts +1 -1
  61. package/dist/router/config/types.d.ts +11 -3
  62. package/dist/router/index.d.ts +41 -11
  63. package/dist/router/index.js +169 -233
  64. package/dist/runner/agent-choices-common.d.ts +1 -1
  65. package/dist/runner/claude/runner.d.ts +20 -2
  66. package/dist/runner/claude/runner.js +2 -15
  67. package/dist/runner/codex/app-server/runner.d.ts +3 -1
  68. package/dist/runner/codex/app-server/translator.d.ts +11 -0
  69. package/dist/runner/codex/app-server/translator.js +56 -149
  70. package/dist/runner/common/acp/base-acp-runner.d.ts +19 -1
  71. package/dist/runner/common/acp/base-acp-runner.js +37 -3
  72. package/dist/runner/common/connection-based-runner.d.ts +3 -1
  73. package/dist/runner/common/jsonrpc/transport.js +17 -7
  74. package/dist/runner/dsh/client.d.ts +1 -4
  75. package/dist/runner/dsh/client.js +7 -55
  76. package/dist/runner/dsh/daemon.js +9 -2
  77. package/dist/runner/dsh/exit-cancel.js +1 -1
  78. package/dist/runner/dsh/mux-stream.js +21 -2
  79. package/dist/runner/dsh/runner.d.ts +21 -1
  80. package/dist/runner/dsh/runner.js +97 -78
  81. package/dist/runner/dsh/translator.d.ts +1 -1
  82. package/dist/runner/index.d.ts +1 -1
  83. package/dist/runner/kimi/acp/runner.d.ts +22 -1
  84. package/dist/runner/kimi/acp/runner.js +83 -60
  85. package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
  86. package/dist/runner/opencode/acp/error-monitor.js +31 -2
  87. package/dist/runner/opencode/acp/runner.d.ts +14 -1
  88. package/dist/runner/opencode/acp/runner.js +13 -2
  89. package/dist/runner/pi/rpc/runner.d.ts +2 -1
  90. package/dist/runner/probe.d.ts +1 -1
  91. package/dist/runner/probe.js +10 -9
  92. package/dist/runner/registry.d.ts +2 -1
  93. package/dist/runner/types.d.ts +20 -154
  94. package/dist/runner/types.js +4 -24
  95. package/dist/session/claude/session-index.d.ts +11 -4
  96. package/dist/session/claude/session-index.js +30 -8
  97. package/dist/session/claude/session-reader.d.ts +1 -1
  98. package/dist/session/claude/sessions.d.ts +2 -2
  99. package/dist/session/claude/sessions.js +41 -26
  100. package/dist/session/codex/rollout-reader.d.ts +1 -1
  101. package/dist/session/codex/rollout-reader.js +14 -13
  102. package/dist/session/codex/sessions.d.ts +1 -1
  103. package/dist/session/codex/sessions.js +4 -2
  104. package/dist/session/common/jsonl.js +7 -1
  105. package/dist/session/common/pagination.d.ts +2 -0
  106. package/dist/session/common/pagination.js +1 -1
  107. package/dist/session/common/recency.d.ts +6 -0
  108. package/dist/session/common/recency.js +7 -5
  109. package/dist/session/common/two-pass.d.ts +1 -1
  110. package/dist/session/common/usage-accumulator.d.ts +1 -1
  111. package/dist/session/dsh/sessions.d.ts +1 -1
  112. package/dist/session/dsh/sessions.js +37 -12
  113. package/dist/session/kimi/sessions.d.ts +1 -1
  114. package/dist/session/kimi/sessions.js +72 -27
  115. package/dist/session/opencode/sessions.d.ts +1 -1
  116. package/dist/session/opencode/sessions.js +164 -81
  117. package/dist/session/pi/sessions.d.ts +1 -1
  118. package/dist/session/pi/sessions.js +20 -7
  119. package/dist/session/registry.d.ts +2 -1
  120. package/dist/session/session-store.d.ts +1 -2
  121. package/dist/session/session-store.js +14 -7
  122. package/dist/session-contract.d.ts +178 -0
  123. package/dist/session-contract.js +9 -0
  124. package/package.json +1 -1
  125. package/dist/router/usage-format.d.ts +0 -60
  126. /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
  127. /package/dist/{runner/dsh → dsh}/home.d.ts +0 -0
  128. /package/dist/{runner/dsh → dsh}/home.js +0 -0
  129. /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
  130. /package/dist/{runner/dsh → dsh}/types.js +0 -0
@@ -582,11 +582,55 @@ export class KimiAcpRunner extends BaseAcpRunner {
582
582
  : undefined;
583
583
  const cwd = params.cwd ?? this.activeCwd ?? undefined;
584
584
  const outputLimit = params.outputByteLimit ?? TERMINAL_DEFAULT_OUTPUT_LIMIT;
585
- // 走 shell seam:argv 逐 token 保留,只把「可执行文件在哪」
586
- // 收口——win32 上 Git Bash 缺失时在这里抛 ShellUnavailableError,由
587
- // handleTerminalRequest 转成明确的 Terminal request failed 回执,而不是一个
588
- // 语焉不详的 spawn ENOENT。
589
- const proc = this.shell.spawnArgv(params.command, params.args ?? [], {
585
+ const proc = this.spawnTerminalProcess(params, cwd, env);
586
+ // spawn 失败(如 ENOENT)只发 'error' 不发 'exit'——一并 resolve,防止
587
+ // wait_for_exit 永久挂起(runner 层 ENOENT 兜底红线)。
588
+ // 用 resolveExit 承接,避免回调在 handle 初始化前引用它(TDZ)。
589
+ let resolveExit;
590
+ const exitPromise = new Promise((resolve) => {
591
+ resolveExit = resolve;
592
+ });
593
+ // 与 resolveExit 同款 TDZ 规避:tool_result 发射闭包在 terminalId 初始化
594
+ // 后赋值,exit/error 回调执行时经 emitResult 间接调用。
595
+ let emitTerminalResult = undefined;
596
+ const emitResult = () => {
597
+ emitTerminalResult?.();
598
+ };
599
+ const handle = {
600
+ proc,
601
+ output: '',
602
+ outputBytes: 0,
603
+ outputLimit,
604
+ truncated: false,
605
+ exitCode: null,
606
+ signal: null,
607
+ exitPromise,
608
+ };
609
+ this.attachTerminalOutputBuffer(proc, handle);
610
+ this.wireTerminalExit(proc, handle, emitResult, () => resolveExit?.());
611
+ const terminalId = `term-${++this.terminalSeq}`;
612
+ this.terminals.set(terminalId, handle);
613
+ // Bash 工具事件由本 runner 自产(kimi 通知已过滤):create 即推 tool_use
614
+ // 带真实命令,让 run 卡立刻出现可识别的 Bash 面板。
615
+ const script = Array.isArray(params.args) && params.args.length > 1 ? params.args[1] : params.command;
616
+ let resultEmitted = false;
617
+ emitTerminalResult = () => {
618
+ if (resultEmitted)
619
+ return;
620
+ resultEmitted = true;
621
+ this.pushTerminalToolResult(terminalId, handle.output, handle.exitCode);
622
+ };
623
+ this.pushTerminalToolUse(terminalId, script, cwd);
624
+ getLogger().info(`[${this.logTag}] terminal created id=${terminalId} command=${params.command} args=${JSON.stringify(params.args ?? [])}`);
625
+ this.currentClient?.respond(id, { terminalId });
626
+ }
627
+ /**
628
+ * 走 shell seam:argv 逐 token 保留,只把「可执行文件在哪」收口——win32 上
629
+ * Git Bash 缺失时在这里抛 ShellUnavailableError,由 handleTerminalRequest 转成
630
+ * 明确的 Terminal request failed 回执,而不是一个语焉不详的 spawn ENOENT。
631
+ */
632
+ spawnTerminalProcess(params, cwd, env) {
633
+ return this.shell.spawnArgv(params.command, params.args ?? [], {
590
634
  cwd,
591
635
  options: {
592
636
  // env 覆盖大小写不敏感合并(win32 PATH/Path 双键防护)
@@ -600,8 +644,12 @@ export class KimiAcpRunner extends BaseAcpRunner {
600
644
  windowsHide: true,
601
645
  },
602
646
  });
603
- // 每个流独立 decoder:多字节字符跨 chunk 拆分时由 decoder 保留不完整
604
- // 序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符。
647
+ }
648
+ /**
649
+ * 有界输出缓冲:每个流独立 decoder(多字节字符跨 chunk 拆分时由 decoder 保留
650
+ * 不完整序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符)。
651
+ */
652
+ attachTerminalOutputBuffer(proc, handle) {
605
653
  const stdoutDecoder = new StringDecoder('utf8');
606
654
  const stderrDecoder = new StringDecoder('utf8');
607
655
  const append = (chunk, decoder) => {
@@ -624,29 +672,11 @@ export class KimiAcpRunner extends BaseAcpRunner {
624
672
  };
625
673
  proc.stdout?.on('data', (chunk) => append(chunk, stdoutDecoder));
626
674
  proc.stderr?.on('data', (chunk) => append(chunk, stderrDecoder));
627
- // spawn 失败(如 ENOENT)只发 'error' 不发 'exit'——一并 resolve,防止
628
- // wait_for_exit 永久挂起(runner 层 ENOENT 兜底红线)。
629
- // 用 resolveExit 承接,避免回调在 handle 初始化前引用它(TDZ)。
630
- let resolveExit;
631
- const exitPromise = new Promise((resolve) => {
632
- resolveExit = resolve;
633
- });
634
- // 与 resolveExit 同款 TDZ 规避:tool_result 发射闭包在 terminalId 初始化
635
- // 后赋值,exit/error 回调执行时经 emitResult 间接调用。
636
- let emitTerminalResult = undefined;
637
- const emitResult = () => {
638
- emitTerminalResult?.();
639
- };
640
- const handle = {
641
- proc,
642
- output: '',
643
- outputBytes: 0,
644
- outputLimit,
645
- truncated: false,
646
- exitCode: null,
647
- signal: null,
648
- exitPromise,
649
- };
675
+ }
676
+ /**
677
+ * 退出/错误收尾管线:tool_result 必须先入队,再放行 `wait_for_exit`。
678
+ */
679
+ wireTerminalExit(proc, handle, emitResult, resolveExit) {
650
680
  proc.once('exit', (code, signal) => {
651
681
  handle.exitCode = code;
652
682
  handle.signal = signal;
@@ -661,7 +691,7 @@ export class KimiAcpRunner extends BaseAcpRunner {
661
691
  const flushThenEmit = () => {
662
692
  clearTimeout(emitTimer);
663
693
  emitResult();
664
- resolveExit?.();
694
+ resolveExit();
665
695
  };
666
696
  const emitTimer = setTimeout(flushThenEmit, 200);
667
697
  emitTimer.unref?.();
@@ -680,35 +710,30 @@ export class KimiAcpRunner extends BaseAcpRunner {
680
710
  }
681
711
  // 同 exit:tool_result 先入队,再放行 wait_for_exit。
682
712
  emitResult();
683
- resolveExit?.();
713
+ resolveExit();
684
714
  });
685
- const terminalId = `term-${++this.terminalSeq}`;
686
- this.terminals.set(terminalId, handle);
687
- // Bash 工具事件由本 runner 自产(kimi 通知已过滤):create 即推 tool_use
688
- // 带真实命令,让 run 卡立刻出现可识别的 Bash 面板。
689
- const script = Array.isArray(params.args) && params.args.length > 1 ? params.args[1] : params.command;
690
- let resultEmitted = false;
691
- emitTerminalResult = () => {
692
- if (resultEmitted)
693
- return;
694
- resultEmitted = true;
695
- this.pushEvents([
696
- {
697
- type: 'user',
698
- message: {
699
- content: [
700
- {
701
- type: 'tool_result',
702
- tool_use_id: terminalId,
703
- content: handle.output,
704
- is_error: handle.exitCode !== 0,
705
- },
706
- ],
707
- },
708
- timestamp: new Date().toISOString(),
715
+ }
716
+ /** Bash 面板的 tool_result 帧(输出全量 + 是否失败)。 */
717
+ pushTerminalToolResult(terminalId, output, exitCode) {
718
+ this.pushEvents([
719
+ {
720
+ type: 'user',
721
+ message: {
722
+ content: [
723
+ {
724
+ type: 'tool_result',
725
+ tool_use_id: terminalId,
726
+ content: output,
727
+ is_error: exitCode !== 0,
728
+ },
729
+ ],
709
730
  },
710
- ]);
711
- };
731
+ timestamp: new Date().toISOString(),
732
+ },
733
+ ]);
734
+ }
735
+ /** Bash 面板的 tool_use 帧(真实命令 + cwd,create 时立刻可见)。 */
736
+ pushTerminalToolUse(terminalId, script, cwd) {
712
737
  this.pushEvents([
713
738
  {
714
739
  type: 'assistant',
@@ -725,8 +750,6 @@ export class KimiAcpRunner extends BaseAcpRunner {
725
750
  timestamp: new Date().toISOString(),
726
751
  },
727
752
  ]);
728
- getLogger().info(`[${this.logTag}] terminal created id=${terminalId} command=${params.command} args=${JSON.stringify(params.args ?? [])}`);
729
- this.currentClient?.respond(id, { terminalId });
730
753
  }
731
754
  /** terminal/output: 回累计全量输出 + truncated + 当前退出状态(可未退出)。 */
732
755
  handleTerminalOutput(id, params) {
@@ -45,6 +45,12 @@ export declare class OpencodeLogErrorMonitor {
45
45
  private timer;
46
46
  private sessionId;
47
47
  private onError;
48
+ /**
49
+ * IO 故障留痕的节流状态:poll 每 2s 一次,同一条 EACCES 不得刷满日志,
50
+ * 但也不能像以前那样完全静默(监视器自己坏了没人知道)。
51
+ */
52
+ private lastIoError;
53
+ private lastIoErrorAt;
48
54
  constructor(opts: OpencodeLogErrorMonitorOptions);
49
55
  /**
50
56
  * Start monitoring the given session. Replaces any previous monitor state;
@@ -54,6 +60,11 @@ export declare class OpencodeLogErrorMonitor {
54
60
  /** Stop monitoring and clear all state. Safe to call multiple times. */
55
61
  stop(): void;
56
62
  private currentSize;
63
+ /**
64
+ * 监视器自身读不到日志时留痕(同一条最多每分钟一次)。ENOENT 通常是日志
65
+ * 还没生成,属正常空态,不记。
66
+ */
67
+ private noteIoError;
57
68
  private poll;
58
69
  /** Match a main-turn stream error line for the monitored session with a
59
70
  * limit/quota reason; returns the extracted error text or null. */
@@ -27,9 +27,12 @@
27
27
  import { closeSync, openSync, readSync, statSync } from 'node:fs';
28
28
  import { homedir } from 'node:os';
29
29
  import { join } from 'node:path';
30
+ import { getLogger } from '../../../logger/index.js';
30
31
  /** Error text we fail fast on: quota/limit conditions opencode backs off on. */
31
32
  const LIMIT_ERROR_RE = /usage limit|quota|rate limit|rate_limit|too many requests|insufficient balance|free usage/i;
32
33
  const STREAM_ERROR_RE = /message="stream error"/;
34
+ /** 同一条 IO 故障的日志节流窗口。 */
35
+ const IO_ERROR_LOG_INTERVAL_MS = 60_000;
33
36
  const MAIN_TURN_RE = /small=false/;
34
37
  const ERROR_MESSAGE_RE = /error\.error="((?:[^"\\]|\\.)*)"/;
35
38
  /**
@@ -50,6 +53,12 @@ export class OpencodeLogErrorMonitor {
50
53
  timer = null;
51
54
  sessionId = null;
52
55
  onError = null;
56
+ /**
57
+ * IO 故障留痕的节流状态:poll 每 2s 一次,同一条 EACCES 不得刷满日志,
58
+ * 但也不能像以前那样完全静默(监视器自己坏了没人知道)。
59
+ */
60
+ lastIoError = '';
61
+ lastIoErrorAt = 0;
53
62
  constructor(opts) {
54
63
  this.logPath = opts.logPath;
55
64
  this.pollIntervalMs = opts.pollIntervalMs ?? 2000;
@@ -81,10 +90,28 @@ export class OpencodeLogErrorMonitor {
81
90
  try {
82
91
  return statSync(this.logPath).size;
83
92
  }
84
- catch {
93
+ catch (err) {
94
+ this.noteIoError(err);
85
95
  return 0;
86
96
  }
87
97
  }
98
+ /**
99
+ * 监视器自身读不到日志时留痕(同一条最多每分钟一次)。ENOENT 通常是日志
100
+ * 还没生成,属正常空态,不记。
101
+ */
102
+ noteIoError(err) {
103
+ if (err.code === 'ENOENT')
104
+ return;
105
+ const message = err?.message ?? String(err);
106
+ const now = Date.now();
107
+ if (message === this.lastIoError && now - this.lastIoErrorAt < IO_ERROR_LOG_INTERVAL_MS) {
108
+ return;
109
+ }
110
+ this.lastIoError = message;
111
+ this.lastIoErrorAt = now;
112
+ getLogger().warn(`[opencode-error-monitor] cannot read ${this.logPath}: ${message}; ` +
113
+ 'limit errors will fall back to the idle watchdog');
114
+ }
88
115
  poll() {
89
116
  if (this.sessionId === null || this.onError === null)
90
117
  return;
@@ -107,9 +134,11 @@ export class OpencodeLogErrorMonitor {
107
134
  closeSync(fd);
108
135
  }
109
136
  }
110
- catch {
137
+ catch (err) {
138
+ this.noteIoError(err);
111
139
  return;
112
140
  }
141
+ this.lastIoError = '';
113
142
  this.offset = size;
114
143
  this.pendingTail += chunk;
115
144
  }
@@ -19,7 +19,9 @@
19
19
  * - OpencodeLogErrorMonitor tails opencode's own log for LLM stream errors
20
20
  * the ACP protocol never forwards
21
21
  */
22
- import type { AgentKind, AgentSessionReader, AgentStatusInfo } from '../../types.js';
22
+ import type { AgentKind } from '../../../agent-kinds.js';
23
+ import type { AgentSessionReader } from '../../../session-contract.js';
24
+ import type { AgentStatusInfo } from '../../types.js';
23
25
  import { JsonRpcClient } from '../../common/jsonrpc/client.js';
24
26
  import { OpencodeAcpTranslator } from './translator.js';
25
27
  import { type SessionNewResult, type SessionResumeResult, type SessionPromptResult } from '../../common/acp/protocol-types.js';
@@ -89,9 +91,20 @@ export declare class OpencodeAcpRunner extends BaseAcpRunner<OpencodeAcpTranslat
89
91
  * starting or still activating location plugins when the first session is
90
92
  * created; both surface as a JSON-RPC internal error. Retry a few times
91
93
  * before failing the turn.
94
+ *
95
+ * 单次 attempt 自身还要等 opencode 侧超时(~10s),所以真实窗口 ≈ Σdelay +
96
+ * 每次的等待。2026-10-30 现场:daemon 刚重启,`/api/model` 因模型目录拉取
97
+ * 超时连续 503 约 7 分钟,旧退避 0.5/1.5/3s(合计 ~45s)全落在这段里,4 次
98
+ * 重试无一成功。现在铺到 ~75s + 每次超时 ≈ 2 分钟,覆盖一次冷启动窗口,
99
+ * 又不至于让真故障的用户干等太久。
92
100
  */
93
101
  protected sessionSetupRetryDelaysMs(): number[];
94
102
  protected isRetryableSessionSetupError(err: unknown): boolean;
103
+ /**
104
+ * 用户可见文案。这里刻意**不**建议 `opencode service restart`:卡住的是
105
+ * opencode 的模型目录/位置服务冷启动,重启只会清掉刚热好的状态、重新回到
106
+ * 同一个窗口(2026-10-30 现场:照提示重启两次仍然失败,等几分钟自己好了)。
107
+ */
95
108
  protected enrichSessionSetupError(err: unknown): Error;
96
109
  getStatusInfo(): AgentStatusInfo;
97
110
  /**
@@ -173,20 +173,31 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
173
173
  * starting or still activating location plugins when the first session is
174
174
  * created; both surface as a JSON-RPC internal error. Retry a few times
175
175
  * before failing the turn.
176
+ *
177
+ * 单次 attempt 自身还要等 opencode 侧超时(~10s),所以真实窗口 ≈ Σdelay +
178
+ * 每次的等待。2026-10-30 现场:daemon 刚重启,`/api/model` 因模型目录拉取
179
+ * 超时连续 503 约 7 分钟,旧退避 0.5/1.5/3s(合计 ~45s)全落在这段里,4 次
180
+ * 重试无一成功。现在铺到 ~75s + 每次超时 ≈ 2 分钟,覆盖一次冷启动窗口,
181
+ * 又不至于让真故障的用户干等太久。
176
182
  */
177
183
  sessionSetupRetryDelaysMs() {
178
- return [500, 1500, 3000];
184
+ return [500, 2000, 8000, 20000, 45000];
179
185
  }
180
186
  isRetryableSessionSetupError(err) {
181
187
  const message = err instanceof Error ? err.message : String(err);
182
188
  return /ServiceUnavailableError|Location plugins did not finish activating|Internal service failure|ECONNREFUSED|fetch failed/i.test(message);
183
189
  }
190
+ /**
191
+ * 用户可见文案。这里刻意**不**建议 `opencode service restart`:卡住的是
192
+ * opencode 的模型目录/位置服务冷启动,重启只会清掉刚热好的状态、重新回到
193
+ * 同一个窗口(2026-10-30 现场:照提示重启两次仍然失败,等几分钟自己好了)。
194
+ */
184
195
  enrichSessionSetupError(err) {
185
196
  if (!this.isRetryableSessionSetupError(err)) {
186
197
  return super.enrichSessionSetupError(err);
187
198
  }
188
199
  const message = err instanceof Error ? err.message : String(err);
189
- return new Error(`opencode 2.0 后台服务不可用(${message})。请在终端执行 \`opencode service restart\` 后重试。`, { cause: err });
200
+ return new Error(`opencode 后台服务尚未就绪(${message})。这是 opencode 冷启动 / 模型目录加载期间的临时状态:等 1–2 分钟重发即可,重启服务只会重新进入同一窗口。`, { cause: err });
190
201
  }
191
202
  getStatusInfo() {
192
203
  return {
@@ -17,7 +17,8 @@
17
17
  * - pi,极简风格,没有 Ask User question。
18
18
  * - `prompt` ack ≠ turn completion; the turn ends on `agent_settled`.
19
19
  */
20
- import type { AgentSessionReader, AgentEvent, AgentStatusInfo, SpawnOptions } from '../../types.js';
20
+ import type { AgentSessionReader } from '../../../session-contract.js';
21
+ import type { AgentEvent, AgentStatusInfo, SpawnOptions } from '../../types.js';
21
22
  import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
22
23
  import { PiRpcClient } from './client.js';
23
24
  import { type PiRpcTranslatorEvent } from './translator.js';
@@ -17,7 +17,7 @@
17
17
  * does PATH + X_OK on posix (same semantics) and PATH × PATHEXT on win32,
18
18
  * with no child process at all.
19
19
  */
20
- import type { AgentKind } from './types.js';
20
+ import type { AgentKind } from '../agent-kinds.js';
21
21
  /**
22
22
  * Probe a single agent, using cache when fresh.
23
23
  * Updates cache on completion.
@@ -18,6 +18,7 @@
18
18
  * with no child process at all.
19
19
  */
20
20
  import { resolveExecutable } from '../platform/command.js';
21
+ import { TtlCache } from '../common/ttl-cache.js';
21
22
  /** Map from AgentKind to its CLI binary name. */
22
23
  const BINARY_MAP = {
23
24
  claude: 'claude',
@@ -30,8 +31,11 @@ const BINARY_MAP = {
30
31
  dsh: '',
31
32
  };
32
33
  const CACHE_TTL_MS = 5 * 60_000;
33
- /** Module-level cache, survives across calls within the process. */
34
- const cache = new Map();
34
+ /**
35
+ * Module-level cache, survives across calls within the process. 键集就是
36
+ * AgentKind(有界),因此不设条目上限;共享 `TtlCache` 与 session readers 同语义。
37
+ */
38
+ const cache = new TtlCache(CACHE_TTL_MS);
35
39
  /**
36
40
  * Probe a single agent's availability (pure PATH lookup; dsh has no CLI
37
41
  * binary and is always unavailable here).
@@ -49,10 +53,10 @@ async function probeOne(kind) {
49
53
  */
50
54
  export async function probeAgentAvailability(kind) {
51
55
  const cached = cache.get(kind);
52
- if (cached && Date.now() - cached.ts < CACHE_TTL_MS)
53
- return cached.ok;
56
+ if (cached !== undefined)
57
+ return cached;
54
58
  const ok = await probeOne(kind);
55
- cache.set(kind, { ts: Date.now(), ok });
59
+ cache.set(kind, ok);
56
60
  return ok;
57
61
  }
58
62
  /**
@@ -76,10 +80,7 @@ export async function probeAllAgents() {
76
80
  * Used by synchronous code paths (e.g. buildConfigCard) that cannot await.
77
81
  */
78
82
  export function getCachedAvailability(kind) {
79
- const cached = cache.get(kind);
80
- if (!cached || Date.now() - cached.ts >= CACHE_TTL_MS)
81
- return undefined;
82
- return cached.ok;
83
+ return cache.get(kind);
83
84
  }
84
85
  /** Clear all cache entries. Exported for test use only (cache auto-expires via TTL). */
85
86
  export function _clearCacheForTest() {
@@ -1,4 +1,5 @@
1
- import type { AgentKind, Runner } from './index.js';
1
+ import type { AgentKind } from '../agent-kinds.js';
2
+ import type { Runner } from './index.js';
2
3
  /**
3
4
  * Registry of agent factories, keyed by `AgentKind`. `index.ts` registers one
4
5
  * factory per available agent at startup; `Bridge.getRunner(cwd)` looks
@@ -3,7 +3,10 @@
3
3
  *
4
4
  * 本文件包含所有 Agent 相关的类型定义,从 runner/index.ts 提取。
5
5
  * 所有下游模块应从本文件 import 类型,而非直接依赖实现。
6
+ * (例外:`AgentKind` 住在 `src/agent-kinds.ts`,见文件底部说明。)
6
7
  */
8
+ import type { AgentKind } from '../agent-kinds.js';
9
+ import type { AgentSessionReader } from '../session-contract.js';
7
10
  export interface SystemInitEvent {
8
11
  type: 'system';
9
12
  subtype: 'init';
@@ -552,18 +555,7 @@ export interface Runner {
552
555
  */
553
556
  unregisterExitHandlers?(): void;
554
557
  }
555
- /**
556
- * Agent 名单单源(运行时数组 + 类型同源,二者不可能漂移)。
557
- *
558
- * 历史上这份名单被手抄成多份(router 的 VALID_AGENTS、bridge 的 resume.compact
559
- * 校验、各 agent 的 config builder…),漂移过:dsh 加入后 bridge 的
560
- * resume.compact 仍只有 5 个,带 dsh 的 resume 卡片会静默退回 defaultAgent。
561
- * 新增 agent 只改这里。
562
- */
563
- export declare const AGENT_KINDS: readonly ["claude", "codex", "opencode", "pi", "kimi", "dsh"];
564
- export type AgentKind = (typeof AGENT_KINDS)[number];
565
- /** 用户可见 agent 显示名单源:启动注册与无 registry 的回退路径共用。 */
566
- export declare const AGENT_DISPLAY_NAMES: Record<AgentKind, string>;
558
+ export { AGENT_DISPLAY_NAMES } from '../agent-kinds.js';
567
559
  /**
568
560
  * Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
569
561
  *
@@ -579,147 +571,6 @@ export declare const AGENT_DISPLAY_NAMES: Record<AgentKind, string>;
579
571
  export declare const COMPACT_CAPABILITY: Readonly<Record<AgentKind, boolean>>;
580
572
  /** 该 agent 是否声明支持 compact(渲染门控与执行门控共用的唯一判据)。 */
581
573
  export declare function supportsCompact(kind: AgentKind): boolean;
582
- /** Unified session descriptor returned by every AgentSessionReader. */
583
- export interface AgentSession {
584
- sessionId: string;
585
- summary: string;
586
- mtime: number;
587
- /** DSH:session 创建时固定的 preset;其他 agent 无此概念(undefined)。 */
588
- agentPreset?: string;
589
- /**
590
- * 服务端投影已有的会话标题(DSH `projections.values.title`)。
591
- *
592
- * 与 `summary` 的区别:`summary` 是「首条用户输入」的兜底口径(claude /
593
- * codex / pi / kimi 无标题字段),`title` 是 agent 自己起的标题。列表卡
594
- * 用它决定标签文案(标题 / 会话摘要),缺省时回退 `summary`。
595
- */
596
- title?: string;
597
- /**
598
- * 最后一轮用户输入(服务端投影,单行预览)。
599
- *
600
- * 只给「列表响应里已顺带返回」的 reader 用(DSH 的 `turnOutline` 末条),
601
- * 目的是让列表卡不必为每行做一次全量内容读取。
602
- */
603
- lastUserMessage?: string;
604
- /** 最后一轮助手输出(服务端投影已截断,单行预览)。 */
605
- lastAssistantMessage?: string;
606
- }
607
- /** A single renderable event from session history. */
608
- export interface AgentSessionContentEvent {
609
- type: string;
610
- /** 正文(展开后的面板内容)。 */
611
- content: string;
612
- /**
613
- * 面板标题(可选,覆盖 reader 的默认标签)。
614
- *
615
- * 折叠面板默认只显示标题:工具调用这种"标题即全部信息"的事件必须把
616
- * `bash — npm test` 这类摘要放进标题,否则折叠态只剩 `🔧 工具调用`。
617
- */
618
- title?: string;
619
- timestamp?: string;
620
- }
621
- export interface AgentSessionUsage {
622
- inputTokens: number;
623
- outputTokens: number;
624
- contextLength: number;
625
- /** Agent 上报的当前模型 context window 上限(codex token_count.info.model_context_window)。
626
- * 仅 codex 提供;缺省时卡片只显示绝对量、不显示百分比。 */
627
- contextLimit?: number;
628
- /** Claude auto-compact 次数(数 compact_boundary 事件);codex/opencode 无此概念,留 undefined。 */
629
- compactCount?: number;
630
- /** 压缩前上下文水位(codex compact 后从会话 jsonl 读取,仅会话以压缩收尾时有值)。 */
631
- compactPreContextLength?: number;
632
- /** 从缓存读取的 token 数(节省的费用)。 */
633
- cacheReadTokens?: number;
634
- /** 新建缓存的 token 数。 */
635
- cacheCreationTokens?: number;
636
- /** Agent 声明的总量;展示用 max(total, 分项和)。 */
637
- totalTokens?: number;
638
- /** 会话累计 total token(所有 run 之和,含本次)。用于 Run 卡片"累计"展示。 */
639
- cumulativeTotalTokens?: number;
640
- /** 会话累计 input token(所有 run 之和,含本次)。用于 Run 卡片"累计"展示。 */
641
- cumulativeInputTokens?: number;
642
- /** 会话累计 output token(所有 run 之和,含本次)。 */
643
- cumulativeOutputTokens?: number;
644
- /** 会话累计 cache read token(所有 run 之和)。用于 Run 卡片展示累计缓存命中。 */
645
- cumulativeCacheReadTokens?: number;
646
- /** 会话累计 cache creation token(所有 run 之和)。 */
647
- cumulativeCacheCreationTokens?: number;
648
- }
649
- /** Unified session content payload returned by every AgentSessionReader. */
650
- export interface SessionContent {
651
- events: AgentSessionContentEvent[];
652
- usage?: AgentSessionUsage;
653
- aiTitle?: string;
654
- recap?: string;
655
- displayTitle?: string;
656
- }
657
- /**
658
- * Lightweight session summary used by `/resume` list prefetch.
659
- *
660
- * Carries only the display fields (title / recap / usage) — **no `events`
661
- * array**. Readers that can extract these without constructing the full event
662
- * list (e.g. codex, which would otherwise JSON.parse every response_item and
663
- * build an `events[]` that the prefetch immediately discards) implement the
664
- * optional `readSessionSummary` on `AgentSessionReader`.
665
- */
666
- export interface SessionSummary {
667
- aiTitle?: string;
668
- recap?: string;
669
- displayTitle?: string;
670
- usage?: AgentSessionUsage;
671
- }
672
- /**
673
- * Reads an agent's session history. Each agent implements its own (Claude reads
674
- * `~/.claude/projects/`, Codex calls `thread/list`, OpenCode calls `GET /session`).
675
- *
676
- * Q-C decision: `projectsDir` and other agent-specific path config live on the
677
- * concrete reader's constructor, NOT on these method opts. The router/bridge
678
- * depend only on this interface and never pass path config.
679
- */
680
- export interface AgentSessionReader {
681
- /**
682
- * List sessions for a cwd, newest first by mtime.
683
- *
684
- * `sessions` is the page `[offset, offset+limit)` of the full set ordered
685
- * by mtime desc, with same-mtime ties broken by a deterministic secondary
686
- * key (sessionId/file path) so ordering is stable across calls. When
687
- * `limit` is omitted, codex/pi/kimi return their default-sized page
688
- * (default 20); claude/opencode return the full remaining set from
689
- * `offset`. `total` is ALWAYS the size of the full cwd-matched set before
690
- * pagination — never the truncated page length. A negative `offset` is
691
- * clamped to 0 (first page).
692
- */
693
- listSessions(cwd: string, opts?: {
694
- limit?: number;
695
- offset?: number;
696
- }): {
697
- sessions: AgentSession[];
698
- total: number;
699
- };
700
- getNewestSession(cwd: string): AgentSession | null;
701
- readSessionContent(sessionId: string, cwd: string, opts?: {
702
- maxEvents?: number;
703
- }): SessionContent;
704
- /**
705
- * Optional lightweight summary for list-prefetch.
706
- *
707
- * Returns only the display fields (aiTitle/recap/displayTitle/usage) without
708
- * constructing the full `events` array. When absent, callers fall back to
709
- * `readSessionContent` and take the needed fields.
710
- */
711
- readSessionSummary?(sessionId: string, cwd: string): SessionSummary;
712
- /**
713
- * Whether a session is still active (process may still be running).
714
- *
715
- * Dormant API: no production caller reads this today — the router decides
716
- * "is an agent running" via the in-memory `bridge.isBusyFor(cwd)` (see
717
- * `cmdPs`), not by probing session files. Retained on the reader contract
718
- * because all 5 readers implement it and it is the natural hook if a
719
- * file-based liveness check is ever needed; not wired into the bridge.
720
- */
721
- isSessionActive(sessionId: string, cwd: string): boolean;
722
- }
723
574
  /**
724
575
  * AgentRunner extends Runner with agent identity and a session reader.
725
576
  * Bridge/Router depend on this interface so adding a new agent is
@@ -732,6 +583,22 @@ export interface AgentRunner extends Runner {
732
583
  getStatusInfo(): AgentStatusInfo;
733
584
  /** Whether this runner provides live usage data (vs. file-based). */
734
585
  getUsageAuthority?(): 'live' | 'jsonl';
586
+ /**
587
+ * 回传审批决策(codex thread/permissions、ACP elicitation、DSH approval
588
+ * resolve)。未实现的 agent 由协调器按「无人应答」超时收尾。
589
+ */
590
+ respondApproval?(requestId: number | string, response: unknown): Promise<void>;
591
+ /**
592
+ * 停止 agent 侧的一个后台任务(DSH 的 background job)。只有支持后台任务的
593
+ * agent 实现;调用方按能力存在与否门控。
594
+ */
595
+ killJob?(sessionId: string, jobId: string): Promise<boolean>;
596
+ /**
597
+ * 热更审批模式/权限预设(config.save 后推给在跑的 runner,不必重启会话)。
598
+ * 各家 settings 词表不同(codex: approvalPolicy/sandbox;kimi: permissionMode;
599
+ * opencode: mode;dsh: permissionPreset),故为开放载荷。
600
+ */
601
+ updateApprovalMode?(settings: Record<string, unknown>): Promise<void>;
735
602
  }
736
603
  /** Agent self-describing status info for /status display. */
737
604
  export interface AgentStatusInfo {
@@ -746,4 +613,3 @@ export interface AgentStatusInfo {
746
613
  /** Agent-specific extra fields (e.g. codex.sandbox). */
747
614
  extras?: Record<string, string>;
748
615
  }
749
- export {};
@@ -1,30 +1,10 @@
1
- /**
2
- * Runner 模块类型定义
3
- *
4
- * 本文件包含所有 Agent 相关的类型定义,从 runner/index.ts 提取。
5
- * 所有下游模块应从本文件 import 类型,而非直接依赖实现。
6
- */
7
1
  // =============================================================================
8
2
  // Multi-Agent Adapter Types
9
3
  // =============================================================================
10
- /**
11
- * Agent 名单单源(运行时数组 + 类型同源,二者不可能漂移)。
12
- *
13
- * 历史上这份名单被手抄成多份(router 的 VALID_AGENTS、bridge 的 resume.compact
14
- * 校验、各 agent 的 config builder…),漂移过:dsh 加入后 bridge 的
15
- * resume.compact 仍只有 5 个,带 dsh 的 resume 卡片会静默退回 defaultAgent。
16
- * 新增 agent 只改这里。
17
- */
18
- export const AGENT_KINDS = ['claude', 'codex', 'opencode', 'pi', 'kimi', 'dsh'];
19
- /** 用户可见 agent 显示名单源:启动注册与无 registry 的回退路径共用。 */
20
- export const AGENT_DISPLAY_NAMES = {
21
- claude: 'Claude',
22
- codex: 'Codex',
23
- opencode: 'Opencode',
24
- pi: 'Pi',
25
- kimi: 'Kimi',
26
- dsh: 'DSH',
27
- };
4
+ // Agent 名单与显示名住在 `src/agent-kinds.ts`(中立层,config/session/card 都
5
+ // 要用);本模块只 import `AgentKind` 类型来签名 runner 契约。保留旧路径的
6
+ // re-export 兼容调用方,但值仍只由 agent-kinds.ts 定义。
7
+ export { AGENT_DISPLAY_NAMES } from '../agent-kinds.js';
28
8
  /**
29
9
  * Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
30
10
  *