lark-remote 0.4.5 → 0.5.1
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 +34 -0
- package/dist/bridge/approval-coordinator.d.ts +1 -1
- package/dist/bridge/approval-coordinator.js +3 -3
- package/dist/bridge/inbound-media.js +10 -7
- package/dist/bridge/index.d.ts +55 -17
- package/dist/bridge/index.js +145 -74
- package/dist/bridge/queue-manager.d.ts +71 -20
- package/dist/bridge/queue-manager.js +139 -57
- package/dist/card/approval-render.d.ts +1 -1
- package/dist/card/approval-render.js +6 -6
- package/dist/card/bash-card-session.d.ts +3 -3
- package/dist/card/bash-card-session.js +3 -3
- package/dist/card/bash-renderer.js +1 -1
- package/dist/card/card-session.d.ts +4 -4
- package/dist/card/card-session.js +6 -6
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.d.ts +1 -1
- package/dist/card/run-renderer.js +14 -14
- package/dist/card/run-state.d.ts +2 -2
- package/dist/card/run-state.js +3 -3
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +6 -6
- package/dist/cli.js +2 -1
- package/dist/clone.d.ts +5 -3
- package/dist/clone.js +8 -4
- package/dist/common/truncate.d.ts +1 -1
- package/dist/common/truncate.js +2 -2
- package/dist/common/ttl-cache.d.ts +2 -2
- package/dist/common/ttl-cache.js +2 -2
- package/dist/config/codex-config.d.ts +9 -9
- package/dist/config/codex-config.js +18 -18
- package/dist/config/dir.js +5 -2
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +6 -6
- package/dist/config/opencode-config.js +3 -3
- package/dist/connector/index.d.ts +5 -5
- package/dist/connector/index.js +24 -11
- package/dist/inbound/event.d.ts +1 -1
- package/dist/inbound/event.js +1 -1
- package/dist/inbound/placeholder.d.ts +2 -2
- package/dist/inbound/placeholder.js +2 -2
- package/dist/index.js +23 -12
- package/dist/instance-lock.js +4 -3
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.js +4 -4
- package/dist/persistence/atomic-write.d.ts +1 -1
- package/dist/persistence/atomic-write.js +6 -2
- package/dist/platform/agent-stopper.d.ts +1 -1
- package/dist/platform/identity.d.ts +7 -1
- package/dist/platform/identity.js +2 -2
- package/dist/platform/path.d.ts +6 -0
- package/dist/platform/path.js +14 -8
- package/dist/platform/select.d.ts +1 -1
- package/dist/platform/select.js +1 -1
- package/dist/platform/shell.d.ts +2 -3
- package/dist/platform/spawn.d.ts +2 -2
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/terminator-posix.js +1 -1
- package/dist/platform/terminator-win32.js +2 -2
- package/dist/platform/terminator.d.ts +3 -3
- package/dist/platform/terminator.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/restart.d.ts +1 -1
- package/dist/restart.js +5 -3
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +2 -2
- package/dist/router/config/opencode.js +1 -1
- package/dist/router/index.d.ts +65 -16
- package/dist/router/index.js +141 -47
- package/dist/router/order-exec-dispatch.js +2 -2
- package/dist/runner/bash/runner.d.ts +2 -2
- package/dist/runner/bash/runner.js +6 -6
- package/dist/runner/claude/runner.js +1 -1
- package/dist/runner/claude/session.d.ts +2 -2
- package/dist/runner/claude/session.js +14 -14
- package/dist/runner/codex/app-server/runner.d.ts +28 -0
- package/dist/runner/codex/app-server/runner.js +57 -4
- package/dist/runner/codex/app-server/translator.d.ts +2 -2
- package/dist/runner/codex/app-server/translator.js +5 -5
- package/dist/runner/common/acp/acp-approval.d.ts +2 -2
- package/dist/runner/common/acp/acp-approval.js +3 -3
- package/dist/runner/common/acp/base-acp-runner.js +1 -1
- package/dist/runner/common/acp/base-translator.d.ts +3 -3
- package/dist/runner/common/acp/base-translator.js +4 -4
- package/dist/runner/common/acp/protocol-types.js +1 -1
- package/dist/runner/common/connection-based-runner.d.ts +1 -1
- package/dist/runner/common/connection-based-runner.js +2 -2
- package/dist/runner/common/jsonl-stream.d.ts +2 -2
- package/dist/runner/common/jsonl-stream.js +12 -12
- package/dist/runner/common/jsonrpc/client.d.ts +1 -1
- package/dist/runner/common/jsonrpc/client.js +1 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +1 -1
- package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +2 -2
- package/dist/runner/common/spawning-runner.d.ts +21 -8
- package/dist/runner/common/spawning-runner.js +41 -15
- package/dist/runner/dsh/runner.js +2 -2
- package/dist/runner/kimi/acp/runner.d.ts +3 -3
- package/dist/runner/kimi/acp/runner.js +9 -9
- package/dist/runner/kimi/acp/translator.d.ts +2 -2
- package/dist/runner/kimi/acp/translator.js +4 -4
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +3 -3
- package/dist/runner/opencode/acp/translator.js +1 -1
- package/dist/runner/pi/rpc/runner.js +2 -2
- package/dist/runner/pi/rpc/translator.d.ts +1 -1
- package/dist/runner/pi/rpc/translator.js +4 -4
- package/dist/runner/types.d.ts +11 -2
- package/dist/runner/types.js +12 -1
- package/dist/session/claude/session-index.d.ts +12 -0
- package/dist/session/claude/session-index.js +20 -5
- package/dist/session/claude/sessions.js +10 -10
- package/dist/session/codex/rollout-reader.d.ts +4 -4
- package/dist/session/codex/rollout-reader.js +12 -12
- package/dist/session/common/two-pass.js +2 -2
- package/dist/session/common/usage-accumulator.d.ts +2 -2
- package/dist/session/common/usage-accumulator.js +2 -2
- package/dist/session/dsh/sessions.js +2 -2
- package/dist/session/kimi/sessions.d.ts +2 -2
- package/dist/session/kimi/sessions.js +11 -11
- package/dist/session/opencode/sessions.d.ts +3 -3
- package/dist/session/opencode/sessions.js +12 -10
- package/dist/session/pi/sessions.js +13 -12
- package/dist/session/session-store.d.ts +1 -1
- package/dist/session/session-store.js +1 -1
- package/package.json +1 -1
|
@@ -43,7 +43,7 @@ const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
|
43
43
|
const COMPACTION_RESULT_SUBTYPES = new Set(['compact', 'compaction']);
|
|
44
44
|
/**
|
|
45
45
|
* ClaudeSession extends SpawningRunner 复用其 spawn/pid 文件/killOrphan/
|
|
46
|
-
* Terminator/SpawnHeartbeat
|
|
46
|
+
* Terminator/SpawnHeartbeat/退出分发器机制(契约保持),
|
|
47
47
|
* 但 run() 覆盖为「长驻 + 按 turn 消费」:进程跨 turn 存活,turn 结束以
|
|
48
48
|
* stream-json result 事件(非 compact)为界,而不是进程退出。
|
|
49
49
|
*/
|
|
@@ -127,7 +127,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
127
127
|
// 或 --resume 目标会话)。比较进程当前 session(init 报告值)而非 spawn
|
|
128
128
|
// 请求值:bridge 会把 init 的 session_id 写回 SessionStore,第二条消息
|
|
129
129
|
// 带着它来;若拿 spawn 请求值('')比较,每条消息都会误判切换、杀进程
|
|
130
|
-
// 重启,长驻设计失效(2026-08-16
|
|
130
|
+
// 重启,长驻设计失效(2026-08-16)。
|
|
131
131
|
if (this.isRunning && this.sessionId !== (opts.sessionId ?? '')) {
|
|
132
132
|
getLogger().info(`[${this.logTag}] session mismatch: process=${this.sessionId || '(fresh)'} ` +
|
|
133
133
|
`requested=${opts.sessionId ?? '(fresh)'}, recycling process`);
|
|
@@ -174,7 +174,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
174
174
|
getLogger().warn(`[${this.logTag}] respondPermission skipped: process not running requestId=${requestId}`);
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
|
-
//
|
|
177
|
+
// requestId 不在待审批表(已 control_cancel / 进程回收)时
|
|
178
178
|
// 丢弃响应,不能以空 updatedInput allow(claude 会拿空输入执行工具)。
|
|
179
179
|
if (!this.pendingToolInputs.has(requestId)) {
|
|
180
180
|
getLogger().warn(`[${this.logTag}] respondPermission dropped: unknown requestId=${requestId}`);
|
|
@@ -204,7 +204,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
204
204
|
async respondAskUserQuestion(requestId, answers) {
|
|
205
205
|
if (!this.isRunning)
|
|
206
206
|
return;
|
|
207
|
-
//
|
|
207
|
+
// 未知 requestId 时不能回写缺原始 questions 的 answers
|
|
208
208
|
// (官方要求 updatedInput 必须包含原始 questions)。
|
|
209
209
|
if (!this.pendingToolInputs.has(requestId)) {
|
|
210
210
|
getLogger().warn(`[${this.logTag}] respondAskUserQuestion dropped: unknown requestId=${requestId}`);
|
|
@@ -285,7 +285,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
285
285
|
*/
|
|
286
286
|
async startProcess(opts) {
|
|
287
287
|
if (this.isRunning) {
|
|
288
|
-
//
|
|
288
|
+
// 复用进程(跨 turn 长驻)时清掉上一轮的空闲回收 timer——
|
|
289
289
|
// 否则长 turn 中它会空转触发一次(回调因 turnActive 空转返回,无害但
|
|
290
290
|
// 多余);新一轮结束后的 armIdleTimer 会重新武装。
|
|
291
291
|
this.clearIdleTimer();
|
|
@@ -381,7 +381,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
381
381
|
// =========================================================================
|
|
382
382
|
/**
|
|
383
383
|
* 消费事件队列直到本 turn 的 result 事件(非 compact/compaction)。
|
|
384
|
-
* 消费者提前关闭生成器(bridge 循环体抛错)→
|
|
384
|
+
* 消费者提前关闭生成器(bridge 循环体抛错)→ 杀进程防孤儿(语义)。
|
|
385
385
|
*
|
|
386
386
|
* result = turn 终态(2026-08-16 真实 claude 实测:长驻模式下 result 之后
|
|
387
387
|
* 不再有 stdout 事件,后台任务完成是静默的;旧 -p 流程「等进程退出再收尾」
|
|
@@ -554,7 +554,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
554
554
|
async translateEvent(raw) {
|
|
555
555
|
const event = raw;
|
|
556
556
|
const type = event.type;
|
|
557
|
-
//
|
|
557
|
+
// --resume 会先重放上一轮历史事件(旧 result 之外还可能有
|
|
558
558
|
// 旧 assistant/user 内容)。init 之前除 init 本身外一律丢弃,避免历史
|
|
559
559
|
// 内容混进当前卡片(result 分支的 sawInit 守卫升级为全类型守卫)。
|
|
560
560
|
if (type !== 'system' && !this.sawInit)
|
|
@@ -678,7 +678,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
678
678
|
const request = (raw.request ?? {});
|
|
679
679
|
const subtype = request.subtype;
|
|
680
680
|
if (subtype !== 'can_use_tool') {
|
|
681
|
-
//
|
|
681
|
+
// 未知 subtype 不能静默丢弃——claude 在等 control_response,
|
|
682
682
|
// 不回应 turn 会永久挂起(无审批卡、无超时)。deny 兜底让 claude 继续。
|
|
683
683
|
getLogger().warn(`[${this.logTag}] unknown control request subtype=${String(subtype)} requestId=${requestId}`);
|
|
684
684
|
void this.writeControlResponse(requestId, {
|
|
@@ -692,7 +692,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
692
692
|
const toolName = String(request.tool_name ?? '');
|
|
693
693
|
const input = (request.input ?? {});
|
|
694
694
|
this.pendingToolInputs.set(requestId, input);
|
|
695
|
-
//
|
|
695
|
+
// 允许所有只放行工具权限;AskUserQuestion 不能被空 answers
|
|
696
696
|
// 自动放行(claude 会把空答案当作已作答),必须继续上抛卡片让用户回答。
|
|
697
697
|
if (this.autoApprove && toolName !== 'AskUserQuestion') {
|
|
698
698
|
getLogger().info(`[${this.logTag}] auto-approving requestId=${requestId} tool=${toolName}`);
|
|
@@ -702,7 +702,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
702
702
|
}).catch((err) => {
|
|
703
703
|
getLogger().warn(`[${this.logTag}] auto-approve write failed requestId=${requestId}: ${err.message}`);
|
|
704
704
|
});
|
|
705
|
-
//
|
|
705
|
+
// 自动放行不经过协调器/respondPermission,条目不会随
|
|
706
706
|
// 响应被删除——立即释放,否则长会话(允许所有)下 Map 无界增长。
|
|
707
707
|
this.pendingToolInputs.delete(requestId);
|
|
708
708
|
return [];
|
|
@@ -717,7 +717,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
717
717
|
behavior: 'deny',
|
|
718
718
|
message: '无法解析 AskUserQuestion 选项',
|
|
719
719
|
}).catch(() => { });
|
|
720
|
-
//
|
|
720
|
+
// 解析失败 deny 后同样释放条目(不经过 respondPermission)。
|
|
721
721
|
this.pendingToolInputs.delete(requestId);
|
|
722
722
|
return [];
|
|
723
723
|
}
|
|
@@ -760,7 +760,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
760
760
|
];
|
|
761
761
|
}
|
|
762
762
|
// 非命令工具的兜底防御:input 无 command 字段时 JSON 序列化,但空对象
|
|
763
|
-
// 序列化成 `{}`
|
|
763
|
+
// 序列化成 `{}` 对用户无意义——内容为空时回退展示工具名。
|
|
764
764
|
const rawCommand = typeof input.command === 'string' ? input.command : JSON.stringify(input);
|
|
765
765
|
const command = rawCommand && rawCommand !== '{}' && rawCommand !== 'null'
|
|
766
766
|
? rawCommand
|
|
@@ -817,7 +817,7 @@ function parseUserQuestions(input) {
|
|
|
817
817
|
const question = typeof q.question === 'string' ? q.question : '';
|
|
818
818
|
if (!question)
|
|
819
819
|
continue;
|
|
820
|
-
//
|
|
820
|
+
// answers 字典以问题文本为 key,重复文本会互相覆盖——
|
|
821
821
|
// 视为解析失败(调用方 deny 兜底),不把坏问题上抛给用户。
|
|
822
822
|
if (seenQuestions.has(question))
|
|
823
823
|
return [];
|
|
@@ -839,7 +839,7 @@ function parseUserQuestions(input) {
|
|
|
839
839
|
});
|
|
840
840
|
}
|
|
841
841
|
}
|
|
842
|
-
//
|
|
842
|
+
// 零选项问题无法作答(多选连自定义答案都没有),视为
|
|
843
843
|
// 解析失败(调用方 deny 兜底),不把坏问题上抛给用户。
|
|
844
844
|
if (options.length === 0)
|
|
845
845
|
return [];
|
|
@@ -42,6 +42,8 @@ export declare class CodexAppServerRunner extends ConnectionBasedRunner<JsonRpcC
|
|
|
42
42
|
private connectionManager;
|
|
43
43
|
private currentTranslator;
|
|
44
44
|
private activeThreadId;
|
|
45
|
+
/** 本轮已记录过日志的外来(子 agent)线程 id,避免逐条 delta 刷盘。 */
|
|
46
|
+
private foreignThreadIds;
|
|
45
47
|
private model?;
|
|
46
48
|
private modelProvider?;
|
|
47
49
|
private reasoningEffort?;
|
|
@@ -91,7 +93,33 @@ export declare class CodexAppServerRunner extends ConnectionBasedRunner<JsonRpcC
|
|
|
91
93
|
* thread/resume(按 sessionId 恢复既有线程)→ turn/start。
|
|
92
94
|
*/
|
|
93
95
|
protected setupTurn(message: string, opts: SpawnOptions): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Route one server notification into the current turn's translator.
|
|
98
|
+
*
|
|
99
|
+
* App-server 把已知线程的通知全部复用同一条 stdio 连接:agent 用
|
|
100
|
+
* multi-agent v2 派生子 agent 后,子线程的 `turn/started`、`item/*`、
|
|
101
|
+
* `turn/completed`、`thread/tokenUsage/updated` 会与本轮通知交错到达
|
|
102
|
+
* (2026-10-09 实测)。一个 turn 只属于一个线程,因此外来线程的通知一律
|
|
103
|
+
* 丢弃——否则子线程的 `turn/started` 会覆盖本轮 threadId,会话写回把子
|
|
104
|
+
* 线程 id 当成顶层会话指针存盘(冷连接 `thread/resume` 直接报
|
|
105
|
+
* "cannot resume an unloaded multi-agent v2 sub-agent"),且子线程的
|
|
106
|
+
* `turn/completed` 会提前结束本轮 run、token 用量也会串台。
|
|
107
|
+
*/
|
|
94
108
|
private handleNotification;
|
|
109
|
+
/**
|
|
110
|
+
* 线程归属闸门:通知是否属于本轮 turn 的线程。
|
|
111
|
+
*
|
|
112
|
+
* - 无 threadId 的通知(如部分 warning)没有线程维度可用,放行;
|
|
113
|
+
* - `serverRequest/resolved` 保持线程无关:审批按 requestId 关联(桥侧
|
|
114
|
+
* ApprovalCoordinator 跟踪),子 agent 的审批同样必须能结算,否则卡片
|
|
115
|
+
* 上的审批条目会一直挂在 pending。
|
|
116
|
+
*/
|
|
117
|
+
private belongsToActiveThread;
|
|
118
|
+
/**
|
|
119
|
+
* 每个被忽略的外来线程只记一条日志:这是会话指针被污染的入口,出问题时
|
|
120
|
+
* 必须能从日志直接看到「本轮忽略了哪些子线程」,同时避免逐条 delta 刷盘。
|
|
121
|
+
*/
|
|
122
|
+
private noteForeignThread;
|
|
95
123
|
private handleServerRequest;
|
|
96
124
|
private buildGrantedPermissions;
|
|
97
125
|
/** 拒绝权限审批:空 fileSystem + 关闭网络(真实 GrantedPermissionProfile)。 */
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { ConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
|
|
14
14
|
import { CodexAppServerTranslator } from './translator.js';
|
|
15
|
+
import { NotificationMethod, } from './protocol-types.js';
|
|
15
16
|
import { RpcErrorCode } from '../../common/acp/protocol-types.js';
|
|
16
17
|
import { mapAnswersByIndex } from '../../question-common.js';
|
|
17
18
|
import { getLogger } from '../../../logger/index.js';
|
|
@@ -102,6 +103,8 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
102
103
|
connectionManager;
|
|
103
104
|
currentTranslator = null;
|
|
104
105
|
activeThreadId = null;
|
|
106
|
+
/** 本轮已记录过日志的外来(子 agent)线程 id,避免逐条 delta 刷盘。 */
|
|
107
|
+
foreignThreadIds = new Set();
|
|
105
108
|
model;
|
|
106
109
|
modelProvider;
|
|
107
110
|
reasoningEffort;
|
|
@@ -136,7 +139,7 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
136
139
|
},
|
|
137
140
|
};
|
|
138
141
|
this.connectionManager = new ConnectionManager(managerOpts);
|
|
139
|
-
//
|
|
142
|
+
// 协议停止通道:codex 的 cancel 是 `turn/interrupt` 请求,已由
|
|
140
143
|
// cancelCurrentTurn 实现;登记后 win32 优雅停止才有通道可用(否则一路树杀)。
|
|
141
144
|
this.connectionManager.stopper = ({ client }) => this.buildCooperativeStop(client);
|
|
142
145
|
}
|
|
@@ -171,6 +174,7 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
171
174
|
clearTurnState() {
|
|
172
175
|
this.currentTranslator = null;
|
|
173
176
|
this.activeThreadId = null;
|
|
177
|
+
this.foreignThreadIds.clear();
|
|
174
178
|
}
|
|
175
179
|
async releaseConnection(cwd) {
|
|
176
180
|
await this.connectionManager.release(cwd);
|
|
@@ -235,7 +239,7 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
235
239
|
else if (pending.kind === 'permissions') {
|
|
236
240
|
// 权限审批的响应没有 decision 字段(真实协议只回 granted profile +
|
|
237
241
|
// scope):decline/cancel 必须返回空授权(拒绝全部),否则会把用户已
|
|
238
|
-
// 勾选的条目当作授予返回(2026-08-12
|
|
242
|
+
// 勾选的条目当作授予返回(2026-08-12 :勾选后点「拒绝」实
|
|
239
243
|
// 际授予了所选权限)。accept/acceptForSession 才带用户勾选的条目。
|
|
240
244
|
const denied = action === 'decline' || action === 'cancel';
|
|
241
245
|
client.respond(requestId, {
|
|
@@ -358,11 +362,12 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
358
362
|
}
|
|
359
363
|
else {
|
|
360
364
|
const threadResult = await client.request('thread/start', threadParams);
|
|
361
|
-
//
|
|
365
|
+
// 会话键假设:主线程 thread.id === session_meta.session_id,
|
|
362
366
|
// 故 thread.id 可同时用作协议 threadId 与 store/session reader 的 session
|
|
363
367
|
// 键(bridge 用 turn_started 通知的 threadId 写回,/resume、Compact 按此
|
|
364
368
|
// 键定位 JSONL)。forked/subagent 线程二者会分叉(openai/codex#29327),
|
|
365
|
-
//
|
|
369
|
+
// 桥只把主线程作为顶层会话——子线程的通知由 handleNotification 的线程
|
|
370
|
+
// 归属闸门挡掉,不会进来(见 belongsToActiveThread)。
|
|
366
371
|
threadId = threadResult.thread.id;
|
|
367
372
|
}
|
|
368
373
|
this.activeThreadId = threadId;
|
|
@@ -372,10 +377,58 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
372
377
|
const turnResult = await client.request('turn/start', turnParams);
|
|
373
378
|
this.currentTurnId = turnResult.turn.id;
|
|
374
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Route one server notification into the current turn's translator.
|
|
382
|
+
*
|
|
383
|
+
* App-server 把已知线程的通知全部复用同一条 stdio 连接:agent 用
|
|
384
|
+
* multi-agent v2 派生子 agent 后,子线程的 `turn/started`、`item/*`、
|
|
385
|
+
* `turn/completed`、`thread/tokenUsage/updated` 会与本轮通知交错到达
|
|
386
|
+
* (2026-10-09 实测)。一个 turn 只属于一个线程,因此外来线程的通知一律
|
|
387
|
+
* 丢弃——否则子线程的 `turn/started` 会覆盖本轮 threadId,会话写回把子
|
|
388
|
+
* 线程 id 当成顶层会话指针存盘(冷连接 `thread/resume` 直接报
|
|
389
|
+
* "cannot resume an unloaded multi-agent v2 sub-agent"),且子线程的
|
|
390
|
+
* `turn/completed` 会提前结束本轮 run、token 用量也会串台。
|
|
391
|
+
*/
|
|
375
392
|
handleNotification(method, params) {
|
|
393
|
+
if (!this.belongsToActiveThread(method, params)) {
|
|
394
|
+
this.noteForeignThread(params);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
376
397
|
const events = this.currentTranslator?.handleNotification(method, params) ?? [];
|
|
377
398
|
this.pushEvents(events);
|
|
378
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* 线程归属闸门:通知是否属于本轮 turn 的线程。
|
|
402
|
+
*
|
|
403
|
+
* - 无 threadId 的通知(如部分 warning)没有线程维度可用,放行;
|
|
404
|
+
* - `serverRequest/resolved` 保持线程无关:审批按 requestId 关联(桥侧
|
|
405
|
+
* ApprovalCoordinator 跟踪),子 agent 的审批同样必须能结算,否则卡片
|
|
406
|
+
* 上的审批条目会一直挂在 pending。
|
|
407
|
+
*/
|
|
408
|
+
belongsToActiveThread(method, params) {
|
|
409
|
+
const active = this.activeThreadId;
|
|
410
|
+
if (!active)
|
|
411
|
+
return true;
|
|
412
|
+
if (method === NotificationMethod.SERVER_REQUEST_RESOLVED)
|
|
413
|
+
return true;
|
|
414
|
+
const threadId = params?.threadId;
|
|
415
|
+
if (typeof threadId !== 'string' || threadId.length === 0)
|
|
416
|
+
return true;
|
|
417
|
+
return threadId === active;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* 每个被忽略的外来线程只记一条日志:这是会话指针被污染的入口,出问题时
|
|
421
|
+
* 必须能从日志直接看到「本轮忽略了哪些子线程」,同时避免逐条 delta 刷盘。
|
|
422
|
+
*/
|
|
423
|
+
noteForeignThread(params) {
|
|
424
|
+
const threadId = params?.threadId;
|
|
425
|
+
if (typeof threadId !== 'string' || threadId.length === 0)
|
|
426
|
+
return;
|
|
427
|
+
if (this.foreignThreadIds.has(threadId))
|
|
428
|
+
return;
|
|
429
|
+
this.foreignThreadIds.add(threadId);
|
|
430
|
+
getLogger().info(`[${this.logTag}] ignoring notifications from foreign thread threadId=${threadId} activeThreadId=${this.activeThreadId}`);
|
|
431
|
+
}
|
|
379
432
|
handleServerRequest(id, method, params) {
|
|
380
433
|
const events = this.currentTranslator?.handleServerRequest(id, method, params) ?? [];
|
|
381
434
|
if (events.length === 0) {
|
|
@@ -26,7 +26,7 @@ export declare class CodexAppServerTranslator {
|
|
|
26
26
|
/** Full command output per item id (item-scoped snapshot). */
|
|
27
27
|
private toolOutputByItem;
|
|
28
28
|
/**
|
|
29
|
-
* Tool identity per item id (信息保真
|
|
29
|
+
* Tool identity per item id (信息保真): recorded at item/started, spread
|
|
30
30
|
* into every toolOutput turn_diff, released at item/completed.
|
|
31
31
|
*/
|
|
32
32
|
private toolItemMeta;
|
|
@@ -85,7 +85,7 @@ export declare class CodexAppServerTranslator {
|
|
|
85
85
|
private handlePlanUpdated;
|
|
86
86
|
private handleTokenUsageUpdated;
|
|
87
87
|
/**
|
|
88
|
-
* warning 通知 → notice 事件(信息保真
|
|
88
|
+
* warning 通知 → notice 事件(信息保真 运行期告警不再静默丢弃)。
|
|
89
89
|
*/
|
|
90
90
|
private handleWarning;
|
|
91
91
|
/**
|
|
@@ -27,7 +27,7 @@ export class CodexAppServerTranslator {
|
|
|
27
27
|
/** Full command output per item id (item-scoped snapshot). */
|
|
28
28
|
toolOutputByItem = new Map();
|
|
29
29
|
/**
|
|
30
|
-
* Tool identity per item id (信息保真
|
|
30
|
+
* Tool identity per item id (信息保真): recorded at item/started, spread
|
|
31
31
|
* into every toolOutput turn_diff, released at item/completed.
|
|
32
32
|
*/
|
|
33
33
|
toolItemMeta = new Map();
|
|
@@ -326,7 +326,7 @@ export class CodexAppServerTranslator {
|
|
|
326
326
|
return [];
|
|
327
327
|
}
|
|
328
328
|
this.toolOutputByItem.set(item.id, authoritative);
|
|
329
|
-
// 信息保真
|
|
329
|
+
// 信息保真 完成事件同样携带身份,并在发出后释放 meta。
|
|
330
330
|
const meta = this.toolItemMeta.get(item.id);
|
|
331
331
|
this.toolItemMeta.delete(item.id);
|
|
332
332
|
return [
|
|
@@ -383,7 +383,7 @@ export class CodexAppServerTranslator {
|
|
|
383
383
|
];
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
|
-
// 信息保真
|
|
386
|
+
// 信息保真 webSearch/mcpToolCall/dynamicToolCall 等无专属 completed 分支的
|
|
387
387
|
// 工具 item 也必须收尾——否则 run-state 侧工具块永挂 running(无 completedAt/终态),
|
|
388
388
|
// toolItemMeta 泄漏到下个 turn 的 clear() 才被释放。
|
|
389
389
|
if (this.startedToolItems.has(item.id)) {
|
|
@@ -466,7 +466,7 @@ export class CodexAppServerTranslator {
|
|
|
466
466
|
return [];
|
|
467
467
|
}
|
|
468
468
|
/**
|
|
469
|
-
* warning 通知 → notice 事件(信息保真
|
|
469
|
+
* warning 通知 → notice 事件(信息保真 运行期告警不再静默丢弃)。
|
|
470
470
|
*/
|
|
471
471
|
handleWarning(params) {
|
|
472
472
|
return [
|
|
@@ -653,7 +653,7 @@ export class CodexAppServerTranslator {
|
|
|
653
653
|
// item/started 锚定工具块位置(真实时序):命令开始时即创建工具块,
|
|
654
654
|
// 而不是等到首个输出 delta——否则后续 reasoning item 会插到 command 之前。
|
|
655
655
|
this.startedToolItems.add(item.id);
|
|
656
|
-
// 信息保真
|
|
656
|
+
// 信息保真 记录工具身份,随 turn_diff 透传(run-state 首见时写入)。
|
|
657
657
|
this.toolItemMeta.set(item.id, {
|
|
658
658
|
toolName: 'Bash',
|
|
659
659
|
toolInput: { command: item.command, ...(item.cwd ? { cwd: item.cwd } : {}) },
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ACP approval/permission mapping and lifecycle shared by the kimi and
|
|
3
3
|
* opencode ACP runners.
|
|
4
4
|
*
|
|
5
|
-
* Extracted from runner.ts and translator.ts to avoid duplication
|
|
5
|
+
* Extracted from runner.ts and translator.ts to avoid duplication.
|
|
6
6
|
* Shape source: approval.ts:28-29 — optionId is opaque, echo back as-is.
|
|
7
7
|
*/
|
|
8
8
|
import type { ApprovalView } from '../../types.js';
|
|
@@ -33,7 +33,7 @@ export declare function buildAcpPermissionOutcome(action: string, options: Permi
|
|
|
33
33
|
/**
|
|
34
34
|
* 从服务端 options kind 派生审批决定列表:accept/decline/cancel 恒有;
|
|
35
35
|
* 带 always 类 option(kimi approve_always / opencode allow_always)才提供
|
|
36
|
-
* acceptForSession
|
|
36
|
+
* acceptForSession(「本会话总是允许」)。kimi/opencode 两侧共用。
|
|
37
37
|
*/
|
|
38
38
|
export declare function deriveAcpAvailableDecisions(options: PermissionOption[]): string[];
|
|
39
39
|
/** 截断长文本并追加省略号(translator 审批 reason 用,原先两份相同拷贝)。 */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ACP approval/permission mapping and lifecycle shared by the kimi and
|
|
3
3
|
* opencode ACP runners.
|
|
4
4
|
*
|
|
5
|
-
* Extracted from runner.ts and translator.ts to avoid duplication
|
|
5
|
+
* Extracted from runner.ts and translator.ts to avoid duplication.
|
|
6
6
|
* Shape source: approval.ts:28-29 — optionId is opaque, echo back as-is.
|
|
7
7
|
*/
|
|
8
8
|
import { getLogger } from '../../../logger/index.js';
|
|
@@ -10,7 +10,7 @@ import { getLogger } from '../../../logger/index.js';
|
|
|
10
10
|
* Find an option by matching its `kind` against a list of candidate values,
|
|
11
11
|
* and return the `optionId` to echo back in the approval response.
|
|
12
12
|
* ACP option kind values vary between kimi versions; this handles both
|
|
13
|
-
* the documented names and the observed real names
|
|
13
|
+
* the documented names and the observed real names.
|
|
14
14
|
*/
|
|
15
15
|
function findOptionIdByKind(options, candidateKinds) {
|
|
16
16
|
for (const kind of candidateKinds) {
|
|
@@ -54,7 +54,7 @@ export function buildAcpPermissionOutcome(action, options, kinds) {
|
|
|
54
54
|
/**
|
|
55
55
|
* 从服务端 options kind 派生审批决定列表:accept/decline/cancel 恒有;
|
|
56
56
|
* 带 always 类 option(kimi approve_always / opencode allow_always)才提供
|
|
57
|
-
* acceptForSession
|
|
57
|
+
* acceptForSession(「本会话总是允许」)。kimi/opencode 两侧共用。
|
|
58
58
|
*/
|
|
59
59
|
export function deriveAcpAvailableDecisions(options) {
|
|
60
60
|
const decisions = ['accept', 'decline', 'cancel'];
|
|
@@ -15,7 +15,7 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
|
|
|
15
15
|
constructor(opts, connectionManager) {
|
|
16
16
|
super(opts);
|
|
17
17
|
this.connectionManager = connectionManager;
|
|
18
|
-
//
|
|
18
|
+
// 协议停止通道:ACP 的 cancel 就是 `session/cancel`,已由
|
|
19
19
|
// cancelCurrentTurn 实现。这里只做登记接线——manager 建在 super() 实参里,
|
|
20
20
|
// 那时 this 不可用,所以只能等 super() 返回后再赋值。
|
|
21
21
|
connectionManager.stopper = ({ client }) => this.buildCooperativeStop(client);
|
|
@@ -114,7 +114,7 @@ export declare abstract class BaseAcpTranslator {
|
|
|
114
114
|
*/
|
|
115
115
|
setOperationKind(kind: 'turn' | 'compact'): void;
|
|
116
116
|
/**
|
|
117
|
-
* plan update → PlanEvent(信息保真
|
|
117
|
+
* plan update → PlanEvent(信息保真 不再显式丢弃)。
|
|
118
118
|
* ACP PlanEntry: { content, priority, status }(status: pending/in_progress/
|
|
119
119
|
* completed)。基类具体方法:kimi/opencode wire 结构相同,直接复用。
|
|
120
120
|
*/
|
|
@@ -125,7 +125,7 @@ export declare abstract class BaseAcpTranslator {
|
|
|
125
125
|
* JSON.parse (older agents send JSON strings) — keeps the card from
|
|
126
126
|
* crashing on undefined input.
|
|
127
127
|
*
|
|
128
|
-
* 信息保真
|
|
128
|
+
* 信息保真 kind 命中 KIND_TO_TOOL 时用映射名渲染结构化面板;
|
|
129
129
|
* title 与 name 不同时把 title 存到 content block 的 summary 字段
|
|
130
130
|
* (渲染层做面板副标题)。
|
|
131
131
|
*/
|
|
@@ -144,7 +144,7 @@ export declare abstract class BaseAcpTranslator {
|
|
|
144
144
|
/** tool_use name recorded at tool_call time (kind mapping wins, else title). */
|
|
145
145
|
protected getToolCallName(toolCallId: string): string | undefined;
|
|
146
146
|
/**
|
|
147
|
-
* kind → canonical tool name (信息保真
|
|
147
|
+
* kind → canonical tool name (信息保真). Shared by the initial tool_call
|
|
148
148
|
* handler and the tool_call_update patch builder so both agree on the name.
|
|
149
149
|
*/
|
|
150
150
|
protected toolNameFromKind(kind: string | undefined): string | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NotificationMethod, ServerRequestMethod, SessionEventType, } from './protocol-types.js';
|
|
2
2
|
/**
|
|
3
|
-
* ACP tool kind →
|
|
3
|
+
* ACP tool kind → 常用工具名映射(信息保真,数据驱动无 agent 分支)。
|
|
4
4
|
* kind 命中映射时用映射名渲染结构化面板;否则回落 title(维持现状)。
|
|
5
5
|
*/
|
|
6
6
|
const KIND_TO_TOOL = {
|
|
@@ -199,7 +199,7 @@ export class BaseAcpTranslator {
|
|
|
199
199
|
// Shared notification handlers
|
|
200
200
|
// =========================================================================
|
|
201
201
|
/**
|
|
202
|
-
* plan update → PlanEvent(信息保真
|
|
202
|
+
* plan update → PlanEvent(信息保真 不再显式丢弃)。
|
|
203
203
|
* ACP PlanEntry: { content, priority, status }(status: pending/in_progress/
|
|
204
204
|
* completed)。基类具体方法:kimi/opencode wire 结构相同,直接复用。
|
|
205
205
|
*/
|
|
@@ -224,7 +224,7 @@ export class BaseAcpTranslator {
|
|
|
224
224
|
* JSON.parse (older agents send JSON strings) — keeps the card from
|
|
225
225
|
* crashing on undefined input.
|
|
226
226
|
*
|
|
227
|
-
* 信息保真
|
|
227
|
+
* 信息保真 kind 命中 KIND_TO_TOOL 时用映射名渲染结构化面板;
|
|
228
228
|
* title 与 name 不同时把 title 存到 content block 的 summary 字段
|
|
229
229
|
* (渲染层做面板副标题)。
|
|
230
230
|
*/
|
|
@@ -327,7 +327,7 @@ export class BaseAcpTranslator {
|
|
|
327
327
|
return this.toolCallNameByCallId.get(toolCallId);
|
|
328
328
|
}
|
|
329
329
|
/**
|
|
330
|
-
* kind → canonical tool name (信息保真
|
|
330
|
+
* kind → canonical tool name (信息保真). Shared by the initial tool_call
|
|
331
331
|
* handler and the tool_call_update patch builder so both agree on the name.
|
|
332
332
|
*/
|
|
333
333
|
toolNameFromKind(kind) {
|
|
@@ -45,7 +45,7 @@ export const RpcErrorCode = {
|
|
|
45
45
|
//
|
|
46
46
|
// REAL envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}}.
|
|
47
47
|
// The discriminator field is `update.sessionUpdate` — there is NO nested
|
|
48
|
-
// `event.type` / `event.delta` (
|
|
48
|
+
// `event.type` / `event.delta` (旧实现读 params.event,
|
|
49
49
|
// 真实通知全部静默丢弃)。
|
|
50
50
|
// =============================================================================
|
|
51
51
|
/** Notification method names for ACP protocol. */
|
|
@@ -51,7 +51,7 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
|
|
|
51
51
|
/** Set up the turn: acquire + thread|session new/resume + prompt. */
|
|
52
52
|
protected abstract setupTurn(message: string, opts: SpawnOptions): Promise<void>;
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* 本连接的协议停止通道。
|
|
55
55
|
*
|
|
56
56
|
* win32 上「优雅停止」没有可拦截的跨进程 SIGTERM,只能经 agent 自有协议通道
|
|
57
57
|
* 请求对方收摊。子类把这个方法的结果交给 ConnectionManager.stopper,由后者按
|
|
@@ -28,7 +28,7 @@ export class ConnectionBasedRunner {
|
|
|
28
28
|
this.turnIdleTimeoutMs = opts.turnIdleTimeoutMs ?? defaultTurnIdleTimeoutMs;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* 本连接的协议停止通道。
|
|
32
32
|
*
|
|
33
33
|
* win32 上「优雅停止」没有可拦截的跨进程 SIGTERM,只能经 agent 自有协议通道
|
|
34
34
|
* 请求对方收摊。子类把这个方法的结果交给 ConnectionManager.stopper,由后者按
|
|
@@ -141,7 +141,7 @@ export class ConnectionBasedRunner {
|
|
|
141
141
|
async stop(_opts) {
|
|
142
142
|
if (!this._isRunning)
|
|
143
143
|
return;
|
|
144
|
-
//
|
|
144
|
+
// 启动阶段(client/session 未建立)也必须记录停止意图,否则 /stop 被吞、
|
|
145
145
|
// turn 照常执行。executeTurn 在 setup 完成后检查 stopRequested 补发 cancel。
|
|
146
146
|
this.stopRequested = true;
|
|
147
147
|
this.forceFinish = true;
|
|
@@ -4,7 +4,7 @@ import type { Readable } from 'node:stream';
|
|
|
4
4
|
interface JSONLStreamOptions {
|
|
5
5
|
onParseError?: (line: string) => void;
|
|
6
6
|
/**
|
|
7
|
-
* Pause stdout once `lineQueue` exceeds this depth (
|
|
7
|
+
* Pause stdout once `lineQueue` exceeds this depth (backpressure).
|
|
8
8
|
* Default 100 (matches production; tests may pass a smaller value).
|
|
9
9
|
*/
|
|
10
10
|
pauseThreshold?: number;
|
|
@@ -23,7 +23,7 @@ interface JSONLStreamOptions {
|
|
|
23
23
|
* - JSON parse errors: when `options.onParseError` is provided, bad lines are
|
|
24
24
|
* forwarded to the callback; otherwise a warning is logged via getLogger().warn
|
|
25
25
|
* and the line is skipped.
|
|
26
|
-
* - Optional backpressure
|
|
26
|
+
* - Optional backpressure: when `pauseThreshold` is set, stdout is
|
|
27
27
|
* paused once the parsed-line queue exceeds the threshold and resumed once
|
|
28
28
|
* the consumer drains it back below `resumeThreshold`, bounding memory in
|
|
29
29
|
* long runs where the producer (agent stdout) outpaces the consumer (card
|
|
@@ -8,7 +8,7 @@ import { getLogger } from '../../logger/index.js';
|
|
|
8
8
|
* - JSON parse errors: when `options.onParseError` is provided, bad lines are
|
|
9
9
|
* forwarded to the callback; otherwise a warning is logged via getLogger().warn
|
|
10
10
|
* and the line is skipped.
|
|
11
|
-
* - Optional backpressure
|
|
11
|
+
* - Optional backpressure: when `pauseThreshold` is set, stdout is
|
|
12
12
|
* paused once the parsed-line queue exceeds the threshold and resumed once
|
|
13
13
|
* the consumer drains it back below `resumeThreshold`, bounding memory in
|
|
14
14
|
* long runs where the producer (agent stdout) outpaces the consumer (card
|
|
@@ -22,7 +22,7 @@ import { getLogger } from '../../logger/index.js';
|
|
|
22
22
|
*/
|
|
23
23
|
export function createJSONLStream(stdout, options) {
|
|
24
24
|
const lineQueue = [];
|
|
25
|
-
//
|
|
25
|
+
// accumulate partial-line fragments in an array instead of repeatedly
|
|
26
26
|
// string-concatenating a growing `partialLine`. A huge single JSON line
|
|
27
27
|
// (large tool_result / text_end) spanning many chunks caused O(n²) copies:
|
|
28
28
|
// each chunk rebuilt `partialLine + text` over the whole accumulated prefix.
|
|
@@ -30,7 +30,7 @@ export function createJSONLStream(stdout, options) {
|
|
|
30
30
|
// and push complete lines; the residual tail stays as array fragments until
|
|
31
31
|
// the next newline or stream end. Net allocation: O(n) instead of O(n²).
|
|
32
32
|
const partialFragments = [];
|
|
33
|
-
//
|
|
33
|
+
// accumulated byte length of partialFragments. Backpressure (above)
|
|
34
34
|
// only counts COMPLETE lines in lineQueue; a never-newline stream (malformed
|
|
35
35
|
// output, binary garbage) accumulates fragments without ever completing a
|
|
36
36
|
// line, so pause never fires and memory grows unbounded. Cap the partial
|
|
@@ -41,13 +41,13 @@ export function createJSONLStream(stdout, options) {
|
|
|
41
41
|
const completeLines = [];
|
|
42
42
|
let done = false;
|
|
43
43
|
let resolveNext = null;
|
|
44
|
-
//
|
|
44
|
+
// backpressure thresholds. Default matches production (100).
|
|
45
45
|
const pauseThreshold = options?.pauseThreshold ?? 100;
|
|
46
46
|
// Default low-water mark is half the high-water mark for meaningful hysteresis
|
|
47
47
|
// (avoids per-line pause/resume thrashing when callers don't set it explicitly).
|
|
48
48
|
let resumeThreshold = options?.resumeThreshold ?? Math.max(0, Math.floor(pauseThreshold / 2));
|
|
49
49
|
// Clamp misconfiguration: resumeThreshold > pauseThreshold would resume at queue
|
|
50
|
-
// high water then immediately re-pause on the next line → thrash (
|
|
50
|
+
// high water then immediately re-pause on the next line → thrash (nit).
|
|
51
51
|
if (resumeThreshold > pauseThreshold)
|
|
52
52
|
resumeThreshold = pauseThreshold;
|
|
53
53
|
let paused = false;
|
|
@@ -58,7 +58,7 @@ export function createJSONLStream(stdout, options) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
// Flush any trailing partial line and mark the stream finished. Shared by the
|
|
61
|
-
// 'end'/'close'/'error' handlers so flush logic lives in one place (
|
|
61
|
+
// 'end'/'close'/'error' handlers so flush logic lives in one place (nit).
|
|
62
62
|
function flushAndFinish() {
|
|
63
63
|
if (partialFragments.length > 0) {
|
|
64
64
|
const trimmed = partialFragments.join('').trim();
|
|
@@ -70,7 +70,7 @@ export function createJSONLStream(stdout, options) {
|
|
|
70
70
|
done = true;
|
|
71
71
|
notify();
|
|
72
72
|
}
|
|
73
|
-
//
|
|
73
|
+
// flush an oversize partial line as a bad line. When partialFragments
|
|
74
74
|
// exceeds the byte cap, the partial line is treated as malformed (a single
|
|
75
75
|
// JSON line should never reach 10MB) and reported via onParseError rather
|
|
76
76
|
// than accumulated indefinitely. Resets the buffer so a pathological stream
|
|
@@ -93,7 +93,7 @@ export function createJSONLStream(stdout, options) {
|
|
|
93
93
|
}
|
|
94
94
|
// Apply backpressure after pushing new lines: pause stdout when the queue
|
|
95
95
|
// exceeds the high-water mark so the producer stops feeding until the
|
|
96
|
-
// consumer drains it.
|
|
96
|
+
// consumer drains it.bounds memory in long runs.
|
|
97
97
|
function maybePause() {
|
|
98
98
|
if (pauseThreshold > 0 && !paused && lineQueue.length > pauseThreshold) {
|
|
99
99
|
paused = true;
|
|
@@ -110,7 +110,7 @@ export function createJSONLStream(stdout, options) {
|
|
|
110
110
|
}
|
|
111
111
|
stdout.on('data', (chunk) => {
|
|
112
112
|
const text = chunk.toString('utf-8');
|
|
113
|
-
//
|
|
113
|
+
// split on newlines. The first segment continues the pending partial
|
|
114
114
|
// line (joined with accumulated fragments — one join per chunk, not per
|
|
115
115
|
// byte); subsequent segments are complete lines. The last segment is the
|
|
116
116
|
// new partial tail (kept as a single fragment).
|
|
@@ -119,7 +119,7 @@ export function createJSONLStream(stdout, options) {
|
|
|
119
119
|
// No newline in this chunk: the whole chunk is a partial fragment.
|
|
120
120
|
partialFragments.push(segments[0]);
|
|
121
121
|
partialBytes += segments[0].length;
|
|
122
|
-
//
|
|
122
|
+
// cap the partial line. A never-newline stream would otherwise
|
|
123
123
|
// accumulate fragments without bound (backpressure only counts complete
|
|
124
124
|
// lines). Flush as a bad line once the byte cap is exceeded.
|
|
125
125
|
if (partialBytes > PARTIAL_BYTE_LIMIT) {
|
|
@@ -140,7 +140,7 @@ export function createJSONLStream(stdout, options) {
|
|
|
140
140
|
// Last segment is the new partial tail.
|
|
141
141
|
partialFragments.push(segments[segments.length - 1]);
|
|
142
142
|
partialBytes = segments[segments.length - 1].length;
|
|
143
|
-
//
|
|
143
|
+
// the trailing partial tail can itself exceed the cap if a single
|
|
144
144
|
// chunk is larger than 10MB with no internal newline.
|
|
145
145
|
if (partialBytes > PARTIAL_BYTE_LIMIT) {
|
|
146
146
|
flushOversizePartial();
|
|
@@ -162,7 +162,7 @@ export function createJSONLStream(stdout, options) {
|
|
|
162
162
|
stdout.on('close', () => {
|
|
163
163
|
flushAndFinish();
|
|
164
164
|
});
|
|
165
|
-
//
|
|
165
|
+
// handle stdout 'error' so the generator terminates instead of
|
|
166
166
|
// hanging forever on the `await new Promise` below. Without this, a stdout
|
|
167
167
|
// error (e.g. broken pipe, runner crash) leaves the consumer awaiting a
|
|
168
168
|
// notify that never comes → bridge serial queue blocks permanently. We do
|
|
@@ -44,7 +44,7 @@ export declare class JsonRpcClient<InitializeResult = unknown> {
|
|
|
44
44
|
* Replace the per-run hooks (used by the runner to bridge
|
|
45
45
|
* notifications/server requests into the active turn's translator).
|
|
46
46
|
* Connection-layer hooks (slot cleanup / onConnectionLost) are preserved:
|
|
47
|
-
*
|
|
47
|
+
* an overwrite here previously silenced onClose slot cleanup,
|
|
48
48
|
* leaving dead slots behind after the server process was killed.
|
|
49
49
|
*/
|
|
50
50
|
setHooks(hooks: ClientHooks): void;
|