lark-remote 0.5.19 → 0.5.20

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 (119) hide show
  1. package/CHANGELOG.md +19 -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 +1 -1
  10. package/dist/bridge/index.d.ts +97 -153
  11. package/dist/bridge/index.js +182 -809
  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 +109 -97
  18. package/dist/card/card-budget.js +1 -1
  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 +34 -93
  24. package/dist/card/run-state.d.ts +1 -1
  25. package/dist/card/run-state.js +28 -90
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +1 -1
  28. package/dist/card/usage-format.d.ts +12 -0
  29. package/dist/card/usage-format.js +8 -3
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/config/codex-config.js +130 -106
  33. package/dist/config/index.d.ts +16 -2
  34. package/dist/config/index.js +23 -8
  35. package/dist/connector/index.js +6 -6
  36. package/dist/error-classification.js +3 -2
  37. package/dist/inbound/placeholder.d.ts +1 -1
  38. package/dist/inbound/placeholder.js +1 -1
  39. package/dist/index.js +203 -133
  40. package/dist/logger/index.js +6 -6
  41. package/dist/message-handlers.d.ts +0 -5
  42. package/dist/message-handlers.js +1 -1
  43. package/dist/model-catalog.d.ts +2 -2
  44. package/dist/model-catalog.js +3 -8
  45. package/dist/platform/identity.js +1 -1
  46. package/dist/platform/path.d.ts +1 -1
  47. package/dist/platform/path.js +1 -1
  48. package/dist/platform/sleep-blocker.js +1 -1
  49. package/dist/platform/terminator-win32.js +2 -1
  50. package/dist/router/commands/resume.js +2 -20
  51. package/dist/router/config/dsh.d.ts +4 -8
  52. package/dist/router/config/dsh.js +3 -10
  53. package/dist/router/config/kimi.js +1 -1
  54. package/dist/router/config/types.d.ts +1 -1
  55. package/dist/router/config-diff.js +1 -1
  56. package/dist/router/index.d.ts +56 -11
  57. package/dist/router/index.js +219 -200
  58. package/dist/runner/claude/session.d.ts +1 -1
  59. package/dist/runner/claude/session.js +6 -4
  60. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  61. package/dist/runner/codex/app-server/runner.js +30 -25
  62. package/dist/runner/codex/app-server/translator.d.ts +0 -5
  63. package/dist/runner/codex/app-server/translator.js +4 -17
  64. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  65. package/dist/runner/common/acp/acp-approval.js +1 -1
  66. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  67. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  68. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  69. package/dist/runner/common/acp/base-translator.js +31 -9
  70. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  71. package/dist/runner/common/acp/protocol-types.js +2 -2
  72. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  73. package/dist/runner/common/connection-based-runner.js +32 -2
  74. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  75. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  76. package/dist/runner/common/spawning-runner.d.ts +1 -1
  77. package/dist/runner/common/spawning-runner.js +1 -1
  78. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  79. package/dist/runner/dsh/catalog-probe.js +10 -0
  80. package/dist/runner/dsh/client.d.ts +1 -3
  81. package/dist/runner/dsh/client.js +2 -4
  82. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  83. package/dist/runner/dsh/exit-cancel.js +2 -2
  84. package/dist/runner/dsh/inflight.d.ts +1 -1
  85. package/dist/runner/dsh/inflight.js +1 -1
  86. package/dist/runner/dsh/runner.d.ts +40 -1
  87. package/dist/runner/dsh/runner.js +272 -199
  88. package/dist/runner/dsh/translator.d.ts +1 -1
  89. package/dist/runner/dsh/translator.js +1 -1
  90. package/dist/runner/index.d.ts +1 -1
  91. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  92. package/dist/runner/kimi/acp/runner.js +1 -2
  93. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  94. package/dist/runner/kimi/acp/translator.js +3 -4
  95. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  96. package/dist/runner/opencode/acp/runner.js +4 -4
  97. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  98. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  99. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  100. package/dist/runner/pi/rpc/runner.js +27 -42
  101. package/dist/runner/types.d.ts +2 -2
  102. package/dist/runner/types.js +1 -1
  103. package/dist/session/claude/sessions.d.ts +45 -0
  104. package/dist/session/claude/sessions.js +97 -71
  105. package/dist/session/codex/rollout-reader.js +4 -4
  106. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  107. package/dist/session/common/throw-session-list-error.js +16 -0
  108. package/dist/session/dsh/sessions.js +11 -5
  109. package/dist/session/kimi/sessions.d.ts +40 -1
  110. package/dist/session/kimi/sessions.js +244 -192
  111. package/dist/session/opencode/sessions.d.ts +153 -1
  112. package/dist/session/opencode/sessions.js +354 -321
  113. package/dist/session/pi/sessions.js +4 -5
  114. package/dist/session/session-store.d.ts +1 -1
  115. package/dist/session/session-store.js +1 -1
  116. package/dist/update/version-check.js +2 -1
  117. package/dist/workspace/index.d.ts +1 -1
  118. package/dist/workspace/index.js +1 -1
  119. 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,25 +129,45 @@ 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();
