lark-remote 0.5.13 → 0.5.15

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 (130) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/agent-kinds.d.ts +27 -0
  3. package/dist/agent-kinds.js +38 -0
  4. package/dist/bridge/approval-actions.d.ts +95 -0
  5. package/dist/bridge/approval-actions.js +104 -0
  6. package/dist/bridge/approval-coordinator.d.ts +13 -0
  7. package/dist/bridge/approval-coordinator.js +18 -0
  8. package/dist/bridge/final-usage.d.ts +8 -17
  9. package/dist/bridge/final-usage.js +1 -18
  10. package/dist/bridge/index.d.ts +46 -39
  11. package/dist/bridge/index.js +269 -240
  12. package/dist/bridge/queue-manager.d.ts +25 -1
  13. package/dist/bridge/queue-manager.js +179 -160
  14. package/dist/bridge/run-approval.d.ts +2 -1
  15. package/dist/bridge/run-usage.d.ts +21 -30
  16. package/dist/bridge/run-usage.js +47 -51
  17. package/dist/bridge/session-writeback.d.ts +3 -2
  18. package/dist/card/approval-render.d.ts +3 -2
  19. package/dist/card/approval-render.js +3 -2
  20. package/dist/{router → card}/card-helpers.d.ts +7 -7
  21. package/dist/{router → card}/card-helpers.js +8 -8
  22. package/dist/card/card-session.d.ts +4 -2
  23. package/dist/card/card-session.js +8 -6
  24. package/dist/card/card-shared.d.ts +1 -1
  25. package/dist/card/card-shared.js +1 -1
  26. package/dist/card/final-answer-overflow.d.ts +0 -6
  27. package/dist/card/final-answer-overflow.js +1 -1
  28. package/dist/card/run-card-session.d.ts +2 -2
  29. package/dist/card/run-card-session.js +2 -2
  30. package/dist/card/run-renderer.js +29 -30
  31. package/dist/card/run-state.d.ts +28 -50
  32. package/dist/card/run-state.js +23 -0
  33. package/dist/card/usage-format.d.ts +30 -0
  34. package/dist/{router → card}/usage-format.js +3 -10
  35. package/dist/clone.js +6 -1
  36. package/dist/config/index.d.ts +16 -20
  37. package/dist/config/index.js +0 -16
  38. package/dist/config/pi-config.js +6 -4
  39. package/dist/connector/index.d.ts +2 -4
  40. package/dist/connector/index.js +7 -11
  41. package/dist/{runner/dsh → dsh}/auth.js +1 -1
  42. package/dist/dsh/protocol.d.ts +9 -0
  43. package/dist/dsh/protocol.js +61 -0
  44. package/dist/index.js +10 -5
  45. package/dist/model-catalog.d.ts +14 -0
  46. package/dist/model-catalog.js +25 -0
  47. package/dist/platform/path.d.ts +9 -0
  48. package/dist/platform/path.js +18 -0
  49. package/dist/router/active-run-usage.d.ts +4 -0
  50. package/dist/router/active-run-usage.js +12 -0
  51. package/dist/router/commands/ls.js +1 -1
  52. package/dist/router/commands/order.js +1 -1
  53. package/dist/router/commands/resume.d.ts +5 -4
  54. package/dist/router/commands/resume.js +4 -4
  55. package/dist/router/commands/ws.js +1 -1
  56. package/dist/router/config/claude.js +2 -1
  57. package/dist/router/config/common/render.d.ts +5 -1
  58. package/dist/router/config/common/render.js +6 -13
  59. package/dist/router/config/dsh.d.ts +1 -1
  60. package/dist/router/config/index.d.ts +1 -1
  61. package/dist/router/config/types.d.ts +11 -3
  62. package/dist/router/index.d.ts +41 -11
  63. package/dist/router/index.js +169 -233
  64. package/dist/runner/agent-choices-common.d.ts +1 -1
  65. package/dist/runner/claude/runner.d.ts +20 -2
  66. package/dist/runner/claude/runner.js +2 -15
  67. package/dist/runner/codex/app-server/runner.d.ts +3 -1
  68. package/dist/runner/codex/app-server/translator.d.ts +11 -0
  69. package/dist/runner/codex/app-server/translator.js +56 -149
  70. package/dist/runner/common/acp/base-acp-runner.d.ts +19 -1
  71. package/dist/runner/common/acp/base-acp-runner.js +37 -3
  72. package/dist/runner/common/connection-based-runner.d.ts +3 -1
  73. package/dist/runner/common/jsonrpc/transport.js +17 -7
  74. package/dist/runner/dsh/client.d.ts +1 -4
  75. package/dist/runner/dsh/client.js +7 -55
  76. package/dist/runner/dsh/daemon.js +9 -2
  77. package/dist/runner/dsh/exit-cancel.js +1 -1
  78. package/dist/runner/dsh/mux-stream.js +21 -2
  79. package/dist/runner/dsh/runner.d.ts +21 -1
  80. package/dist/runner/dsh/runner.js +97 -78
  81. package/dist/runner/dsh/translator.d.ts +1 -1
  82. package/dist/runner/index.d.ts +1 -1
  83. package/dist/runner/kimi/acp/runner.d.ts +22 -1
  84. package/dist/runner/kimi/acp/runner.js +83 -60
  85. package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
  86. package/dist/runner/opencode/acp/error-monitor.js +31 -2
  87. package/dist/runner/opencode/acp/runner.d.ts +14 -1
  88. package/dist/runner/opencode/acp/runner.js +13 -2
  89. package/dist/runner/pi/rpc/runner.d.ts +2 -1
  90. package/dist/runner/probe.d.ts +1 -1
  91. package/dist/runner/probe.js +10 -9
  92. package/dist/runner/registry.d.ts +2 -1
  93. package/dist/runner/types.d.ts +20 -154
  94. package/dist/runner/types.js +4 -24
  95. package/dist/session/claude/session-index.d.ts +11 -4
  96. package/dist/session/claude/session-index.js +30 -8
  97. package/dist/session/claude/session-reader.d.ts +1 -1
  98. package/dist/session/claude/sessions.d.ts +2 -2
  99. package/dist/session/claude/sessions.js +41 -26
  100. package/dist/session/codex/rollout-reader.d.ts +1 -1
  101. package/dist/session/codex/rollout-reader.js +14 -13
  102. package/dist/session/codex/sessions.d.ts +1 -1
  103. package/dist/session/codex/sessions.js +4 -2
  104. package/dist/session/common/jsonl.js +7 -1
  105. package/dist/session/common/pagination.d.ts +2 -0
  106. package/dist/session/common/pagination.js +1 -1
  107. package/dist/session/common/recency.d.ts +6 -0
  108. package/dist/session/common/recency.js +7 -5
  109. package/dist/session/common/two-pass.d.ts +1 -1
  110. package/dist/session/common/usage-accumulator.d.ts +1 -1
  111. package/dist/session/dsh/sessions.d.ts +1 -1
  112. package/dist/session/dsh/sessions.js +37 -12
  113. package/dist/session/kimi/sessions.d.ts +1 -1
  114. package/dist/session/kimi/sessions.js +72 -27
  115. package/dist/session/opencode/sessions.d.ts +1 -1
  116. package/dist/session/opencode/sessions.js +164 -81
  117. package/dist/session/pi/sessions.d.ts +1 -1
  118. package/dist/session/pi/sessions.js +20 -7
  119. package/dist/session/registry.d.ts +2 -1
  120. package/dist/session/session-store.d.ts +1 -2
  121. package/dist/session/session-store.js +14 -7
  122. package/dist/session-contract.d.ts +178 -0
  123. package/dist/session-contract.js +9 -0
  124. package/package.json +1 -1
  125. package/dist/router/usage-format.d.ts +0 -60
  126. /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
  127. /package/dist/{runner/dsh → dsh}/home.d.ts +0 -0
  128. /package/dist/{runner/dsh → dsh}/home.js +0 -0
  129. /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
  130. /package/dist/{runner/dsh → dsh}/types.js +0 -0
