lark-remote 0.5.19 → 0.5.21

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 (136) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bridge/approval-actions.d.ts +1 -1
  3. package/dist/bridge/approval-actions.js +1 -1
  4. package/dist/bridge/bash-run.d.ts +71 -0
  5. package/dist/bridge/bash-run.js +217 -0
  6. package/dist/bridge/compact.d.ts +155 -0
  7. package/dist/bridge/compact.js +450 -0
  8. package/dist/bridge/final-usage.js +4 -4
  9. package/dist/bridge/inbound-media.js +72 -46
  10. package/dist/bridge/index.d.ts +106 -153
  11. package/dist/bridge/index.js +400 -1000
  12. package/dist/bridge/job-watch.d.ts +68 -0
  13. package/dist/bridge/job-watch.js +146 -0
  14. package/dist/bridge/queue-manager.d.ts +3 -3
  15. package/dist/bridge/queue-manager.js +10 -29
  16. package/dist/card/approval-render.d.ts +2 -2
  17. package/dist/card/approval-render.js +141 -130
  18. package/dist/card/card-budget.js +123 -98
  19. package/dist/card/card-helpers.d.ts +3 -3
  20. package/dist/card/card-helpers.js +6 -6
  21. package/dist/card/final-answer-file.js +1 -1
  22. package/dist/card/run-renderer.d.ts +3 -3
  23. package/dist/card/run-renderer.js +114 -159
  24. package/dist/card/run-state.d.ts +1 -15
  25. package/dist/card/run-state.js +358 -292
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +21 -17
  28. package/dist/card/usage-format.d.ts +22 -7
  29. package/dist/card/usage-format.js +119 -100
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/common/text-width.js +36 -24
  33. package/dist/common/tool-summary.js +132 -90
  34. package/dist/config/codex-config.js +130 -106
  35. package/dist/config/index.d.ts +16 -2
  36. package/dist/config/index.js +23 -8
  37. package/dist/connector/index.js +6 -6
  38. package/dist/error-classification.js +3 -2
  39. package/dist/inbound/placeholder.d.ts +1 -1
  40. package/dist/inbound/placeholder.js +1 -1
  41. package/dist/index.js +387 -281
  42. package/dist/logger/index.js +6 -6
  43. package/dist/message-handlers.d.ts +0 -5
  44. package/dist/message-handlers.js +254 -237
  45. package/dist/model-catalog.d.ts +2 -2
  46. package/dist/model-catalog.js +3 -8
  47. package/dist/platform/identity.js +1 -1
  48. package/dist/platform/path.d.ts +1 -1
  49. package/dist/platform/path.js +1 -1
  50. package/dist/platform/sleep-blocker.js +1 -1
  51. package/dist/platform/terminator-win32.js +2 -1
  52. package/dist/router/commands/ls.d.ts +3 -0
  53. package/dist/router/commands/ls.js +256 -175
  54. package/dist/router/commands/order.js +184 -159
  55. package/dist/router/commands/resume.js +190 -152
  56. package/dist/router/commands/ws.js +219 -174
  57. package/dist/router/config/dsh.d.ts +4 -8
  58. package/dist/router/config/dsh.js +3 -10
  59. package/dist/router/config/kimi.js +1 -1
  60. package/dist/router/config/types.d.ts +1 -1
  61. package/dist/router/config-diff.js +1 -1
  62. package/dist/router/index.d.ts +56 -11
  63. package/dist/router/index.js +401 -391
  64. package/dist/runner/bash/runner.js +2 -4
  65. package/dist/runner/claude/session.d.ts +1 -1
  66. package/dist/runner/claude/session.js +6 -4
  67. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  68. package/dist/runner/codex/app-server/runner.js +30 -25
  69. package/dist/runner/codex/app-server/translator.d.ts +2 -5
  70. package/dist/runner/codex/app-server/translator.js +291 -238
  71. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  72. package/dist/runner/common/acp/acp-approval.js +1 -1
  73. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  74. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  75. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  76. package/dist/runner/common/acp/base-translator.js +50 -26
  77. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  78. package/dist/runner/common/acp/protocol-types.js +2 -2
  79. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  80. package/dist/runner/common/connection-based-runner.js +32 -2
  81. package/dist/runner/common/jsonl-stream.js +154 -187
  82. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  83. package/dist/runner/common/jsonrpc/client.js +43 -12
  84. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  85. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  86. package/dist/runner/common/spawning-runner.d.ts +6 -1
  87. package/dist/runner/common/spawning-runner.js +9 -4
  88. package/dist/runner/common/turn-started.d.ts +8 -0
  89. package/dist/runner/common/turn-started.js +11 -0
  90. package/dist/runner/dsh/approval.js +84 -51
  91. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  92. package/dist/runner/dsh/catalog-probe.js +10 -0
  93. package/dist/runner/dsh/client.d.ts +7 -11
  94. package/dist/runner/dsh/client.js +356 -289
  95. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  96. package/dist/runner/dsh/exit-cancel.js +2 -2
  97. package/dist/runner/dsh/inflight.d.ts +1 -1
  98. package/dist/runner/dsh/inflight.js +1 -1
  99. package/dist/runner/dsh/mux-stream.js +28 -8
  100. package/dist/runner/dsh/runner.d.ts +44 -4
  101. package/dist/runner/dsh/runner.js +296 -199
  102. package/dist/runner/dsh/translator.d.ts +1 -31
  103. package/dist/runner/dsh/translator.js +180 -178
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  106. package/dist/runner/kimi/acp/runner.js +82 -46
  107. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  108. package/dist/runner/kimi/acp/translator.js +3 -4
  109. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  110. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  111. package/dist/runner/opencode/acp/runner.js +4 -4
  112. package/dist/runner/pi/rpc/client.js +4 -13
  113. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  114. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  115. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  116. package/dist/runner/pi/rpc/runner.js +27 -42
  117. package/dist/runner/pi/rpc/translator.js +61 -60
  118. package/dist/runner/types.d.ts +2 -2
  119. package/dist/runner/types.js +1 -1
  120. package/dist/session/claude/sessions.d.ts +45 -0
  121. package/dist/session/claude/sessions.js +160 -122
  122. package/dist/session/codex/rollout-reader.js +181 -182
  123. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  124. package/dist/session/common/throw-session-list-error.js +16 -0
  125. package/dist/session/dsh/sessions.js +291 -235
  126. package/dist/session/kimi/sessions.d.ts +40 -8
  127. package/dist/session/kimi/sessions.js +373 -301
  128. package/dist/session/opencode/sessions.d.ts +142 -1
  129. package/dist/session/opencode/sessions.js +378 -321
  130. package/dist/session/pi/sessions.js +4 -5
  131. package/dist/session/session-store.d.ts +9 -1
  132. package/dist/session/session-store.js +99 -99
  133. package/dist/update/version-check.js +2 -1
  134. package/dist/workspace/index.d.ts +1 -1
  135. package/dist/workspace/index.js +1 -1
  136. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { loadConfig, getAgentConfig } from './config/index.js';