140
+ const claude = new ClaudeSessionReader();
141
+ const codex = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
142
+ // Register OpencodeSessionReader (CLI version)
143
+ // Using `opencode session list` and `session export` instead of HTTP API.
144
+ // Share the configured binary with the ACP runner so `opencode2` (v2) can
145
+ // be used without the reader silently falling back to a v1 `opencode`.
146
+ // The reader is registered once; changing acp.binary at runtime requires a
147
+ // lark-remote restart.
148
+ const opencode = new OpencodeSessionReader({
149
+ binary: getAgentConfig(config, 'opencode')?.acp?.binary,
150
+ });
151
+ const pi = new PiSessionReader();
152
+ const kimi = new KimiSessionReader();
153
+ const dsh = new DshSessionReader({
154
+ // hostProvider 每次调用读最新 config:/config 修改 host 后 runner 重建为
155
+ // 新 host,reader(/resume、用量、完成卡)也跟随新 host。
156
+ hostProvider: () => getAgentConfig(latest(), 'dsh')?.host,
157
+ dshHomeProvider: () => getAgentConfig(latest(), 'dsh')?.dshHome,
158
+ tokenProvider: () => getAgentConfig(latest(), 'dsh')?.token,
159
+ credentialsFileProvider: () => getAgentConfig(latest(), 'dsh')?.credentialsFile,
160
+ });
161
+ return { claude, codex, opencode, pi, kimi, dsh };
162
+ }
163
+ /**
164
+ * 按 agent 家族注册 runner factory。
165
+ *
166
+ * factory 一律从 configContainer 读最新 config(bridge.setConfig() +
167
+ * clearRunners() 后运行时改动生效);`latest()` 未接线时回退启动快照。
168
+ */
169
+ function registerRunners(agentRegistry, deps) {
170
+ const { configDir, cliArgs, latest, dshDaemon, readers } = deps;
144
171
  // Claude factory reads from configContainer (not closure) so runtime
145
172
  // config changes (model, effort, stopGraceMs) take effect after
146
173
  // bridge.setConfig() + clearRunners(). Same pattern as codex/pi/kimi.
@@ -153,7 +180,7 @@ function initializeRunner(config, configDir, cliArgs) {
153
180
  settings: cliArgs.settings,
154
181
  pidDir: configDir,
155
182
  cwd: ws,
156
- sessionReader: claudeSessionReader,
183
+ sessionReader: readers.claude,
157
184
  permissionMode: claudeConfig.permissionMode,
158
185
  idleTtlMs: claudeConfig.idleTtlMinutes != null ? claudeConfig.idleTtlMinutes * 60_000 : undefined,
159
186
  });
