lark-remote 0.5.10 → 0.5.13

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 (126) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.en.md +1 -14
  3. package/README.md +1 -14
  4. package/dist/binder.js +7 -1
  5. package/dist/bridge/approval-coordinator.d.ts +1 -0
  6. package/dist/bridge/approval-coordinator.js +12 -24
  7. package/dist/bridge/index.d.ts +7 -0
  8. package/dist/bridge/index.js +89 -230
  9. package/dist/bridge/run-approval.d.ts +15 -0
  10. package/dist/bridge/run-approval.js +41 -0
  11. package/dist/bridge/run-usage.d.ts +46 -0
  12. package/dist/bridge/run-usage.js +96 -0
  13. package/dist/bridge/session-writeback.d.ts +18 -0
  14. package/dist/bridge/session-writeback.js +36 -0
  15. package/dist/card/approval-render.d.ts +14 -0
  16. package/dist/card/approval-render.js +155 -29
  17. package/dist/card/bash-card-session.js +10 -5
  18. package/dist/card/bash-renderer.js +11 -23
  19. package/dist/card/card-budget.js +24 -4
  20. package/dist/card/card-session.d.ts +2 -0
  21. package/dist/card/card-session.js +20 -22
  22. package/dist/card/card-shared.d.ts +4 -4
  23. package/dist/card/card-shared.js +3 -9
  24. package/dist/card/collapsible.js +3 -2
  25. package/dist/card/final-answer-overflow.d.ts +7 -0
  26. package/dist/card/final-answer-overflow.js +46 -0
  27. package/dist/card/run-card-session.js +2 -7
  28. package/dist/card/run-renderer.d.ts +14 -0
  29. package/dist/card/run-renderer.js +150 -10
  30. package/dist/card/run-state.d.ts +12 -1
  31. package/dist/card/run-state.js +73 -34
  32. package/dist/common/text-width.js +5 -0
  33. package/dist/common/tool-summary.js +4 -2
  34. package/dist/config/codex-config.js +5 -3
  35. package/dist/config/index.d.ts +4 -2
  36. package/dist/config/index.js +19 -5
  37. package/dist/inbound/turn-assembler.js +6 -0
  38. package/dist/index.js +15 -391
  39. package/dist/instance-lock.d.ts +9 -1
  40. package/dist/instance-lock.js +42 -7
  41. package/dist/message-handlers.d.ts +26 -0
  42. package/dist/message-handlers.js +275 -0
  43. package/dist/platform/identity.js +4 -3
  44. package/dist/platform/path.js +3 -1
  45. package/dist/platform/terminator-win32.d.ts +13 -1
  46. package/dist/platform/terminator-win32.js +4 -14
  47. package/dist/router/card-action-payload.d.ts +1 -1
  48. package/dist/router/card-action-payload.js +1 -1
  49. package/dist/router/commands/ls.d.ts +9 -0
  50. package/dist/router/commands/ls.js +243 -0
  51. package/dist/router/commands/order.d.ts +12 -0
  52. package/dist/router/commands/order.js +178 -0
  53. package/dist/router/commands/resume.d.ts +47 -0
  54. package/dist/router/commands/resume.js +246 -0
  55. package/dist/router/commands/ws.d.ts +15 -0
  56. package/dist/router/commands/ws.js +189 -0
  57. package/dist/router/config/claude.js +3 -3
  58. package/dist/router/config/codex.js +2 -6
  59. package/dist/router/config/common/turn-idle.d.ts +2 -0
  60. package/dist/router/config/common/turn-idle.js +9 -0
  61. package/dist/router/config/kimi.js +2 -6
  62. package/dist/router/config/opencode.js +2 -6
  63. package/dist/router/index.d.ts +14 -22
  64. package/dist/router/index.js +93 -1133
  65. package/dist/router/paging.d.ts +13 -0
  66. package/dist/router/paging.js +20 -0
  67. package/dist/router/path-validation.d.ts +27 -0
  68. package/dist/router/path-validation.js +45 -0
  69. package/dist/runner/claude/runner.d.ts +3 -4
  70. package/dist/runner/claude/runner.js +4 -5
  71. package/dist/runner/claude/session.js +1 -1
  72. package/dist/runner/codex/app-server/runner.js +12 -0
  73. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  74. package/dist/runner/codex/app-server/translator.js +65 -9
  75. package/dist/runner/common/acp/base-translator.d.ts +15 -8
  76. package/dist/runner/common/acp/base-translator.js +38 -7
  77. package/dist/runner/common/jsonl-stream.js +12 -1
  78. package/dist/runner/common/jsonrpc/connection-manager.js +11 -1
  79. package/dist/runner/common/jsonrpc/transport.d.ts +2 -1
  80. package/dist/runner/common/jsonrpc/transport.js +7 -1
  81. package/dist/runner/common/spawning-runner.js +10 -4
  82. package/dist/runner/common/turn-started.d.ts +2 -0
  83. package/dist/runner/common/turn-started.js +10 -0
  84. package/dist/runner/dsh/approval.js +16 -3
  85. package/dist/runner/dsh/auth.js +2 -3
  86. package/dist/runner/dsh/client.d.ts +32 -7
  87. package/dist/runner/dsh/client.js +115 -149
  88. package/dist/runner/dsh/daemon.d.ts +0 -2
  89. package/dist/runner/dsh/daemon.js +1 -6
  90. package/dist/runner/dsh/errors.d.ts +5 -0
  91. package/dist/runner/dsh/errors.js +11 -0
  92. package/dist/runner/dsh/exit-cancel.d.ts +0 -2
  93. package/dist/runner/dsh/exit-cancel.js +63 -24
  94. package/dist/runner/dsh/home.d.ts +2 -0
  95. package/dist/runner/dsh/home.js +8 -0
  96. package/dist/runner/dsh/inflight.d.ts +0 -2
  97. package/dist/runner/dsh/inflight.js +1 -1
  98. package/dist/runner/dsh/mux-stream.d.ts +26 -0
  99. package/dist/runner/dsh/mux-stream.js +157 -0
  100. package/dist/runner/dsh/runner.d.ts +7 -0
  101. package/dist/runner/dsh/runner.js +42 -5
  102. package/dist/runner/dsh/translator.d.ts +0 -10
  103. package/dist/runner/dsh/translator.js +1 -1
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.js +42 -5
  106. package/dist/runner/kimi/acp/translator.d.ts +8 -0
  107. package/dist/runner/kimi/acp/translator.js +11 -12
  108. package/dist/runner/opencode/acp/translator.js +1 -12
  109. package/dist/runner/pi/rpc/runner.js +33 -5
  110. package/dist/runner/pi/rpc/translator.d.ts +2 -8
  111. package/dist/runner/pi/rpc/translator.js +2 -7
  112. package/dist/runner/registry.d.ts +0 -19
  113. package/dist/runner/registry.js +0 -32
  114. package/dist/runner/resolve-agent-choices.js +5 -1
  115. package/dist/runner/sync-agent-choices.js +4 -1
  116. package/dist/runner/types.d.ts +17 -1
  117. package/dist/session/claude/session-index.js +13 -4
  118. package/dist/session/codex/rollout-reader.js +51 -26
  119. package/dist/session/common/usage-accumulator.d.ts +10 -1
  120. package/dist/session/common/usage-accumulator.js +10 -2
  121. package/dist/session/dsh/sessions.js +29 -9
  122. package/dist/session/kimi/sessions.js +11 -7
  123. package/dist/session/opencode/sessions.js +15 -7
  124. package/dist/session/pi/sessions.js +3 -2
  125. package/dist/session/session-store.js +4 -3
  126. package/package.json +1 -1
