lark-remote 0.5.2 → 0.5.3

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.3] - 2026-10-12
9
+
10
+ ### 修复
11
+
12
+ - **中断后紧接着发下一条消息,不再报 `thread already has an active writer`**:点停止打断正在运行的 codex 时,中断流程会把复用的运行实例从缓存摘掉并立即销毁连接,而队列链在这一步的等待窗口里就把下一条消息发了出去、并已取到同一个实例——那条运行的长驻连接于是挂在无人托管的实例上,`codex app-server` 进程要等空闲超时(默认 30 分钟)才回收,期间一直持有该会话的写锁,同一会话的下一次恢复因此被拒。现在中断只把实例标为「待替换」,统一由下一次运行收尾时回收,标过的实例不再返回缓存;被摘掉的实例也会在运行结束时主动关闭连接,不再挂 30 分钟的超时。逐条消息新建实例的 agent 不受影响,行为照旧
13
+
14
+ ### 变更
15
+
16
+ - **注释里的运维现场数据与仓库外引用清理**:一处测试注释记录的崩溃进程号、实例名与精确到秒的重启时间改为占位描述;若干注释不再指向仓库之外的维护规范文件名。无行为变化
17
+
8
18
  ## [0.5.2] - 2026-10-11
9
19
 
10
20
  ### 修复
