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 +10 -0
- package/dist/bridge/index.js +27 -1
- package/dist/cli.js +2 -2
- package/dist/inbound/receipt.d.ts +1 -1
- package/dist/inbound/receipt.js +1 -1
- package/dist/platform/terminator-posix.js +1 -1
- package/dist/platform/terminator-win32.js +1 -1
- package/dist/runner/common/connection-based-runner.d.ts +14 -0
- package/dist/runner/common/connection-based-runner.js +29 -1
- package/package.json +1 -1
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
|
### 修复
|
package/dist/bridge/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
*
|
|
9
|
+
* 命名纪律:用户可见文案指代本程序时只能写 `lark-remote`。
|
|
10
10
|
*/
|
|
11
11
|
import type { PlaceholderKind } from './placeholder.js';
|
|
12
12
|
import type { RejectedItem } from './turn.js';
|
package/dist/inbound/receipt.js
CHANGED
|
@@ -9,7 +9,7 @@ function defaultLog(level, message) {
|
|
|
9
9
|
else
|
|
10
10
|
logger.info(message);
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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",
|