@@ -0,0 +1,157 @@
1
+ import { getLogger } from '../../logger/index.js';
2
+ import { DshError } from './errors.js';
3
+ export async function* openMuxStream(opts) {
4
+ const cookie = await opts.authCookie();
5
+ const socket = opts.webSocketFactory(`${opts.baseUrl.replace(/^http/, 'ws')}/api/remote.mux`, cookie ? { headers: { cookie } } : undefined);
6
+ const streamId = `lr-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
7
+ const queue = [];
8
+ let waiter = null;
9
+ let ended = false;
10
+ let failure;
11
+ let opened = false;
12
+ const settle = (frame) => {
13
+ if (waiter) {
14
+ const resolve = waiter;
15
+ waiter = null;
16
+ resolve(frame);
17
+ return;
18
+ }
19
+ if (frame)
20
+ queue.push(frame);
21
+ else
22
+ ended = true;
23
+ };
24
+ const connectTimer = setTimeout(() => {
25
+ if (opened || ended)
26
+ return;
27
+ failure = new DshError('transport', 'DSH remote mux WebSocket connect timeout');
28
+ ended = true;
29
+ settle(undefined);
30
+ try {
31
+ socket.close();
32
+ }
33
+ catch {
34
+ /* ignore close error */
35
+ }
36
+ }, opts.requestTimeoutMs);
37
+ connectTimer.unref?.();
38
+ socket.onopen = () => {
39
+ opened = true;
40
+ clearTimeout(connectTimer);
41
+ if (ended)
42
+ return;
43
+ try {
44
+ socket.send(JSON.stringify({
45
+ type: 'open',
46
+ streamId,
47
+ endpoint: opts.endpoint,
48
+ payload: opts.payload,
49
+ }));
50
+ }
51
+ catch (err) {
52
+ failure = new DshError('transport', `DSH remote mux send error: ${err.message}`);
53
+ ended = true;
54
+ settle(undefined);
55
+ }
56
+ };
57
+ socket.onmessage = (event) => {
58
+ if (ended)
59
+ return;
60
+ const text = typeof event.data === 'string'
61
+ ? event.data
62
+ : Buffer.isBuffer(event.data)
63
+ ? event.data.toString('utf8')
64
+ : String(event.data);
65
+ let message;
66
+ try {
67
+ message = JSON.parse(text);
68
+ }
69
+ catch {
70
+ getLogger().warn('[dsh] dropped malformed remote mux frame');
71
+ return;
72
+ }
73
+ if (message.streamId !== streamId)
74
+ return;
75
+ if (message.type === 'item' && message.value) {
76
+ settle(message.value);
77
+ return;
78
+ }
79
+ if (message.type === 'end') {
80
+ ended = true;
81
+ settle(undefined);
82
+ return;
83
+ }
84
+ if (message.type === 'error') {
85
+ failure = new DshError(message.error?.code ?? 'stream', message.error?.message ?? 'DSH remote stream failed', message.error?.details ?? {});
86
+ ended = true;
87
+ settle(undefined);
88
+ }
89
+ };
90
+ socket.onerror = () => {
91
+ if (ended)
92
+ return;
93
+ failure = new DshError('transport', 'DSH remote mux WebSocket error');
94
+ ended = true;
95
+ settle(undefined);
96
+ };
97
+ socket.onunexpectedresponse = (event) => {
98
+ if (ended)
99
+ return;
100
+ const status = event.statusCode;
101
+ failure = new DshError('http', `DSH remote mux WebSocket HTTP ${status === undefined ? 'error' : status}`, status === undefined ? {} : { status });
102
+ ended = true;
103
+ settle(undefined);
104
+ };
105
+ socket.onclose = () => {
106
+ if (ended)
107
+ return;
108
+ ended = true;
109
+ settle(undefined);
110
+ };
111
+ const onAbort = () => {
112
+ if (ended)
113
+ return;
114
+ ended = true;
115
+ settle(undefined);
116
+ try {
117
+ socket.close();
118
+ }
119
+ catch {
120
+ /* ignore close error */
121
+ }
122
+ };
123
+ opts.signal.addEventListener('abort', onAbort, { once: true });
124
+ const next = () => new Promise((resolve) => {
125
+ if (queue.length > 0) {
126
+ resolve(queue.shift());
127
+ return;
128
+ }
129
+ if (ended) {
130
+ resolve(undefined);
131
+ return;
132
+ }
133
+ waiter = resolve;
134
+ });
135
+ try {
136
+ while (!opts.signal.aborted) {
137
+ const frame = await next();
138
+ if (frame === undefined) {
139
+ if (failure && !opts.signal.aborted)
140
+ throw failure;
141
+ return;
142
+ }
143
+ yield frame;
144
+ }
145
+ }
146
+ finally {
147
+ clearTimeout(connectTimer);
148
+ opts.signal.removeEventListener('abort', onAbort);
149
+ try {
150
+ socket.close();
151
+ }
152
+ catch {
153
+ /* ignore close error */
154
+ }
155
+ }
156
+ }
157
+ //# sourceMappingURL=mux-stream.js.map
@@ -230,6 +230,13 @@ export declare class DshRunner implements AgentRunner {
230
230
  * that is the only client-side protection against a late orphan turn.
231
231
  */
232
232
  private findPendingPromptWithin;
233
+ /**
234
+ * cancel 确认 closed 后的收尾:cancel 是 keepInbox,prompt 可能仍在 inbox。
235
+ * 在则撤回(防被下一个 turn 隐形认领),确认不在(已被本 turn 消费)或
236
+ * 撤回成功才清 inflight 标记;探测/撤回失败保留标记,交给下次启动的
237
+ * reconcile(findPendingPrompt 落空即自然清除,无副作用)。
238
+ */
239
+ private withdrawPendingPromptAfterCancel;
233
240
  private removePendingPrompt;
234
241
  /** One cancel attempt, then one retry (the request is idempotent when idle). */
235
242
  private cancelWithRetry;
@@ -1216,8 +1216,14 @@ export class DshRunner {
1216
1216
  const state = await this.waitForClosed(sessionId, deadline);
1217
1217
  if (state === 'closed') {
1218
1218
  releaseGate();
1219
- if (promptRequestId)
1220
- this.inflight?.clear(sessionId, promptRequestId);
1219
+ // cancel 是 keepInbox:prompt 可能仍在 inbox 里,等下一个无人订阅的
1220
+ // turn 认领(探测超时/迟到 admit 退到 cancel 分支时,
1221
+ // 旧实现直接清 inflight 标记——被停的 prompt 会被后续 turn 隐形执行
1222
+ //(耗 token、改工作区,卡片不可见),启动对账安全网也被解除)。
1223
+ // closed 后补一次撤回:在 inbox 就撤走,确认不在才清标记。
1224
+ if (promptRequestId) {
1225
+ await this.withdrawPendingPromptAfterCancel(sessionId, promptRequestId, deadline);
1226
+ }
1221
1227
  getLogger().info(`[dsh] cancelled session ${sessionId}`);
1222
1228
  return { status: 'cancelled', sessionId };
1223
1229
  }
@@ -1233,8 +1239,9 @@ export class DshRunner {
1233
1239
  const retryState = await this.waitForClosed(sessionId, deadline);
1234
1240
  releaseGate();
1235
1241
  if (retryState === 'closed') {
1236
- if (promptRequestId)
1237
- this.inflight?.clear(sessionId, promptRequestId);
1242
+ if (promptRequestId) {
1243
+ await this.withdrawPendingPromptAfterCancel(sessionId, promptRequestId, deadline);
1244
+ }
1238
1245
  return { status: 'cancelled', sessionId };
1239
1246
  }
1240
1247
  getLogger().warn(`[dsh] stop unconfirmed session=${sessionId}: turn still open`);
@@ -1268,12 +1275,42 @@ export class DshRunner {
1268
1275
  return undefined;
1269
1276
  }
1270
1277
  catch (err) {
1278
+ // 单次探测错误只消耗一次尝试(不提前放弃):探测失败就 return 会把
1279
+ // 仍在 inbox 的 prompt 交给 cancel 分支处理——cancel 是 keepInbox,
1280
+ // prompt 会留到下一个无人订阅的 turn 被隐形执行。
1271
1281
  getLogger().warn(`[dsh] stop inbox probe failed session=${sessionId}: ${err.message}`);
1272
- return undefined;
1282
+ continue;
1273
1283
  }
1274
1284
  }
1275
1285
  return undefined;
1276
1286
  }
1287
+ /**
1288
+ * cancel 确认 closed 后的收尾:cancel 是 keepInbox,prompt 可能仍在 inbox。
1289
+ * 在则撤回(防被下一个 turn 隐形认领),确认不在(已被本 turn 消费)或
1290
+ * 撤回成功才清 inflight 标记;探测/撤回失败保留标记,交给下次启动的
1291
+ * reconcile(findPendingPrompt 落空即自然清除,无副作用)。
1292
+ */
1293
+ async withdrawPendingPromptAfterCancel(sessionId, promptRequestId, deadline) {
1294
+ try {
1295
+ const itemId = await this.client.findPendingPrompt(sessionId, promptRequestId, {
1296
+ timeoutMs: this.stopProbeTimeout(deadline),
1297
+ });
1298
+ if (itemId) {
1299
+ const removed = await this.client.removeQueuedPrompt(sessionId, itemId, {
1300
+ timeoutMs: this.stopProbeTimeout(deadline),
1301
+ });
1302
+ if (removed) {
1303
+ getLogger().info(`[dsh] stop withdrew inbox prompt left behind by cancel session=${sessionId}`);
1304
+ this.inflight?.clear(sessionId, promptRequestId);
1305
+ }
1306
+ return;
1307
+ }
1308
+ this.inflight?.clear(sessionId, promptRequestId);
1309
+ }
1310
+ catch (err) {
1311
+ getLogger().warn(`[dsh] post-cancel withdraw probe failed session=${sessionId}: ${err.message}`);
1312
+ }
1313
+ }
1277
1314
  async removePendingPrompt(sessionId, itemId, deadline) {
1278
1315
  try {
1279
1316
  return await this.client.removeQueuedPrompt(sessionId, itemId, {
@@ -25,13 +25,6 @@
25
25
  */
26
26
  import type { AgentEvent, ResultEvent } from '../types.js';
27
27
  import type { DshAssistantStreamFrame, DshSessionEvent, DshTokenUsage } from './types.js';
28
- /** 本 turn 各采样用量之和(input 是非缓存输入,与 ccusage/ResultEvent 口径一致)。 */
29
- interface UsageTotals {
30
- input: number;
31
- output: number;
32
- cacheRead: number;
33
- cacheWrite: number;
34
- }
35
28
  export declare class DshTranslator {
36
29
  /** 本 turn 累计用量:DSH 每次模型调用(step)一条 usage,工具往返 = 多条。 */
37
30
  private turnUsage;
@@ -63,6 +56,3 @@ export declare class DshTranslator {
63
56
  }
64
57
  /** Map DSH TokenUsage → ResultEvent.usage (cacheWrite = cache creation). */
65
58
  export declare function mapUsage(u: DshTokenUsage): NonNullable<ResultEvent['usage']>;
66
- /** Map a turn's summed usage → ResultEvent.usage (cacheWrite = cache creation). */
67
- export declare function mapAggregatedUsage(t: UsageTotals): NonNullable<ResultEvent['usage']>;
68
- export {};
@@ -265,7 +265,7 @@ export function mapUsage(u) {
265
265
  });
266
266
  }
267
267
  /** Map a turn's summed usage → ResultEvent.usage (cacheWrite = cache creation). */
268
- export function mapAggregatedUsage(t) {
268
+ function mapAggregatedUsage(t) {
269
269
  return {
270
270
  input_tokens: t.input,
271
271
  output_tokens: t.output,
@@ -15,6 +15,6 @@ export { KimiAcpRunner } from './kimi/index.js';
15
15
  export { DshRunner } from './dsh/index.js';
16
16
  export { BashProcessRunner, type BashRunner } from './bash/index.js';
17
17
  export { AgentRegistry } from './registry.js';
18
- export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, SessionSummary, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, NoticeEvent, SessionInfoEvent, ToolHint, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, GoalView, GoalStateEvent, } from './types.js';
18
+ export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, SessionSummary, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, FinalAnswerEvent, NoticeEvent, SessionInfoEvent, ToolHint, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, GoalView, GoalStateEvent, } from './types.js';
19
19
  export { resolveAgentChoices } from './resolve-agent-choices.js';
20
20
  export { syncAgentChoices } from './sync-agent-choices.js';
@@ -144,6 +144,11 @@ export class KimiAcpRunner extends BaseAcpRunner {
144
144
  */
145
145
  compactBaselineRecords = [];
146
146
  /** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
147
+ // 已知限制(待验证 kimi 0.4x 契约):句柄只在
148
+ // terminal/release 与连接关闭(cleanupTerminals)时删除。若服务端只 kill
149
+ // 不 release(中断 turn 的场景),已退出进程的句柄(含 ≤4MB output 缓冲)
150
+ // 会滞留到连接释放。kimi 契约上工具结束会发 release(mock 测试同驱动);
151
+ // 若真机确认存在只 kill 不 release 的路径,再补 exit 后延迟回收。
147
152
  terminals = new Map();
148
153
  /** terminalId 自增序号(term-1, term-2, …)。 */
149
154
  terminalSeq = 0;
@@ -422,6 +427,11 @@ export class KimiAcpRunner extends BaseAcpRunner {
422
427
  while (true) {
423
428
  if (this.forceFinish || this.stopRequested)
424
429
  return 'stopped';
430
+ // turn 已死亡(failTurn,如连接关闭)时不再轮询:forceFinish 与
431
+ // stopRequested 均为 false 的死亡路径会让循环空转到 10 分钟沉默超时,
432
+ // 结果还被 isCurrent() 丢弃——纯浪费的每秒文件读。
433
+ if (!this._isRunning)
434
+ return 'unknown';
425
435
  const records = reader.call(this.sessionReader, sessionId, opts.cwd);
426
436
  if (records.length !== lastTotalCount) {
427
437
  lastTotalCount = records.length;
@@ -506,8 +516,15 @@ export class KimiAcpRunner extends BaseAcpRunner {
506
516
  // (error = "rejected"), otherwise the server hangs waiting for a
507
517
  // response. For question elicitation (§5.4), auto-respond cancelled.
508
518
  const rpParams = params;
509
- if (method === ServerRequestMethod.REQUEST_PERMISSION &&
510
- (rpParams.isQuestion || !rpParams.toolCall)) {
519
+ // elicitation/create 的表单属性全不可解析时(translator 返回 [])同样回
520
+ // cancelled 而不是 METHOD_NOT_FOUND:§5.4 的不变量是「识别到非工具审批
521
+ // 形态 → 自动回应 cancelled,防止服务端挂起」;错误响应虽也能解悬挂,
522
+ // 但 kimi 侧可能把 JSON-RPC error 当工具级失败让整个 turn 报错,而不是
523
+ // 跳过提问继续。
524
+ const isQuestionElicitation = method === ServerRequestMethod.REQUEST_PERMISSION &&
525
+ (rpParams.isQuestion || !rpParams.toolCall);
526
+ const isUnparseableElicitationForm = method === ServerRequestMethod.ELICITATION_CREATE;
527
+ if (isQuestionElicitation || isUnparseableElicitationForm) {
511
528
  this.currentClient?.respond(id, { outcome: { outcome: 'cancelled' } });
512
529
  getLogger().info(`[${this.logTag}] auto-responded cancelled to question elicitation requestId=${id}`);
513
530
  }
@@ -633,8 +650,27 @@ export class KimiAcpRunner extends BaseAcpRunner {
633
650
  proc.once('exit', (code, signal) => {
634
651
  handle.exitCode = code;
635
652
  handle.signal = signal;
636
- resolveExit?.();
637
- emitResult();
653
+ // 'exit' 可先于 stdout 缓冲派发:立刻 emit 会按旧快照推送 tool_result,
654
+ // 卡片面板丢尾部输出(terminal/output 反向 RPC 读到的是补齐后的全量,
655
+ // 只影响卡片)。等流 end 或 200ms 宽限——不等 'close':它要等所有共享
656
+ // 管道的子孙进程退出,`nohup &` 类后台任务会把它拖到永远。
657
+ //
658
+ // wait_for_exit 必须在 tool_result 入队之后才 resolve:服务端拿到响应
659
+ // 后会立刻 release + end_turn,先 resolve 会让回合结果早于延迟推送的
660
+ // tool_result 到达,Bash 输出被丢弃(卡片面板空输出)。
661
+ const flushThenEmit = () => {
662
+ clearTimeout(emitTimer);
663
+ emitResult();
664
+ resolveExit?.();
665
+ };
666
+ const emitTimer = setTimeout(flushThenEmit, 200);
667
+ emitTimer.unref?.();
668
+ if (proc.stdout && !proc.stdout.destroyed) {
669
+ proc.stdout.once('end', flushThenEmit);
670
+ }
671
+ else {
672
+ flushThenEmit();
673
+ }
638
674
  });
639
675
  proc.once('error', (err) => {
640
676
  getLogger().warn(`[${this.logTag}] terminal process error: ${err.message}`);
@@ -642,8 +678,9 @@ export class KimiAcpRunner extends BaseAcpRunner {
642
678
  handle.exitCode = 1;
643
679
  handle.signal = null;
644
680
  }
645
- resolveExit?.();
681
+ // 同 exit:tool_result 先入队,再放行 wait_for_exit。
646
682
  emitResult();
683
+ resolveExit?.();
647
684
  });
648
685
  const terminalId = `term-${++this.terminalSeq}`;
649
686
  this.terminals.set(terminalId, handle);
@@ -29,6 +29,14 @@ import { type RequestPermissionParams, type AgentMessageChunkEvent, type AgentTh
29
29
  import { BaseAcpTranslator, type AcpTranslatorEvent } from '../../common/acp/base-translator.js';
30
30
  export declare class KimiAcpTranslator extends BaseAcpTranslator {
31
31
  protected readonly logTag = "kimi-acp-translator";
32
+ /**
33
+ * 非终态 tool_call_update 不附带空 tool_result:kimi 非 Bash 工具的
34
+ * in-progress 只带流式 args(rawOutput 缺省 → content ''),空 tool_result
35
+ * 会让 reducer 把运行中的工具提前置 ✅ + 空输出(终态到达后自愈,但终态
36
+ * 丢失时面板停在错误的「已完成」)。opencode 走基类默认 true(其
37
+ * in-progress 带输出快照,seam 契约钉死要发)。
38
+ */
39
+ protected emitToolResultForNonTerminalUpdate(content: unknown): boolean;
32
40
  /** Accumulated text per tool-call or message (keyed by a logical item id). */
33
41
  private textByItem;
34
42
  /** Accumulated thinking per item. */
@@ -42,6 +42,16 @@ const THINKING_ITEM_ID = 'thinking';
42
42
  // =============================================================================
43
43
  export class KimiAcpTranslator extends BaseAcpTranslator {
44
44
  logTag = 'kimi-acp-translator';
45
+ /**
46
+ * 非终态 tool_call_update 不附带空 tool_result:kimi 非 Bash 工具的
47
+ * in-progress 只带流式 args(rawOutput 缺省 → content ''),空 tool_result
48
+ * 会让 reducer 把运行中的工具提前置 ✅ + 空输出(终态到达后自愈,但终态
49
+ * 丢失时面板停在错误的「已完成」)。opencode 走基类默认 true(其
50
+ * in-progress 带输出快照,seam 契约钉死要发)。
51
+ */
52
+ emitToolResultForNonTerminalUpdate(content) {
53
+ return !(content === undefined || content === null || content === '');
54
+ }
45
55
  /** Accumulated text per tool-call or message (keyed by a logical item id). */
46
56
  textByItem = new Map();
47
57
  /** Accumulated thinking per item. */
@@ -149,18 +159,7 @@ export class KimiAcpTranslator extends BaseAcpTranslator {
149
159
  // 才提供「本会话总是允许」(acceptForSession);否则与旧行为一致。
150
160
  availableDecisions: deriveAcpAvailableDecisions(params.options ?? []),
151
161
  };
152
- return [
153
- {
154
- type: 'approval_requested',
155
- requestId,
156
- kind: 'command',
157
- threadId: params.sessionId,
158
- turnId: '',
159
- itemId: '',
160
- view,
161
- timestamp: new Date().toISOString(),
162
- },
163
- ];
162
+ return this.approvalRequestedEvents(requestId, params.sessionId, view);
164
163
  }
165
164
  handleOtherServerRequest(requestId, method, params) {
166
165
  if (method !== ServerRequestMethod.ELICITATION_CREATE) {
@@ -189,18 +189,7 @@ export class OpencodeAcpTranslator extends BaseAcpTranslator {
189
189
  // 才提供「本会话总是允许」(acceptForSession)。
190
190
  availableDecisions: deriveAcpAvailableDecisions(params.options ?? []),
191
191
  };
192
- return [
193
- {
194
- type: 'approval_requested',
195
- requestId,
196
- kind: 'command',
197
- threadId: params.sessionId,
198
- turnId: '',
199
- itemId: '',
200
- view,
201
- timestamp: new Date().toISOString(),
202
- },
203
- ];
192
+ return this.approvalRequestedEvents(requestId, params.sessionId, view);
204
193
  }
205
194
  }
206
195
  // =============================================================================
@@ -113,9 +113,24 @@ export class PiRpcRunner extends ConnectionBasedRunner {
113
113
  const client = await this.manager.acquire(opts.cwd, { sessionId: opts.sessionId });
114
114
  this.manager.notifyActivity(opts.cwd);
115
115
  this.currentClient = client;
116
+ // 轮次代际锚点(与 BaseAcpRunner 同口径):pi 的 shouldReuse 要求
117
+ // sessionId 匹配,/new、/resume 切换后的首条消息会让 acquire **同步
118
+ // dispose** 旧连接;terminator 等到进程退出后,旧 client 上残留的上一轮
119
+ // onClose 闭包会把假错误推进新 turn 的共享队列(用户消息未发出即丢失)。
120
+ // 旧轮迟到的 agent_settled 同理(新 turn 空转立即"成功")。所有 hook
121
+ // 落地前必须过 isCurrentTurn。
122
+ const generation = this.turnGeneration;
116
123
  client.setHooks({
117
- onEvent: (evt) => this.handleEvent(evt),
118
- onClose: () => this.failTurn('Pi RPC connection closed'),
124
+ onEvent: (evt) => {
125
+ if (!this.isCurrentTurn(generation, client))
126
+ return;
127
+ this.handleEvent(evt);
128
+ },
129
+ onClose: () => {
130
+ if (!this.isCurrentTurn(generation, client))
131
+ return;
132
+ this.failTurn('Pi RPC connection closed');
133
+ },
119
134
  });
120
135
  let sessionId = opts.sessionId;
121
136
  if (!sessionId) {
@@ -157,9 +172,19 @@ export class PiRpcRunner extends ConnectionBasedRunner {
157
172
  const client = await this.manager.acquire(opts.cwd, { sessionId: opts.sessionId });
158
173
  this.manager.notifyActivity(opts.cwd);
159
174
  this.currentClient = client;
175
+ // 同 setupTurn:hook 带轮次代际锚点,旧连接的迟到回调不得进入本轮。
176
+ const generation = this.turnGeneration;
160
177
  client.setHooks({
161
- onEvent: (evt) => this.handleEvent(evt),
162
- onClose: () => this.failTurn('Pi RPC connection closed'),
178
+ onEvent: (evt) => {
179
+ if (!this.isCurrentTurn(generation, client))
180
+ return;
181
+ this.handleEvent(evt);
182
+ },
183
+ onClose: () => {
184
+ if (!this.isCurrentTurn(generation, client))
185
+ return;
186
+ this.failTurn('Pi RPC connection closed');
187
+ },
163
188
  });
164
189
  this.activeSessionId = opts.sessionId;
165
190
  const translator = new PiRpcTranslator();
@@ -170,7 +195,10 @@ export class PiRpcRunner extends ConnectionBasedRunner {
170
195
  this.currentTurnId = turnId;
171
196
  this.pushEvents([translator.produceTurnStarted(opts.sessionId, turnId)]);
172
197
  this.promptSettled = false;
173
- const response = await client.request({ type: 'compact' });
198
+ // compact 是完整的 LLM 请求(同步跑完才回响应,协议文档自述),大上下文
199
+ // 压缩轻松超过默认 60s 请求超时——超时只会出假错误卡,压缩仍在后台完成。
200
+ // 活性交给 turn idle 看门狗与 /stop(与 ACP 侧 turn 级请求同口径)。
201
+ const response = await client.request({ type: 'compact' }, Number.POSITIVE_INFINITY);
174
202
  this.promptSettled = true;
175
203
  // Guard: a stop may have already ended this turn (deferred-stop during
176
204
  // compaction) — don't push a stale result into a (possibly reused) queue.
@@ -10,15 +10,9 @@
10
10
  * `result` from that response.
11
11
  * - `agent_settled` is ignored for compaction turns (no double result).
12
12
  */
13
- import type { AgentEvent, ResultEvent } from '../../types.js';
13
+ import type { AgentEvent, ResultEvent, TurnStartedEvent } from '../../types.js';
14
14
  import { type PiRpcEvent, type PiRpcResponse } from './protocol-types.js';
15
- export interface PiRpcTurnStartedEvent {
16
- type: 'turn_started';
17
- threadId: string;
18
- turnId: string;
19
- operationKind: 'turn' | 'compaction';
20
- timestamp?: string;
21
- }
15
+ export type PiRpcTurnStartedEvent = TurnStartedEvent;
22
16
  export type PiRpcTranslatorEvent = AgentEvent | PiRpcTurnStartedEvent;
23
17
  export declare class PiRpcTranslator {
24
18
  private content;
@@ -10,6 +10,7 @@
10
10
  * `result` from that response.
11
11
  * - `agent_settled` is ignored for compaction turns (no double result).
12
12
  */
13
+ import { makeTurnStartedEvent } from '../../common/turn-started.js';
13
14
  export class PiRpcTranslator {
14
15
  content = [];
15
16
  currentContentIndex = -1;
@@ -135,13 +136,7 @@ export class PiRpcTranslator {
135
136
  /** Self-produced turn_started (no wire equivalent). */
136
137
  produceTurnStarted(sessionId, turnId) {
137
138
  this.sessionId = sessionId;
138
- return {
139
- type: 'turn_started',
140
- threadId: sessionId,
141
- turnId,
142
- operationKind: this.operationKind === 'compact' ? 'compaction' : 'turn',
143
- timestamp: new Date().toISOString(),
144
- };
139
+ return makeTurnStartedEvent(sessionId, turnId, this.operationKind);
145
140
  }
146
141
  /** Result for a completed normal turn (agent_settled). */
147
142
  produceResultFromSettled() {
@@ -14,28 +14,9 @@ import type { AgentKind, Runner } from './index.js';
14
14
  */
15
15
  export declare class AgentRegistry {
16
16
  private readonly factories;
17
- private readonly displayNames;
18
17
  /** Container for runtime config updates (pi provider dynamic reload). */
19
18
  private configContainer?;
20
19
  register(kind: AgentKind, factory: (cwd: string) => Runner): void;
21
- /**
22
- * 注册 agent 的显示名。应在 register(kind, factory) 后调用。
23
- * 用于 agentDisplayName 等全局查询,实现单点真相。
24
- */
25
- registerDisplayName(kind: AgentKind, displayName: string): void;
26
- /**
27
- * 获取 agent 显示名。未注册时返回原始 kind。
28
- */
29
- getDisplayName(kind: AgentKind): string;
30
- /**
31
- * 设置全局 registry 实例,供 agentDisplayName 等全局函数使用。
32
- * 在 index.ts 初始化所有 agents 后调用。
33
- */
34
- static setGlobalInstance(reg: AgentRegistry | undefined): void;
35
- /**
36
- * 获取全局 registry 实例。
37
- */
38
- static getGlobalInstance(): AgentRegistry | undefined;
39
20
  /**
40
21
  * Set config container reference. Call after bridge config is updated.
41
22
  * Factory functions can read latest config from here.
@@ -1,8 +1,3 @@
1
- /**
2
- * 单例 registry 实例,供 agentDisplayName 等全局查询使用。
3
- * 在 index.ts 初始化后设置。
4
- */
5
- let globalRegistry;
6
1
  /**
7
2
  * Registry of agent factories, keyed by `AgentKind`. `index.ts` registers one
8
3
  * factory per available agent at startup; `Bridge.getRunner(cwd)` looks
@@ -18,38 +13,11 @@ let globalRegistry;
18
13
  */
19
14
  export class AgentRegistry {
20
15
  factories = new Map();
21
- displayNames = new Map();
22
16
  /** Container for runtime config updates (pi provider dynamic reload). */
23
17
  configContainer;
24
18
  register(kind, factory) {
25
19
  this.factories.set(kind, factory);
26
20
  }
27
- /**
28
- * 注册 agent 的显示名。应在 register(kind, factory) 后调用。
29
- * 用于 agentDisplayName 等全局查询,实现单点真相。
30
- */
31
- registerDisplayName(kind, displayName) {
32
- this.displayNames.set(kind, displayName);
33
- }
34
- /**
35
- * 获取 agent 显示名。未注册时返回原始 kind。
36
- */
37
- getDisplayName(kind) {
38
- return this.displayNames.get(kind) ?? kind;
39
- }
40
- /**
41
- * 设置全局 registry 实例,供 agentDisplayName 等全局函数使用。
42
- * 在 index.ts 初始化所有 agents 后调用。
43
- */
44
- static setGlobalInstance(reg) {
45
- globalRegistry = reg;
46
- }
47
- /**
48
- * 获取全局 registry 实例。
49
- */
50
- static getGlobalInstance() {
51
- return globalRegistry;
52
- }
53
21
  /**
54
22
  * Set config container reference. Call after bridge config is updated.
55
23
  * Factory functions can read latest config from here.
@@ -25,7 +25,11 @@ export function resolveAgentChoices(config) {
25
25
  const source = agentChoices;
26
26
  for (const { configKey, choicesKey } of fields) {
27
27
  const value = source[choicesKey];
28
- if (!target[configKey] && value) {
28
+ // 只填「配置里根本没写」的字段(undefined):显式清空(config.yaml 写了
29
+ // `model: ""`)是用户意志,旧真值判断 `!target[configKey]` 会把空串当
30
+ // 「未配置」,用残留 choices 把清空值填回去——重启后永远回不到默认。
31
+ // value 为空串同样不填(choices 里记录的「跟随默认」)。
32
+ if (target[configKey] === undefined && value) {
29
33
  target[configKey] = value;
30
34
  }
31
35
  }
@@ -14,7 +14,10 @@ export function syncAgentChoices(config, agent) {
14
14
  const source = agentCfg;
15
15
  for (const { configKey, choicesKey } of fields) {
16
16
  const value = source[configKey];
17
- if (value)
17
+ // 显式空串也要同步(用户清空 = 回到默认):旧真值判断不同步空串,
18
+ // agentChoices 里残留上一次的选择——/config 卡片显示的与实际生效的
19
+ // 配置从此分叉,重启后 resolve 还会把残留填回 config。
20
+ if (value !== undefined)
18
21
  agentChoices[choicesKey] = value;
19
22
  }
20
23
  return updated;