lark-remote 0.5.3 → 0.5.5

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 (66) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/bridge/approval-coordinator.d.ts +18 -1
  5. package/dist/bridge/approval-coordinator.js +57 -10
  6. package/dist/bridge/index.js +90 -17
  7. package/dist/card/approval-render.js +18 -0
  8. package/dist/card/run-state.js +10 -5
  9. package/dist/config/index.d.ts +16 -0
  10. package/dist/config/index.js +46 -6
  11. package/dist/config/opencode-config.d.ts +3 -3
  12. package/dist/config/opencode-config.js +38 -22
  13. package/dist/inbound/authorize.d.ts +34 -0
  14. package/dist/inbound/authorize.js +31 -0
  15. package/dist/index.js +116 -63
  16. package/dist/instance-lock.d.ts +9 -0
  17. package/dist/instance-lock.js +113 -42
  18. package/dist/router/config/dsh.d.ts +51 -21
  19. package/dist/router/config/dsh.js +255 -66
  20. package/dist/router/config/opencode.js +16 -6
  21. package/dist/router/config/types.d.ts +6 -3
  22. package/dist/router/index.d.ts +1 -0
  23. package/dist/router/index.js +13 -5
  24. package/dist/runner/codex/app-server/translator.js +21 -7
  25. package/dist/runner/common/acp/base-acp-runner.d.ts +12 -0
  26. package/dist/runner/common/acp/base-acp-runner.js +83 -8
  27. package/dist/runner/common/acp/protocol-types.d.ts +10 -2
  28. package/dist/runner/common/acp/protocol-types.js +3 -1
  29. package/dist/runner/common/connection-based-runner.d.ts +15 -0
  30. package/dist/runner/common/connection-based-runner.js +40 -5
  31. package/dist/runner/common/jsonrpc/connection-manager.d.ts +15 -0
  32. package/dist/runner/common/jsonrpc/connection-manager.js +83 -50
  33. package/dist/runner/dsh/approval.d.ts +53 -0
  34. package/dist/runner/dsh/approval.js +161 -0
  35. package/dist/runner/dsh/auth.d.ts +36 -0
  36. package/dist/runner/dsh/auth.js +118 -0
  37. package/dist/runner/dsh/client.d.ts +161 -42
  38. package/dist/runner/dsh/client.js +551 -244
  39. package/dist/runner/dsh/daemon.d.ts +85 -0
  40. package/dist/runner/dsh/daemon.js +272 -0
  41. package/dist/runner/dsh/merge.d.ts +9 -0
  42. package/dist/runner/dsh/merge.js +51 -0
  43. package/dist/runner/dsh/runner.d.ts +94 -11
  44. package/dist/runner/dsh/runner.js +553 -67
  45. package/dist/runner/dsh/translator.d.ts +43 -3
  46. package/dist/runner/dsh/translator.js +115 -38
  47. package/dist/runner/dsh/types.d.ts +189 -27
  48. package/dist/runner/dsh/types.js +11 -5
  49. package/dist/runner/kimi/acp/runner.js +15 -3
  50. package/dist/runner/opencode/acp/error-monitor.d.ts +2 -2
  51. package/dist/runner/opencode/acp/error-monitor.js +2 -2
  52. package/dist/runner/opencode/acp/runner.d.ts +9 -0
  53. package/dist/runner/opencode/acp/runner.js +40 -1
  54. package/dist/runner/opencode/acp/translator.d.ts +4 -2
  55. package/dist/runner/opencode/acp/translator.js +12 -3
  56. package/dist/runner/pi/rpc/translator.d.ts +2 -0
  57. package/dist/runner/pi/rpc/translator.js +27 -1
  58. package/dist/runner/types.d.ts +34 -0
  59. package/dist/runner/types.js +24 -0
  60. package/dist/session/codex/rollout-reader.js +44 -7
  61. package/dist/session/dsh/sessions.d.ts +45 -6
  62. package/dist/session/dsh/sessions.js +171 -27
  63. package/dist/session/opencode/sessions.d.ts +18 -7
  64. package/dist/session/opencode/sessions.js +294 -15
  65. package/dist/session/pi/sessions.js +6 -2
  66. package/package.json +3 -1
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Global DSH Web Host lifecycle.
3
+ *
4
+ * One machine runs one DSH per `DSH_HOME`. lark-remote reuses an existing
5
+ * process, or starts `dsh web` detached when none is running. The child is
6
+ * deliberately independent: lark-remote exiting must not stop the shared DSH.
7
+ *
8
+ * Authentication is separate. DSH's `/api` and `/api/remote.mux` require a
9
+ * signed browser cookie; lark-remote mints a v1 cookie from the durable
10
+ * `client-connection/browser-session` secret in `$DSH_HOME/.credentials.yaml`,
11
+ * so a DSH restart never needs a copied launch token.
12
+ */
13
+ /** Configuration providers are read on every resolve so `/config` hot edits land. */
14
+ export interface DshDaemonOptions {
15
+ readonly host: () => string | undefined;
16
+ readonly dshHome?: () => string | undefined;
17
+ readonly dshCommand?: () => string | undefined;
18
+ readonly manageProcess?: () => boolean;
19
+ readonly startTimeoutMs?: () => number | undefined;
20
+ }
21
+ export type DshDaemonErrorCode = 'stopped' | 'start-lock' | 'spawn' | 'start-timeout';
22
+ /** User-facing daemon failure; the bridge renders `message` in the chat card. */
23
+ export declare class DshDaemonError extends Error {
24
+ readonly code: DshDaemonErrorCode;
25
+ constructor(code: DshDaemonErrorCode, message: string, options?: ErrorOptions);
26
+ }
27
+ interface SpawnedDaemon {
28
+ failure(): Error | undefined;
29
+ exited(): boolean;
30
+ }
31
+ /** Resolve the same Harness home DSH resolves: configured, env, then ~/.dsh. */
32
+ export declare function resolveDshHome(configured?: string, env?: NodeJS.ProcessEnv): string;
33
+ /** True only for the loopback HTTP address this process may start and probe. */
34
+ export declare function isLoopbackHttpUrl(value: string): boolean;
35
+ /**
36
+ * Build `dsh web` arguments for one managed base URL.
37
+ *
38
+ * `--host` takes one concrete IP literal, so `localhost` becomes 127.0.0.1.
39
+ * The port is always explicit: a managed DSH must be reachable at the address
40
+ * lark-remote probes, and `--port 0` would make rediscovery impossible.
41
+ */
42
+ export declare function dshDaemonArgs(baseUrl: string): string[];
43
+ /**
44
+ * One process-wide DSH resolver shared by the DSH runner and startup warmup.
45
+ * Concurrent callers share one in-flight start.
46
+ */
47
+ export declare class DshDaemon {
48
+ private readonly options;
49
+ private inFlight;
50
+ constructor(options: DshDaemonOptions);
51
+ /** The configured base URL, always without a trailing slash. */
52
+ baseUrl(): string;
53
+ /** The configured Harness home, used for the start lock and DSH log. */
54
+ home(): string;
55
+ /** The detached DSH stderr log for start diagnostics. */
56
+ logFile(): string;
57
+ dshCommand(): string;
58
+ /**
59
+ * Return a reachable DSH base URL, starting it when needed.
60
+ *
61
+ * A successful probe always wins. Only a genuinely unreachable loopback
62
+ * address is started, so a DSH already running on the configured port is
63
+ * never duplicated.
64
+ */
65
+ ensure(): Promise<string>;
66
+ private resolve;
67
+ private withStartLock;
68
+ private lockIsStale;
69
+ private waitUntilReady;
70
+ protected startTimeoutMs(): number;
71
+ protected manageProcess(): boolean;
72
+ /**
73
+ * Probe the API prefix, not `/`: a listening server whose plugin tree has
74
+ * not mounted yet answers 404 there, while a ready DSH answers 401 before
75
+ * dispatch. Any other status still proves the listener is alive.
76
+ */
77
+ protected probe(baseUrl: string): Promise<boolean>;
78
+ /**
79
+ * Start one detached DSH. stdout is discarded because it carries the
80
+ * tokenized startup URL; stderr goes to a 0600 log for failure diagnosis.
81
+ */
82
+ protected spawnDaemon(baseUrl: string): SpawnedDaemon;
83
+ protected sleep(ms: number): Promise<void>;
84
+ }
85
+ export {};
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Global DSH Web Host lifecycle.
3
+ *
4
+ * One machine runs one DSH per `DSH_HOME`. lark-remote reuses an existing
5
+ * process, or starts `dsh web` detached when none is running. The child is
6
+ * deliberately independent: lark-remote exiting must not stop the shared DSH.
7
+ *
8
+ * Authentication is separate. DSH's `/api` and `/api/remote.mux` require a
9
+ * signed browser cookie; lark-remote mints a v1 cookie from the durable
10
+ * `client-connection/browser-session` secret in `$DSH_HOME/.credentials.yaml`,
11
+ * so a DSH restart never needs a copied launch token.
12
+ */
13
+ import fs from 'node:fs';
14
+ import fsp from 'node:fs/promises';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+ import { DEFAULT_DSH_HOST } from '../../config/index.js';
18
+ import { mergeProcessEnv, spawnProcess } from '../../platform/spawn.js';
19
+ const DEFAULT_START_TIMEOUT_MS = 30_000;
20
+ const READY_POLL_MS = 100;
21
+ /** User-facing daemon failure; the bridge renders `message` in the chat card. */
22
+ export class DshDaemonError extends Error {
23
+ code;
24
+ constructor(code, message, options) {
25
+ super(message, options);
26
+ this.name = 'DshDaemonError';
27
+ this.code = code;
28
+ }
29
+ }
30
+ function stripTrailingSlash(value) {
31
+ return value.replace(/\/+$/, '');
32
+ }
33
+ /** Resolve the same Harness home DSH resolves: configured, env, then ~/.dsh. */
34
+ export function resolveDshHome(configured, env = process.env) {
35
+ const value = configured?.trim() || env.DSH_HOME?.trim();
36
+ return path.resolve(value && value.length > 0 ? value : path.join(os.homedir(), '.dsh'));
37
+ }
38
+ /** True only for the loopback HTTP address this process may start and probe. */
39
+ export function isLoopbackHttpUrl(value) {
40
+ try {
41
+ const url = new URL(value);
42
+ if (url.protocol !== 'http:')
43
+ return false;
44
+ return (url.hostname === '127.0.0.1' ||
45
+ url.hostname === 'localhost' ||
46
+ url.hostname === '[::1]' ||
47
+ url.hostname === '::1');
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ /**
54
+ * Build `dsh web` arguments for one managed base URL.
55
+ *
56
+ * `--host` takes one concrete IP literal, so `localhost` becomes 127.0.0.1.
57
+ * The port is always explicit: a managed DSH must be reachable at the address
58
+ * lark-remote probes, and `--port 0` would make rediscovery impossible.
59
+ */
60
+ export function dshDaemonArgs(baseUrl) {
61
+ const url = new URL(baseUrl);
62
+ const hostname = url.hostname.replace(/^\[|\]$/g, '');
63
+ const host = hostname === 'localhost' ? '127.0.0.1' : hostname;
64
+ return ['web', '--host', host, '--port', url.port || '3080', '--no-open'];
65
+ }
66
+ /**
67
+ * One process-wide DSH resolver shared by the DSH runner and startup warmup.
68
+ * Concurrent callers share one in-flight start.
69
+ */
70
+ export class DshDaemon {
71
+ options;
72
+ inFlight;
73
+ constructor(options) {
74
+ this.options = options;
75
+ }
76
+ /** The configured base URL, always without a trailing slash. */
77
+ baseUrl() {
78
+ return stripTrailingSlash(this.options.host()?.trim() || DEFAULT_DSH_HOST);
79
+ }
80
+ /** The configured Harness home, used for the start lock and DSH log. */
81
+ home() {
82
+ return resolveDshHome(this.options.dshHome?.());
83
+ }
84
+ /** The detached DSH stderr log for start diagnostics. */
85
+ logFile() {
86
+ return path.join(this.home(), 'lark-remote', 'dsh-web.log');
87
+ }
88
+ dshCommand() {
89
+ return this.options.dshCommand?.()?.trim() || 'dsh';
90
+ }
91
+ /**
92
+ * Return a reachable DSH base URL, starting it when needed.
93
+ *
94
+ * A successful probe always wins. Only a genuinely unreachable loopback
95
+ * address is started, so a DSH already running on the configured port is
96
+ * never duplicated.
97
+ */
98
+ async ensure() {
99
+ const baseUrl = this.baseUrl();
100
+ if (await this.probe(baseUrl))
101
+ return baseUrl;
102
+ if (this.inFlight === undefined) {
103
+ const pending = this.resolve(baseUrl);
104
+ this.inFlight = pending;
105
+ pending
106
+ .finally(() => {
107
+ if (this.inFlight === pending)
108
+ this.inFlight = undefined;
109
+ })
110
+ .catch(() => {
111
+ // The racing caller observes the same failure.
112
+ });
113
+ }
114
+ return this.inFlight;
115
+ }
116
+ async resolve(baseUrl) {
117
+ if (!isLoopbackHttpUrl(baseUrl) || !this.manageProcess()) {
118
+ throw new DshDaemonError('stopped', `DSH 未运行(${baseUrl}),且当前配置不会由 lark-remote 启动它。` +
119
+ `请先启动:\n ${this.dshCommand()} web --port ${portOf(baseUrl)} --no-open`);
120
+ }
121
+ return this.withStartLock(baseUrl, async () => {
122
+ if (await this.probe(baseUrl))
123
+ return baseUrl;
124
+ let spawned;
125
+ try {
126
+ spawned = this.spawnDaemon(baseUrl);
127
+ }
128
+ catch (error) {
129
+ throw new DshDaemonError('spawn', `DSH 启动失败:${error.message}\n日志:${this.logFile()}`, { cause: error });
130
+ }
131
+ if (await this.waitUntilReady(baseUrl, this.startTimeoutMs(), spawned))
132
+ return baseUrl;
133
+ const failure = spawned.failure();
134
+ if (failure !== undefined) {
135
+ throw new DshDaemonError('spawn', `DSH 启动失败:${failure.message}\n日志:${this.logFile()}`, { cause: failure });
136
+ }
137
+ if (spawned.exited()) {
138
+ throw new DshDaemonError('spawn', `DSH 启动进程提前退出(端口可能被其他进程占用),${baseUrl} 未就绪。\n` +
139
+ `日志:${this.logFile()}`);
140
+ }
141
+ throw new DshDaemonError('start-timeout', `DSH 启动超时(${String(this.startTimeoutMs())}ms),${baseUrl} 仍不可达。\n` +
142
+ `日志:${this.logFile()}`);
143
+ });
144
+ }
145
+ async withStartLock(baseUrl, run) {
146
+ const lockDir = path.join(this.home(), 'lark-remote', 'start.lock');
147
+ await fsp.mkdir(path.dirname(lockDir), { recursive: true, mode: 0o700 });
148
+ await fsp.chmod(path.dirname(lockDir), 0o700).catch(() => { });
149
+ const deadline = Date.now() + this.startTimeoutMs();
150
+ while (true) {
151
+ if (await this.probe(baseUrl))
152
+ return baseUrl;
153
+ try {
154
+ await fsp.mkdir(lockDir, { mode: 0o700 });
155
+ break;
156
+ }
157
+ catch (error) {
158
+ if (error.code !== 'EEXIST')
159
+ throw error;
160
+ if (await this.lockIsStale(lockDir)) {
161
+ await fsp.rm(lockDir, { recursive: true, force: true });
162
+ continue;
163
+ }
164
+ if (Date.now() >= deadline) {
165
+ throw new DshDaemonError('start-lock', `等待其他 lark-remote 启动 DSH 超时。锁文件:${lockDir}`);
166
+ }
167
+ await this.sleep(READY_POLL_MS);
168
+ }
169
+ }
170
+ try {
171
+ return await run();
172
+ }
173
+ finally {
174
+ await fsp.rm(lockDir, { recursive: true, force: true });
175
+ }
176
+ }
177
+ async lockIsStale(lockDir) {
178
+ try {
179
+ const stats = await fsp.stat(lockDir);
180
+ return Date.now() - stats.mtimeMs > this.startTimeoutMs() * 2;
181
+ }
182
+ catch {
183
+ return false;
184
+ }
185
+ }
186
+ async waitUntilReady(baseUrl, timeoutMs, spawned) {
187
+ const deadline = Date.now() + timeoutMs;
188
+ while (Date.now() < deadline) {
189
+ if (await this.probe(baseUrl))
190
+ return true;
191
+ if (spawned.failure() !== undefined || spawned.exited())
192
+ return false;
193
+ await this.sleep(READY_POLL_MS);
194
+ }
195
+ return this.probe(baseUrl);
196
+ }
197
+ startTimeoutMs() {
198
+ const configured = this.options.startTimeoutMs?.();
199
+ return configured !== undefined && configured > 0 ? configured : DEFAULT_START_TIMEOUT_MS;
200
+ }
201
+ manageProcess() {
202
+ return this.options.manageProcess?.() ?? true;
203
+ }
204
+ /**
205
+ * Probe the API prefix, not `/`: a listening server whose plugin tree has
206
+ * not mounted yet answers 404 there, while a ready DSH answers 401 before
207
+ * dispatch. Any other status still proves the listener is alive.
208
+ */
209
+ async probe(baseUrl) {
210
+ try {
211
+ const response = await fetch(`${baseUrl}/api/`, {
212
+ method: 'GET',
213
+ redirect: 'manual',
214
+ signal: AbortSignal.timeout(1_500),
215
+ });
216
+ return response.status !== 404;
217
+ }
218
+ catch {
219
+ return false;
220
+ }
221
+ }
222
+ /**
223
+ * Start one detached DSH. stdout is discarded because it carries the
224
+ * tokenized startup URL; stderr goes to a 0600 log for failure diagnosis.
225
+ */
226
+ spawnDaemon(baseUrl) {
227
+ const home = this.home();
228
+ const log = this.logFile();
229
+ fs.mkdirSync(path.dirname(log), { recursive: true, mode: 0o700 });
230
+ try {
231
+ fs.chmodSync(path.dirname(log), 0o700);
232
+ }
233
+ catch {
234
+ // Non-POSIX filesystems keep their native ACLs.
235
+ }
236
+ const fd = fs.openSync(log, 'a', 0o600);
237
+ try {
238
+ fs.chmodSync(log, 0o600);
239
+ }
240
+ catch {
241
+ // Non-POSIX filesystems keep their native ACLs.
242
+ }
243
+ let failure;
244
+ let exited = false;
245
+ try {
246
+ const child = spawnProcess(this.dshCommand(), dshDaemonArgs(baseUrl), {
247
+ detached: true,
248
+ stdio: ['ignore', 'ignore', fd],
249
+ cwd: home,
250
+ env: mergeProcessEnv(process.env, { DSH_HOME: home }),
251
+ });
252
+ child.once('error', (error) => {
253
+ failure = error;
254
+ });
255
+ child.once('exit', () => {
256
+ exited = true;
257
+ });
258
+ child.unref();
259
+ return { failure: () => failure, exited: () => exited };
260
+ }
261
+ finally {
262
+ fs.closeSync(fd);
263
+ }
264
+ }
265
+ async sleep(ms) {
266
+ await new Promise((resolve) => setTimeout(resolve, ms));
267
+ }
268
+ }
269
+ function portOf(baseUrl) {
270
+ return new URL(baseUrl).port || '3080';
271
+ }
272
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Merge independent async iterables into one consumer stream.
3
+ *
4
+ * DSH delivers a turn over `session/follow` and approvals over `$events`.
5
+ * Both must feed one runner loop without assuming which transport emits first.
6
+ * Source failures are rethrown to the consumer; normal source completion does
7
+ * not end the merged stream until every source has completed.
8
+ */
9
+ export declare function mergeAsync<T>(sources: readonly AsyncIterable<T>[]): AsyncGenerator<T>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Merge independent async iterables into one consumer stream.
3
+ *
4
+ * DSH delivers a turn over `session/follow` and approvals over `$events`.
5
+ * Both must feed one runner loop without assuming which transport emits first.
6
+ * Source failures are rethrown to the consumer; normal source completion does
7
+ * not end the merged stream until every source has completed.
8
+ */
9
+ export async function* mergeAsync(sources) {
10
+ const queue = [];
11
+ const waiters = [];
12
+ let completed = 0;
13
+ let failure;
14
+ let hasFailure = false;
15
+ const wake = () => {
16
+ for (const resolve of waiters.splice(0))
17
+ resolve();
18
+ };
19
+ for (const source of sources) {
20
+ void (async () => {
21
+ try {
22
+ for await (const item of source) {
23
+ queue.push(item);
24
+ wake();
25
+ }
26
+ }
27
+ catch (err) {
28
+ if (!hasFailure) {
29
+ hasFailure = true;
30
+ failure = err;
31
+ }
32
+ }
33
+ finally {
34
+ completed += 1;
35
+ wake();
36
+ }
37
+ })();
38
+ }
39
+ while (true) {
40
+ if (hasFailure)
41
+ throw failure;
42
+ if (queue.length > 0) {
43
+ yield queue.shift();
44
+ continue;
45
+ }
46
+ if (completed >= sources.length)
47
+ return;
48
+ await new Promise((resolve) => waiters.push(resolve));
49
+ }
50
+ }
51
+ //# sourceMappingURL=merge.js.map
@@ -2,26 +2,39 @@
2
2
  * DshRunner — workspace-lifetime runner bridging to the DSH Web Host
3
3
  * (DeepSeek Harness) over plain HTTP. No local subprocess is spawned.
4
4
  *
5
- * Flow per run: session.create(cwd) (when no sessionId) → synthetic system/init
6
- * (DSH has no init event) → open per-session mux subscription → session.prompt
7
- * (mode:'queue') → consume subscription until turn/end → result event.
5
+ * Flow per run: session/create(cwd) (when no sessionId) → model/permission
6
+ * alignment → synthetic system/init (DSH has no init event) → session/prompt
7
+ * (mode:'queue') → consume `session/follow` + `$events` until turn/end.
8
8
  *
9
- * stop() → session.cancel + abort the mux subscription. killOrphan /
10
- * registerExitHandlers are no-ops (no local process). approval/requested is
11
- * surfaced as a WARN log + an assistant text prompt pointing to the DSH Web UI
12
- * (never silently parked — the turn stays live until the user resolves it in
13
- * the UI or the turn is cancelled/times out).
9
+ * stop() → session/cancel + abort the mux subscription. killOrphan /
10
+ * registerExitHandlers are no-ops (no local process). DSH approvals are
11
+ * two-channel: durable `approval/asked` / `approval/decided` drive the Feishu
12
+ * card, while the answerable `$events` waterfall supplies eventId/clientId.
13
+ * If `$events` is unavailable, the runner falls back to the Web UI prompt.
14
14
  */
15
15
  import type { AgentEvent, AgentSessionReader, AgentStatusInfo, SpawnOptions } from '../types.js';
16
16
  import type { AgentRunner } from '../types.js';
17
17
  import { DshClient, type WebSocketFactory } from './client.js';
18
+ import type { DshDaemon } from './daemon.js';
18
19
  export interface DshRunnerOptions {
19
20
  kind: 'dsh';
20
21
  sessionReader: AgentSessionReader;
22
+ /** Global DSH process manager; omitted in tests that drive a fake server. */
23
+ daemon?: DshDaemon;
21
24
  /** DSH Web Host base URL, e.g. http://127.0.0.1:3080. */
22
25
  host?: string;
26
+ /** Configured DSH home; selects `.credentials.yaml` when credentialsFile is unset. */
27
+ dshHome?: string;
28
+ /** Optional `dsh web` launch token (or its startup URL). */
29
+ token?: string;
30
+ /** DSH credentials file with the durable browser-session signing secret. */
31
+ credentialsFile?: string;
23
32
  /** Session preset(agentPreset)固定于 session 创建时;undefined = 服务端默认。 */
24
33
  agentPreset?: string;
34
+ /** 权限预设(沙箱 + 审批策略);undefined = 不干预。 */
35
+ permissionPreset?: string;
36
+ /** Provider route id (e.g. deepseek-official / anthropic); undefined = legacy default. */
37
+ provider?: string;
25
38
  /** Display model name. */
26
39
  model?: string;
27
40
  /** 推理强度(off/low/high/max);undefined = 服务端默认。 */
@@ -30,29 +43,99 @@ export interface DshRunnerOptions {
30
43
  fetchImpl?: typeof fetch;
31
44
  /** Injectable WebSocket factory (defaults to global WebSocket). */
32
45
  webSocketImpl?: WebSocketFactory;
46
+ /** Unary request timeout in ms (default 60s); 0 disables. */
47
+ requestTimeoutMs?: number;
33
48
  }
34
49
  export declare class DshRunner implements AgentRunner {
35
50
  readonly kind: "dsh";
51
+ /**
52
+ * Session identity lives in the DSH Web Host (server-side), not in a local
53
+ * process, so a finished run is still addressable — the same property the
54
+ * bridge uses before recording a run as compactable. Without it the Compact
55
+ * button renders (capability is declared) but every click answers
56
+ * 「该任务已结束或不属于当前会话」.
57
+ */
58
+ readonly lifetime: "workspace";
36
59
  readonly sessionReader: AgentSessionReader;
37
60
  readonly client: DshClient;
38
61
  /** 用户配置的模型 ID(run 前 selectModel 对齐用);undefined = 服务端默认。 */
39
62
  private readonly configuredModel;
63
+ /** 用户配置的 provider route;undefined = 兼容旧配置,按 deepseek-official 处理。 */
64
+ private readonly configuredProvider;
40
65
  private readonly agentPreset;
66
+ private configuredPermissionPreset;
41
67
  private readonly reasoningEffort;
42
68
  private activeAbort;
43
69
  private activeSessionId;
44
70
  private running;
71
+ private readonly daemon;
45
72
  /**
46
73
  * 已对齐模型的 session 集合:同一 session 二次 run 不再重发 selectModel
47
- * (session.selectModel 会写服务端全局默认,每次 run 都写是写放大)。
48
- * configuredModel 在实例生命周期内不变(/config 改模型会 clearRunners
49
- * 重建实例 + 换新 sessionId),故「每 session 对齐一次」足以覆盖配置变化。
74
+ * (session/selectModel 会写服务端全局默认,每次 run 都写是写放大)。
75
+ * configuredModel / configuredProvider 在实例生命周期内不变(/config 改动
76
+ * 会 clearRunners 重建实例),故「每 session 对齐一次」足以覆盖配置变化。
50
77
  */
51
78
  private readonly alignedSessions;
79
+ /** 已对齐权限预设的 session;同一 session 只执行一次 `/permission`。 */
80
+ private readonly permissionAlignedSessions;
81
+ /** `$events` approval waterfall state for the current run. */
82
+ private readonly approvalTracker;
83
+ /** callId → tool call, used to render the command in the approval card. */
84
+ private readonly toolCalls;
85
+ /** False after `$events` is unavailable; approval/asked then falls back to text. */
86
+ private approvalChannelAvailable;
52
87
  constructor(opts: DshRunnerOptions);
53
88
  get isRunning(): boolean;
54
89
  getStatusInfo(): AgentStatusInfo;
90
+ /**
91
+ * Resolve the configured provider/model pair.
92
+ *
93
+ * A model-only config keeps the legacy `deepseek-official` route, so
94
+ * existing configs behave exactly as before. A provider-only config asks
95
+ * the live catalog for that provider's default (or first) model.
96
+ */
97
+ private resolveModelSelection;
55
98
  run(message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
99
+ /**
100
+ * Consume one turn from both transports. `session/follow` owns transcript
101
+ * events; `$events` owns the answerable approval waterfall. The merged
102
+ * stream keeps either transport from starving the other.
103
+ */
104
+ private runTurn;
105
+ /** Apply the configured permission preset once per session, before prompt. */
106
+ private alignPermissionPreset;
107
+ /** Hot-update the configured preset; the next run aligns it. */
108
+ updateApprovalMode(settings: {
109
+ permissionPreset?: string;
110
+ }): Promise<void>;
111
+ /**
112
+ * Answer one DSH approval. `accept` / `decline` are the only user decisions
113
+ * exposed by the DSH Web UI; `cancel` stops the turn through session/cancel.
114
+ */
115
+ respondApproval(requestId: number | string, response: unknown): Promise<void>;
116
+ private buildApprovalRequested;
117
+ private approvalResolved;
118
+ private approvalFallbackText;
119
+ /**
120
+ * Compact the session through DSH's `/compact` command.
121
+ *
122
+ * Commands are a separate RPC from turns (`commands/execute`, idle-only), so
123
+ * this never prompts the model and never waits for a `turn/end`: the RPC
124
+ * itself settles when the compaction finishes, which is why the unary budget
125
+ * is disabled here (stop + the idle watchdog bound it instead). DSH reports
126
+ * expected refusals (busy / nothing to compact / summary failure) as
127
+ * `kind:'error'` values rather than RPC errors — those must become an error
128
+ * result, not a success card.
129
+ */
130
+ runCompact(_message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
131
+ /**
132
+ * Resolve with the promise's value, or `undefined` as soon as `signal` aborts.
133
+ * The underlying promise keeps running (its own timeout still applies) — the
134
+ * caller decides what to do with a late settlement.
135
+ */
136
+ private awaitOrAbort;
137
+ /** Send session/cancel with a short bound; never throws (cleanup path). */
138
+ private cancelSessionQuietly;
56
139
  stop(_opts?: {
57
140
  immediate?: boolean;
58
141
  }): Promise<void>;