1
+ import { loadConfig, getAgentConfig, PI_DEFAULTS, KIMI_DEFAULTS, } from './config/index.js';
2
2
  import { resolveAgentChoices } from './runner/index.js';
3
3
  import { ensureConfig } from './config/wizard.js';
4
4
  import { parseCliArgs, resolveConfigDir, setConfigDir, printHelp, printVersion, } from './config/dir.js';
@@ -29,6 +29,13 @@ import { buildSessionHistoryCard, pickResumeCardEvents } from './card/card-helpe
29
29
  import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
30
30
  import path from 'node:path';
31
31
  import { setupMessageHandlers } from './message-handlers.js';
32
+ /**
33
+ * 分钟 → 毫秒(turn idle timeout 等 config 字段共用)。
34
+ * 未配置(undefined/null)→ undefined = 不设该超时,与原三处内联三元逐字等价。
35
+ */
36
+ function minutesToMs(minutes) {
37
+ return minutes != null ? minutes * 60_000 : undefined;
38
+ }
32
39
  /** Send auto-resume card for a restored session */
33
40
  async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false, stillRunning = false) {
34
41
  if (!contact.userId)
@@ -38,7 +45,7 @@ async function sendAutoResumeCard(connector, contact, cwd, session, state, compa
38
45
  return;
39
46
  // Limit history events to prevent card size exceeded error (11310).
40
47
  // 窗口按"最后 N 条"截,但强制保住最后一条用户消息与最后一条 assistant 正文,
41
- // 否则工具往返会把它俩一起挤出卡(2026-10-19 恢复卡只剩 5 个 bash 面板)。
48
+ // 否则工具往返多时(如连续 bash 面板占满窗口)会把这两条挤出卡。
42
49
  const MAX_HISTORY_EVENTS = 5;
43
50
  const eventsToShow = pickResumeCardEvents(state.events, MAX_HISTORY_EVENTS);
44
51
  const hiddenCount = state.events.length - eventsToShow.length;
@@ -122,151 +129,28 @@ async function setupInstanceLockAndHandlers(instanceLock, logger, configDir) {
122
129
  }
123
130
  });
124
131
  }