@@ -162,7 +189,6 @@ function initializeRunner(config, configDir, cliArgs) {
162
189
  // config from container.current so runtime config changes (model, sandbox,
163
190
  // approvalPolicy, reasoningEffort, etc.) take effect after bridge.setConfig()
164
191
  // + clearRunners().
165
- const codexSessionReader = new CodexSessionReader({ codexHome: process.env.CODEX_HOME });
166
192
  agentRegistry.register('codex', (_ws) => {
167
193
  const codexConfig = getAgentConfig(latest(), 'codex');
168
194
  return new CodexAppServerRunner({
@@ -170,31 +196,16 @@ function initializeRunner(config, configDir, cliArgs) {
170
196
  model: codexConfig?.model,
171
197
  modelProvider: codexConfig?.modelProvider,
172
198
  reasoningEffort: codexConfig?.reasoningEffort,
173
- sessionReader: codexSessionReader,
199
+ sessionReader: readers.codex,
174
200
  sandbox: codexConfig?.sandbox,
175
201
  approvalPolicy: codexConfig?.approvalPolicy,
176
202
  binary: codexConfig?.appServer?.binary,
177
203
  requestTimeoutMs: codexConfig?.appServer?.requestTimeoutMs,
178
204
  idleTtlMs: codexConfig?.appServer?.idleTtlMs,
179
- turnTimeoutMs: codexConfig?.appServer?.turnIdleTimeoutMinutes != null
180
- ? codexConfig.appServer.turnIdleTimeoutMinutes * 60_000
181
- : undefined,
205
+ turnTimeoutMs: minutesToMs(codexConfig?.appServer?.turnIdleTimeoutMinutes),
182
206
  });
183
207
  });
184
208
  // 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);
189
- // Register OpencodeSessionReader (CLI version)
190
- // Using `opencode session list` and `session export` instead of HTTP API.
191
- // Share the configured binary with the ACP runner so `opencode2` (v2) can
192
- // be used without the reader silently falling back to a v1 `opencode`.
193
- // The reader is registered once; changing acp.binary at runtime requires a
194
- // lark-remote restart.
195
- const opencodeSessionReader = new OpencodeSessionReader({
196
- binary: getAgentConfig(config, 'opencode')?.acp?.binary,
197
- });
198
209
  agentRegistry.register('opencode', (_ws) => {
199
210
  const ocConfig = getAgentConfig(latest(), 'opencode');
200
211
  // Build model string with validation: provider/model format
@@ -212,68 +223,50 @@ function initializeRunner(config, configDir, cliArgs) {
212
223
  return new OpencodeAcpRunner({
213
224
  kind: 'opencode',
214
225
  model,
215
- sessionReader: opencodeSessionReader,
226
+ sessionReader: readers.opencode,
216
227
  mode: ocConfig?.mode ?? 'build',
217
228
  binary: ocConfig?.acp?.binary,
218
229
  requestTimeoutMs: ocConfig?.acp?.requestTimeoutMs,
219
230
  idleTtlMs: ocConfig?.acp?.idleTtlMs,
220
- turnIdleTimeoutMs: ocConfig?.acp?.turnIdleTimeoutMinutes != null
221
- ? ocConfig.acp.turnIdleTimeoutMinutes * 60_000
222
- : undefined,
231
+ turnIdleTimeoutMs: minutesToMs(ocConfig?.acp?.turnIdleTimeoutMinutes),
223
232
  });
224
233
  });
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();
234
+ // Register PiRpcRunner.
235
+ // factory 闭包必须读最新 config 而不是启动时的快照,否则 /config 保存后
236
+ // provider 等字段对 pi 不生效:config 引用放在可变容器里(见上文
237
+ // setConfigContainer),factory 每次从容器取最新值。
238
+ // PiSessionReader 的创建与注册见 registerSessionReaders。
233
239
  agentRegistry.register('pi', (ws) => {
234
240
  const piConf = getAgentConfig(latest(), 'pi');
235
241
  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')
242
+ provider: piConf?.provider ?? PI_DEFAULTS.provider,
243
+ model: piConf?.model ?? PI_DEFAULTS.model,
244
+ thinking: piConf?.thinking ?? PI_DEFAULTS.thinking,
245
+ tools: (piConf?.tools ?? PI_DEFAULTS.tools)
240
246
  .split(',')
241
247
  .map((t) => t.trim())
242
248
  .filter(Boolean),
243
249
  cwd: ws,
244
- sessionReader: piSessionReader,
250
+ sessionReader: readers.pi,
245
251
  });
246
252
  });
247
- sessionReaderRegistry.register('pi', piSessionReader);
248
- // Register KimiAcpRunner (pure ACP mode) and KimiSessionReader
249
- const kimiSessionReader = new KimiSessionReader();
253
+ // Register KimiAcpRunner (pure ACP mode)
250
254
  agentRegistry.register('kimi', (_ws) => {
251
255
  const kimiConf = getAgentConfig(latest(), 'kimi');
252
256
  const acpConf = kimiConf?.acp;
253
257
  return new KimiAcpRunner({
254
258
  kind: 'kimi',
255
- sessionReader: kimiSessionReader,
259
+ sessionReader: readers.kimi,
256
260
  binary: acpConf?.binary ?? 'kimi',
257
261
  requestTimeoutMs: acpConf?.requestTimeoutMs,
258
262
  idleTtlMs: acpConf?.idleTtlMs,
259
- turnIdleTimeoutMs: acpConf?.turnIdleTimeoutMinutes != null
260
- ? acpConf.turnIdleTimeoutMinutes * 60_000
261
- : undefined,
262
- model: kimiConf?.model ?? 'kimi-code/k3',
263
+ turnIdleTimeoutMs: minutesToMs(acpConf?.turnIdleTimeoutMinutes),
264
+ model: kimiConf?.model ?? KIMI_DEFAULTS.model,
263
265
  thinkingEffort: kimiConf?.thinkingEffort,
264
266
  permissionMode: kimiConf?.permissionMode ?? 'manual',
265
267
  });
266
268
  });
267
- sessionReaderRegistry.register('kimi', kimiSessionReader);
268
- // Register DshRunner (HTTP-only DSH Web Host agent) and DshSessionReader.
269
- const dshSessionReader = new DshSessionReader({
270
- // hostProvider 每次调用读最新 config:/config 修改 host 后 runner 重建为
271
- // 新 host,reader(/resume、用量、完成卡)也跟随新 host。
272
- hostProvider: () => getAgentConfig(latest(), 'dsh')?.host,
273
- dshHomeProvider: () => getAgentConfig(latest(), 'dsh')?.dshHome,
274
- tokenProvider: () => getAgentConfig(latest(), 'dsh')?.token,
275
- credentialsFileProvider: () => getAgentConfig(latest(), 'dsh')?.credentialsFile,
276
- });
269
+ // Register DshRunner (HTTP-only DSH Web Host agent).
277
270
  agentRegistry.register('dsh', (_ws) => {
278
271
  const dshConf = getAgentConfig(latest(), 'dsh');
279
272
  // Warm the shared process in the background so the first message and
@@ -284,7 +277,7 @@ function initializeRunner(config, configDir, cliArgs) {
284
277
  });
285
278
  return new DshRunner({
286
279
  kind: 'dsh',
287
- sessionReader: dshSessionReader,
280
+ sessionReader: readers.dsh,
288
281
  daemon: dshDaemon,
289
282
  host: dshConf?.host,
290
283
  dshHome: dshConf?.dshHome,
@@ -300,7 +293,37 @@ function initializeRunner(config, configDir, cliArgs) {
300
293
  inflightPath: dshInflightPath(configDir),
301
294
  });
302
295
  });
303
- sessionReaderRegistry.register('dsh', dshSessionReader);
296
+ }
297
+ /** 按 agent 家族注册 session reader(与 runner factory 共享同一实例)。 */
298
+ function registerSessionReaders(registry, readers) {
299
+ registry.register('claude', readers.claude);
300
+ registry.register('codex', readers.codex);
301
+ registry.register('opencode', readers.opencode);
302
+ registry.register('pi', readers.pi);
303
+ registry.register('kimi', readers.kimi);
304
+ registry.register('dsh', readers.dsh);
305
+ }
306
+ /** Initialize per-workspace runner factory + agent registry. */
307
+ function initializeRunner(config, configDir, cliArgs) {
308
+ const agentRegistry = new AgentRegistry();
309
+ // 单点化:runner factory 取最新 config 的唯一入口(语义保持,
310
+ // container 未接线时回退启动快照)。
311
+ const latest = () => agentRegistry.getConfigContainer()?.current ?? config;
312
+ // 用可变容器存储 config 引用,registry 持有容器引用,factory 从容器读取最新值。
313
+ agentRegistry.setConfigContainer({ current: config });
314
+ // One global DSH per machine/DSH_HOME. This resolver reuses a listening
315
+ // process or starts `dsh web` detached; it never owns the child's lifetime.
316
+ const dshDaemon = new DshDaemon({
317
+ host: () => getAgentConfig(latest(), 'dsh')?.host,
318
+ dshHome: () => getAgentConfig(latest(), 'dsh')?.dshHome,
319
+ dshCommand: () => getAgentConfig(latest(), 'dsh')?.dshCommand,
320
+ manageProcess: () => getAgentConfig(latest(), 'dsh')?.manageProcess ?? true,
321
+ startTimeoutMs: () => getAgentConfig(latest(), 'dsh')?.startTimeoutMs,
322
+ });
323
+ const readers = createSessionReaders(config, latest);
324
+ registerRunners(agentRegistry, { configDir, cliArgs, latest, dshDaemon, readers });
325
+ const sessionReaderRegistry = new SessionReaderRegistry();
326
+ registerSessionReaders(sessionReaderRegistry, readers);
304
327
  if (config.defaultAgent === 'dsh') {
305
328
  void dshDaemon.ensure().catch((error) => {
306
329
  getLogger().warn(`[dsh] startup probe failed: ${error.message}`);
@@ -328,8 +351,14 @@ function initializeRunner(config, configDir, cliArgs) {
328
351
  warmCodexCatalogCache();
329
352
  return { agentRegistry, sessionReaderRegistry };
330
353
  }
331
- async function main() {
332
- const cliArgs = parseCliArgs();
354
+ /**
355
+ * CLI 早期退出分支:--help / --version / --update。
356
+ *
357
+ * 红线:三者都必须在 config 加载与单例锁之前完成——--help/--version 是纯信息
358
+ * 查询,另一实例在跑、或还没有 config 时也必须能应答;--update 不是守护类命令,
359
+ * 等锁只会误报 "already running"。返回 true 表示进程应停止。
360
+ */
361
+ async function handleCliEarlyExit(cliArgs) {
333
362
  // --help/--version must bypass config loading and singleton lock — it's a pure
334
363
  // informational query that must work even when another instance is running
335
364
  // or no config exists yet.
@@ -367,6 +396,14 @@ async function main() {
367
396
  process.exit(1);
368
397
  }
369
398
  }
399
+ return false;
400
+ }
401
+ /**
402
+ * Bootstrap:configDir → config → logger → 单例锁 → 防休眠 → 启动配置日志。
403
+ * 顺序红线:waitForPreviousInstance 必须先于 instanceLock.acquire()(/restart
404
+ * 替补进程要等旧实例释放锁才能拿到单例锁);PID 存活校验在 acquire 内完成。
405
+ */
406
+ async function bootstrap(cliArgs) {
370
407
  const { config, configDir, logger, instanceLock } = await initializeCliAndConfig();
371
408
  // Restart handoff: if spawned as a /restart replacement, wait for the old
372
409
  // lark-remote to die (and release the instance lock) before acquiring it.
@@ -387,6 +424,11 @@ async function main() {
387
424
  if (cliArgs.settings) {
388
425
  logger.info(`claude.settings = ${cliArgs.settings}`);
389
426
  }
427
+ return { cliArgs, config, configDir, logger };
428
+ }
429
+ /** Wiring:装配 connector / stores / bridge / router / 消息处理器(不连接)。 */
430
+ function wire(boot) {
431
+ const { cliArgs, config, configDir, logger } = boot;
390
432
  const { agentRegistry, sessionReaderRegistry } = initializeRunner(config, configDir, cliArgs);
391
433
  const connector = new FeishuConnector(config);
392
434
  const startupContactStore = new StartupContactStore(path.join(configDir, 'startup-contact.json'));
@@ -438,6 +480,89 @@ async function main() {
438
480
  config,
439
481
  cloneSession,
440
482
  });
483
+ return {
484
+ cliArgs,
485
+ config,
486
+ configDir,
487
+ logger,
488
+ connector,
489
+ startupContactStore,
490
+ binder,
491
+ sessionStore,
492
+ bridge,
493
+ sessionReaderRegistry,
494
+ };
495
+ }
496
+ /**
497
+ * Auto-restore: resume the persisted sessionId if available.
498
+ * 非关键启动优化:发送失败只告警(logger 已初始化),不允许经
499
+ * main().catch → process.exit(1) 拖垮整个桥接。
500
+ */
501
+ async function restoreLastSession(app) {
502
+ const { config, logger, sessionStore, sessionReaderRegistry, bridge, connector, startupContactStore, } = app;
503
+ const restoredContact = startupContactStore.getContact();
504
+ if (!restoredContact?.userId)
505
+ return;
506
+ const restoredCwd = sessionStore.getCwd(restoredContact.userId);
507
+ if (!restoredCwd)
508
+ return;
509
+ const sessionIdToResume = sessionStore.getSessionId(restoredContact.userId, config.defaultAgent);
510
+ if (!sessionIdToResume)
511
+ return;
512
+ logger.info(`[startup] auto-resuming persisted session ${sessionIdToResume} in ${restoredCwd}`);
513
+ const startupReader = sessionReaderRegistry.get(config.defaultAgent);
514
+ sessionStore.setSessionIdAndCwd(restoredContact.userId, config.defaultAgent, sessionIdToResume, restoredCwd);
515
+ const content = startupReader.readSessionContent(sessionIdToResume, restoredCwd);
516
+ // 该会话可能仍有在途 turn(上一个实例退出/崩溃后留在常驻 host 里)。
517
+ // 不取消、不接管,但必须在卡上说明,否则用户会把它当"卡住了"。
518
+ //
519
+ // 只有 DSH reader 的 isSessionActive() 是服务端的 running 事实。其他
520
+ // reader 的同名方法只表示"最近一段时间有写入",拿它当 turn 在跑会在
521
+ // 每次自动恢复时误报。
522
+ const stillRunning = config.defaultAgent === 'dsh'
523
+ ? (() => {
524
+ try {
525
+ return startupReader.isSessionActive(sessionIdToResume, restoredCwd);
526
+ }
527
+ catch (err) {
528
+ getLogger().warn(`[startup] isSessionActive failed: ${err.message}`);
529
+ return false;
530
+ }
531
+ })()
532
+ : false;
533
+ if (stillRunning) {
534
+ getLogger().warn(`[startup] session ${sessionIdToResume} still has a running turn; auto-resume card is a snapshot`);
535
+ }
536
+ // 完整透传 usage(含 inputTokens/outputTokens/totalTokens),
537
+ // 让 formatUsageStats 走真实值路径,与终态卡片//resume 卡片口径一致。
538
+ const usage = content.usage;
539
+ // Build auto-resume card and send to the user. Auto-resume is a
540
+ // non-critical startup nicety: a sendWithRetry failure here (e.g.
541
+ // transient feishu API error) must not crash the bridge via
542
+ // main().catch → process.exit(1). Logger is already initialized.
543
+ try {
544
+ await sendAutoResumeCard(connector, restoredContact, restoredCwd, { sessionId: sessionIdToResume, summary: '' }, {
545
+ events: content.events,
546
+ usage,
547
+ aiTitle: content.aiTitle,
548
+ recap: content.recap,
549
+ displayTitle: content.displayTitle,
550
+ agentKind: config.defaultAgent,
551
+ },
552
+ // Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
553
+ bridge.hasRunCompact(restoredCwd, config.defaultAgent), stillRunning);
554
+ }
555
+ catch (err) {
556
+ getLogger().warn('[startup] sendAutoResumeCard failed:', err);
557
+ }
558
+ }
559
+ async function main() {
560
+ const cliArgs = parseCliArgs();
561
+ if (await handleCliEarlyExit(cliArgs))
562
+ return;
563
+ const boot = await bootstrap(cliArgs);
564
+ const app = wire(boot);
565
+ const { config, configDir, logger, connector, startupContactStore, binder } = app;
441
566
  try {
442
567
  await connector.connect();
443
568
  }
@@ -447,7 +572,7 @@ async function main() {
447
572
  }
448
573
  if (binder.isBound()) {
449
574
  // 仅已绑定时发送启动通知;未绑定时不打扰(PIN 引导已在控制台输出)
450
- await sendStartupHello(connector, startupContactStore, { dev: cliArgs.dev });
575
+ await sendStartupHello(connector, startupContactStore, { dev: app.cliArgs.dev });
451
576
  // 启动时静默检查版本(由 checkUpdateOnStartup 控制,默认关闭)
452
577
  if (config.checkUpdateOnStartup) {
453
578
  void (async () => {
@@ -497,62 +622,7 @@ async function main() {
497
622
  logger.warn(`[startup] dsh inflight reconcile failed: ${err.message}`);
498
623
  });
499
624
  }
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
- }
554
- }
555
- }
625
+ await restoreLastSession(app);
556
626
  logger.info('lark-remote is running, press Ctrl+C to exit');
557
627
  }
558
628
  main().catch((err) => {
@@ -21,12 +21,12 @@ function todayStr(d) {
21
21
  return `${y}-${m}-${day}`;
22
22
  }
23
23
  function timestampStr(d) {
24
- // local ISO 8601 with milliseconds and explicit offset, e.g.
25
- // 2026-08-03T01:30:00.000+08:00. Previously this used toISOString() (UTC)
26
- // while the daily directory boundary (todayStr) used local time — after
27
- // local midnight in a +08:00 zone the new day's directory held lines
28
- // timestamped with the previous UTC day. Local offset keeps the timestamp's
29
- // calendar date consistent with the directory date across all timezones.
24
+ // Local ISO 8601 with milliseconds and explicit offset, e.g.
25
+ // 2026-08-03T01:30:00.000+08:00. Must match the daily directory boundary
26
+ // (todayStr), which is local time: a UTC timestamp would put lines written
27
+ // after local midnight into the previous UTC day's file. Local offset keeps
28
+ // the timestamp's calendar date consistent with the directory date across
29
+ // all timezones.
30
30
  const pad = (n) => String(n).padStart(2, '0');
31
31
  const y = d.getFullYear();
32
32
  const mo = pad(d.getMonth() + 1);
@@ -7,11 +7,6 @@ import type { SessionStore } from './session/index.js';
7
7
  import type { WorkspaceStore } from './workspace/index.js';
8
8
  import type { OwnerBinder } from './binder.js';
9
9
  import type { CloneSession } from './clone.js';
10
- /**
11
- * /stop 命令别名单源:普通消息的停止分支与 clone 活跃期拦截的排除条件
12
- * 共用同一判定,防止将来加别名时两处漂移。
13
- */
14
- export declare function isStopCommand(text: string): boolean;
15
10
  export interface MessageHandlerDeps {
16
11
  connector: FeishuConnector;
17
12
  router: CommandRouter;
@@ -14,7 +14,7 @@ const SESSION_MUTATING_FLUSH_BUDGET_MS = 2_000;
14
14
  * /stop 命令别名单源:普通消息的停止分支与 clone 活跃期拦截的排除条件
15
15
  * 共用同一判定,防止将来加别名时两处漂移。
16
16
  */
17
- export function isStopCommand(text) {
17
+ function isStopCommand(text) {
18
18
  const t = text.trim().toLowerCase();
19
19
  return t === '/stop' || t === '/t';
20
20
  }
@@ -6,9 +6,9 @@
6
6
  * 「地基层被 runner 反向 import」;挪到这里后三方共同引用同一份。
7
7
  *
8
8
  * 表内容来自 Claude CLI 的模型别名(alias 是 CLI 侧的稳定名字,model ID 会随
9
- * 版本变),新增一行只改这里。
9
+ * 版本变),新增一行只改 MODEL_ID_TO_ALIAS,反向表自动派生。
10
10
  */
11
11
  /** 模型 ID → alias 映射。 */
12
12
  export declare const MODEL_ID_TO_ALIAS: Record<string, string>;
13
- /** alias → model ID 反向映射。 */
13
+ /** alias → model ID 反向映射(从正向表派生,新增别名只改上面一张表)。 */
14
14
  export declare const MODEL_ALIAS_TO_ID: Record<string, string>;
@@ -6,7 +6,7 @@
6
6
  * 「地基层被 runner 反向 import」;挪到这里后三方共同引用同一份。
7
7
  *
8
8
  * 表内容来自 Claude CLI 的模型别名(alias 是 CLI 侧的稳定名字,model ID 会随
9
- * 版本变),新增一行只改这里。
9
+ * 版本变),新增一行只改 MODEL_ID_TO_ALIAS,反向表自动派生。
10
10
  */
11
11
  /** 模型 ID → alias 映射。 */
12
12
  export const MODEL_ID_TO_ALIAS = {
@@ -15,11 +15,6 @@ export const MODEL_ID_TO_ALIAS = {
15
15
  'claude-haiku-4-5-20250501': 'haiku',
16
16
  'claude-fable-5': 'fable',
17
17
  };
18
- /** alias → model ID 反向映射。 */
19
- export const MODEL_ALIAS_TO_ID = {
20
- opus: 'claude-opus-4-8',
21
- sonnet: 'claude-sonnet-4-6',
22
- haiku: 'claude-haiku-4-5-20250501',
23
- fable: 'claude-fable-5',
24
- };
18
+ /** alias → model ID 反向映射(从正向表派生,新增别名只改上面一张表)。 */
19
+ export const MODEL_ALIAS_TO_ID = Object.fromEntries(Object.entries(MODEL_ID_TO_ALIAS).map(([id, alias]) => [alias, id]));
25
20
  //# sourceMappingURL=model-catalog.js.map
@@ -154,7 +154,7 @@ export function binaryName(token, platform = currentPlatform) {
154
154
  return caseInsensitive(platform) ? name.toLowerCase() : name;
155
155
  }
156
156
  /**
157
- * 命令行与期望二进制是否属于同一程序(review 2026-09-05 收紧 + 2026-09-20 分档)。
157
+ * 命令行与期望二进制是否属于同一程序。
158
158
  *
159
159
  * 1. 可执行位匹配:首 token 的 basename 相等——`/usr/local/bin/claude --verbose`
160
160
  * 匹配 claude;native 单文件 CLI(claude 的 `bin/claude.exe`)也走这条;
@@ -21,7 +21,7 @@ export declare function samePath(a: string, b: string, opts?: PathOptions): bool
21
21
  * cwd → 会话项目目录名(Claude/pi 的 on-disk 编码,lossy N-to-N)。
22
22
  *
23
23
  * 只可用于**定位**目录,绝不可反解回 cwd——cwd 必须从 JSONL 内容读取
24
- * (回归 2026-06-21 /resume & /cd 路径错乱)。
24
+ * (否则 /resume & /cd 路径会错乱)。
25
25
  *
26
26
  * posix:`/` → `-`(`_` 同样归一,与 Claude Code 一致)。
27
27
  * win32:`\` 与 `/` **都是**分隔符、`:` 是非法文件名字符,三者都必须归一,
@@ -63,7 +63,7 @@ export function samePath(a, b, opts) {
63
63
  * cwd → 会话项目目录名(Claude/pi 的 on-disk 编码,lossy N-to-N)。
64
64
  *
65
65
  * 只可用于**定位**目录,绝不可反解回 cwd——cwd 必须从 JSONL 内容读取
66
- * (回归 2026-06-21 /resume & /cd 路径错乱)。
66
+ * (否则 /resume & /cd 路径会错乱)。
67
67
  *
68
68
  * posix:`/` → `-`(`_` 同样归一,与 Claude Code 一致)。
69
69
  * win32:`\` 与 `/` **都是**分隔符、`:` 是非法文件名字符,三者都必须归一,
@@ -6,7 +6,7 @@ import { getLogger } from '../logger/index.js';
6
6
  * **必须按无符号 32 位取值**:`0x80000001` = 2147483649 超出 Int32 正数上界
7
7
  * (2147483647),而 PowerShell 的数字字面量按 Int32 解析 —— 十六进制写法一旦
8
8
  * 进了脚本就会被解析成 **-2147483647**,传给 P/Invoke 声明的 `uint` 形参时抛
9
- * `MethodArgumentConversionInvalidCastArgument`(2026-09-22 真机复现)。
9
+ * `MethodArgumentConversionInvalidCastArgument`。
10
10
  */
11
11
  const ES_FLAGS_UINT32 = (0x80000000 | 0x00000001) >>> 0;
12
12
  /**
@@ -42,8 +42,9 @@ export function createWin32Terminator(deps) {
42
42
  stdio: 'ignore',
43
43
  windowsHide: true,
44
44
  });
45
- killer?.on?.('error', () => {
45
+ killer?.on?.('error', (err) => {
46
46
  // taskkill 拉起失败不致命:调用方仍可走 killOrphan / 陈旧锁自愈
47
+ log?.(`win32 树杀失败:taskkill error 事件 ${err?.message ?? err}`);
47
48
  });
48
49
  }
49
50
  catch (error) {
@@ -1,5 +1,5 @@
1
1
  import { isAgentKind } from '../../agent-kinds.js';
2
- import { agentDisplayName } from '../../card/card-shared.js';
2
+ import { agentDisplayName, cardButton } from '../../card/card-shared.js';
3
3
  import { buildSessionHistoryCard, paginationBar } from '../../card/card-helpers.js';
4
4
  import { pageInfo } from '../paging.js';
5
5
  import { clampInt } from '../../common/clamp.js';
@@ -194,25 +194,7 @@ export function buildResumeCommand(args, ctx, offset, deps) {
194
194
  tag: 'column',
195
195
  width: 'auto',
196
196
  elements: [
197
- {
198
- tag: 'button',
199
- text: {
200
- tag: 'plain_text',
201
- content: isCurrent ? '✓ 当前会话' : '恢复此会话',
202
- },
203
- type: isCurrent ? 'default' : 'primary',
204
- disabled: isCurrent,
205
- behaviors: [
206
- {
207
- type: 'callback',
208
- value: {
209
- cmd: 'resume.use',
210
- sessionId: session.sessionId,
211
- agent: agentKind,
212
- },
213
- },
214
- ],
215
- },
197
+ cardButton(isCurrent ? '✓ 当前会话' : '恢复此会话', { cmd: 'resume.use', sessionId: session.sessionId, agent: agentKind }, { type: isCurrent ? 'default' : 'primary', disabled: isCurrent }),
216
198
  ],
217
199
  },
218
200
  ],