@@ -320,6 +320,17 @@ export class Bridge {
320
320
  if (evicted && typeof evicted.unregisterExitHandlers === 'function') {
321
321
  evicted.unregisterExitHandlers();
322
322
  }
323
+ // 2026-10-11:先标记 orphan 再 dispose。evict 与「run 刚拿走本实例」竞态时
324
+ // (queue 链在 interruptCurrentRun await 期间推进),dispose 只能关掉当时
325
+ // 已存在的连接,run 后续 acquire 的新连接会挂在无人托管的实例上——orphan
326
+ // 标记让 executeTurn 在 run 结束时主动 dispose 这批连接,而不是挂 idle
327
+ // timer 等待 30 分钟(期间持有 agent 侧会话写锁,codex 报
328
+ // "thread already has an active writer")。鸭子探测与 dispose/unregister
329
+ // 一致:claude(stream-json,无长驻连接)无此方法,跳过。
330
+ if (evicted &&
331
+ typeof evicted.markOrphaned === 'function') {
332
+ evicted.markOrphaned();
333
+ }
323
334
  // App-server runner owns a persistent connection — dispose it so the
324
335
  // `codex app-server` child process does not leak across runs/evictions.
325
336
  if (evicted && typeof evicted.dispose === 'function') {
@@ -574,7 +585,22 @@ export class Bridge {
574
585
  // (eviction must also unregister the runner from the exit dispatcher)
575
586
  // evict by the run's captured agentKind, not the live
576
587
  // defaultAgent (may have changed mid-run via /config).
577
- this.evictRunnerSlot(cwd, active.agentKind);
588
+ //
589
+ // 2026-10-11:workspace-lifetime runner 不能在这里立即 evict+dispose。
590
+ // interruptCurrentRun 的 await 窗口内 queue 链会推进下一条消息,其
591
+ // getRunner 可能刚拿走本实例(缓存命中先于本行执行)——立即 evict 会
592
+ // 让那个 run 的长驻连接脱管:进程残留至 idle TTL 才回收,期间持有
593
+ // agent 侧会话写锁(codex "thread already has an active writer",
594
+ // 线上实测)。改为标 stale,由 finalizeRun 在下一次 run 结束时统一
595
+ // evict+dispose;「stop 后下一 run 换新实例」的语义
596
+ // 保留(stale 命中即最后一轮复用)。run-per-message runner 无长驻
597
+ // 连接,保持立即 evict 不变。
598
+ if (active.runner.lifetime === 'workspace') {
599
+ this.staleRunnerSlots.add(`${cwd}\u0000${active.agentKind}`);
600
+ }
601
+ else {
602
+ this.evictRunnerSlot(cwd, active.agentKind);
603
+ }
578
604
  // Reset queue executing count: the task was interrupted externally, so
579
605
  // its promise-chain settle (.then/.catch) may lag behind the process
580
606
  // kill. Without this reset, subsequent messages would incorrectly show
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
  /**
3
3
  * CLI 引导层(bootstrap):在 import 应用模块图之前决定运行时(bun/node)并拉起它。
4
4
  *
5
- * **这是全仓唯一被认可的 spawn 收口例外**(即 AGENTS.md「子进程 spawn 只走
6
- * platform/spawn.ts」红线的显式豁免)。三个理由缺一不可:
5
+ * **这是全仓唯一被认可的 spawn 收口例外**(红线:子进程 spawn 只走
6
+ * platform/spawn.ts,此处显式豁免)。三个理由缺一不可:
7
7
  * 1. 时序:preflight(Node 版本守卫、-v/-h/--advanced-help)必须在 import 应用
8
8
  * 模块图之前跑完——dist/cli.js 刻意不使用 ES2020+ 语法以便在 node 12 上仍
9
9
  * 可被解析,而 dist/index.js 是 ES2022;先 import platform/spawn 就等于先把
@@ -6,7 +6,7 @@
6
6
  * 2. 拿不到的东西(不支持类型 / 超限 / 下载失败 / 媒体保存已关闭):明确告知,
7
7
  * 而不是把占位符塞给 agent。
8
8
  *
9
- * 用户可见文案指代本程序时只能写 `lark-remote`(AGENTS.md 命名纪律)。
9
+ * 命名纪律:用户可见文案指代本程序时只能写 `lark-remote`。
10
10
  */
11
11
  import type { PlaceholderKind } from './placeholder.js';
12
12
  import type { RejectedItem } from './turn.js';
@@ -6,7 +6,7 @@
6
6
  * 2. 拿不到的东西(不支持类型 / 超限 / 下载失败 / 媒体保存已关闭):明确告知,
7
7
  * 而不是把占位符塞给 agent。
8
8
  *
9
- * 用户可见文案指代本程序时只能写 `lark-remote`(AGENTS.md 命名纪律)。
9
+ * 命名纪律:用户可见文案指代本程序时只能写 `lark-remote`。
10
10
  */
11
11
  import path from 'node:path';
12
12
  /** 单条回执最多列出的文件数;超出折叠为 "… 等 N 个文件"(沿用旧提示口径)。 */
@@ -9,7 +9,7 @@ function defaultLog(level, message) {
9
9
  else
10
10
  logger.info(message);
11
11
  }
12
- /** 退出判定统一口径(CLAUDE.md 红线):exitCode !== null || signalCode !== null。 */
12
+ /** 退出判定统一口径(红线):exitCode !== null || signalCode !== null。 */
13
13
  function isAlive(proc) {
14
14
  return proc.exitCode === null && proc.signalCode === null;
15
15
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { spawn as nodeSpawn } from 'node:child_process';
14
14
  import { agentStopperRegistry } from './agent-stopper.js';
15
- /** 退出判定统一口径(CLAUDE.md 红线):exitCode !== null || signalCode !== null。 */
15
+ /** 退出判定统一口径(红线):exitCode !== null || signalCode !== null。 */
16
16
  function isAlive(proc) {
17
17
  return proc.exitCode === null && proc.signalCode === null;
18
18
  }
@@ -31,6 +31,10 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
31
31
  protected forceFinish: boolean;
32
32
  /** stop() 在 turn/start 或 prompt 响应前到达时置位,完成后再补发 cancel/interrupt。 */
33
33
  protected stopRequested: boolean;
34
+ /** 缓存层除名标记(evictRunnerSlot → markOrphaned)。置位后 run 结束时主动
35
+ * dispose 全部连接而不是挂 idle timer:被除名实例上创建的连接无人托管,
36
+ * 不主动收会残留进程至 idle TTL(30 分钟),期间持有 agent 侧会话写锁。 */
37
+ private orphaned;
34
38
  protected constructor(opts: {
35
39
  kind: AgentKind;
36
40
  sessionReader: AgentSessionReader;
@@ -96,6 +100,16 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
96
100
  * Dispose the runner: release the connection.
97
101
  */
98
102
  dispose(): Promise<void>;
103
+ /**
104
+ * Mark this runner as evicted from the bridge's runner cache while a run may
105
+ * still be holding/creating connections on it (queue-advance race during
106
+ * interruptCurrentRun's await window, 2026-10-11). The active run's finally
107
+ * disposes connections eagerly instead of arming the idle TTL timer —
108
+ * otherwise the agent server process lingers for up to 30 minutes holding
109
+ * its session write lock (codex: "thread already has an active writer").
110
+ * One-way by design: an evicted instance never returns to the cache.
111
+ */
112
+ markOrphaned(): void;
99
113
  getUsageAuthority(): 'live';
100
114
  killOrphan(): void;
101
115
  registerExitHandlers(): void;
@@ -22,6 +22,10 @@ export class ConnectionBasedRunner {
22
22
  forceFinish = false;
23
23
  /** stop() 在 turn/start 或 prompt 响应前到达时置位,完成后再补发 cancel/interrupt。 */
24
24
  stopRequested = false;
25
+ /** 缓存层除名标记(evictRunnerSlot → markOrphaned)。置位后 run 结束时主动
26
+ * dispose 全部连接而不是挂 idle timer:被除名实例上创建的连接无人托管,
27
+ * 不主动收会残留进程至 idle TTL(30 分钟),期间持有 agent 侧会话写锁。 */
28
+ orphaned = false;
25
29
  constructor(opts, defaultTurnIdleTimeoutMs = TURN_IDLE_TIMEOUT_MS) {
26
30
  this.kind = opts.kind;
27
31
  this.sessionReader = opts.sessionReader;
@@ -130,7 +134,19 @@ export class ConnectionBasedRunner {
130
134
  this.currentTurnId = null;
131
135
  this.stopRequested = false;
132
136
  this.clearTurnState();
133
- this.notifyIdle(opts.cwd);
137
+ if (this.orphaned) {
138
+ // 已被缓存层除名(evictRunnerSlot → markOrphaned):本次 run 创建的
139
+ // 连接挂在无人托管的实例上。主动收掉而不是挂 idle timer——否则 agent
140
+ // server 进程残留至 idle TTL,期间持有 agent 侧会话写锁(codex
141
+ // "thread already has an active writer",2026-10-11 线上实测)。
142
+ // dispose() 内部自带 try/catch,这里 fire-and-forget 不阻塞事件流收尾。
143
+ void this.disposeConnections().catch((err) => {
144
+ getLogger().warn(`[${this.logTag}] orphan dispose error: ${err.message}`);
145
+ });
146
+ }
147
+ else {
148
+ this.notifyIdle(opts.cwd);
149
+ }
134
150
  }
135
151
  }
136
152
  /**
@@ -178,6 +194,18 @@ export class ConnectionBasedRunner {
178
194
  getLogger().warn(`[${this.logTag}] dispose error: ${err.message}`);
179
195
  }
180
196
  }
197
+ /**
198
+ * Mark this runner as evicted from the bridge's runner cache while a run may
199
+ * still be holding/creating connections on it (queue-advance race during
200
+ * interruptCurrentRun's await window, 2026-10-11). The active run's finally
201
+ * disposes connections eagerly instead of arming the idle TTL timer —
202
+ * otherwise the agent server process lingers for up to 30 minutes holding
203
+ * its session write lock (codex: "thread already has an active writer").
204
+ * One-way by design: an evicted instance never returns to the cache.
205
+ */
206
+ markOrphaned() {
207
+ this.orphaned = true;
208
+ }
181
209
  getUsageAuthority() {
182
210
  return 'live';
183
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",