lark-remote 0.5.18 → 0.5.20

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 (123) hide show
  1. package/CHANGELOG.md +32 -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 +1 -1
  10. package/dist/bridge/index.d.ts +120 -119
  11. package/dist/bridge/index.js +257 -695
  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 +109 -97
  18. package/dist/card/bash-card-session.js +2 -0
  19. package/dist/card/card-budget.js +1 -1
  20. package/dist/card/card-helpers.d.ts +3 -3
  21. package/dist/card/card-helpers.js +6 -6
  22. package/dist/card/card-session.d.ts +17 -0
  23. package/dist/card/card-session.js +35 -0
  24. package/dist/card/final-answer-file.js +1 -1
  25. package/dist/card/run-card-session.js +2 -0
  26. package/dist/card/run-renderer.d.ts +3 -3
  27. package/dist/card/run-renderer.js +77 -102
  28. package/dist/card/run-state.d.ts +35 -8
  29. package/dist/card/run-state.js +117 -90
  30. package/dist/card/tool-render.d.ts +1 -1
  31. package/dist/card/tool-render.js +1 -1
  32. package/dist/card/usage-format.d.ts +12 -0
  33. package/dist/card/usage-format.js +8 -3
  34. package/dist/cli.js +15 -3
  35. package/dist/clone.js +2 -2
  36. package/dist/config/codex-config.js +130 -106
  37. package/dist/config/index.d.ts +16 -2
  38. package/dist/config/index.js +23 -8
  39. package/dist/connector/index.js +6 -6
  40. package/dist/error-classification.js +3 -2
  41. package/dist/inbound/placeholder.d.ts +1 -1
  42. package/dist/inbound/placeholder.js +1 -1
  43. package/dist/index.js +203 -133
  44. package/dist/logger/index.js +6 -6
  45. package/dist/message-handlers.d.ts +0 -5
  46. package/dist/message-handlers.js +4 -2
  47. package/dist/model-catalog.d.ts +2 -2
  48. package/dist/model-catalog.js +3 -8
  49. package/dist/platform/identity.js +1 -1
  50. package/dist/platform/path.d.ts +1 -1
  51. package/dist/platform/path.js +1 -1
  52. package/dist/platform/sleep-blocker.js +1 -1
  53. package/dist/platform/terminator-win32.js +2 -1
  54. package/dist/router/commands/resume.js +2 -20
  55. package/dist/router/config/dsh.d.ts +4 -8
  56. package/dist/router/config/dsh.js +3 -10
  57. package/dist/router/config/kimi.js +1 -1
  58. package/dist/router/config/types.d.ts +1 -1
  59. package/dist/router/config-diff.js +1 -1
  60. package/dist/router/index.d.ts +56 -11
  61. package/dist/router/index.js +222 -202
  62. package/dist/runner/claude/session.d.ts +1 -1
  63. package/dist/runner/claude/session.js +6 -4
  64. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  65. package/dist/runner/codex/app-server/runner.js +30 -25
  66. package/dist/runner/codex/app-server/translator.d.ts +0 -5
  67. package/dist/runner/codex/app-server/translator.js +4 -17
  68. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  69. package/dist/runner/common/acp/acp-approval.js +1 -1
  70. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  71. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  72. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  73. package/dist/runner/common/acp/base-translator.js +31 -9
  74. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  75. package/dist/runner/common/acp/protocol-types.js +2 -2
  76. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  77. package/dist/runner/common/connection-based-runner.js +32 -2
  78. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  79. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  80. package/dist/runner/common/spawning-runner.d.ts +1 -1
  81. package/dist/runner/common/spawning-runner.js +1 -1
  82. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  83. package/dist/runner/dsh/catalog-probe.js +10 -0
  84. package/dist/runner/dsh/client.d.ts +13 -8
  85. package/dist/runner/dsh/client.js +23 -15
  86. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  87. package/dist/runner/dsh/exit-cancel.js +2 -2
  88. package/dist/runner/dsh/inflight.d.ts +1 -1
  89. package/dist/runner/dsh/inflight.js +1 -1
  90. package/dist/runner/dsh/runner.d.ts +48 -2
  91. package/dist/runner/dsh/runner.js +288 -199
  92. package/dist/runner/dsh/translator.d.ts +1 -1
  93. package/dist/runner/dsh/translator.js +1 -1
  94. package/dist/runner/index.d.ts +1 -1
  95. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  96. package/dist/runner/kimi/acp/runner.js +1 -2
  97. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  98. package/dist/runner/kimi/acp/translator.js +3 -4
  99. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  100. package/dist/runner/opencode/acp/runner.js +4 -4
  101. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  102. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  103. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  104. package/dist/runner/pi/rpc/runner.js +27 -42
  105. package/dist/runner/types.d.ts +25 -8
  106. package/dist/runner/types.js +1 -1
  107. package/dist/session/claude/sessions.d.ts +45 -0
  108. package/dist/session/claude/sessions.js +97 -71
  109. package/dist/session/codex/rollout-reader.js +4 -4
  110. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  111. package/dist/session/common/throw-session-list-error.js +16 -0
  112. package/dist/session/dsh/sessions.js +11 -5
  113. package/dist/session/kimi/sessions.d.ts +40 -1
  114. package/dist/session/kimi/sessions.js +244 -192
  115. package/dist/session/opencode/sessions.d.ts +153 -1
  116. package/dist/session/opencode/sessions.js +354 -321
  117. package/dist/session/pi/sessions.js +4 -5
  118. package/dist/session/session-store.d.ts +1 -1
  119. package/dist/session/session-store.js +1 -1
  120. package/dist/update/version-check.js +2 -1
  121. package/dist/workspace/index.d.ts +1 -1
  122. package/dist/workspace/index.js +1 -1
  123. package/package.json +1 -1