125
- /** Initialize per-workspace runner factory + agent registry. */
126
- function initializeRunner(config, configDir, cliArgs) {
127
- const agentRegistry = new AgentRegistry();
128
- // 单点化:runner factory 取最新 config 的唯一入口(语义保持,
129
- // container 未接线时回退启动快照)。
130
- const latest = () => agentRegistry.getConfigContainer()?.current ?? config;
131
- // One global DSH per machine/DSH_HOME. This resolver reuses a listening
132
- // process or starts `dsh web` detached; it never owns the child's lifetime.
133
- const dshDaemon = new DshDaemon({
134
- host: () => getAgentConfig(latest(), 'dsh')?.host,
135
- dshHome: () => getAgentConfig(latest(), 'dsh')?.dshHome,
136
- dshCommand: () => getAgentConfig(latest(), 'dsh')?.dshCommand,
137
- manageProcess: () => getAgentConfig(latest(), 'dsh')?.manageProcess ?? true,
138
- startTimeoutMs: () => getAgentConfig(latest(), 'dsh')?.startTimeoutMs,
139
- });
132
+ /**
133
+ * 创建全部 session reader。runner 侧不 import session 层(否则 runner ↔ session
134
+ * 互相依赖),统一由组合根注入。
135
+ */
136
+ function createSessionReaders(config, latest) {
140
137
  // claude 的会话读取器与其余 agent 一样由组合根注入:runner 侧不 import session
141
138
  // 层(否则 runner ↔ session 互相依赖),且 bridge 读 usage / router 列 session
142
139
  // 用的是同一个实例(缓存共用)。
143
- const claudeSessionReader = new ClaudeSessionReader();
144
- // Claude factory reads from configContainer (not closure) so runtime
145
- // config changes (model, effort, stopGraceMs) take effect after
146
- // bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
147
- agentRegistry.register('claude', (ws) => {
148
- const claudeConfig = latest().claude;
149
- return new ClaudeRunner({
150
- model: claudeConfig.model,
151
- effort: claudeConfig.effort,
152
- stopGraceMs: claudeConfig.stopGraceMs,
153
- settings: cliArgs.settings,
154
- pidDir: configDir,
155
- cwd: ws,
156
- sessionReader: claudeSessionReader,
157
- permissionMode: claudeConfig.permissionMode,
158
- idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
159
- });
160
- });
161
- // Register Codex runner (app-server only). The registry factory reads latest
162
- // config from container.current so runtime config changes (model, sandbox,
163
- // approvalPolicy, reasoningEffort, etc.) take effect after bridge.setConfig()
164
- // + clearRunners().
165
- const codexSessionReader = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
166
- agentRegistry.register('codex', (_ws) => {
167
- const codexConfig = getAgentConfig(latest(), 'codex');
168
- return new CodexAppServerRunner({
169
- kind: 'codex',
170
- model: codexConfig?.model,
171
- modelProvider: codexConfig?.modelProvider,
172
- reasoningEffort: codexConfig?.reasoningEffort,
173
- sessionReader: codexSessionReader,
174
- sandbox: codexConfig?.sandbox,
175
- approvalPolicy: codexConfig?.approvalPolicy,
176
- binary: codexConfig?.appServer?.binary,
177
- requestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
178
- idleTtlMs: codexConfig?.appServer?.idleTtlMs,
179
- turnTimeoutMs: codexConfig?.appServer?.turnIdleTimeoutMinutes != null
180
- ? codexConfig.appServer.turnIdleTimeoutMinutes * 60_000
181
- : undefined,
182
- });
183
- });
184
- // Register OpencodeAcpRunner (pure ACP mode, `opencode acp` JSON-RPC over stdio)
185
- // Register session reader (same instance as used by runner)
186
- const sessionReaderRegistry = new SessionReaderRegistry();
187
- sessionReaderRegistry.register('claude', claudeSessionReader);
188
- sessionReaderRegistry.register('codex', codexSessionReader);
140
+ const claude = new ClaudeSessionReader();
141
+ const codex = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
189
142
  // Register OpencodeSessionReader (CLI version)
190
143
  // Using `opencode session list` and `session export` instead of HTTP API.
191
144
  // Share the configured binary with the ACP runner so `opencode2` (v2) can
192
145
  // be used without the reader silently falling back to a v1 `opencode`.
193
146
  // The reader is registered once; changing acp.binary at runtime requires a
194
147
  // lark-remote restart.
195
- const opencodeSessionReader = new OpencodeSessionReader({
148
+ const opencode = new OpencodeSessionReader({
196
149
  binary: getAgentConfig(config, 'opencode')?.acp?.binary,
197
150
  });
198
- agentRegistry.register('opencode', (_ws) => {
199
- const ocConfig = getAgentConfig(latest(), 'opencode');
200
- // Build model string with validation: provider/model format
201
- let model;
202
- if (ocConfig?.modelID) {
203
- const provider = ocConfig.providerID ?? 'anthropic';
204
- // Validate: if modelID contains '/', it's already in provider/model format
205
- if (ocConfig.modelID.includes('/')) {
206
- model = ocConfig.modelID;
207
- }
208
- else {
209
- model = `${provider}/${ocConfig.modelID}`;
210
- }
211
- }
212
- return new OpencodeAcpRunner({
213
- kind: 'opencode',
214
- model,
215
- sessionReader: opencodeSessionReader,
216
- mode: ocConfig?.mode ?? 'build',
217
- binary: ocConfig?.acp?.binary,
218
- requestTimeoutMs: ocConfig?.acp?.requestTimeoutMs,
219
- idleTtlMs: ocConfig?.acp?.idleTtlMs,
220
- turnIdleTimeoutMs: ocConfig?.acp?.turnIdleTimeoutMinutes != null
221
- ? ocConfig.acp.turnIdleTimeoutMinutes * 60_000
222
- : undefined,
223
- });
224
- });
225
- sessionReaderRegistry.register('opencode', opencodeSessionReader);
226
- // Register PiRpcRunner and PiSessionReader
227
- // 2026-07-12: 修复 config.save 后 pi provider 不生效的问题
228
- // 关键:factory 闭包必须能访问到最新的 config,而不是启动时的快照
229
- // 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值
230
- const configContainer = { current: config };
231
- agentRegistry.setConfigContainer(configContainer);
232
- const piSessionReader = new PiSessionReader();
233
- agentRegistry.register('pi', (ws) => {
234
- const piConf = getAgentConfig(latest(), 'pi');
235
- return new PiRpcRunner({
236
- provider: piConf?.provider ?? 'Volcano',
237
- model: piConf?.model ?? 'glm-5.2',
238
- thinking: piConf?.thinking ?? 'medium',
239
- tools: (piConf?.tools ?? 'read,bash,edit,write,grep,find,ls')
240
- .split(',')
241
- .map((t) => t.trim())
242
- .filter(Boolean),
243
- cwd: ws,
244
- sessionReader: piSessionReader,
245
- });
246
- });
247
- sessionReaderRegistry.register('pi', piSessionReader);
248
- // Register KimiAcpRunner (pure ACP mode) and KimiSessionReader
249
- const kimiSessionReader = new KimiSessionReader();
250
- agentRegistry.register('kimi', (_ws) => {
251
- const kimiConf = getAgentConfig(latest(), 'kimi');
252
- const acpConf = kimiConf?.acp;
253
- return new KimiAcpRunner({
254
- kind: 'kimi',
255
- sessionReader: kimiSessionReader,
256
- binary: acpConf?.binary ?? 'kimi',
257
- requestTimeoutMs: acpConf?.requestTimeoutMs,
258
- idleTtlMs: acpConf?.idleTtlMs,
259
- turnIdleTimeoutMs: acpConf?.turnIdleTimeoutMinutes != null
260
- ? acpConf.turnIdleTimeoutMinutes * 60_000
261
- : undefined,
262
- model: kimiConf?.model ?? 'kimi-code/k3',
263
- thinkingEffort: kimiConf?.thinkingEffort,
264
- permissionMode: kimiConf?.permissionMode ?? 'manual',
265
- });
266
- });
267
- sessionReaderRegistry.register('kimi', kimiSessionReader);
268
- // Register DshRunner (HTTP-only DSH Web Host agent) and DshSessionReader.
269
- const dshSessionReader = new DshSessionReader({
151
+ const pi = new PiSessionReader();
152
+ const kimi = new KimiSessionReader();
153
+ const dsh = new DshSessionReader({
270
154
  // hostProvider 每次调用读最新 config:/config 修改 host 后 runner 重建为
271
155
  // 新 host,reader(/resume、用量、完成卡)也跟随新 host。
272
156
  hostProvider: () => getAgentConfig(latest(), 'dsh')?.host,
@@ -274,53 +158,217 @@ function initializeRunner(config, configDir, cliArgs) {
274
158
  tokenProvider: () => getAgentConfig(latest(), 'dsh')?.token,
275
159
  credentialsFileProvider: () => getAgentConfig(latest(), 'dsh')?.credentialsFile,
276
160
  });
161
+ return { claude, codex, opencode, pi, kimi, dsh };
162
+ }
163
+ // Claude factory reads from configContainer (not closure) so runtime
164
+ // config changes (model, effort, stopGraceMs) take effect after
165
+ // bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
166
+ function claudeRunnerOptions(deps, cwdOpt) {
167
+ const { cliArgs, configDir, latest, readers } = deps;
168
+ const claudeConfig = latest().claude;
169
+ return {
170
+ model: claudeConfig.model,
171
+ effort: claudeConfig.effort,
172
+ stopGraceMs: claudeConfig.stopGraceMs,
173
+ settings: cliArgs.settings,
174
+ pidDir: configDir,
175
+ cwd: cwdOpt.cwd,
176
+ sessionReader: readers.claude,
177
+ permissionMode: claudeConfig.permissionMode,
178
+ idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
179
+ };
180
+ }
181
+ // Register Codex runner (app-server only). The registry factory reads latest
182
+ // config from container.current so runtime config changes (model, sandbox,
183
+ // approvalPolicy, reasoningEffort, etc.) take effect after bridge.setConfig()
184
+ // + clearRunners().
185
+ function codexRunnerOptions(deps) {
186
+ const codexConfig = getAgentConfig(deps.latest(), 'codex');
187
+ return {
188
+ kind: 'codex',
189
+ model: codexConfig?.model,
190
+ modelProvider: codexConfig?.modelProvider,
191
+ reasoningEffort: codexConfig?.reasoningEffort,
192
+ sessionReader: deps.readers.codex,
193
+ sandbox: codexConfig?.sandbox,
194
+ approvalPolicy: codexConfig?.approvalPolicy,
195
+ binary: codexConfig?.appServer?.binary,
196
+ requestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
197
+ idleTtlMs: codexConfig?.appServer?.idleTtlMs,
198
+ turnTimeoutMs: minutesToMs(codexConfig?.appServer?.turnIdleTimeoutMinutes),
199
+ };
200
+ }
201
+ // Register OpencodeAcpRunner (pure ACP mode, `opencode acp` JSON-RPC over stdio)
202
+ function opencodeRunnerOptions(deps) {
203
+ const ocConfig = getAgentConfig(deps.latest(), 'opencode');
204
+ // Build model string with validation: provider/model format
205
+ let model;
206
+ if (ocConfig?.modelID) {
207
+ const provider = ocConfig.providerID ?? 'anthropic';
208
+ // Validate: if modelID contains '/', it's already in provider/model format
209
+ if (ocConfig.modelID.includes('/')) {
210
+ model = ocConfig.modelID;
211
+ }
212
+ else {
213
+ model = `${provider}/${ocConfig.modelID}`;
214
+ }
215
+ }
216
+ return {
217
+ kind: 'opencode',
218
+ model,
219
+ sessionReader: deps.readers.opencode,
220
+ mode: ocConfig?.mode ?? 'build',
221
+ binary: ocConfig?.acp?.binary,
222
+ requestTimeoutMs: ocConfig?.acp?.requestTimeoutMs,
223
+ idleTtlMs: ocConfig?.acp?.idleTtlMs,
224
+ turnIdleTimeoutMs: minutesToMs(ocConfig?.acp?.turnIdleTimeoutMinutes),
225
+ };
226
+ }
227
+ // Register PiRpcRunner.
228
+ // factory 闭包必须读最新 config 而不是启动时的快照,否则 /config 保存后
229
+ // provider 等字段对 pi 不生效:config 引用放在可变容器里(见上文
230
+ // setConfigContainer),factory 每次从容器取最新值。
231
+ // PiSessionReader 的创建与注册见 registerSessionReaders。
232
+ function piRunnerOptions(deps, cwdOpt) {
233
+ const piConf = getAgentConfig(deps.latest(), 'pi');
234
+ return {
235
+ provider: piConf?.provider ?? PI_DEFAULTS.provider,
236
+ model: piConf?.model ?? PI_DEFAULTS.model,
237
+ thinking: piConf?.thinking ?? PI_DEFAULTS.thinking,
238
+ tools: (piConf?.tools ?? PI_DEFAULTS.tools)
239
+ .split(',')
240
+ .map((t) => t.trim())
241
+ .filter(Boolean),
242
+ cwd: cwdOpt.cwd,
243
+ sessionReader: deps.readers.pi,
244
+ };
245
+ }
246
+ // Register KimiAcpRunner (pure ACP mode)
247
+ function kimiRunnerOptions(deps) {
248
+ const kimiConf = getAgentConfig(deps.latest(), 'kimi');
249
+ const acpConf = kimiConf?.acp;
250
+ return {
251
+ kind: 'kimi',
252
+ sessionReader: deps.readers.kimi,
253
+ binary: acpConf?.binary ?? 'kimi',
254
+ requestTimeoutMs: acpConf?.requestTimeoutMs,
255
+ idleTtlMs: acpConf?.idleTtlMs,
256
+ turnIdleTimeoutMs: minutesToMs(acpConf?.turnIdleTimeoutMinutes),
257
+ model: kimiConf?.model ?? KIMI_DEFAULTS.model,
258
+ thinkingEffort: kimiConf?.thinkingEffort,
259
+ permissionMode: kimiConf?.permissionMode ?? 'manual',
260
+ };
261
+ }
262
+ // Warm the shared process in the background so the first message and
263
+ // `/resume` do not pay the full startup delay. Failures surface from
264
+ // DshRunner.run(), which awaits the same resolver.
265
+ function warmDshDaemon(dshDaemon, failureLabel) {
266
+ void dshDaemon.ensure().catch((error) => {
267
+ getLogger().warn(`[dsh] ${failureLabel}: ${error.message}`);
268
+ });
269
+ }
270
+ // Register DshRunner (HTTP-only DSH Web Host agent).
271
+ function dshRunnerOptions(deps) {
272
+ const dshConf = getAgentConfig(deps.latest(), 'dsh');
273
+ return {
274
+ kind: 'dsh',
275
+ sessionReader: deps.readers.dsh,
276
+ daemon: deps.dshDaemon,
277
+ host: dshConf?.host,
278
+ dshHome: dshConf?.dshHome,
279
+ token: dshConf?.token,
280
+ credentialsFile: dshConf?.credentialsFile,
281
+ agentPreset: dshConf?.agentPreset,
282
+ permissionPreset: dshConf?.permissionPreset,
283
+ provider: dshConf?.provider,
284
+ model: dshConf?.model,
285
+ reasoningEffort: dshConf?.reasoningEffort,
286
+ // Durable markers for turns that outlive this process; reconciled on the
287
+ // next startup (see reconcileInflightDshRuns).
288
+ inflightPath: dshInflightPath(deps.configDir),
289
+ };
290
+ }
291
+ function registerRunners(agentRegistry, deps) {
292
+ agentRegistry.register('claude', (ws) => {
293
+ return new ClaudeRunner(claudeRunnerOptions(deps, { cwd: ws }));
294
+ });
295
+ agentRegistry.register('codex', (_ws) => {
296
+ return new CodexAppServerRunner(codexRunnerOptions(deps));
297
+ });
298
+ agentRegistry.register('opencode', (_ws) => {
299
+ return new OpencodeAcpRunner(opencodeRunnerOptions(deps));
300
+ });
301
+ agentRegistry.register('pi', (ws) => {
302
+ return new PiRpcRunner(piRunnerOptions(deps, { cwd: ws }));
303
+ });
304
+ agentRegistry.register('kimi', (_ws) => {
305
+ return new KimiAcpRunner(kimiRunnerOptions(deps));
306
+ });
277
307
  agentRegistry.register('dsh', (_ws) => {
278
- const dshConf = getAgentConfig(latest(), 'dsh');
279
- // Warm the shared process in the background so the first message and
280
- // `/resume` do not pay the full startup delay. Failures surface from
281
- // DshRunner.run(), which awaits the same resolver.
282
- void dshDaemon.ensure().catch((error) => {
283
- getLogger().warn(`[dsh] background start failed: ${error.message}`);
284
- });
285
- return new DshRunner({
286
- kind: 'dsh',
287
- sessionReader: dshSessionReader,
288
- daemon: dshDaemon,
289
- host: dshConf?.host,
290
- dshHome: dshConf?.dshHome,
291
- token: dshConf?.token,
292
- credentialsFile: dshConf?.credentialsFile,
293
- agentPreset: dshConf?.agentPreset,
294
- permissionPreset: dshConf?.permissionPreset,
295
- provider: dshConf?.provider,
296
- model: dshConf?.model,
297
- reasoningEffort: dshConf?.reasoningEffort,
298
- // Durable markers for turns that outlive this process; reconciled on the
299
- // next startup (see reconcileInflightDshRuns).
300
- inflightPath: dshInflightPath(configDir),
301
- });
308
+ warmDshDaemon(deps.dshDaemon, 'background start failed');
309
+ return new DshRunner(dshRunnerOptions(deps));
302
310
  });
303
- sessionReaderRegistry.register('dsh', dshSessionReader);
311
+ }
312
+ /** 按 agent 家族注册 session reader(与 runner factory 共享同一实例)。 */
313
+ function registerSessionReaders(registry, readers) {
314
+ registry.register('claude', readers.claude);
315
+ registry.register('codex', readers.codex);
316
+ registry.register('opencode', readers.opencode);
317
+ registry.register('pi', readers.pi);
318
+ registry.register('kimi', readers.kimi);
319
+ registry.register('dsh', readers.dsh);
320
+ }
321
+ /** 探测 agent CLI 可用性(fire-and-forget,填充 /config 卡片缓存)。 */
322
+ function probeAgentsInBackground() {
323
+ probeAllAgents().then(logAgentProbeResults).catch(logAgentProbeFailure);
324
+ }
325
+ function logAgentProbeResults(availability) {
326
+ const unavailable = [...availability.entries()].filter(([, ok]) => !ok).map(([kind]) => kind);
327
+ if (unavailable.length > 0) {
328
+ getLogger().warn(`[probe] agent CLI not found or not functional: ${unavailable.join(', ')}`);
329
+ }
330
+ else {
331
+ getLogger().info('[probe] all agent CLIs available');
332
+ }
333
+ }
334
+ function logAgentProbeFailure(err) {
335
+ getLogger().warn(`[probe] background probe failed: ${err.message}`);
336
+ }
337
+ /** runner factory 取最新 config 的唯一入口(container 未接线时回退启动快照)。 */
338
+ function createConfigLatest(agentRegistry, config) {
339
+ return () => agentRegistry.getConfigContainer()?.current ?? config;
340
+ }
341
+ /** One global DSH per machine/DSH_HOME:provider 箭头在模块级函数内定义。 */
342
+ function createDshDaemon(latest) {
343
+ return new DshDaemon({
344
+ host: () => getAgentConfig(latest(), 'dsh')?.host,
345
+ dshHome: () => getAgentConfig(latest(), 'dsh')?.dshHome,
346
+ dshCommand: () => getAgentConfig(latest(), 'dsh')?.dshCommand,
347
+ manageProcess: () => getAgentConfig(latest(), 'dsh')?.manageProcess ?? true,
348
+ startTimeoutMs: () => getAgentConfig(latest(), 'dsh')?.startTimeoutMs,
349
+ });
350
+ }
351
+ /** Initialize per-workspace runner factory + agent registry. */
352
+ function initializeRunner(config, configDir, cliArgs) {
353
+ const agentRegistry = new AgentRegistry();
354
+ // 单点化:runner factory 取最新 config 的唯一入口(语义保持,
355
+ // container 未接线时回退启动快照)。
356
+ const latest = createConfigLatest(agentRegistry, config);
357
+ // 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值。
358
+ agentRegistry.setConfigContainer({ current: config });
359
+ // One global DSH per machine/DSH_HOME. This resolver reuses a listening
360
+ // process or starts `dsh web` detached; it never owns the child's lifetime.
361
+ const dshDaemon = createDshDaemon(latest);
362
+ const readers = createSessionReaders(config, latest);
363
+ registerRunners(agentRegistry, { configDir, cliArgs, latest, dshDaemon, readers });
364
+ const sessionReaderRegistry = new SessionReaderRegistry();
365
+ registerSessionReaders(sessionReaderRegistry, readers);
304
366
  if (config.defaultAgent === 'dsh') {
305
- void dshDaemon.ensure().catch((error) => {
306
- getLogger().warn(`[dsh] startup probe failed: ${error.message}`);
307
- });
367
+ warmDshDaemon(dshDaemon, 'startup probe failed');
308
368
  }
309
369
  // Probe agent CLI availability (fire-and-forget, populates cache for /config card).
310
370
  // Don't block startup — log unavailable agents as warnings.
311
- probeAllAgents()
312
- .then((availability) => {
313
- const unavailable = [...availability.entries()].filter(([, ok]) => !ok).map(([kind]) => kind);
314
- if (unavailable.length > 0) {
315
- getLogger().warn(`[probe] agent CLI not found or not functional: ${unavailable.join(', ')}`);
316
- }
317
- else {
318
- getLogger().info('[probe] all agent CLIs available');
319
- }
320
- })
321
- .catch((err) => {
322
- getLogger().warn(`[probe] background probe failed: ${err.message}`);
323
- });
371
+ probeAgentsInBackground();
324
372
  // Warm the codex model catalog in the background so the first Codex /config
325
373
  // card open reads a warm cache instead of blocking the event loop for up to
326
374
  // 8s (execFileSync cold path). Fire-and-forget; failure lands in negative
@@ -328,8 +376,14 @@ function initializeRunner(config, configDir, cliArgs) {
328
376
  warmCodexCatalogCache();
329
377
  return { agentRegistry, sessionReaderRegistry };
330
378
  }
331
- async function main() {
332
- const cliArgs = parseCliArgs();
379
+ /**
380
+ * CLI 早期退出分支:--help / --version / --update。
381
+ *
382
+ * 红线:三者都必须在 config 加载与单例锁之前完成——--help/--version 是纯信息
383
+ * 查询,另一实例在跑、或还没有 config 时也必须能应答;--update 不是守护类命令,
384
+ * 等锁只会误报 "already running"。返回 true 表示进程应停止。
385
+ */
386
+ async function handleCliEarlyExit(cliArgs) {
333
387
  // --help/--version must bypass config loading and singleton lock — it's a pure
334
388
  // informational query that must work even when another instance is running
335
389
  // or no config exists yet.
@@ -367,6 +421,14 @@ async function main() {
367
421
  process.exit(1);
368
422
  }
369
423
  }
424
+ return false;
425
+ }
426
+ /**
427
+ * Bootstrap:configDir → config → logger → 单例锁 → 防休眠 → 启动配置日志。
428
+ * 顺序红线:waitForPreviousInstance 必须先于 instanceLock.acquire()(/restart
429
+ * 替补进程要等旧实例释放锁才能拿到单例锁);PID 存活校验在 acquire 内完成。
430
+ */
431
+ async function bootstrap(cliArgs) {
370
432
  const { config, configDir, logger, instanceLock } = await initializeCliAndConfig();
371
433
  // Restart handoff: if spawned as a /restart replacement, wait for the old
372
434
  // lark-remote to die (and release the instance lock) before acquiring it.
@@ -387,6 +449,11 @@ async function main() {
387
449
  if (cliArgs.settings) {
388
450
  logger.info(`claude.settings = ${cliArgs.settings}`);
389
451
  }
452
+ return { cliArgs, config, configDir, logger };
453
+ }
454
+ /** Wiring:装配 connector / stores / bridge / router / 消息处理器(不连接)。 */
455
+ function wire(boot) {
456
+ const { cliArgs, config, configDir, logger } = boot;
390
457
  const { agentRegistry, sessionReaderRegistry } = initializeRunner(config, configDir, cliArgs);
391
458
  const connector = new FeishuConnector(config);
392
459
  const startupContactStore = new StartupContactStore(path.join(configDir, 'startup-contact.json'));
@@ -438,6 +505,143 @@ async function main() {
438
505
  config,
439
506
  cloneSession,
440
507
  });
508
+ return {
509
+ cliArgs,
510
+ config,
511
+ configDir,
512
+ logger,
513
+ connector,
514
+ startupContactStore,
515
+ binder,
516
+ sessionStore,
517
+ bridge,
518
+ sessionReaderRegistry,
519
+ };
520
+ }
521
+ /**
522
+ * Auto-restore: resume the persisted sessionId if available.
523
+ * 非关键启动优化:发送失败只告警(logger 已初始化),不允许经
524
+ * main().catch → process.exit(1) 拖垮整个桥接。
525
+ */
526
+ async function restoreLastSession(app) {
527
+ const { config, logger, sessionStore, sessionReaderRegistry, bridge, connector, startupContactStore, } = app;
528
+ const restoredContact = startupContactStore.getContact();
529
+ if (!restoredContact?.userId)
530
+ return;
531
+ const restoredCwd = sessionStore.getCwd(restoredContact.userId);
532
+ if (!restoredCwd)
533
+ return;
534
+ const sessionIdToResume = sessionStore.getSessionId(restoredContact.userId, config.defaultAgent);
535
+ if (!sessionIdToResume)
536
+ return;
537
+ logger.info(`[startup] auto-resuming persisted session ${sessionIdToResume} in ${restoredCwd}`);
538
+ const startupReader = sessionReaderRegistry.get(config.defaultAgent);
539
+ sessionStore.setSessionIdAndCwd(restoredContact.userId, config.defaultAgent, sessionIdToResume, restoredCwd);
540
+ const content = startupReader.readSessionContent(sessionIdToResume, restoredCwd);
541
+ // 该会话可能仍有在途 turn(上一个实例退出/崩溃后留在常驻 host 里)。
542
+ // 不取消、不接管,但必须在卡上说明,否则用户会把它当"卡住了"。
543
+ //
544
+ // 只有 DSH reader 的 isSessionActive() 是服务端的 running 事实。其他
545
+ // reader 的同名方法只表示"最近一段时间有写入",拿它当 turn 在跑会在
546
+ // 每次自动恢复时误报。
547
+ const stillRunning = config.defaultAgent === 'dsh'
548
+ ? (() => {
549
+ try {
550
+ return startupReader.isSessionActive(sessionIdToResume, restoredCwd);
551
+ }
552
+ catch (err) {
553
+ getLogger().warn(`[startup] isSessionActive failed: ${err.message}`);
554
+ return false;
555
+ }
556
+ })()
557
+ : false;
558
+ if (stillRunning) {
559
+ getLogger().warn(`[startup] session ${sessionIdToResume} still has a running turn; auto-resume card is a snapshot`);
560
+ }
561
+ // 完整透传 usage(含 inputTokens/outputTokens/totalTokens),
562
+ // 让 formatUsageStats 走真实值路径,与终态卡片//resume 卡片口径一致。
563
+ const usage = content.usage;
564
+ // Build auto-resume card and send to the user. Auto-resume is a
565
+ // non-critical startup nicety: a sendWithRetry failure here (e.g.
566
+ // transient feishu API error) must not crash the bridge via
567
+ // main().catch → process.exit(1). Logger is already initialized.
568
+ try {
569
+ await sendAutoResumeCard(connector, restoredContact, restoredCwd, { sessionId: sessionIdToResume, summary: '' }, {
570
+ events: content.events,
571
+ usage,
572
+ aiTitle: content.aiTitle,
573
+ recap: content.recap,
574
+ displayTitle: content.displayTitle,
575
+ agentKind: config.defaultAgent,
576
+ },
577
+ // Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
578
+ bridge.hasRunCompact(restoredCwd, config.defaultAgent), stillRunning);
579
+ }
580
+ catch (err) {
581
+ getLogger().warn('[startup] sendAutoResumeCard failed:', err);
582
+ }
583
+ }
584
+ /**
585
+ * 启动时静默检查版本(由 checkUpdateOnStartup 控制,默认关闭)。
586
+ * 抽成模块级 async 函数(原为 main 内的 fire-and-forget IIFE);失败只告警,
587
+ * 不允许经 main().catch → process.exit(1) 拖垮整个桥接。
588
+ */
589
+ async function checkUpdateOnStartupNotify(app) {
590
+ try {
591
+ const cachePath = path.join(app.configDir, 'update-cache.json');
592
+ const { current, latest } = await checkLatestVersion({ cachePath });
593
+ const hint = formatUpdateHint(current, latest);
594
+ if (hint) {
595
+ const contact = app.startupContactStore.getContact();
596
+ const recipient = contact?.chatId ?? contact?.userId;
597
+ if (recipient) {
598
+ await app.connector.sendWithRetry(recipient, { text: hint });
599
+ }
600
+ }
601
+ }
602
+ catch (err) {
603
+ // Non-fatal: startup check failure must not crash the bridge
604
+ app.logger.warn(`[startup] update check failed: ${err.message}`);
605
+ }
606
+ }
607
+ /**
608
+ * 上次进程可能死在 DSH turn 中途:清掉残留的待执行 prompt,并对仍在跑的
609
+ * turn 提示 owner(不自动 cancel——它可能是 Web UI 的 turn)。清理不依赖是否
610
+ * 已绑定;没有收件人时只是不发提示。
611
+ */
612
+ function reconcileDshInflightOnStartup(app) {
613
+ const dshConf = getAgentConfig(app.config, 'dsh');
614
+ if (!dshConf)
615
+ return;
616
+ const contact = app.startupContactStore.getContact();
617
+ const recipient = contact?.chatId ?? contact?.userId;
618
+ void reconcileInflightDshRuns({
619
+ store: new DshInflightStore(dshInflightPath(app.configDir)),
620
+ client: new DshClient(dshConf.host ?? 'http://127.0.0.1:3080', {
621
+ dshHome: dshConf.dshHome,
622
+ token: dshConf.token,
623
+ credentialsFile: dshConf.credentialsFile,
624
+ // Host may be down: bound the probe instead of waiting the default 60s.
625
+ requestTimeoutMs: 3_000,
626
+ }),
627
+ ...(recipient
628
+ ? {
629
+ notify: async (text) => {
630
+ await app.connector.sendWithRetry(recipient, { text });
631
+ },
632
+ }
633
+ : {}),
634
+ }).catch((err) => {
635
+ app.logger.warn(`[startup] dsh inflight reconcile failed: ${err.message}`);
636
+ });
637
+ }
638
+ async function main() {
639
+ const cliArgs = parseCliArgs();
640
+ if (await handleCliEarlyExit(cliArgs))
641
+ return;
642
+ const boot = await bootstrap(cliArgs);
643
+ const app = wire(boot);
644
+ const { config, logger, connector, startupContactStore, binder } = app;
441
645
  try {
442
646
  await connector.connect();
443
647
  }
@@ -447,112 +651,14 @@ async function main() {
447
651
  }
448
652
  if (binder.isBound()) {
449
653
  // 仅已绑定时发送启动通知;未绑定时不打扰(PIN 引导已在控制台输出)
450
- await sendStartupHello(connector, startupContactStore, { dev: cliArgs.dev });
654
+ await sendStartupHello(connector, startupContactStore, { dev: app.cliArgs.dev });
451
655
  // 启动时静默检查版本(由 checkUpdateOnStartup 控制,默认关闭)
452
656
  if (config.checkUpdateOnStartup) {
453
- void (async () => {
454
- try {
455
- const cachePath = path.join(configDir, 'update-cache.json');
456
- const { current, latest } = await checkLatestVersion({ cachePath });
457
- const hint = formatUpdateHint(current, latest);
458
- if (hint) {
459
- const contact = startupContactStore.getContact();
460
- const recipient = contact?.chatId ?? contact?.userId;
461
- if (recipient) {
462
- await connector.sendWithRetry(recipient, { text: hint });
463
- }
464
- }
465
- }
466
- catch (err) {
467
- // Non-fatal: startup check failure must not crash the bridge
468
- logger.warn(`[startup] update check failed: ${err.message}`);
469
- }
470
- })();
471
- }
472
- }
473
- // 上次进程可能死在 DSH turn 中途:清掉残留的待执行 prompt,并对仍在跑的
474
- // turn 提示 owner(不自动 cancel——它可能是 Web UI 的 turn)。清理不依赖是否
475
- // 已绑定;没有收件人时只是不发提示。
476
- const dshConf = getAgentConfig(config, 'dsh');
477
- if (dshConf) {
478
- const contact = startupContactStore.getContact();
479
- const recipient = contact?.chatId ?? contact?.userId;
480
- void reconcileInflightDshRuns({
481
- store: new DshInflightStore(dshInflightPath(configDir)),
482
- client: new DshClient(dshConf.host ?? 'http://127.0.0.1:3080', {
483
- dshHome: dshConf.dshHome,
484
- token: dshConf.token,
485
- credentialsFile: dshConf.credentialsFile,
486
- // Host may be down: bound the probe instead of waiting the default 60s.
487
- requestTimeoutMs: 3_000,
488
- }),
489
- ...(recipient
490
- ? {
491
- notify: async (text) => {
492
- await connector.sendWithRetry(recipient, { text });
493
- },
494
- }
495
- : {}),
496
- }).catch((err) => {
497
- logger.warn(`[startup] dsh inflight reconcile failed: ${err.message}`);
498
- });
499
- }
500
- // Auto-restore: resume the persisted sessionId if available
501
- const restoredContact = startupContactStore.getContact();
502
- if (restoredContact?.userId) {
503
- const restoredCwd = sessionStore.getCwd(restoredContact.userId);
504
- if (restoredCwd) {
505
- const sessionIdToResume = sessionStore.getSessionId(restoredContact.userId, config.defaultAgent);
506
- if (sessionIdToResume) {
507
- logger.info(`[startup] auto-resuming persisted session ${sessionIdToResume} in ${restoredCwd}`);
508
- const startupReader = sessionReaderRegistry.get(config.defaultAgent);
509
- sessionStore.setSessionIdAndCwd(restoredContact.userId, config.defaultAgent, sessionIdToResume, restoredCwd);
510
- const content = startupReader.readSessionContent(sessionIdToResume, restoredCwd);
511
- // 该会话可能仍有在途 turn(上一个实例退出/崩溃后留在常驻 host 里)。
512
- // 不取消、不接管,但必须在卡上说明,否则用户会把它当"卡住了"。
513
- //
514
- // 只有 DSH reader 的 isSessionActive() 是服务端的 running 事实。其他
515
- // reader 的同名方法只表示"最近一段时间有写入",拿它当 turn 在跑会在
516
- // 每次自动恢复时误报。
517
- const stillRunning = config.defaultAgent === 'dsh'
518
- ? (() => {
519
- try {
520
- return startupReader.isSessionActive(sessionIdToResume, restoredCwd);
521
- }
522
- catch (err) {
523
- getLogger().warn(`[startup] isSessionActive failed: ${err.message}`);
524
- return false;
525
- }
526
- })()
527
- : false;
528
- if (stillRunning) {
529
- getLogger().warn(`[startup] session ${sessionIdToResume} still has a running turn; auto-resume card is a snapshot`);
530
- }
531
- // 完整透传 usage(含 inputTokens/outputTokens/totalTokens),
532
- // 让 formatUsageStats 走真实值路径,与终态卡片//resume 卡片口径一致。
533
- const usage = content.usage;
534
- // Build auto-resume card and send to the user. Auto-resume is a
535
- // non-critical startup nicety: a sendWithRetry failure here (e.g.
536
- // transient feishu API error) must not crash the bridge via
537
- // main().catch → process.exit(1). Logger is already initialized.
538
- try {
539
- await sendAutoResumeCard(connector, restoredContact, restoredCwd, { sessionId: sessionIdToResume, summary: '' }, {
540
- events: content.events,
541
- usage,
542
- aiTitle: content.aiTitle,
543
- recap: content.recap,
544
- displayTitle: content.displayTitle,
545
- agentKind: config.defaultAgent,
546
- },
547
- // Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
548
- bridge.hasRunCompact(restoredCwd, config.defaultAgent), stillRunning);
549
- }
550
- catch (err) {
551
- getLogger().warn('[startup] sendAutoResumeCard failed:', err);
552
- }
553
- }
657
+ void checkUpdateOnStartupNotify(app);
554
658
  }
555
659
  }
660
+ reconcileDshInflightOnStartup(app);
661
+ await restoreLastSession(app);
556
662
  logger.info('lark-remote is running, press Ctrl+C to exit');
557
663
  }
558
664
  main().catch((err) => {