lark-remote 0.5.19 → 0.5.21

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 (136) hide show
  1. package/CHANGELOG.md +29 -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 +72 -46
  10. package/dist/bridge/index.d.ts +106 -153
  11. package/dist/bridge/index.js +400 -1000
  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 +141 -130
  18. package/dist/card/card-budget.js +123 -98
  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 +114 -159
  24. package/dist/card/run-state.d.ts +1 -15
  25. package/dist/card/run-state.js +358 -292
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +21 -17
  28. package/dist/card/usage-format.d.ts +22 -7
  29. package/dist/card/usage-format.js +119 -100
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/common/text-width.js +36 -24
  33. package/dist/common/tool-summary.js +132 -90
  34. package/dist/config/codex-config.js +130 -106
  35. package/dist/config/index.d.ts +16 -2
  36. package/dist/config/index.js +23 -8
  37. package/dist/connector/index.js +6 -6
  38. package/dist/error-classification.js +3 -2
  39. package/dist/inbound/placeholder.d.ts +1 -1
  40. package/dist/inbound/placeholder.js +1 -1
  41. package/dist/index.js +387 -281
  42. package/dist/logger/index.js +6 -6
  43. package/dist/message-handlers.d.ts +0 -5
  44. package/dist/message-handlers.js +254 -237
  45. package/dist/model-catalog.d.ts +2 -2
  46. package/dist/model-catalog.js +3 -8
  47. package/dist/platform/identity.js +1 -1
  48. package/dist/platform/path.d.ts +1 -1
  49. package/dist/platform/path.js +1 -1
  50. package/dist/platform/sleep-blocker.js +1 -1
  51. package/dist/platform/terminator-win32.js +2 -1
  52. package/dist/router/commands/ls.d.ts +3 -0
  53. package/dist/router/commands/ls.js +256 -175
  54. package/dist/router/commands/order.js +184 -159
  55. package/dist/router/commands/resume.js +190 -152
  56. package/dist/router/commands/ws.js +219 -174
  57. package/dist/router/config/dsh.d.ts +4 -8
  58. package/dist/router/config/dsh.js +3 -10
  59. package/dist/router/config/kimi.js +1 -1
  60. package/dist/router/config/types.d.ts +1 -1
  61. package/dist/router/config-diff.js +1 -1
  62. package/dist/router/index.d.ts +56 -11
  63. package/dist/router/index.js +401 -391
  64. package/dist/runner/bash/runner.js +2 -4
  65. package/dist/runner/claude/session.d.ts +1 -1
  66. package/dist/runner/claude/session.js +6 -4
  67. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  68. package/dist/runner/codex/app-server/runner.js +30 -25
  69. package/dist/runner/codex/app-server/translator.d.ts +2 -5
  70. package/dist/runner/codex/app-server/translator.js +291 -238
  71. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  72. package/dist/runner/common/acp/acp-approval.js +1 -1
  73. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  74. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  75. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  76. package/dist/runner/common/acp/base-translator.js +50 -26
  77. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  78. package/dist/runner/common/acp/protocol-types.js +2 -2
  79. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  80. package/dist/runner/common/connection-based-runner.js +32 -2
  81. package/dist/runner/common/jsonl-stream.js +154 -187
  82. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  83. package/dist/runner/common/jsonrpc/client.js +43 -12
  84. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  85. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  86. package/dist/runner/common/spawning-runner.d.ts +6 -1
  87. package/dist/runner/common/spawning-runner.js +9 -4
  88. package/dist/runner/common/turn-started.d.ts +8 -0
  89. package/dist/runner/common/turn-started.js +11 -0
  90. package/dist/runner/dsh/approval.js +84 -51
  91. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  92. package/dist/runner/dsh/catalog-probe.js +10 -0
  93. package/dist/runner/dsh/client.d.ts +7 -11
  94. package/dist/runner/dsh/client.js +356 -289
  95. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  96. package/dist/runner/dsh/exit-cancel.js +2 -2
  97. package/dist/runner/dsh/inflight.d.ts +1 -1
  98. package/dist/runner/dsh/inflight.js +1 -1
  99. package/dist/runner/dsh/mux-stream.js +28 -8
  100. package/dist/runner/dsh/runner.d.ts +44 -4
  101. package/dist/runner/dsh/runner.js +296 -199
  102. package/dist/runner/dsh/translator.d.ts +1 -31
  103. package/dist/runner/dsh/translator.js +180 -178
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  106. package/dist/runner/kimi/acp/runner.js +82 -46
  107. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  108. package/dist/runner/kimi/acp/translator.js +3 -4
  109. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  110. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  111. package/dist/runner/opencode/acp/runner.js +4 -4
  112. package/dist/runner/pi/rpc/client.js +4 -13
  113. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  114. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  115. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  116. package/dist/runner/pi/rpc/runner.js +27 -42
  117. package/dist/runner/pi/rpc/translator.js +61 -60
  118. package/dist/runner/types.d.ts +2 -2
  119. package/dist/runner/types.js +1 -1
  120. package/dist/session/claude/sessions.d.ts +45 -0
  121. package/dist/session/claude/sessions.js +160 -122
  122. package/dist/session/codex/rollout-reader.js +181 -182
  123. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  124. package/dist/session/common/throw-session-list-error.js +16 -0
  125. package/dist/session/dsh/sessions.js +291 -235
  126. package/dist/session/kimi/sessions.d.ts +40 -8
  127. package/dist/session/kimi/sessions.js +373 -301
  128. package/dist/session/opencode/sessions.d.ts +142 -1
  129. package/dist/session/opencode/sessions.js +378 -321
  130. package/dist/session/pi/sessions.js +4 -5
  131. package/dist/session/session-store.d.ts +9 -1
  132. package/dist/session/session-store.js +99 -99
  133. package/dist/update/version-check.js +2 -1
  134. package/dist/workspace/index.d.ts +1 -1
  135. package/dist/workspace/index.js +1 -1
  136. package/package.json +1 -1