@@ -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
  {
@@ -151,8 +151,8 @@ export declare class ConnectionManager<TClient extends ConnectionClient = JsonRp
151
151
  * 定时器回调带 **slot 身份校验**:武装之后 slot 可能已被替换——进程自退走
152
152
  * baseOnClose(删 slot 但不经过 disposeSlot)、或同 cwd 已 acquire 了新连接。
153
153
  * 这两种情况下本定时器都已过期,不得回收「当时恰好挂在该 cwd 上」的连接:
154
- * 2026-10-24 线上事故即一条泄漏的过期 TTL 杀掉继任连接的在途 turn,卡片
155
- * 误报 "Codex app-server connection closed"。
154
+ * 一条泄漏的过期 TTL 会杀掉继任连接的在途 turn,卡片误报
155
+ * "Codex app-server connection closed"。
156
156
  */
157
157
  notifyIdle(cwd: string): void;
158
158
  private createClient;
@@ -197,8 +197,8 @@ export class ConnectionManager {
197
197
  * 定时器回调带 **slot 身份校验**:武装之后 slot 可能已被替换——进程自退走
198
198
  * baseOnClose(删 slot 但不经过 disposeSlot)、或同 cwd 已 acquire 了新连接。
199
199
  * 这两种情况下本定时器都已过期,不得回收「当时恰好挂在该 cwd 上」的连接:
200
- * 2026-10-24 线上事故即一条泄漏的过期 TTL 杀掉继任连接的在途 turn,卡片
201
- * 误报 "Codex app-server connection closed"。
200
+ * 一条泄漏的过期 TTL 会杀掉继任连接的在途 turn,卡片误报
201
+ * "Codex app-server connection closed"。
202
202
  */
203
203
  notifyIdle(cwd) {
204
204
  if (this.idleTtlMs <= 0)
@@ -245,8 +245,8 @@ export class ConnectionManager {
245
245
  if (current?.client === client) {
246
246
  // 进程自退(codex app-server 偶发 exit 0,非本程序发起,不经
247
247
  // disposeSlot)也要撤掉挂在旧 slot 上的 idle TTL:定时器不随 slot
248
- // 删除而取消,到期时会按 cwd 误杀继任连接的在途 turn(2026-10-24
249
- // 线上事故,见 notifyIdle 的身份校验与对应回归测试)。
248
+ // 删除而取消,到期时会按 cwd 误杀继任连接的在途 turn(见 notifyIdle
249
+ // 的身份校验与对应回归测试)。
250
250
  this.clearIdleTimer(current);
251
251
  this.slots.delete(cwd);
252
252
  this.onConnectionLost?.(cwd);
@@ -4,7 +4,7 @@ import { type Terminator } from '../../platform/terminator.js';
4
4
  import { SpawnHeartbeat } from './spawn-heartbeat.js';
5
5
  import type { SpawnOptions } from '../types.js';
6
6
  /**
7
- * 进程级退出监听单例分发(修复,2026-08-02)。
7
+ * 进程级退出监听单例分发。
8
8
  *
9
9
  * 背景:各 agent runner 的 registerExitHandlers 曾是同构复制,每个实例
10
10
  * process.on('exit'|'SIGINT'|'SIGTERM') 注册 3 个永不移除的闭包(捕获整个
@@ -357,7 +357,7 @@ export class SpawningRunner {
357
357
  // matchMode 'agent-invocation'(不是默认的 'executable'):agent CLI 普遍是
358
358
  // `#!/usr/bin/env node` 脚本,ps 看到的是 `node <安装路径>/…/cli.js`,名字只在
359
359
  // 路径段里(pi → @earendil-works/pi-coding-agent/…)。用默认档会恒判 mismatch
360
- // → 这些 agent 的孤儿永远回收不掉(2026-09-20 实测安装形态)。
360
+ // → 这些 agent 的孤儿永远回收不掉。
361
361
  const verdict = verifyPidIdentityVerdictSync(pid, {
362
362
  expectedBinary,
363
363
  matchMode: 'agent-invocation',
@@ -0,0 +1,17 @@
1
+ /**
2
+ * DSH 服务端目录探测(/config 卡片动态清单用)。
3
+ *
4
+ * 接口只声明卡片预取需要的三个 RPC 方法;具体传输客户端的构造(含 token
5
+ * 解析路径)收在本模块——router 只依赖接口,不持有 DshClient。
6
+ */
7
+ import type { DshModelCatalogValue, DshPermissionCatalogValue, DshPresetListValue } from '../../dsh/types.js';
8
+ /** 卡片预取只依赖的 client 方法子集。 */
9
+ export interface DshCatalogClient {
10
+ listModels(): Promise<DshModelCatalogValue>;
11
+ listPresets(): Promise<DshPresetListValue>;
12
+ listPermissionPresets(): Promise<DshPermissionCatalogValue>;
13
+ }
14
+ /** 目录预取客户端工厂类型(测试注入 fake 用)。 */
15
+ export type DshCatalogClientFactory = (baseUrl: string, token?: string, credentialsFile?: string) => DshCatalogClient;
16
+ /** 生产实现:卡片预取是交互路径,DSH 卡死时最多等 5 秒后回退固定清单。 */
17
+ export declare function createDshCatalogClient(baseUrl: string, token?: string, credentialsFile?: string): DshCatalogClient;
@@ -0,0 +1,10 @@
1
+ import { DshClient } from './client.js';
2
+ /** 生产实现:卡片预取是交互路径,DSH 卡死时最多等 5 秒后回退固定清单。 */
3
+ export function createDshCatalogClient(baseUrl, token, credentialsFile) {
4
+ return new DshClient(baseUrl, {
5
+ token,
6
+ credentialsFile,
7
+ requestTimeoutMs: 5_000,
8
+ });
9
+ }
10
+ //# sourceMappingURL=catalog-probe.js.map
@@ -19,7 +19,7 @@
19
19
  * `sessionEvents()` adds per-session filtering, reconnect + follow-snapshot
20
20
  * replay, and seq-based dedup.
21
21
  */
22
- import type { DshApprovalStreamItem, DshCommandResult, DshModelCatalogValue, DshModelSelection, DshOpenTurnState, DshPermissionCatalogValue, DshJobView, DshPresetListValue, DshProjectionItem, DshProjectionSnapshot, DshQuestion, DshRemoteEventOutcome, DshSessionPageValue, DshStreamItem, DshTokenUsage } from '../../dsh/types.js';
22
+ import type { DshApprovalStreamItem, DshCommandResult, DshModelCatalogValue, DshModelSelection, DshOpenTurnState, DshPermissionCatalogValue, DshJobView, DshPresetListValue, DshProjectionItem, DshProjectionSnapshot, DshQuestion, DshRemoteEventOutcome, DshSessionPageValue, DshStreamItem } from '../../dsh/types.js';
23
23
  import type { GoalView } from '../types.js';
24
24
  export { DshError } from './errors.js';
25
25
  interface WebSocketMessageEvent {
@@ -325,13 +325,20 @@ export declare class DshClient {
325
325
  */
326
326
  controlFrames(signal: AbortSignal): AsyncGenerator<DshProjectionItem>;
327
327
  /**
328
- * One session's current background-job roster.
328
+ * Subscribe to one session's background-job roster as a live stream.
329
329
  *
330
- * `job/list` 是流(生命周期变化时推整份名单),但"此刻有哪些任务在跑"只需
331
- * 要第一帧:文档明确首帧就是当前真相(whole-set replacement)。取到即断开,
332
- * 不长期持有连接——任务列表只用于收尾时的一句话与停止按钮。
330
+ * `job/list` 是整份替换流(whole-set replacement):首帧就是当前真相,之后
331
+ * 任务生命周期一变就再推一整份。**run 结束之后**必须用这个长订阅——后台任务
332
+ * 跨 turn 存活,任务结束这件事只会在这条流上出现;收尾时取首帧即断开
333
+ * ({@link jobRows})只够画那一瞬间的卡片。
334
+ */
335
+ watchJobs(sessionId: string, signal: AbortSignal): AsyncGenerator<DshJobView[]>;
336
+ /**
337
+ * One session's current background-job roster (snapshot).
333
338
  *
334
- * 失败返回 undefined(与"没有任务"区分):拿不到就别在卡片上写"0 个后台任务"。
339
+ * "此刻有哪些任务在跑"只需要第一帧:文档明确首帧就是当前真相,取到即断开,
340
+ * 不长期持有连接。失败返回 undefined(与"没有任务"区分):拿不到就别在卡片
341
+ * 上写"0 个后台任务"。
335
342
  */
336
343
  jobRows(sessionId: string, opts?: {
337
344
  signal?: AbortSignal;
@@ -341,8 +348,6 @@ export declare class DshClient {
341
348
  jobKill(sessionId: string, jobId: string, opts?: {
342
349
  signal?: AbortSignal;
343
350
  }): Promise<boolean>;
344
- /** Extract TokenUsage from an assistant/message data payload (absent → undefined). */
345
- static usageOf(data: Record<string, unknown>): DshTokenUsage | undefined;
346
351
  }
347
352
  /** Stable client-side prompt identity persisted on the accepted user message. */
348
353
  export declare function newPromptRequestId(): string;
@@ -239,6 +239,7 @@ export class DshClient {
239
239
  err instanceof DshError &&
240
240
  err.code === 'http' &&
241
241
  err.details.status === 401) {
242
+ getLogger().warn(`[dsh] 401 while authenticating ${method}, re-minting cookie once`);
242
243
  continue;
243
244
  }
244
245
  throw err;
@@ -271,6 +272,7 @@ export class DshClient {
271
272
  }
272
273
  if (res.status === 401 && attempt === 0) {
273
274
  this.cookiePromise = undefined;
275
+ getLogger().warn(`[dsh] 401 for ${method}, re-minting cookie once`);
274
276
  continue;
275
277
  }
276
278
  if (res.status === 401) {
@@ -927,25 +929,35 @@ export class DshClient {
927
929
  }
928
930
  }
929
931
  /**
930
- * One session's current background-job roster.
932
+ * Subscribe to one session's background-job roster as a live stream.
931
933
  *
932
- * `job/list` 是流(生命周期变化时推整份名单),但"此刻有哪些任务在跑"只需
933
- * 要第一帧:文档明确首帧就是当前真相(whole-set replacement)。取到即断开,
934
- * 不长期持有连接——任务列表只用于收尾时的一句话与停止按钮。
934
+ * `job/list` 是整份替换流(whole-set replacement):首帧就是当前真相,之后
935
+ * 任务生命周期一变就再推一整份。**run 结束之后**必须用这个长订阅——后台任务
936
+ * 跨 turn 存活,任务结束这件事只会在这条流上出现;收尾时取首帧即断开
937
+ * ({@link jobRows})只够画那一瞬间的卡片。
938
+ */
939
+ async *watchJobs(sessionId, signal) {
940
+ for await (const frame of this.muxStream('job/list', { args: dshArgs('job/list', { sessionId }) }, signal)) {
941
+ const rows = frame;
942
+ if (rows?.type !== 'rows')
943
+ continue;
944
+ yield Array.isArray(rows.jobs) ? rows.jobs : [];
945
+ }
946
+ }
947
+ /**
948
+ * One session's current background-job roster (snapshot).
935
949
  *
936
- * 失败返回 undefined(与"没有任务"区分):拿不到就别在卡片上写"0 个后台任务"。
950
+ * "此刻有哪些任务在跑"只需要第一帧:文档明确首帧就是当前真相,取到即断开,
951
+ * 不长期持有连接。失败返回 undefined(与"没有任务"区分):拿不到就别在卡片
952
+ * 上写"0 个后台任务"。
937
953
  */
938
954
  async jobRows(sessionId, opts = {}) {
939
955
  const timer = new AbortController();
940
956
  const timeout = setTimeout(() => timer.abort(), opts.timeoutMs ?? 2_000);
941
957
  const signal = opts.signal ? AbortSignal.any([opts.signal, timer.signal]) : timer.signal;
942
958
  try {
943
- for await (const frame of this.muxStream('job/list', { args: dshArgs('job/list', { sessionId }) }, signal)) {
944
- const rows = frame;
945
- if (rows?.type === 'rows') {
946
- return Array.isArray(rows.jobs) ? rows.jobs : [];
947
- }
948
- }
959
+ for await (const rows of this.watchJobs(sessionId, signal))
960
+ return rows;
949
961
  return undefined;
950
962
  }
951
963
  catch (err) {
@@ -968,10 +980,6 @@ export class DshClient {
968
980
  return false;
969
981
  }
970
982
  }
971
- /** Extract TokenUsage from an assistant/message data payload (absent → undefined). */
972
- static usageOf(data) {
973
- return data.usage ?? undefined;
974
- }
975
983
  }
976
984
  /** Stable client-side prompt identity persisted on the accepted user message. */
977
985
  export function newPromptRequestId() {
@@ -5,8 +5,8 @@
5
5
  * `dsh web` host (`DshDaemon`). So the usual "kill my spawned child" exit
6
6
  * cleanup does nothing for it, and a bridge that exits mid-turn leaves the
7
7
  * turn running with no subscriber — an orphan turn that keeps consuming tokens
8
- * and mutating the workspace invisibly (2026-10-19: one ran 2h21m / 345 steps
9
- * after its bridge died).
8
+ * and mutating the workspace invisibly, potentially for hours after the bridge
9
+ * died.
10
10
  *
11
11
  * `process.on('exit')` cannot await, so this path uses the same synchronous
12
12
  * curl transport the session reader already uses: bounded by `timeoutMs` and
@@ -5,8 +5,8 @@
5
5
  * `dsh web` host (`DshDaemon`). So the usual "kill my spawned child" exit
6
6
  * cleanup does nothing for it, and a bridge that exits mid-turn leaves the
7
7
  * turn running with no subscriber — an orphan turn that keeps consuming tokens
8
- * and mutating the workspace invisibly (2026-10-19: one ran 2h21m / 345 steps
9
- * after its bridge died).
8
+ * and mutating the workspace invisibly, potentially for hours after the bridge
9
+ * died.
10
10
  *
11
11
  * `process.on('exit')` cannot await, so this path uses the same synchronous
12
12
  * curl transport the session reader already uses: bounded by `timeoutMs` and
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A DSH turn runs inside the shared `dsh web` host, not in a child process. If
5
5
  * the bridge dies (SIGKILL, crash) the turn keeps running with no subscriber —
6
- * the 2026-10-19 incident ran 2h21m / 345 steps after its bridge exited. The
6
+ * one orphaned turn kept going for hours after its bridge exited. The
7
7
  * runner writes one entry when it submits a prompt and removes it once the run
8
8
  * (or a verified stop) accounts for that prompt; the next startup reconciles
9
9
  * anything left behind:
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A DSH turn runs inside the shared `dsh web` host, not in a child process. If
5
5
  * the bridge dies (SIGKILL, crash) the turn keeps running with no subscriber —
6
- * the 2026-10-19 incident ran 2h21m / 345 steps after its bridge exited. The
6
+ * one orphaned turn kept going for hours after its bridge exited. The
7
7
  * runner writes one entry when it submits a prompt and removes it once the run
8
8
  * (or a verified stop) accounts for that prompt; the next startup reconciles
9
9
  * anything left behind:
@@ -18,7 +18,7 @@
18
18
  * back to the Web UI prompt.
19
19
  */
20
20
  import type { AgentSessionReader } from '../../session-contract.js';
21
- import type { AgentEvent, AgentStatusInfo, SpawnOptions, StopOutcome } from '../types.js';
21
+ import type { AgentEvent, AgentStatusInfo, BackgroundJobRoster, SpawnOptions, StopOutcome } from '../types.js';
22
22
  import type { AgentRunner } from '../types.js';
23
23
  import { DshClient, type WebSocketFactory } from './client.js';
24
24
  import type { DshDaemon } from './daemon.js';
@@ -155,12 +155,51 @@ export declare class DshRunner implements AgentRunner {
155
155
  */
156
156
  private submitPrompt;
157
157
  run(message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
158
+ /**
159
+ * 准备一个 turn(run() 的准备阶段):模型对齐 + 权限预设对齐 + goal seed 探测。
160
+ *
161
+ * 红线(逐条保留):
162
+ * - 对齐失败 fail closed:显式配置 provider 时 selectModel 失败必须 throw
163
+ * (静默回退到其他 provider 会让用户以为切换成功,实际模型与账单都错);
164
+ * 权限预设失败由 alignPermissionPreset throw(用户明确选了安全策略,不能在
165
+ * 错误的权限下继续跑 prompt)。
166
+ * - 准备期间被 stop(abort)→ 返回 { ok:false },由调用方 yield interrupted
167
+ * result 收尾。
168
+ * - goal seed 只探测不推断:读不到按 unknown 上报,不冒充「没有 goal」。
169
+ */
170
+ private prepareTurn;
158
171
  /**
159
172
  * Consume one turn from both transports. `session/follow` owns transcript
160
173
  * events; `$events` owns the answerable approval waterfall. The merged
161
174
  * stream keeps either transport from starving the other.
175
+ *
176
+ * 事件源分派:projection(goal + 状态条)→ handleProjectionItem;
177
+ * $events waterfall(审批/提问)→ handleApprovalWaterfallItem;
178
+ * assistant-stream 直转发(translator 持有本 turn 的 usage 累计状态);
179
+ * session/follow 事件 → handleSessionEvent(含 result 收尾的 jobs + goal 复核)。
162
180
  */
163
181
  private runTurn;
182
+ /**
183
+ * 事件源 handler:Host-wide 投影(goal + 状态条)。
184
+ * 返回要回放的事件(可能为空——未变化/非本 session 的帧不产事件)。
185
+ */
186
+ private handleProjectionItem;
187
+ /**
188
+ * 事件源 handler:$events 审批/提问 waterfall(channel 状态、审批 waterfall
189
+ * 登记、提问卡、cancel 回流、审批卡)。
190
+ * 返回要回放的事件;channel 状态类帧(ready/unavailable/request)只改内部
191
+ * 状态、不产事件。
192
+ */
193
+ private handleApprovalWaterfallItem;
194
+ /**
195
+ * 事件源 handler:`session/follow` 会话事件。
196
+ * - goal/change:权威变更,不等投影直接更新;
197
+ * - tool/call:先记账(审批卡命令展示用)再落入 translator(卡片工具行来源);
198
+ * - approval/decided:审批回流,清审批卡;
199
+ * - 其余事件经 translator 翻译;result 事件先做收尾(后台任务名单 + goal
200
+ * 复核)再随 result 一起返回。
201
+ */
202
+ private handleSessionEvent;
164
203
  /**
165
204
  * 收尾时仍在跑的后台任务。
166
205
  *
@@ -169,6 +208,13 @@ export declare class DshRunner implements AgentRunner {
169
208
  * 任务列表不等于有任务在跑。
170
209
  */
171
210
  private collectBackgroundJobs;
211
+ /**
212
+ * 订阅本 session 的后台任务名单(跨 turn 生命周期)。
213
+ *
214
+ * 卡片收尾后仍要靠它把「任务结束了」推给用户:run 的事件流在 result 就关了,
215
+ * 而后台任务可能再跑半小时。名单是整份替换,消费方自己做 diff。
216
+ */
217
+ watchJobs(sessionId: string, signal: AbortSignal): AsyncGenerator<ReadonlyArray<BackgroundJobRoster>>;
172
218
  /** Kill one background job of this runner's session (card button path). */
173
219
  killJob(sessionId: string, jobId: string): Promise<boolean>;
174
220
  /** Apply the configured permission preset once per session, before prompt. */
@@ -272,7 +318,7 @@ export declare class DshRunner implements AgentRunner {
272
318
  *
273
319
  * turn 跑在常驻共享的 `dsh web` host 里,不在本进程的子进程树上;所以
274
320
  * "杀自己的子进程"这套退出清理对它无效,桥接一退出,turn 继续无人接收地跑
275
- * (2026-10-19 实测:一个孤儿 turn 在桥接死后又跑了 2h21m / 345 个 step)。
321
+ * (孤儿 turn 在桥接死后可能再跑数小时)。
276
322
  * 这里复用进程级退出分发器,用同步 curl 发一次 `session/cancel`。
277
323
  */
278
324
  registerExitHandlers(): void;