lark-remote 0.5.12 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/README.en.md +1 -14
  3. package/README.md +1 -14
  4. package/dist/bridge/approval-coordinator.d.ts +1 -0
  5. package/dist/bridge/approval-coordinator.js +12 -24
  6. package/dist/bridge/index.d.ts +7 -0
  7. package/dist/bridge/index.js +77 -227
  8. package/dist/bridge/run-approval.d.ts +15 -0
  9. package/dist/bridge/run-approval.js +41 -0
  10. package/dist/bridge/run-usage.d.ts +46 -0
  11. package/dist/bridge/run-usage.js +96 -0
  12. package/dist/bridge/session-writeback.d.ts +18 -0
  13. package/dist/bridge/session-writeback.js +36 -0
  14. package/dist/card/bash-card-session.js +2 -5
  15. package/dist/card/card-session.d.ts +2 -0
  16. package/dist/card/card-session.js +20 -22
  17. package/dist/card/card-shared.d.ts +4 -4
  18. package/dist/card/card-shared.js +3 -9
  19. package/dist/card/collapsible.js +3 -2
  20. package/dist/card/final-answer-overflow.d.ts +7 -0
  21. package/dist/card/final-answer-overflow.js +46 -0
  22. package/dist/card/run-card-session.js +2 -7
  23. package/dist/card/run-renderer.d.ts +14 -0
  24. package/dist/card/run-renderer.js +119 -1
  25. package/dist/card/run-state.d.ts +12 -1
  26. package/dist/card/run-state.js +73 -34
  27. package/dist/config/codex-config.js +4 -4
  28. package/dist/config/index.d.ts +4 -2
  29. package/dist/config/index.js +6 -4
  30. package/dist/inbound/turn-assembler.js +6 -0
  31. package/dist/index.js +15 -409
  32. package/dist/message-handlers.d.ts +26 -0
  33. package/dist/message-handlers.js +275 -0
  34. package/dist/router/card-action-payload.d.ts +1 -1
  35. package/dist/router/card-action-payload.js +1 -1
  36. package/dist/router/commands/ls.d.ts +9 -0
  37. package/dist/router/commands/ls.js +243 -0
  38. package/dist/router/commands/order.d.ts +12 -0
  39. package/dist/router/commands/order.js +178 -0
  40. package/dist/router/commands/resume.d.ts +47 -0
  41. package/dist/router/commands/resume.js +246 -0
  42. package/dist/router/commands/ws.d.ts +15 -0
  43. package/dist/router/commands/ws.js +189 -0
  44. package/dist/router/config/claude.js +3 -3
  45. package/dist/router/config/codex.js +2 -6
  46. package/dist/router/config/common/turn-idle.d.ts +2 -0
  47. package/dist/router/config/common/turn-idle.js +9 -0
  48. package/dist/router/config/kimi.js +2 -6
  49. package/dist/router/config/opencode.js +2 -6
  50. package/dist/router/index.d.ts +14 -22
  51. package/dist/router/index.js +70 -1152
  52. package/dist/router/paging.d.ts +13 -0
  53. package/dist/router/paging.js +20 -0
  54. package/dist/router/path-validation.d.ts +27 -0
  55. package/dist/router/path-validation.js +45 -0
  56. package/dist/runner/claude/runner.d.ts +3 -4
  57. package/dist/runner/claude/runner.js +4 -5
  58. package/dist/runner/claude/session.js +1 -1
  59. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  60. package/dist/runner/codex/app-server/translator.js +65 -9
  61. package/dist/runner/common/acp/base-translator.d.ts +3 -8
  62. package/dist/runner/common/acp/base-translator.js +16 -7
  63. package/dist/runner/common/spawning-runner.js +10 -4
  64. package/dist/runner/common/turn-started.d.ts +2 -0
  65. package/dist/runner/common/turn-started.js +10 -0
  66. package/dist/runner/dsh/auth.js +2 -3
  67. package/dist/runner/dsh/client.d.ts +1 -6
  68. package/dist/runner/dsh/client.js +11 -165
  69. package/dist/runner/dsh/daemon.d.ts +0 -2
  70. package/dist/runner/dsh/daemon.js +1 -6
  71. package/dist/runner/dsh/errors.d.ts +5 -0
  72. package/dist/runner/dsh/errors.js +11 -0
  73. package/dist/runner/dsh/exit-cancel.d.ts +0 -2
  74. package/dist/runner/dsh/exit-cancel.js +1 -1
  75. package/dist/runner/dsh/home.d.ts +2 -0
  76. package/dist/runner/dsh/home.js +8 -0
  77. package/dist/runner/dsh/inflight.d.ts +0 -2
  78. package/dist/runner/dsh/inflight.js +1 -1
  79. package/dist/runner/dsh/mux-stream.d.ts +26 -0
  80. package/dist/runner/dsh/mux-stream.js +157 -0
  81. package/dist/runner/dsh/translator.d.ts +0 -10
  82. package/dist/runner/dsh/translator.js +1 -1
  83. package/dist/runner/index.d.ts +1 -1
  84. package/dist/runner/kimi/acp/translator.js +1 -12
  85. package/dist/runner/opencode/acp/translator.js +1 -12
  86. package/dist/runner/pi/rpc/translator.d.ts +2 -8
  87. package/dist/runner/pi/rpc/translator.js +2 -7
  88. package/dist/runner/registry.d.ts +0 -19
  89. package/dist/runner/registry.js +0 -32
  90. package/dist/runner/types.d.ts +17 -1
  91. package/dist/session/claude/session-index.js +4 -2
  92. package/dist/session/kimi/sessions.js +2 -2
  93. package/dist/session/opencode/sessions.js +2 -1
  94. package/dist/session/pi/sessions.js +3 -2
  95. package/package.json +1 -1