@@ -21,8 +21,9 @@
21
21
  */
22
22
  import { randomUUID } from 'node:crypto';
23
23
  import WebSocket from 'ws';
24
+ import { dshArgs, extractDshToken } from '../../dsh/protocol.js';
24
25
  import { getLogger } from '../../logger/index.js';
25
- import { DSH_COOKIE_LIFETIME_MS, isDshCredentialsNotFound, mintDshBrowserCookie, readDshBrowserSecret, resolveDshCredentialsFile, } from './auth.js';
26
+ import { DSH_COOKIE_LIFETIME_MS, isDshCredentialsNotFound, mintDshBrowserCookie, readDshBrowserSecret, resolveDshCredentialsFile, } from '../../dsh/auth.js';
26
27
  import { DEFAULT_REQUEST_TIMEOUT_MS } from '../common/defaults.js';
27
28
  import { goalFromProjection } from './goal-view.js';
28
29
  import { DshError } from './errors.js';
@@ -111,60 +112,6 @@ function inboxItemsOf(values) {
111
112
  }
112
113
  return out;
113
114
  }
114
- /** Extract the `token` query parameter from a pasted `dsh web` URL. */
115
- export function extractDshToken(raw) {
116
- const value = (raw ?? '').trim();
117
- if (!value)
118
- return '';
119
- if (!value.includes('token=') && !value.includes('://'))
120
- return value;
121
- try {
122
- const url = new URL(value.startsWith('?') ? `http://dsh.invalid/${value}` : value);
123
- const token = url.searchParams.get('token');
124
- if (token)
125
- return token;
126
- // 带 scheme 却没有 token 参数的输入是误贴的 host URL(如
127
- // `http://127.0.0.1:3080`):整串不是合法 token,参与握手只会得到
128
- // 指向性缺失的 401。返回空串让上游走「token 缺失」的诊断路径。
129
- if (value.includes('://'))
130
- return '';
131
- return value;
132
- }
133
- catch {
134
- return value;
135
- }
136
- }
137
- /**
138
- * Remote methods take named parameters. Most session methods call their single
139
- * argument `request`; `session/list` reserves `_request`; catalog methods take
140
- * no arguments. `commands/*` address the receiving agent by its wire field
141
- * `agentId`, so their payload is already in wire shape (pass-through).
142
- * The gateway rejects any other `args` shape.
143
- */
144
- const DSH_ARGS_KEY = {
145
- 'session/list': '_request',
146
- 'session/create': 'request',
147
- 'session/prompt': 'request',
148
- 'session/cancel': 'request',
149
- 'session/selectModel': 'request',
150
- 'session/projections': 'request',
151
- 'session/page': 'request',
152
- 'session/follow': 'request',
153
- 'session/updateQueue': 'request',
154
- 'job/list': 'request',
155
- 'job/kill': 'request',
156
- 'session/modelCatalog': null,
157
- 'agentPresets/list': null,
158
- 'permissionPresets/catalog': null,
159
- };
160
- export function dshArgs(method, payload) {
161
- const key = DSH_ARGS_KEY[method];
162
- if (key === null)
163
- return {};
164
- if (key === undefined)
165
- return payload;
166
- return { [key]: payload };
167
- }
168
115
  export class DshClient {
169
116
  baseUrl;
170
117
  token;
@@ -483,6 +430,11 @@ export class DshClient {
483
430
  // 拿不到投影 = 宿主没这个会话的投影,不等于"没有 goal"。
484
431
  if (!snapshot)
485
432
  return { known: false, goal: null };
433
+ // 投影快照存在但没有 goal 域 = 宿主明确没有目标(未用过 goal 的会话就是这样)。
434
+ // 不能把缺失当成"读不懂":goalFromProjection(undefined) 也返回 undefined,
435
+ // 那会把每次普通会话都误报成「目标状态未确认」。
436
+ if (!('goal' in snapshot.values))
437
+ return { known: true, goal: null };
486
438
  const parsed = goalFromProjection(snapshot.values.goal);
487
439
  if (parsed === undefined)
488
440
  return { known: false, goal: null };
@@ -15,7 +15,8 @@ import fsp from 'node:fs/promises';
15
15
  import path from 'node:path';
16
16
  import { DEFAULT_DSH_HOST } from '../../config/index.js';
17
17
  import { mergeProcessEnv, spawnProcess } from '../../platform/spawn.js';
18
- import { resolveDshHome } from './home.js';
18
+ import { getLogger } from '../../logger/index.js';
19
+ import { resolveDshHome } from '../../dsh/home.js';
19
20
  const DEFAULT_START_TIMEOUT_MS = 30_000;
20
21
  const READY_POLL_MS = 100;
21
22
  /** User-facing daemon failure; the bridge renders `message` in the chat card. */
@@ -174,7 +175,13 @@ export class DshDaemon {
174
175
  const stats = await fsp.stat(lockDir);
175
176
  return Date.now() - stats.mtimeMs > this.startTimeoutMs() * 2;
176
177
  }
177
- catch {
178
+ catch (err) {
179
+ // ENOENT = 锁目录刚被持有者清掉(正常,等下一轮重查)。
180
+ // 其余(EACCES/EPERM…)= 无法判断是否陈旧:保守判「不陈旧」继续等,
181
+ // 但必须留痕,否则超时文案会误报成「等待其他 lark-remote」。
182
+ if (err.code !== 'ENOENT') {
183
+ getLogger().warn(`[dsh] cannot stat start lock ${lockDir}: ${err?.message ?? String(err)}`);
184
+ }
178
185
  return false;
179
186
  }
180
187
  }
@@ -18,7 +18,7 @@ import path from 'node:path';
18
18
  import { getLogger } from '../../logger/index.js';
19
19
  import { isWin32 } from '../../platform/select.js';
20
20
  import { spawnProcessSync } from '../../platform/spawn.js';
21
- import { DSH_COOKIE_LIFETIME_MS, mintDshBrowserCookie, readDshBrowserSecretSync, resolveDshCredentialsFile, } from './auth.js';
21
+ import { DSH_COOKIE_LIFETIME_MS, mintDshBrowserCookie, readDshBrowserSecretSync, resolveDshCredentialsFile, } from '../../dsh/auth.js';
22
22
  /** Upper bound on the exit-path cancel:退出不能被一个卡住的 host 拖住。 */
23
23
  const EXIT_CANCEL_TIMEOUT_MS = 2_000;
24
24
  /**
@@ -1,5 +1,23 @@
1
1
  import { getLogger } from '../../logger/index.js';
2
2
  import { DshError } from './errors.js';
3
+ /**
4
+ * onerror 的事件对象在 ws 实现之间形态不一({message} / {error} / {type})。
5
+ * 尽力取出可读描述;取不到就不追加,别拿 '[object Object]' 充数。
6
+ */
7
+ function describeSocketError(event) {
8
+ if (!event || typeof event !== 'object')
9
+ return undefined;
10
+ const e = event;
11
+ if (typeof e.message === 'string' && e.message)
12
+ return e.message;
13
+ if (e.error instanceof Error && e.error.message)
14
+ return e.error.message;
15
+ if (typeof e.error === 'string' && e.error)
16
+ return e.error;
17
+ if (typeof e.type === 'string' && e.type)
18
+ return e.type;
19
+ return undefined;
20
+ }
3
21
  export async function* openMuxStream(opts) {
4
22
  const cookie = await opts.authCookie();
5
23
  const socket = opts.webSocketFactory(`${opts.baseUrl.replace(/^http/, 'ws')}/api/remote.mux`, cookie ? { headers: { cookie } } : undefined);
@@ -87,10 +105,11 @@ export async function* openMuxStream(opts) {
87
105
  settle(undefined);
88
106
  }
89
107
  };
90
- socket.onerror = () => {
108
+ socket.onerror = (event) => {
91
109
  if (ended)
92
110
  return;
93
- failure = new DshError('transport', 'DSH remote mux WebSocket error');
111
+ const detail = describeSocketError(event);
112
+ failure = new DshError('transport', `DSH remote mux WebSocket error${detail ? `: ${detail}` : ''}`);
94
113
  ended = true;
95
114
  settle(undefined);
96
115
  };
@@ -17,7 +17,8 @@
17
17
  * approval on the same card. If `$events` is unavailable, the runner falls
18
18
  * back to the Web UI prompt.
19
19
  */
20
- import type { AgentEvent, AgentSessionReader, AgentStatusInfo, SpawnOptions, StopOutcome } from '../types.js';
20
+ import type { AgentSessionReader } from '../../session-contract.js';
21
+ import type { AgentEvent, AgentStatusInfo, SpawnOptions, StopOutcome } from '../types.js';
21
22
  import type { AgentRunner } from '../types.js';
22
23
  import { DshClient, type WebSocketFactory } from './client.js';
23
24
  import type { DshDaemon } from './daemon.js';
@@ -134,6 +135,25 @@ export declare class DshRunner implements AgentRunner {
134
135
  * the live catalog for that provider's default (or first) model.
135
136
  */
136
137
  private resolveModelSelection;
138
+ /**
139
+ * 新建 DSH session 并取回 sessionId;run 在挂起期间被 stop 时返回 undefined。
140
+ *
141
+ * 红线:不把 run 的 abort signal 直接交给 create —— abort 掉请求会丢掉刚创建的
142
+ * sessionId,服务端若已完成创建就留下一个无法 cancel 的孤儿 session。改为 abort
143
+ * 时立即放行 run(队列不被挂起),create 后台落定后再补 cancel。
144
+ */
145
+ private acquireSessionId;
146
+ /**
147
+ * 把本轮消息交给 DSH 的排队收件箱,返回本轮的身份与起始水位。
148
+ *
149
+ * 三条不变量(都关系到「迟到/抢先的 stop 能否被正确撤销」):
150
+ * - 起始水位取 prompt **之前**的 latestEventSeq:session/page 返回整个会话,
151
+ * 从 0 开始会把首轮已完成 turn 在每次后续消息时重放一遍。
152
+ * - 身份(requestId)在请求发出**之前**就公布:stop 可能落在 session/prompt 的
153
+ * 在途窗口里,那时它必须知道宿主受理后该撤销哪个收件项。
154
+ * - `promptAdmitted` 只在宿主接受后置真:撤销路径据此区分「还没进去」与「已排队」。
155
+ */
156
+ private submitPrompt;
137
157
  run(message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
138
158
  /**
139
159
  * Consume one turn from both transports. `session/follow` owns transcript
@@ -178,6 +178,20 @@ function parseToolArguments(raw) {
178
178
  return {};
179
179
  }
180
180
  }
181
+ /**
182
+ * 用户 stop 的终态结果(run() 的 5 个中止点各 yield 同一形状)。
183
+ *
184
+ * 必须 yield result 而不是裸 return:调用方靠 result 收尾 run 卡,否则卡片
185
+ * 停留在「运行中」(见 run() 内 startup abort 注释)。
186
+ */
187
+ function interruptedResult(sessionId) {
188
+ return {
189
+ type: 'result',
190
+ subtype: 'interrupted',
191
+ session_id: sessionId,
192
+ errorMessage: 'run stopped',
193
+ };
194
+ }
181
195
  export class DshRunner {
182
196
  kind = 'dsh';
183
197
  /**
@@ -323,6 +337,57 @@ export class DshRunner {
323
337
  }
324
338
  return { provider, model, ...(reasoningEffort ? { reasoningEffort } : {}) };
325
339
  }
340
+ /**
341
+ * 新建 DSH session 并取回 sessionId;run 在挂起期间被 stop 时返回 undefined。
342
+ *
343
+ * 红线:不把 run 的 abort signal 直接交给 create —— abort 掉请求会丢掉刚创建的
344
+ * sessionId,服务端若已完成创建就留下一个无法 cancel 的孤儿 session。改为 abort
345
+ * 时立即放行 run(队列不被挂起),create 后台落定后再补 cancel。
346
+ */
347
+ async acquireSessionId(cwd, signal) {
348
+ const createPromise = this.client.createSession({
349
+ cwd,
350
+ ...(this.agentPreset ? { agentPreset: this.agentPreset } : {}),
351
+ });
352
+ const created = await this.awaitOrAbort(createPromise, signal);
353
+ if (created)
354
+ return created.sessionId;
355
+ void createPromise.then((c) => this.cancelSessionQuietly(c.sessionId), () => {
356
+ /* create failed after stop — nothing to cancel */
357
+ });
358
+ getLogger().info('[dsh] run stopped during session/create; will cancel if it settles');
359
+ return undefined;
360
+ }
361
+ /**
362
+ * 把本轮消息交给 DSH 的排队收件箱,返回本轮的身份与起始水位。
363
+ *
364
+ * 三条不变量(都关系到「迟到/抢先的 stop 能否被正确撤销」):
365
+ * - 起始水位取 prompt **之前**的 latestEventSeq:session/page 返回整个会话,
366
+ * 从 0 开始会把首轮已完成 turn 在每次后续消息时重放一遍。
367
+ * - 身份(requestId)在请求发出**之前**就公布:stop 可能落在 session/prompt 的
368
+ * 在途窗口里,那时它必须知道宿主受理后该撤销哪个收件项。
369
+ * - `promptAdmitted` 只在宿主接受后置真:撤销路径据此区分「还没进去」与「已排队」。
370
+ */
371
+ async submitPrompt(sessionId, cwd, message, signal) {
372
+ const prePromptSeq = await this.client.latestEventSeq(sessionId, { signal });
373
+ const promptRequestId = newPromptRequestId();
374
+ this.activePromptRequestId = promptRequestId;
375
+ this.promptAdmitted = false;
376
+ this.inflight?.record({
377
+ sessionId,
378
+ promptRequestId,
379
+ baseUrl: this.client.baseUrl,
380
+ cwd,
381
+ });
382
+ await this.client.unary('session/prompt', {
383
+ requestId: promptRequestId,
384
+ sessionId,
385
+ mode: 'queue',
386
+ content: [{ type: 'text', text: message }],
387
+ }, { signal });
388
+ this.promptAdmitted = true;
389
+ return { prePromptSeq, promptRequestId };
390
+ }
326
391
  async *run(message, opts) {
327
392
  if (this.running) {
328
393
  throw new Error('DshRunner is already running');
@@ -346,29 +411,13 @@ export class DshRunner {
346
411
  // session/create 必须包进同一 try/finally,否则创建失败(服务端不可达/报错)
347
412
  // 会在进入旧 try 之前抛异常,finally 不执行 → running 恒 true、activeAbort 残留。
348
413
  if (!sessionId) {
349
- const createPromise = this.client.createSession({
350
- cwd: opts.cwd,
351
- ...(this.agentPreset ? { agentPreset: this.agentPreset } : {}),
352
- });
353
- // 不把 run 的 abort signal 直接交给 create:abort 掉请求会丢掉刚创建
354
- // 的 sessionId,服务端若已完成创建就留下一个无法 cancel 的孤儿 session。
355
- // 改为 abort 时立即放行 run(队列不被挂起),create 后台落定后再补 cancel。
356
- const created = await this.awaitOrAbort(createPromise, abort.signal);
357
- if (!created) {
358
- void createPromise.then((c) => this.cancelSessionQuietly(c.sessionId), () => {
359
- /* create failed after stop — nothing to cancel */
360
- });
361
- getLogger().info('[dsh] run stopped during session/create; will cancel if it settles');
414
+ const created = await this.acquireSessionId(opts.cwd, abort.signal);
415
+ if (created === undefined) {
362
416
  terminalEmitted = true;
363
- yield {
364
- type: 'result',
365
- subtype: 'interrupted',
366
- session_id: '',
367
- errorMessage: 'run stopped',
368
- };
417
+ yield interruptedResult('');
369
418
  return;
370
419
  }
371
- sessionId = created.sessionId;
420
+ sessionId = created;
372
421
  }
373
422
  this.activeSessionId = sessionId;
374
423
  // Startup abort:createSession 挂起期间 stop() 已 abort(此时 sessionId 尚
@@ -380,12 +429,7 @@ export class DshRunner {
380
429
  await this.cancelSessionQuietly(sessionId);
381
430
  getLogger().info(`[dsh] run aborted during startup, cancelled session ${sessionId}`);
382
431
  terminalEmitted = true;
383
- yield {
384
- type: 'result',
385
- subtype: 'interrupted',
386
- session_id: sessionId,
387
- errorMessage: 'run stopped',
388
- };
432
+ yield interruptedResult(sessionId);
389
433
  return;
390
434
  }
391
435
  // provider/模型/推理强度对齐:会话首次 run 时 selectModel 一次
@@ -403,12 +447,7 @@ export class DshRunner {
403
447
  if (abort.signal.aborted) {
404
448
  getLogger().info(`[dsh] run stopped during selectModel session=${sessionId}`);
405
449
  terminalEmitted = true;
406
- yield {
407
- type: 'result',
408
- subtype: 'interrupted',
409
- session_id: sessionId,
410
- errorMessage: 'run stopped',
411
- };
450
+ yield interruptedResult(sessionId);
412
451
  return;
413
452
  }
414
453
  if (this.configuredProvider) {
@@ -425,12 +464,7 @@ export class DshRunner {
425
464
  // Abort 可能在上述 unary 完成与检查之间到达:不进入 prompt/turn。
426
465
  if (abort.signal.aborted) {
427
466
  terminalEmitted = true;
428
- yield {
429
- type: 'result',
430
- subtype: 'interrupted',
431
- session_id: sessionId,
432
- errorMessage: 'run stopped',
433
- };
467
+ yield interruptedResult(sessionId);
434
468
  return;
435
469
  }
436
470
  yield {
@@ -448,53 +482,29 @@ export class DshRunner {
448
482
  timeoutMs: GOAL_PROBE_TIMEOUT_MS,
449
483
  });
450
484
  const lastGoal = seedGoal.known ? seedGoal.goal : undefined;
451
- // 只有确实存在 goal 才产事件:没有 goal 的会话保持事件序列不变(少一次
452
- // 渲染,也不让"读不到"变成一张多出来的卡片事件)。
453
- if (seedGoal.known && seedGoal.goal)
454
- yield goalStateEvent(seedGoal.goal, true);
485
+ if (seedGoal.known) {
486
+ // 只有确实存在 goal 才产事件:没有 goal 的会话保持事件序列不变(少一次
487
+ // 渲染,也不让"读不到"变成一张多出来的卡片事件)。
488
+ if (seedGoal.goal)
489
+ yield goalStateEvent(seedGoal.goal, true);
490
+ }
491
+ else {
492
+ // 读不到 ≠ 没有目标。必须显式把 unknown 交给卡片,否则收尾只能按
493
+ // 「没有 goal」写成「已完成」——正是 GoalStateEvent 契约禁止的推断。
494
+ yield goalStateEvent(null, false);
495
+ }
455
496
  try {
456
- // Snapshot the pre-prompt high-water mark before mutating the session.
457
- // DSH session/page returns the whole session, so starting from seq 0
458
- // would replay the first completed turn on every follow-up prompt.
459
- const prePromptSeq = await this.client.latestEventSeq(sessionId, {
460
- signal: abort.signal,
461
- });
462
- // Keep the prompt identity: DSH echoes it into the durable user/message
463
- // source.rpcId, which is how runTurn tells this run's turn from a turn
464
- // it is merely queued behind.
465
- promptRequestId = newPromptRequestId();
466
- // Publish the identity before the request leaves: a stop that arrives
467
- // while `session/prompt` is in flight must still know which inbox item
468
- // (if any) to withdraw once the host admits it.
469
- this.activePromptRequestId = promptRequestId;
470
- this.promptAdmitted = false;
471
- this.inflight?.record({
472
- sessionId,
473
- promptRequestId,
474
- baseUrl: this.client.baseUrl,
475
- cwd: opts.cwd,
476
- });
477
- await this.client.unary('session/prompt', {
478
- requestId: promptRequestId,
479
- sessionId,
480
- mode: 'queue',
481
- content: [{ type: 'text', text: message }],
482
- }, { signal: abort.signal });
483
- this.promptAdmitted = true;
497
+ const submitted = await this.submitPrompt(sessionId, opts.cwd, message, abort.signal);
498
+ promptRequestId = submitted.promptRequestId;
484
499
  if (abort.signal.aborted) {
485
500
  // stop() 在 prompt 已受理后到达:cancel 由 stop() 发出,这里直接收尾,
486
501
  // 不再打开 mux(否则会看到 cancel 产生的 turn/end 事件)。
487
502
  terminalEmitted = true;
488
- yield {
489
- type: 'result',
490
- subtype: 'interrupted',
491
- session_id: sessionId,
492
- errorMessage: 'run stopped',
493
- };
503
+ yield interruptedResult(sessionId);
494
504
  return;
495
505
  }
496
506
  try {
497
- for await (const ev of this.runTurn(sessionId, opts.cwd, prePromptSeq, promptRequestId, abort, { last: lastGoal })) {
507
+ for await (const ev of this.runTurn(sessionId, opts.cwd, submitted.prePromptSeq, promptRequestId, abort, { last: lastGoal, known: seedGoal.known })) {
498
508
  yield ev;
499
509
  if (ev.type === 'result') {
500
510
  terminalEmitted = true;
@@ -731,7 +741,16 @@ export class DshRunner {
731
741
  signal,
732
742
  timeoutMs: GOAL_PROBE_TIMEOUT_MS,
733
743
  });
734
- if (finalGoal.known && !sameGoal(finalGoal.goal, goal.last)) {
744
+ if (!finalGoal.known) {
745
+ // 收尾读不到:seed 已报过 unknown 就不重复发,否则把状态降级
746
+ // 交给卡片(卡片据此不再宣布完成)。
747
+ if (goal.known) {
748
+ goal.known = false;
749
+ yield goalStateEvent(null, false);
750
+ }
751
+ }
752
+ else if (!goal.known || !sameGoal(finalGoal.goal, goal.last)) {
753
+ goal.known = true;
735
754
  goal.last = finalGoal.goal;
736
755
  yield goalStateEvent(finalGoal.goal, true);
737
756
  }
@@ -24,7 +24,7 @@
24
24
  * cache_creation_tokens (cacheWrite = cache creation) + total_tokens sum.
25
25
  */
26
26
  import type { AgentEvent, ResultEvent } from '../types.js';
27
- import type { DshAssistantStreamFrame, DshSessionEvent, DshTokenUsage } from './types.js';
27
+ import type { DshAssistantStreamFrame, DshSessionEvent, DshTokenUsage } from '../../dsh/types.js';
28
28
  export declare class DshTranslator {
29
29
  /** 本 turn 累计用量:DSH 每次模型调用(step)一条 usage,工具往返 = 多条。 */
30
30
  private turnUsage;
@@ -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, FinalAnswerEvent, NoticeEvent, SessionInfoEvent, ToolHint, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, GoalView, GoalStateEvent, } from './types.js';
18
+ export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, 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';
@@ -17,7 +17,9 @@
17
17
  * - AskUserQuestion flows through elicitation forms + a request_permission
18
18
  * question bridge (buildQuestionResponse)
19
19
  */
20
- import type { AgentKind, AgentSessionReader, AgentStatusInfo, SpawnOptions } from '../../types.js';
20
+ import type { AgentKind } from '../../../agent-kinds.js';
21
+ import type { AgentSessionReader } from '../../../session-contract.js';
22
+ import type { AgentStatusInfo, SpawnOptions } from '../../types.js';
21
23
  import { JsonRpcClient } from '../../common/jsonrpc/client.js';
22
24
  import { KimiAcpTranslator } from './translator.js';
23
25
  import { type SessionPromptResult } from '../../common/acp/protocol-types.js';
@@ -167,6 +169,25 @@ export declare class KimiAcpRunner extends BaseAcpRunner<KimiAcpTranslator> {
167
169
  * stdout(+stderr) 到缓冲,立即回 {terminalId}。
168
170
  */
169
171
  private handleTerminalCreate;
172
+ /**
173
+ * 走 shell seam:argv 逐 token 保留,只把「可执行文件在哪」收口——win32 上
174
+ * Git Bash 缺失时在这里抛 ShellUnavailableError,由 handleTerminalRequest 转成
175
+ * 明确的 Terminal request failed 回执,而不是一个语焉不详的 spawn ENOENT。
176
+ */
177
+ private spawnTerminalProcess;
178
+ /**
179
+ * 有界输出缓冲:每个流独立 decoder(多字节字符跨 chunk 拆分时由 decoder 保留
180
+ * 不完整序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符)。
181
+ */
182
+ private attachTerminalOutputBuffer;
183
+ /**
184
+ * 退出/错误收尾管线:tool_result 必须先入队,再放行 `wait_for_exit`。
185
+ */
186
+ private wireTerminalExit;
187
+ /** Bash 面板的 tool_result 帧(输出全量 + 是否失败)。 */
188
+ private pushTerminalToolResult;
189
+ /** Bash 面板的 tool_use 帧(真实命令 + cwd,create 时立刻可见)。 */
190
+ private pushTerminalToolUse;
170
191
  /** terminal/output: 回累计全量输出 + truncated + 当前退出状态(可未退出)。 */
171
192
  private handleTerminalOutput;
172
193
  /** terminal/wait_for_exit: 未退出则等 exit promise 再回 {exitCode, signal}。 */