@@ -65,7 +65,7 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
65
65
  * 自身半死:opencode v2 的 location 在每轮拉起的 `opencode acp` 子进程里 boot,
66
66
  * boot 期间后台服务不可用会让它长期停在未激活,后续轮次复用同一连接只会重复
67
67
  * 失败——用户重启 opencode service 也治不好,因为连接缓存在本进程里
68
- * (2026-10-30 现场:4 次重试全失败,重启服务与 lark-remote 都无效)。丢掉连接
68
+ * (4 次重试全失败,重启服务与 lark-remote 都无效)。丢掉连接
69
69
  * 后下一轮重新 spawn,拿到的是新进程的新状态。
70
70
  *
71
71
  * 只对声明为可重试的错误生效:kimi 等沿用默认 `isRetryableSessionSetupError`
@@ -123,6 +123,21 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
123
123
  notifyIdle(cwd) {
124
124
  this.connectionManager.notifyIdle(cwd);
125
125
  }
126
+ notifyConnectionActivity(cwd) {
127
+ this.connectionManager.notifyActivity(cwd);
128
+ }
129
+ /** Wire the acquired client's hooks into the turn dispatcher (anchor-guarded). */
130
+ installTurnHooks(anchor) {
131
+ anchor.client.setHooks({
132
+ onNotification: this.forCurrentTurn(anchor, 'notification', (method, params) => this.handleNotification(method, params)),
133
+ onServerRequest: this.forCurrentTurn(anchor, 'server request', (id, method, params) => this.handleServerRequest(id, method, params)),
134
+ onClose: this.forCurrentTurn(anchor, 'connection close', () => {
135
+ getLogger().warn(`[${this.logTag}] client connection closed`);
136
+ this.onConnectionClosed();
137
+ this.failTurn(this.connectionClosedMessage);
138
+ }),
139
+ });
140
+ }
126
141
  async disposeConnections() {
127
142
  await this.beforeDisposeConnections();
128
143
  await this.connectionManager.disposeAll();
@@ -201,36 +216,12 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
201
216
  * session/resume (existing sessionId) → applyTurnSettings → session/prompt.
202
217
  */
203
218
  async setupTurn(message, opts) {
204
- // 本轮回调归属锚点:代际 + client 都不可变。旧轮迟到的 prompt
205
- // 结果/通知在新轮开始时 stopRequested 已被重置,只有这两个值能识别它。
206
- const generation = this.turnGeneration;
207
- const client = await this.connectionManager.acquire(opts.cwd);
208
- this.connectionManager.notifyActivity(opts.cwd);
209
- this.currentClient = client;
219
+ // 本轮回调归属锚点(代际 + client)由 acquireForTurn 捕获:旧轮迟到的
220
+ // prompt 结果/通知在新轮开始时 stopRequested 已被重置,只有这对值能识别它。
221
+ const anchor = await this.acquireForTurn(opts.cwd, () => this.connectionManager.acquire(opts.cwd));
210
222
  this.onConnectionAcquired(opts);
211
- client.setHooks({
212
- onNotification: (method, params) => {
213
- if (!this.isCurrentTurn(generation, client)) {
214
- getLogger().debug(`[${this.logTag}] dropped notification from a finished turn`);
215
- return;
216
- }
217
- this.handleNotification(method, params);
218
- },
219
- onServerRequest: (id, method, params) => {
220
- if (!this.isCurrentTurn(generation, client)) {
221
- getLogger().debug(`[${this.logTag}] dropped server request from a finished turn`);
222
- return;
223
- }
224
- this.handleServerRequest(id, method, params);
225
- },
226
- onClose: () => {
227
- if (!this.isCurrentTurn(generation, client))
228
- return;
229
- getLogger().warn(`[${this.logTag}] client connection closed`);
230
- this.onConnectionClosed();
231
- this.failTurn(this.connectionClosedMessage);
232
- },
233
- });
223
+ this.installTurnHooks(anchor);
224
+ const { client } = anchor;
234
225
  let sessionId;
235
226
  if (opts.sessionId) {
236
227
  const resumeParams = {
@@ -267,19 +258,7 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
267
258
  this.promptSettled = false;
268
259
  const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
269
260
  this.promptSent = true;
270
- promptPromise.then((result) => {
271
- if (!this.isCurrentTurn(generation, client)) {
272
- getLogger().debug(`[${this.logTag}] dropped late prompt result from a finished turn`);
273
- return;
274
- }
275
- this.handlePromptResolved(result, translator, sessionId);
276
- }, (err) => {
277
- if (!this.isCurrentTurn(generation, client)) {
278
- getLogger().debug(`[${this.logTag}] dropped late prompt rejection from a finished turn`);
279
- return;
280
- }
281
- this.handlePromptRejected(err, translator, sessionId, '');
282
- });
261
+ promptPromise.then(this.forCurrentTurn(anchor, 'late prompt result', (result) => this.handlePromptResolved(result, translator, sessionId)), this.forCurrentTurn(anchor, 'late prompt rejection', (err) => this.handlePromptRejected(err, translator, sessionId, '')));
283
262
  }
284
263
  /**
285
264
  * Run a compact operation on the current session: resume → '/compact'
@@ -287,29 +266,11 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
287
266
  */
288
267
  async *runCompact(_message, opts) {
289
268
  yield* this.executeTurn(opts, async () => {
290
- const generation = this.turnGeneration;
291
- const client = await this.connectionManager.acquire(opts.cwd);
292
- this.connectionManager.notifyActivity(opts.cwd);
293
- this.currentClient = client;
269
+ // 同 setupTurn:acquireForTurn 捕获代际锚点,installTurnHooks 守卫 dispatch。
270
+ const anchor = await this.acquireForTurn(opts.cwd, () => this.connectionManager.acquire(opts.cwd));
294
271
  this.onConnectionAcquired(opts);
295
- client.setHooks({
296
- onNotification: (method, params) => {
297
- if (!this.isCurrentTurn(generation, client))
298
- return;
299
- this.handleNotification(method, params);
300
- },
301
- onServerRequest: (id, method, params) => {
302
- if (!this.isCurrentTurn(generation, client))
303
- return;
304
- this.handleServerRequest(id, method, params);
305
- },
306
- onClose: () => {
307
- if (!this.isCurrentTurn(generation, client))
308
- return;
309
- this.onConnectionClosed();
310
- this.failTurn(this.connectionClosedMessage);
311
- },
312
- });
272
+ this.installTurnHooks(anchor);
273
+ const { client } = anchor;
313
274
  const sessionId = opts.sessionId;
314
275
  if (!sessionId) {
315
276
  throw new Error('compact requires a sessionId');
@@ -4,7 +4,7 @@
4
4
  * Both agents' session/update notification envelopes, prompt-response
5
5
  * stopReason mapping, tool_call/tool_call_update normalization, usage_update
6
6
  * occupancy bookkeeping and self-produced turn_started events are
7
- * line-for-line identical — extracted here in the 2026-09 round-2
7
+ * line-for-line identical — extracted here in the round-2
8
8
  * simplification. What genuinely diverges is the content channel:
9
9
  * kimi accumulates wire deltas into turn_diff snapshots (snapshot semantics,
10
10
  * see KimiAcpTranslator header), opencode forwards `assistant` deltas
@@ -21,6 +21,16 @@
21
21
  */
22
22
  import type { AgentEvent, ApprovalRequestedEvent, ApprovalView, ResultEvent, TurnStartedEvent } from '../../types.js';
23
23
  import { type AgentMessageChunkEvent, type AgentThoughtChunkEvent, type ToolCallEvent, type ToolCallUpdateEvent, type UsageUpdateEvent, type RequestPermissionParams, type AcpPlanEvent } from './protocol-types.js';
24
+ /**
25
+ * Plan entries → 「图标 + 标题」markdown 清单(每行一条,`\n` join)。
26
+ * ACP 与 codex 双抄收编(R5);subtitle 拼接、trim、空清单语义等差异留在
27
+ * 各自调用点。
28
+ */
29
+ export declare function renderPlanMarkdown(entries: ReadonlyArray<{
30
+ title: string;
31
+ subtitle?: string;
32
+ status?: string;
33
+ }>): string;
24
34
  /** Self-produced turn_started event (no wire equivalent — produced on prompt start). */
25
35
  export type AcpTurnStartedEvent = TurnStartedEvent;
26
36
  /**
@@ -58,7 +68,7 @@ export declare abstract class BaseAcpTranslator {
58
68
  /**
59
69
  * Latest streaming-args text per tool call (keyed by wire toolCallId).
60
70
  * kimi ACP sends the permission request BEFORE tool.call.started (verified
61
- * live against kimi 0.42.0, 2026-09-13): the request_permission toolCall
71
+ * against kimi 0.42.0): the request_permission toolCall
62
72
  * carries only title + a truncated summary in content, NO rawInput — the
63
73
  * full args only ever reach the client as accumulated text in the
64
74
  * tool_call/tool_call_update streaming deltas. Tracking them here lets the
@@ -1,4 +1,4 @@
1
- import { makeTurnStartedEvent } from '../turn-started.js';
1
+ import { produceTurnStartedEvent } from '../turn-started.js';
2
2
  import { NotificationMethod, ServerRequestMethod, SessionEventType, } from './protocol-types.js';
3
3
  /**
4
4
  * ACP tool kind → 常用工具名映射(信息保真,数据驱动无 agent 分支)。
@@ -12,6 +12,32 @@ const KIND_TO_TOOL = {
12
12
  search: 'Grep',
13
13
  fetch: 'WebFetch',
14
14
  };
15
+ /**
16
+ * Plan 条目 status → 渲染图标。未知/缺省 status 一律按 pending 渲染——
17
+ * 两处调用点(ACP handlePlan / codex handleTurnPlanUpdated)注释都点名防
18
+ * wire 枚举漂移,新增枚举值时只改这张表。
19
+ */
20
+ const PLAN_STATUS_ICONS = {
21
+ completed: '✅',
22
+ in_progress: '🔄',
23
+ pending: '⬜',
24
+ error: '❌',
25
+ };
26
+ /**
27
+ * Plan entries → 「图标 + 标题」markdown 清单(每行一条,`\n` join)。
28
+ * ACP 与 codex 双抄收编(R5);subtitle 拼接、trim、空清单语义等差异留在
29
+ * 各自调用点。
30
+ */
31
+ export function renderPlanMarkdown(entries) {
32
+ return entries
33
+ .map((entry) => {
34
+ const status = entry.status && entry.status in PLAN_STATUS_ICONS
35
+ ? entry.status
36
+ : 'pending';
37
+ return `${PLAN_STATUS_ICONS[status]} ${entry.title}${entry.subtitle ? ` — ${entry.subtitle}` : ''}`;
38
+ })
39
+ .join('\n');
40
+ }
15
41
  /**
16
42
  * Join the text payloads of a tool_call(/_update) content array
17
43
  * (`[{type:'content', content:{type:'text', text}}]` entries). Returns
@@ -42,6 +68,19 @@ function stringifyUnknown(value) {
42
68
  return String(value);
43
69
  }
44
70
  }
71
+ /**
72
+ * 记录最新值并给 map 封顶(删最老 key,近似 LRU)。
73
+ * `trackToolCallArgs` 与 `trackToolCallName` 曾是逐字相同的两段,合并到这里。
74
+ */
75
+ function trackBounded(map, key, value) {
76
+ map.delete(key);
77
+ map.set(key, value);
78
+ if (map.size > 100) {
79
+ const oldest = map.keys().next().value;
80
+ if (oldest !== undefined)
81
+ map.delete(oldest);
82
+ }
83
+ }
45
84
  // =============================================================================
46
85
  // Translator base class
47
86
  // =============================================================================
@@ -59,7 +98,7 @@ export class BaseAcpTranslator {
59
98
  /**
60
99
  * Latest streaming-args text per tool call (keyed by wire toolCallId).
61
100
  * kimi ACP sends the permission request BEFORE tool.call.started (verified
62
- * live against kimi 0.42.0, 2026-09-13): the request_permission toolCall
101
+ * against kimi 0.42.0): the request_permission toolCall
63
102
  * carries only title + a truncated summary in content, NO rawInput — the
64
103
  * full args only ever reach the client as accumulated text in the
65
104
  * tool_call/tool_call_update streaming deltas. Tracking them here lets the
@@ -145,8 +184,9 @@ export class BaseAcpTranslator {
145
184
  * is sent (no wire notification for this — self-produced).
146
185
  */
147
186
  produceTurnStarted(sessionId, turnId) {
148
- this.currentTurnId = turnId;
149
- return makeTurnStartedEvent(sessionId, turnId, this.operationKind);
187
+ return produceTurnStartedEvent(sessionId, turnId, this.operationKind, (_sessionId, t) => {
188
+ this.currentTurnId = t;
189
+ });
150
190
  }
151
191
  approvalRequestedEvents(requestId, sessionId, view) {
152
192
  return [
@@ -213,16 +253,12 @@ export class BaseAcpTranslator {
213
253
  * completed)。基类具体方法:kimi/opencode wire 结构相同,直接复用。
214
254
  */
215
255
  handlePlan(event) {
216
- const ICON = { completed: '✅', in_progress: '🔄', pending: '⬜', error: '❌' };
217
256
  const entries = Array.isArray(event.entries) ? event.entries : [];
218
- const markdown = entries
219
- .map((e) => {
257
+ const markdown = renderPlanMarkdown(entries.map((e) => {
220
258
  const entry = e;
221
- const status = entry.status && entry.status in ICON ? entry.status : 'pending';
222
- return `${ICON[status]} ${entry.content ?? ''}`;
223
- })
224
- .join('\n')
225
- .trim();
259
+ // ACP 条目的正文字段是 content(codex 是 title),无 subtitle。
260
+ return { title: entry.content ?? '', status: entry.status };
261
+ })).trim();
226
262
  if (!markdown)
227
263
  return [];
228
264
  return [{ type: 'plan', plan: markdown, timestamp: new Date().toISOString() }];
@@ -384,23 +420,11 @@ export class BaseAcpTranslator {
384
420
  }
385
421
  /** Record latest args text, bounded so a long session cannot grow it forever. */
386
422
  trackToolCallArgs(toolCallId, argsText) {
387
- this.toolCallArgsByCallId.delete(toolCallId);
388
- this.toolCallArgsByCallId.set(toolCallId, argsText);
389
- if (this.toolCallArgsByCallId.size > 100) {
390
- const oldest = this.toolCallArgsByCallId.keys().next().value;
391
- if (oldest !== undefined)
392
- this.toolCallArgsByCallId.delete(oldest);
393
- }
423
+ trackBounded(this.toolCallArgsByCallId, toolCallId, argsText);
394
424
  }
395
425
  /** Record tool_use name, bounded to keep a long session from growing it. */
396
426
  trackToolCallName(toolCallId, name) {
397
- this.toolCallNameByCallId.delete(toolCallId);
398
- this.toolCallNameByCallId.set(toolCallId, name);
399
- if (this.toolCallNameByCallId.size > 100) {
400
- const oldest = this.toolCallNameByCallId.keys().next().value;
401
- if (oldest !== undefined)
402
- this.toolCallNameByCallId.delete(oldest);
403
- }
427
+ trackBounded(this.toolCallNameByCallId, toolCallId, name);
404
428
  }
405
429
  /**
406
430
  * tool_result content extraction. Default: rawOutput passthrough (kimi
@@ -238,7 +238,7 @@ export interface PermissionOption {
238
238
  export interface RequestPermissionParams {
239
239
  sessionId: string;
240
240
  /**
241
- * Approval target. Divergence by server (2026-09-13 live probes):
241
+ * Approval target. Divergence by server:
242
242
  * - kimi 0.42: {toolCallId, title: toolName, content: [truncated summary
243
243
  * text]} — NO rawInput; the approval gate fires BEFORE tool.call.started,
244
244
  * so the full args only exist in the pre-approval tool_call/_update
@@ -43,7 +43,7 @@ export const RpcErrorCode = {
43
43
  // =============================================================================
44
44
  // Notifications (server → client, via session/update)
45
45
  // Shape source: kimi-code/packages/acp-server/src/events-map.ts
46
- // (each constructor below) + 2026-08-15 live smoke test against kimi 0.36.0
46
+ // (each constructor below) + verified against kimi 0.36.0
47
47
  //
48
48
  // REAL envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}}.
49
49
  // The discriminator field is `update.sessionUpdate` — there is NO nested
@@ -72,7 +72,7 @@ export const SessionEventType = {
72
72
  // Server Requests (approval — reverse RPC)
73
73
  // Shape source: kimi-code/packages/acp-server/src/approval.ts:28-29
74
74
  // options: [{optionId, name, kind}]; optionId is opaque, echo back as-is
75
- // + 2026-08-15 live smoke test: approve_once / approve_always / reject
75
+ // + verified against kimi 0.36.0: approve_once / approve_always / reject
76
76
  // =============================================================================
77
77
  export const ServerRequestMethod = {
78
78
  REQUEST_PERMISSION: 'session/request_permission',
@@ -18,6 +18,18 @@ import type { AgentKind } from '../../agent-kinds.js';
18
18
  import type { AgentSessionReader } from '../../session-contract.js';
19
19
  import type { AgentEvent, AgentRunner, AgentStatusInfo, SpawnOptions } from '../types.js';
20
20
  import type { AgentStopper } from '../../platform/agent-stopper.js';
21
+ /**
22
+ * 本轮回调的归属锚点(generation + client),由 {@link ConnectionBasedRunner.acquireForTurn}
23
+ * 捕获。两个值在整轮内不可变:旧轮迟到的回调(prompt 结果 / 通知 / onClose)在
24
+ * 新轮开始时 `stopRequested` 已被重置,只有这对值能识别它们——pi 的教训是旧 client
25
+ * 上残留的上一轮 onClose 闭包会把假错误推进新 turn 的共享队列(用户消息未发出即
26
+ * 丢失),迟到的 agent_settled 同理(新 turn 空转立即「成功」)。所有 hook 的事件
27
+ * 落地前必须过 {@link ConnectionBasedRunner.isCurrentTurn} 守卫。
28
+ */
29
+ export interface TurnAnchor<TClient> {
30
+ readonly client: TClient;
31
+ readonly generation: number;
32
+ }
21
33
  export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent> implements AgentRunner {
22
34
  readonly kind: AgentKind;
23
35
  readonly sessionReader: AgentSessionReader;
@@ -115,7 +127,7 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
115
127
  /**
116
128
  * Mark this runner as evicted from the bridge's runner cache while a run may
117
129
  * still be holding/creating connections on it (queue-advance race during
118
- * interruptCurrentRun's await window, 2026-10-11). The active run's finally
130
+ * interruptCurrentRun's await window). The active run's finally
119
131
  * disposes connections eagerly instead of arming the idle TTL timer —
120
132
  * otherwise the agent server process lingers for up to 30 minutes holding
121
133
  * its session write lock (codex: "thread already has an active writer").
@@ -145,6 +157,25 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
145
157
  * server request 一律只记 debug,不得进入共享 notificationQueue。
146
158
  */
147
159
  protected isCurrentTurn(generation: number, client: TClient | null): boolean;
160
+ /**
161
+ * Acquire the per-turn connection and capture the callback anchor
162
+ * ({@link TurnAnchor}): acquire → activity ping → currentClient → anchor.
163
+ * Shared preamble of every setupTurn / runCompact; the acquired anchor is
164
+ * what the turn's hooks (and prompt-settle callbacks) validate against.
165
+ *
166
+ * `acquire` is a closure so subclasses keep their own acquire signatures
167
+ * (pi passes the spawn-bound `{ sessionId }`).
168
+ */
169
+ protected acquireForTurn(cwd: string, acquire: () => Promise<TClient>): Promise<TurnAnchor<TClient>>;
170
+ /** Per-cwd activity ping on the subclass connection manager (mirror of notifyIdle). */
171
+ protected abstract notifyConnectionActivity(cwd: string): void;
172
+ /**
173
+ * Wrap a per-turn callback with the isCurrentTurn guard: events from a
174
+ * finished turn are dropped with a debug log and never reach the turn
175
+ * dispatcher. 6 处守卫原有的日志粒度不一(ACP setupTurn 记 debug,其余静默),
176
+ * 统一为最详细版本:dropped 一律 debug,便于事后追溯。
177
+ */
178
+ protected forCurrentTurn<A extends unknown[]>(anchor: TurnAnchor<TClient>, label: string, handler: (...args: A) => void): (...args: A) => void;
148
179
  protected failTurn(message: string): void;
149
180
  protected resetRunState(): void;
150
181
  /** Release the connection + detach stale client hooks after a lost connection. */
@@ -150,7 +150,7 @@ export class ConnectionBasedRunner {
150
150
  // 已被缓存层除名(evictRunnerSlot → markOrphaned):本次 run 创建的
151
151
  // 连接挂在无人托管的实例上。主动收掉而不是挂 idle timer——否则 agent
152
152
  // server 进程残留至 idle TTL,期间持有 agent 侧会话写锁(codex
153
- // "thread already has an active writer",2026-10-11 线上实测)。
153
+ // "thread already has an active writer")。
154
154
  // dispose() 内部自带 try/catch,这里 fire-and-forget 不阻塞事件流收尾。
155
155
  void this.disposeConnections().catch((err) => {
156
156
  getLogger().warn(`[${this.logTag}] orphan dispose error: ${err.message}`);
@@ -209,7 +209,7 @@ export class ConnectionBasedRunner {
209
209
  /**
210
210
  * Mark this runner as evicted from the bridge's runner cache while a run may
211
211
  * still be holding/creating connections on it (queue-advance race during
212
- * interruptCurrentRun's await window, 2026-10-11). The active run's finally
212
+ * interruptCurrentRun's await window). The active run's finally
213
213
  * disposes connections eagerly instead of arming the idle TTL timer —
214
214
  * otherwise the agent server process lingers for up to 30 minutes holding
215
215
  * its session write lock (codex: "thread already has an active writer").
@@ -331,6 +331,36 @@ export class ConnectionBasedRunner {
331
331
  isCurrentTurn(generation, client) {
332
332
  return this._isRunning && generation === this.turnGeneration && client === this.currentClient;
333
333
  }
334
+ /**
335
+ * Acquire the per-turn connection and capture the callback anchor
336
+ * ({@link TurnAnchor}): acquire → activity ping → currentClient → anchor.
337
+ * Shared preamble of every setupTurn / runCompact; the acquired anchor is
338
+ * what the turn's hooks (and prompt-settle callbacks) validate against.
339
+ *
340
+ * `acquire` is a closure so subclasses keep their own acquire signatures
341
+ * (pi passes the spawn-bound `{ sessionId }`).
342
+ */
343
+ async acquireForTurn(cwd, acquire) {
344
+ const client = await acquire();
345
+ this.notifyConnectionActivity(cwd);
346
+ this.currentClient = client;
347
+ return { client, generation: this.turnGeneration };
348
+ }
349
+ /**
350
+ * Wrap a per-turn callback with the isCurrentTurn guard: events from a
351
+ * finished turn are dropped with a debug log and never reach the turn
352
+ * dispatcher. 6 处守卫原有的日志粒度不一(ACP setupTurn 记 debug,其余静默),
353
+ * 统一为最详细版本:dropped 一律 debug,便于事后追溯。
354
+ */
355
+ forCurrentTurn(anchor, label, handler) {
356
+ return (...args) => {
357
+ if (!this.isCurrentTurn(anchor.generation, anchor.client)) {
358
+ getLogger().debug(`[${this.logTag}] dropped ${label} from a finished turn`);
359
+ return;
360
+ }
361
+ handler(...args);
362
+ };
363
+ }
334
364
  failTurn(message) {
335
365
  this.pushEvents([
336
366
  {