@@ -0,0 +1,13 @@
1
+ export declare function pageInfo(totalCount: number, offset: number, pageSize: number): {
2
+ totalPages: number;
3
+ safeOffset: number;
4
+ currentPage: number;
5
+ };
6
+ export declare function pageSlice<T>(items: readonly T[], offset: number, pageSize: number): {
7
+ totalCount: number;
8
+ totalPages: number;
9
+ safeOffset: number;
10
+ currentPage: number;
11
+ pageItems: T[];
12
+ hasPagination: boolean;
13
+ };
@@ -0,0 +1,20 @@
1
+ import { clampInt } from '../common/clamp.js';
2
+ export function pageInfo(totalCount, offset, pageSize) {
3
+ const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
4
+ const maxOffset = Math.max(0, (totalPages - 1) * pageSize);
5
+ const safeOffset = clampInt(offset, 0, maxOffset);
6
+ const currentPage = Math.floor(safeOffset / pageSize) + 1;
7
+ return { totalPages, safeOffset, currentPage };
8
+ }
9
+ export function pageSlice(items, offset, pageSize) {
10
+ const { totalPages, safeOffset, currentPage } = pageInfo(items.length, offset, pageSize);
11
+ return {
12
+ totalCount: items.length,
13
+ totalPages,
14
+ safeOffset,
15
+ currentPage,
16
+ pageItems: items.slice(safeOffset, safeOffset + pageSize),
17
+ hasPagination: items.length > pageSize,
18
+ };
19
+ }
20
+ //# sourceMappingURL=paging.js.map
@@ -0,0 +1,27 @@
1
+ import fs from 'node:fs';
2
+ export type PathKind = 'directory' | 'file' | 'missing' | 'unreadable';
3
+ export type StatResolution = {
4
+ ok: true;
5
+ stat: fs.Stats;
6
+ } | {
7
+ ok: false;
8
+ reason: 'missing' | 'unreadable';
9
+ };
10
+ export type DirectoryResolution = {
11
+ ok: true;
12
+ canonical: string;
13
+ } | {
14
+ ok: false;
15
+ reason: 'missing' | 'not_directory' | 'unreadable';
16
+ };
17
+ /**
18
+ * Inspect a path without a preceding existsSync call.
19
+ *
20
+ * stat already distinguishes missing paths from files/directories and closes
21
+ * the exists→stat race. Permission and other IO failures stay distinguishable
22
+ * from a normal missing path so callers can keep their user-facing messages.
23
+ */
24
+ export declare function statPath(target: string): StatResolution;
25
+ export declare function inspectPath(target: string): PathKind;
26
+ export declare function resolveDirectory(target: string): DirectoryResolution;
27
+ export declare function isDirectorySafe(target: string): boolean;
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs';
2
+ /**
3
+ * Inspect a path without a preceding existsSync call.
4
+ *
5
+ * stat already distinguishes missing paths from files/directories and closes
6
+ * the exists→stat race. Permission and other IO failures stay distinguishable
7
+ * from a normal missing path so callers can keep their user-facing messages.
8
+ */
9
+ export function statPath(target) {
10
+ try {
11
+ return { ok: true, stat: fs.statSync(target) };
12
+ }
13
+ catch (err) {
14
+ const code = err.code;
15
+ return {
16
+ ok: false,
17
+ reason: code === 'ENOENT' || code === 'ENOTDIR' ? 'missing' : 'unreadable',
18
+ };
19
+ }
20
+ }
21
+ export function inspectPath(target) {
22
+ const resolution = statPath(target);
23
+ if (!resolution.ok)
24
+ return resolution.reason;
25
+ return resolution.stat.isDirectory() ? 'directory' : 'file';
26
+ }
27
+ export function resolveDirectory(target) {
28
+ const kind = inspectPath(target);
29
+ if (kind === 'missing')
30
+ return { ok: false, reason: 'missing' };
31
+ if (kind === 'unreadable')
32
+ return { ok: false, reason: 'unreadable' };
33
+ if (kind === 'file')
34
+ return { ok: false, reason: 'not_directory' };
35
+ try {
36
+ return { ok: true, canonical: fs.realpathSync(target) };
37
+ }
38
+ catch {
39
+ return { ok: false, reason: 'unreadable' };
40
+ }
41
+ }
42
+ export function isDirectorySafe(target) {
43
+ return inspectPath(target) === 'directory';
44
+ }
45
+ //# sourceMappingURL=path-validation.js.map
@@ -3,10 +3,9 @@ import { type AgentEvent, type SpawnOptions, type AgentSessionReader, type Agent
3
3
  * Claude Agent Runner(长驻交互模式)。
4
4
  *
5
5
  * 权限模式(permissionMode)在 spawn 时经 --permission-mode 生效:
6
- * - 默认 bypassPermissions:claude 内部自动放行,无 control_request → 行为与
7
- * 旧版完全一致(无审批卡);
8
- * - 配置为 default/acceptEdits/manual/plan 等:工具调用经 control_request
9
- * 上抛 → bridge ApprovalCoordinator → run 卡审批区。
6
+ * - 默认 auto:低风险操作自动放行,高风险操作的 control_request 上抛 →
7
+ * bridge ApprovalCoordinator → run 卡审批区;
8
+ * - bypassPermissions:claude 内部全部放行,无 control_request(无审批卡)。
10
9
  */
11
10
  export declare class ClaudeRunner implements AgentRunner {
12
11
  readonly kind: "claude";
@@ -24,10 +24,9 @@ const COMPACT_COMMAND = '/compact';
24
24
  * Claude Agent Runner(长驻交互模式)。
25
25
  *
26
26
  * 权限模式(permissionMode)在 spawn 时经 --permission-mode 生效:
27
- * - 默认 bypassPermissions:claude 内部自动放行,无 control_request → 行为与
28
- * 旧版完全一致(无审批卡);
29
- * - 配置为 default/acceptEdits/manual/plan 等:工具调用经 control_request
30
- * 上抛 → bridge ApprovalCoordinator → run 卡审批区。
27
+ * - 默认 auto:低风险操作自动放行,高风险操作的 control_request 上抛 →
28
+ * bridge ApprovalCoordinator → run 卡审批区;
29
+ * - bypassPermissions:claude 内部全部放行,无 control_request(无审批卡)。
31
30
  */
32
31
  export class ClaudeRunner {
33
32
  kind = 'claude';
@@ -40,7 +39,7 @@ export class ClaudeRunner {
40
39
  constructor(opts) {
41
40
  this.defaultModel = opts.model ?? 'claude-opus-4-8';
42
41
  this.defaultEffort = opts.effort ?? 'medium';
43
- this.permissionMode = opts.permissionMode ?? 'bypassPermissions';
42
+ this.permissionMode = opts.permissionMode ?? 'auto';
44
43
  this.sessionReader = opts.sessionReader ?? new ClaudeSessionReader();
45
44
  this.session = new ClaudeSession({
46
45
  pidDir: opts.pidDir,
@@ -100,7 +100,7 @@ export class ClaudeSession extends SpawningRunner {
100
100
  agent: 'claude',
101
101
  });
102
102
  this.binary = 'claude';
103
- this.permissionMode = opts.permissionMode ?? 'bypassPermissions';
103
+ this.permissionMode = opts.permissionMode ?? 'auto';
104
104
  this.settings = opts.settings;
105
105
  this.defaultEffort = opts.effort;
106
106
  this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
@@ -17,6 +17,8 @@ export declare class CodexAppServerTranslator {
17
17
  private turnStatus;
18
18
  /** Full agentMessage text per item id (item-scoped snapshot). */
19
19
  private textByItem;
20
+ /** agentMessage item ids whose protocol phase is `final_answer`. */
21
+ private finalTextItems;
20
22
  /**
21
23
  * Reasoning content per item id: paragraphs keyed by `contentIndex`
22
24
  * (textDelta) plus the summary stream (summaryTextDelta). Each reasoning
@@ -18,6 +18,8 @@ export class CodexAppServerTranslator {
18
18
  turnStatus = 'pending';
19
19
  /** Full agentMessage text per item id (item-scoped snapshot). */
20
20
  textByItem = new Map();
21
+ /** agentMessage item ids whose protocol phase is `final_answer`. */
22
+ finalTextItems = new Set();
21
23
  /**
22
24
  * Reasoning content per item id: paragraphs keyed by `contentIndex`
23
25
  * (textDelta) plus the summary stream (summaryTextDelta). Each reasoning
@@ -141,15 +143,24 @@ export class CodexAppServerTranslator {
141
143
  const text = item.text ?? '';
142
144
  if (text && this.textByItem.get(item.id) !== text) {
143
145
  this.textByItem.set(item.id, text);
144
- events.push({
145
- type: 'turn_diff',
146
- text,
147
- itemId: item.id,
148
- complete: true,
149
- threadId: params?.threadId ?? this.threadId,
150
- turnId: params?.turn?.id ?? this.turnId,
151
- timestamp: this.now(),
152
- });
146
+ const final = item.phase === 'final_answer';
147
+ events.push(final
148
+ ? {
149
+ type: 'final_answer',
150
+ text,
151
+ itemId: item.id,
152
+ complete: true,
153
+ timestamp: this.now(),
154
+ }
155
+ : {
156
+ type: 'turn_diff',
157
+ text,
158
+ itemId: item.id,
159
+ complete: true,
160
+ threadId: params?.threadId ?? this.threadId,
161
+ turnId: params?.turn?.id ?? this.turnId,
162
+ timestamp: this.now(),
163
+ });
153
164
  }
154
165
  }
155
166
  else if (item.type === 'plan') {
@@ -228,6 +239,7 @@ export class CodexAppServerTranslator {
228
239
  // 的 turnId 闸门挡掉。
229
240
  this.usage = { inputTokens: 0, outputTokens: 0 };
230
241
  this.textByItem.clear();
242
+ this.finalTextItems.clear();
231
243
  this.reasoningByItem.clear();
232
244
  this.toolOutputByItem.clear();
233
245
  this.toolItemMeta.clear();
@@ -249,6 +261,17 @@ export class CodexAppServerTranslator {
249
261
  const prev = this.textByItem.get(params.itemId) ?? '';
250
262
  const next = prev + params.delta;
251
263
  this.textByItem.set(params.itemId, next);
264
+ if (this.finalTextItems.has(params.itemId)) {
265
+ return [
266
+ {
267
+ type: 'final_answer',
268
+ text: next,
269
+ itemId: params.itemId,
270
+ complete: false,
271
+ timestamp: this.now(),
272
+ },
273
+ ];
274
+ }
252
275
  return [
253
276
  {
254
277
  type: 'turn_diff',
@@ -358,6 +381,19 @@ export class CodexAppServerTranslator {
358
381
  if (text || accumulated) {
359
382
  const finalText = text || accumulated;
360
383
  this.textByItem.set(item.id, finalText);
384
+ if (item.phase === 'final_answer' ||
385
+ this.finalTextItems.has(item.id)) {
386
+ this.finalTextItems.delete(item.id);
387
+ return [
388
+ {
389
+ type: 'final_answer',
390
+ text: finalText,
391
+ itemId: item.id,
392
+ complete: true,
393
+ timestamp: this.now(),
394
+ },
395
+ ];
396
+ }
361
397
  return [
362
398
  {
363
399
  type: 'turn_diff',
@@ -635,6 +671,26 @@ export class CodexAppServerTranslator {
635
671
  }
636
672
  handleItemStarted(params) {
637
673
  const item = params.item;
674
+ if (item.type === 'agentMessage') {
675
+ const phase = item.phase;
676
+ if (phase === 'final_answer') {
677
+ this.finalTextItems.add(item.id);
678
+ const text = item.text ?? '';
679
+ if (text) {
680
+ this.textByItem.set(item.id, text);
681
+ return [
682
+ {
683
+ type: 'final_answer',
684
+ text,
685
+ itemId: item.id,
686
+ complete: false,
687
+ timestamp: this.now(),
688
+ },
689
+ ];
690
+ }
691
+ }
692
+ return [];
693
+ }
638
694
  if (item.type === 'fileChange' && Array.isArray(item.changes)) {
639
695
  this.fileChangeItems.set(item.id, item.changes);
640
696
  const pending = this.pendingFileApprovalByItemId.get(item.id);
@@ -19,16 +19,10 @@
19
19
  * buildToolUseUpdate — tool_use patch from tool_call_update
20
20
  * onUsageSample — extra usage bookkeeping (opencode: cost)
21
21
  */
22
- import type { AgentEvent, ApprovalRequestedEvent, ResultEvent } from '../../types.js';
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
24
  /** Self-produced turn_started event (no wire equivalent — produced on prompt start). */
25
- export interface AcpTurnStartedEvent {
26
- type: 'turn_started';
27
- threadId: string;
28
- turnId: string;
29
- operationKind: 'turn' | 'compaction';
30
- timestamp?: string;
31
- }
25
+ export type AcpTurnStartedEvent = TurnStartedEvent;
32
26
  /**
33
27
  * Live usage snapshot from usage_update. The ACP wire carries context
34
28
  * occupancy (`used`) and the model context window (`size`) — there is NO
@@ -96,6 +90,7 @@ export declare abstract class BaseAcpTranslator {
96
90
  * is sent (no wire notification for this — self-produced).
97
91
  */
98
92
  produceTurnStarted(sessionId: string, turnId: string): AcpTurnStartedEvent;
93
+ protected approvalRequestedEvents(requestId: number | string, sessionId: string, view: ApprovalView): ApprovalRequestedEvent[];
99
94
  /**
100
95
  * Translate a prompt response into a result event.
101
96
  * stopReason: end_turn → success; cancelled → interrupted (independent
@@ -1,3 +1,4 @@
1
+ import { makeTurnStartedEvent } from '../turn-started.js';
1
2
  import { NotificationMethod, ServerRequestMethod, SessionEventType, } from './protocol-types.js';
2
3
  /**
3
4
  * ACP tool kind → 常用工具名映射(信息保真,数据驱动无 agent 分支)。
@@ -145,13 +146,21 @@ export class BaseAcpTranslator {
145
146
  */
146
147
  produceTurnStarted(sessionId, turnId) {
147
148
  this.currentTurnId = turnId;
148
- return {
149
- type: 'turn_started',
150
- threadId: sessionId,
151
- turnId,
152
- operationKind: this.operationKind === 'compact' ? 'compaction' : 'turn',
153
- timestamp: new Date().toISOString(),
154
- };
149
+ return makeTurnStartedEvent(sessionId, turnId, this.operationKind);
150
+ }
151
+ approvalRequestedEvents(requestId, sessionId, view) {
152
+ return [
153
+ {
154
+ type: 'approval_requested',
155
+ requestId,
156
+ kind: 'command',
157
+ threadId: sessionId,
158
+ turnId: '',
159
+ itemId: '',
160
+ view,
161
+ timestamp: new Date().toISOString(),
162
+ },
163
+ ];
155
164
  }
156
165
  /**
157
166
  * Translate a prompt response into a result event.
@@ -374,13 +374,19 @@ export class SpawningRunner {
374
374
  try {
375
375
  process.kill(-pid, 'SIGTERM');
376
376
  }
377
- catch {
378
- // process may have exited
377
+ catch (err) {
378
+ const code = err.code;
379
+ if (code === 'ESRCH') {
380
+ getLogger().debug(`[${this.logTag}] orphan process group ${-pid} already exited`);
381
+ }
382
+ else {
383
+ getLogger().warn(`[${this.logTag}] killOrphan failed to signal process group ${-pid}: ${err?.message ?? String(err)}`);
384
+ }
379
385
  }
380
386
  bestEffortUnlink(this.pidFilePath);
381
387
  }
382
- catch {
383
- // ignore
388
+ catch (err) {
389
+ getLogger().warn(`[${this.logTag}] killOrphan failed for ${this.pidFilePath}: ${err?.message ?? String(err)}`);
384
390
  }
385
391
  }
386
392
  /**
@@ -0,0 +1,2 @@
1
+ import type { TurnStartedEvent } from '../types.js';
2
+ export declare function makeTurnStartedEvent(sessionId: string, turnId: string, operationKind: 'turn' | 'compact'): TurnStartedEvent;
@@ -0,0 +1,10 @@
1
+ export function makeTurnStartedEvent(sessionId, turnId, operationKind) {
2
+ return {
3
+ type: 'turn_started',
4
+ threadId: sessionId,
5
+ turnId,
6
+ operationKind: operationKind === 'compact' ? 'compaction' : 'turn',
7
+ timestamp: new Date().toISOString(),
8
+ };
9
+ }
10
+ //# sourceMappingURL=turn-started.js.map
@@ -10,10 +10,10 @@
10
10
  */
11
11
  import { createHash, createHmac } from 'node:crypto';
12
12
  import fs from 'node:fs';
13
- import os from 'node:os';
14
13
  import path from 'node:path';
15
14
  import YAML from 'yaml';
16
15
  import { isWin32 } from '../../platform/select.js';
16
+ import { resolveDshHome } from './home.js';
17
17
  /** Credential record that owns the browser-cookie signing secret. */
18
18
  export const DSH_BROWSER_CREDENTIAL_KEY = 'client-connection/browser-session';
19
19
  /** Short lifetime keeps every request safely inside DSH's configurable max age. */
@@ -29,8 +29,7 @@ function isRecord(value) {
29
29
  * file matches the `DSH_HOME` lark-remote passes to a process it starts.
30
30
  */
31
31
  export function resolveDshCredentialsFile(env = process.env, configuredHome) {
32
- const home = configuredHome?.trim() || env.DSH_HOME?.trim();
33
- return path.join(home && home.length > 0 ? home : path.join(os.homedir(), '.dsh'), '.credentials.yaml');
32
+ return path.join(resolveDshHome(configuredHome, env), '.credentials.yaml');
34
33
  }
35
34
  /** Whether a filesystem error means the credentials file is absent. */
36
35
  export function isDshCredentialsNotFound(error) {
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import type { DshApprovalStreamItem, DshCommandResult, DshModelCatalogValue, DshModelSelection, DshOpenTurnState, DshPermissionCatalogValue, DshJobView, DshPresetListValue, DshProjectionItem, DshProjectionSnapshot, DshQuestion, DshRemoteEventOutcome, DshSessionPageValue, DshStreamItem, DshTokenUsage } from './types.js';
23
23
  import type { GoalView } from '../types.js';
24
+ export { DshError } from './errors.js';
24
25
  interface WebSocketMessageEvent {
25
26
  data: unknown;
26
27
  }
@@ -40,11 +41,6 @@ export interface WebSocketLike {
40
41
  close(): void;
41
42
  }
42
43
  export type WebSocketFactory = (url: string, options?: WebSocketOptions) => WebSocketLike;
43
- export declare class DshError extends Error {
44
- readonly code: string;
45
- readonly details: Record<string, unknown>;
46
- constructor(code: string, message: string, details?: Record<string, unknown>);
47
- }
48
44
  export interface DshClientOptions {
49
45
  /** Configured DSH home; selects `.credentials.yaml` when no explicit file is given. */
50
46
  dshHome?: string;
@@ -372,4 +368,3 @@ export declare function dshQuestionsOf(request: Record<string, unknown>): DshQue
372
368
  * silently on the server side.
373
369
  */
374
370
  export declare function isTimedQuestionRequest(request: Record<string, unknown>): boolean;
375
- export {};
@@ -25,6 +25,9 @@ import { getLogger } from '../../logger/index.js';
25
25
  import { DSH_COOKIE_LIFETIME_MS, isDshCredentialsNotFound, mintDshBrowserCookie, readDshBrowserSecret, resolveDshCredentialsFile, } from './auth.js';
26
26
  import { DEFAULT_REQUEST_TIMEOUT_MS } from '../common/defaults.js';
27
27
  import { goalFromProjection } from './goal-view.js';
28
+ import { DshError } from './errors.js';
29
+ import { openMuxStream } from './mux-stream.js';
30
+ export { DshError } from './errors.js';
28
31
  function defaultWebSocketFactory(url, options) {
29
32
  const socket = new WebSocket(url, {
30
33
  ...(options?.headers ? { headers: options.headers } : {}),
@@ -35,16 +38,6 @@ function defaultWebSocketFactory(url, options) {
35
38
  });
36
39
  return socket;
37
40
  }
38
- export class DshError extends Error {
39
- code;
40
- details;
41
- constructor(code, message, details = {}) {
42
- super(message);
43
- this.name = 'DshError';
44
- this.code = code;
45
- this.details = details;
46
- }
47
- }
48
41
  function isDshAuthError(error) {
49
42
  return (error instanceof DshError &&
50
43
  error.code === 'http' &&
@@ -602,162 +595,15 @@ export class DshClient {
602
595
  * close, error, or `signal` abort.
603
596
  */
604
597
  async *muxStream(endpoint, payload, signal) {
605
- const cookie = await this.authCookie();
606
- const socket = this.webSocketImpl(`${this.baseUrl.replace(/^http/, 'ws')}/api/remote.mux`, cookie ? { headers: { cookie } } : undefined);
607
- const streamId = `lr-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
608
- const queue = [];
609
- let waiter = null;
610
- let ended = false;
611
- let failure;
612
- let opened = false;
613
- const settle = (frame) => {
614
- if (waiter) {
615
- const resolve = waiter;
616
- waiter = null;
617
- resolve(frame);
618
- return;
619
- }
620
- if (frame)
621
- queue.push(frame);
622
- else
623
- ended = true;
624
- };
625
- // 建连/首帧超时:服务器接受 TCP 但不完成 WS 握手时 onopen/onerror/onclose
626
- // 都不触发,generator 会永远停在 await next()——unary 挂起有 60s 预算与
627
- // 回归测试(test_anchor_dsh_stop_with_hung_unary_releases_run),mux 的
628
- // 同型问题此前只能靠 idle 看门狗兜底。超时按传输错误走既有重连路径。
629
- const connectTimer = setTimeout(() => {
630
- if (opened || ended)
631
- return;
632
- failure = new DshError('transport', 'DSH remote mux WebSocket connect timeout');
633
- ended = true;
634
- settle(undefined);
635
- try {
636
- socket.close();
637
- }
638
- catch {
639
- /* ignore close error */
640
- }
641
- }, this.requestTimeoutMs);
642
- connectTimer.unref?.();
643
- socket.onopen = () => {
644
- opened = true;
645
- clearTimeout(connectTimer);
646
- if (ended)
647
- return;
648
- try {
649
- socket.send(JSON.stringify({
650
- type: 'open',
651
- streamId,
652
- endpoint,
653
- payload,
654
- }));
655
- }
656
- catch (err) {
657
- failure = new DshError('transport', `DSH remote mux send error: ${err.message}`);
658
- ended = true;
659
- settle(undefined);
660
- }
661
- };
662
- socket.onmessage = (event) => {
663
- if (ended)
664
- return;
665
- const text = typeof event.data === 'string'
666
- ? event.data
667
- : Buffer.isBuffer(event.data)
668
- ? event.data.toString('utf8')
669
- : String(event.data);
670
- let message;
671
- try {
672
- message = JSON.parse(text);
673
- }
674
- catch {
675
- getLogger().warn('[dsh] dropped malformed remote mux frame');
676
- return;
677
- }
678
- if (message.streamId !== streamId)
679
- return;
680
- if (message.type === 'item' && message.value) {
681
- settle(message.value);
682
- return;
683
- }
684
- if (message.type === 'end') {
685
- ended = true;
686
- settle(undefined);
687
- return;
688
- }
689
- if (message.type === 'error') {
690
- failure = new DshError(message.error?.code ?? 'stream', message.error?.message ?? 'DSH remote stream failed', message.error?.details ?? {});
691
- ended = true;
692
- settle(undefined);
693
- }
694
- };
695
- socket.onerror = () => {
696
- if (ended)
697
- return;
698
- failure = new DshError('transport', 'DSH remote mux WebSocket error');
699
- ended = true;
700
- settle(undefined);
701
- };
702
- socket.onunexpectedresponse = (event) => {
703
- if (ended)
704
- return;
705
- const status = event.statusCode;
706
- failure = new DshError('http', `DSH remote mux WebSocket HTTP ${status === undefined ? 'error' : status}`, status === undefined ? {} : { status });
707
- ended = true;
708
- settle(undefined);
709
- };
710
- socket.onclose = () => {
711
- if (ended)
712
- return;
713
- ended = true;
714
- settle(undefined);
715
- };
716
- const onAbort = () => {
717
- if (ended)
718
- return;
719
- ended = true;
720
- settle(undefined);
721
- try {
722
- socket.close();
723
- }
724
- catch {
725
- /* ignore close error */
726
- }
727
- };
728
- signal.addEventListener('abort', onAbort, { once: true });
729
- const next = () => new Promise((resolve) => {
730
- if (queue.length > 0) {
731
- resolve(queue.shift());
732
- return;
733
- }
734
- if (ended) {
735
- resolve(undefined);
736
- return;
737
- }
738
- waiter = resolve;
598
+ yield* openMuxStream({
599
+ baseUrl: this.baseUrl,
600
+ endpoint,
601
+ payload,
602
+ signal,
603
+ requestTimeoutMs: this.requestTimeoutMs,
604
+ authCookie: () => this.authCookie(),
605
+ webSocketFactory: this.webSocketImpl,
739
606
  });
740
- try {
741
- while (!signal.aborted) {
742
- const frame = await next();
743
- if (frame === undefined) {
744
- if (failure && !signal.aborted)
745
- throw failure;
746
- return;
747
- }
748
- yield frame;
749
- }
750
- }
751
- finally {
752
- clearTimeout(connectTimer);
753
- signal.removeEventListener('abort', onAbort);
754
- try {
755
- socket.close();
756
- }
757
- catch {
758
- /* ignore close error */
759
- }
760
- }
761
607
  }
762
608
  /** One logical `session/follow` stream. */
763
609
  async *followStream(sessionId, signal) {
@@ -28,8 +28,6 @@ interface SpawnedDaemon {
28
28
  failure(): Error | undefined;
29
29
  exited(): boolean;
30
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
31
  /** True only for the loopback HTTP address this process may start and probe. */
34
32
  export declare function isLoopbackHttpUrl(value: string): boolean;
35
33
  /**
@@ -12,10 +12,10 @@
12
12
  */
13
13
  import fs from 'node:fs';
14
14
  import fsp from 'node:fs/promises';
15
- import os from 'node:os';
16
15
  import path from 'node:path';
17
16
  import { DEFAULT_DSH_HOST } from '../../config/index.js';
18
17
  import { mergeProcessEnv, spawnProcess } from '../../platform/spawn.js';
18
+ import { resolveDshHome } from './home.js';
19
19
  const DEFAULT_START_TIMEOUT_MS = 30_000;
20
20
  const READY_POLL_MS = 100;
21
21
  /** User-facing daemon failure; the bridge renders `message` in the chat card. */
@@ -30,11 +30,6 @@ export class DshDaemonError extends Error {
30
30
  function stripTrailingSlash(value) {
31
31
  return value.replace(/\/+$/, '');
32
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
33
  /** True only for the loopback HTTP address this process may start and probe. */
39
34
  export function isLoopbackHttpUrl(value) {
40
35
  try {