@deepseek-ai/dsh-client-connection 0.0.1-rc.1 → 0.0.1-rc.2

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.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/connection/README.md
5
- README.md: 85ff46052ba2f032ee6a95b16c396d45e766d3ba
6
- README.zh.md: 89cbb19a984d88e09b7af0890f57ecd15d46d3a5
5
+ README.md: a82bb55ab65df2732ad16248d2cc9aa15b60e94d
6
+ README.zh.md: d4c19db8e74e3562ec55b5ef60675aef79b82008
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [中文](README.zh.md)
4
4
 
5
- Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + current-page loopback state + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` abstraction, and the loop's sink/config types. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for `events.mux` and `events.host`; the in-process carrier satisfies the same two-stream abstraction. The Host half owns the single `/api` route and its Fetch bridge; a registered TypeRT interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the `/api` Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived `ctx.connection.isLoopback` state. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`openDocument`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from — and the agent-preset authoring plane, `agentPreset.read`/`copy`/`openDocument`/`remove`, since a composition names the plugins a session runs, so reading one is reconnaissance, and copy/remove/openDocument manage the roster and drive the host desktop (authoring is copy-only, so none of them accepts composition text or a path); `agentPreset.list` and `agentPreset.select` stay out — the roster carries only ids and trust, and choosing a preset grants nothing `session.create`'s own `agentPreset` did not, over a default that already carries bash) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the [WebSocket downlink carrier Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md).
5
+ Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + current-page loopback state + observable generation-scoped `hostDescription` + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` abstraction, and the loop's sink/config types. Each successful readiness handshake publishes the exact `host.describe` value before `onConnected`; generation loss and explicit stop clear it, so native-capability consumers never retain a disconnected answer. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for `events.mux` and `events.host`; the in-process carrier satisfies the same two-stream abstraction. The Host half owns the single `/api` route and its Fetch bridge; a registered TypeRT interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the `/api` Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived `ctx.connection.isLoopback` state. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`openDocument`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from — and the agent-preset authoring plane, `agentPreset.read`/`copy`/`openDocument`/`remove`, since a composition names the plugins a session runs, so reading one is reconnaissance, and copy/remove/openDocument manage the roster and drive the host desktop (authoring is copy-only, so none of them accepts composition text or a path); `agentPreset.list` and `agentPreset.select` stay out — the roster carries only ids and trust, and choosing a preset grants nothing `session.create`'s own `agentPreset` did not, over a default that already carries bash) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the [WebSocket downlink carrier Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md).
6
6
 
7
7
  ## /api browser-trust fence
8
8
 
package/README.zh.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- 协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 当前页面的 loopback 状态 + 单消费方流循环启动器);导出表层携带协议约定类型、`AbstractApiClient` 抽象,以及循环的 sink/配置类型。浏览器载体以 HTTP POST 发送 unary/respond,并为 `events.mux` 与 `events.host` 各开一条只下行的 WebSocket;进程内载体满足同一双流抽象。Host half 持有唯一 `/api` route 及其 Fetch bridge;已注册的 TypeRT interceptor 会先认领自己的 Remote endpoint,未认领请求再回退 API Proxy。Loopback hostname 判定逻辑留在包内部:`/api` Host fence 与 WebSocket upgrade 会直接使用它,其他客户端插件则消费派生的 `ctx.connection.isLoopback` 状态。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory`、`host.openPath`,以及整个配置面——`settings.describe`/`openDocument`/`update`/`replace`/`mutate` 与 `credentials.describe`/`set`/`unset`;读取与原生操作也在内,因为 describe 会返回已暴露的配置、打开操作会作用于 Host 桌面,而探测任意引用会报出某条凭据来自何处——以及 agent preset 的创作面 `agentPreset.read`/`copy`/`openDocument`/`remove`,因为组装指明了一个会话所运行的插件,读取它是侦察,而 copy/remove/openDocument 管理名单并驱动宿主桌面(创作只有复制一种写入,因此这些方法都不接收组装文本或路径);`agentPreset.list` 与 `agentPreset.select` 不在其中——名单只携带 id 与信任级别,而选择一个 preset 并不比 `session.create` 自带的 `agentPreset` 多给任何能力,何况默认 preset 本就带着 bash)以空信任表过信任 fence,从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台载体与 ConnectionController 循环属于包内部;apply 负责选择并驱动它们。下行边界见 [WebSocket 下行载体 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md)。
5
+ 协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 当前页面的 loopback 状态 + 可观察且按 generation 生效的 `hostDescription` + 单消费方流循环启动器);导出表层携带协议约定类型、`AbstractApiClient` 抽象,以及循环的 sink/配置类型。每次就绪握手成功后,都会在 `onConnected` 之前发布完整的 `host.describe` 值;generation 失效或显式 stop 会清空它,因此原生能力消费者不会保留已经断线的判断。浏览器载体以 HTTP POST 发送 unary/respond,并为 `events.mux` 与 `events.host` 各开一条只下行的 WebSocket;进程内载体满足同一双流抽象。Host half 持有唯一 `/api` route 及其 Fetch bridge;已注册的 TypeRT interceptor 会先认领自己的 Remote endpoint,未认领请求再回退 API Proxy。Loopback hostname 判定逻辑留在包内部:`/api` Host fence 与 WebSocket upgrade 会直接使用它,其他客户端插件则消费派生的 `ctx.connection.isLoopback` 状态。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory`、`host.openPath`,以及整个配置面——`settings.describe`/`openDocument`/`update`/`replace`/`mutate` 与 `credentials.describe`/`set`/`unset`;读取与原生操作也在内,因为 describe 会返回已暴露的配置、打开操作会作用于 Host 桌面,而探测任意引用会报出某条凭据来自何处——以及 agent preset 的创作面 `agentPreset.read`/`copy`/`openDocument`/`remove`,因为组装指明了一个会话所运行的插件,读取它是侦察,而 copy/remove/openDocument 管理名单并驱动宿主桌面(创作只有复制一种写入,因此这些方法都不接收组装文本或路径);`agentPreset.list` 与 `agentPreset.select` 不在其中——名单只携带 id 与信任级别,而选择一个 preset 并不比 `session.create` 自带的 `agentPreset` 多给任何能力,何况默认 preset 本就带着 bash)以空信任表过信任 fence,从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台载体与 ConnectionController 循环属于包内部;apply 负责选择并驱动它们。下行边界见 [WebSocket 下行载体 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md)。
6
6
 
7
7
  ## /api 浏览器信任栅栏
8
8
 
package/lib/client.js CHANGED
@@ -67,6 +67,10 @@ window.__ModuleLoader__.load({
67
67
  isRunning() {
68
68
  return this.running;
69
69
  }
70
+ /** Re-read both mutable liveness guards after a potentially reentrant sink. */
71
+ isGenerationActive(controller) {
72
+ return this.isRunning() && !controller.signal.aborted;
73
+ }
70
74
  async loop() {
71
75
  while (this.running) {
72
76
  const gen = ++this.generation;
@@ -99,7 +103,9 @@ window.__ModuleLoader__.load({
99
103
  if (ac.signal.aborted) throw new Error("generation aborted during readiness handshake");
100
104
  this.attempt = 0;
101
105
  this.emitState("connected");
102
- this.callSink(this.sinks.onConnected);
106
+ if (this.isGenerationActive(ac)) this.callSink(() => {
107
+ this.sinks.onConnected?.(descriptionResult.value);
108
+ });
103
109
  } catch {
104
110
  if (!ac.signal.aborted) ac.abort();
105
111
  }
@@ -131,7 +137,6 @@ window.__ModuleLoader__.load({
131
137
  }
132
138
  /** Sink exception isolation: a business-layer throw is logged only, never affecting pump or reconnect semantics. */
133
139
  callSink(fn) {
134
- if (fn === void 0) return;
135
140
  try {
136
141
  fn();
137
142
  } catch (error) {
@@ -492,7 +497,7 @@ window.__ModuleLoader__.load({
492
497
  }
493
498
  /**
494
499
  * Fold a transport exception into the RpcResult error branch (unified error
495
- * surface; 'internal' as the catch-all code). Lives with RpcResult so every
500
+ * API; 'internal' as the catch-all code). Lives with RpcResult so every
496
501
  * carrier consumer folds the same way.
497
502
  * @param error - the thrown value from the carrier.
498
503
  * @returns the error branch of an RpcResult.
@@ -4880,6 +4885,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4880
4885
  existingCwd: string().optional()
4881
4886
  })
4882
4887
  }),
4888
+ object({
4889
+ code: literal("invalid-time-zone"),
4890
+ message: string(),
4891
+ details: object({ value: string() })
4892
+ }),
4883
4893
  object({
4884
4894
  code: literal("workspace-attach-failed"),
4885
4895
  message: string(),
@@ -5164,7 +5174,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5164
5174
  /**
5165
5175
  * sessions domain zod schemas (names derived from map keys: sessionListRequestSchema /
5166
5176
  * sessionListValueSchema). SessionEvent passthrough = strict envelope (type/seq/time) + wide
5167
- * data: the merge-extensible event surface keeps an unknown-type branch at the union level,
5177
+ * data: the merge-extensible event API keeps an unknown-type branch at the union level,
5168
5178
  * with no field-level passthrough. SessionId brand cast point: sessionIdSchema, and only there.
5169
5179
  */
5170
5180
  /** SessionId: one brand cast after schema validation (the only cast point in this domain). */
@@ -5185,7 +5195,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5185
5195
  time: number(),
5186
5196
  data: unknown(),
5187
5197
  sourceEventSeqs: array(number()).optional(),
5188
- surfaceOp: unknown().optional()
5198
+ surfaceOp: unknown().optional(),
5199
+ ignorable: literal(true).optional()
5189
5200
  });
5190
5201
  /** SessionSummary row of session.list (`projections` reuses the history block's shape and schema). */
5191
5202
  const sessionSummarySchema = object({
@@ -5347,7 +5358,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5347
5358
  object({
5348
5359
  sessionId: sessionIdSchema,
5349
5360
  mode: union([literal("queue"), literal("steer")]),
5350
- content: array(promptContentPartSchema)
5361
+ content: array(promptContentPartSchema),
5362
+ clientTimeZone: string().optional()
5351
5363
  });
5352
5364
  /** session.prompt response value (the command slot appears only when the prompt dispatched a slash command). */
5353
5365
  const sessionPromptValueSchema = object({
@@ -5408,6 +5420,26 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5408
5420
  approvalId: approvalRequestIdSchema,
5409
5421
  outcome: union([literal("allowed-once"), literal("rejected")])
5410
5422
  });
5423
+ /**
5424
+ * One wire task view. `kind` stays an open string because producer plugins
5425
+ * extend the registry's kind map by declaration merging, so the closed set is
5426
+ * not knowable at this boundary.
5427
+ */
5428
+ const taskViewSchema = object({
5429
+ id: string().min(1),
5430
+ kind: string().min(1),
5431
+ label: string().min(1),
5432
+ status: union([
5433
+ literal("running"),
5434
+ literal("stopping"),
5435
+ literal("completed"),
5436
+ literal("killed"),
5437
+ literal("failed")
5438
+ ]),
5439
+ detail: string().optional(),
5440
+ startedAt: number().int().nonnegative(),
5441
+ finishedAt: number().int().nonnegative().optional()
5442
+ });
5411
5443
  //#endregion
5412
5444
  //#region ../../host/apiproxy/src/api/workspace.schema.ts
5413
5445
  /**
@@ -5545,6 +5577,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5545
5577
  message: messageSchema
5546
5578
  }))
5547
5579
  }),
5580
+ object({
5581
+ type: literal("session/tasks"),
5582
+ sessionId: sessionIdSchema,
5583
+ tasks: array(taskViewSchema)
5584
+ }),
5548
5585
  object({
5549
5586
  type: literal("session/projection"),
5550
5587
  sessionId: sessionIdSchema,
@@ -5594,21 +5631,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5594
5631
  type: literal("host/archived-sessions-changed"),
5595
5632
  archivedSessionIds: array(sessionIdSchema)
5596
5633
  }),
5597
- object({ type: literal("host/commands-changed") }),
5598
- object({
5599
- type: literal("host/session-preset-changed"),
5600
- sessionId: sessionIdSchema,
5601
- agentPreset: string()
5602
- }),
5603
- object({
5604
- type: literal("host/settings-changed"),
5605
- ns: string()
5606
- }),
5607
5634
  object({
5608
- type: literal("host/credentials-changed"),
5609
- ref: string()
5635
+ type: literal("host/remote-event"),
5636
+ event: string().min(1),
5637
+ args: array(unknown())
5610
5638
  }),
5611
- object({ type: literal("host/models-changed") }),
5612
5639
  object({
5613
5640
  type: literal("stream/error"),
5614
5641
  error: rpcErrorSchema
@@ -5621,7 +5648,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5621
5648
  cwd: string(),
5622
5649
  provider: string().optional(),
5623
5650
  model: string().optional(),
5624
- attachedSessions: number().int().nonnegative()
5651
+ attachedSessions: number().int().nonnegative(),
5652
+ canOpenPath: boolean()
5625
5653
  });
5626
5654
  object({});
5627
5655
  /** host.pickDirectory response value; null means the user cancelled. */
@@ -5983,7 +6011,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5983
6011
  parentSessionId: sessionIdSchema,
5984
6012
  childSessionId: sessionIdSchema,
5985
6013
  mode: literal("continuable"),
5986
- content: array(contentBlockSchema)
6014
+ content: array(contentBlockSchema),
6015
+ clientTimeZone: string().optional()
5987
6016
  });
5988
6017
  object({
5989
6018
  parentSessionId: sessionIdSchema,
@@ -6354,7 +6383,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6354
6383
  "- first item",
6355
6384
  " - nested item",
6356
6385
  "",
6357
- "| Surface | State |",
6386
+ "| Area | State |",
6358
6387
  "| --- | --- |",
6359
6388
  "| history | rendered |",
6360
6389
  "| streaming | stable |",
@@ -8936,7 +8965,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8936
8965
  describe: (request) => ok(request, {
8937
8966
  version: "0.0.0-fixture",
8938
8967
  cwd: "/tmp/fixture",
8939
- attachedSessions
8968
+ attachedSessions,
8969
+ canOpenPath: true
8940
8970
  }),
8941
8971
  pickDirectory: (request) => ok(request, { path: `${FIXTURE_HOME}/Documents/project` }),
8942
8972
  listDirectory: (request) => {
@@ -9594,7 +9624,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9594
9624
  outcome: message.result.ok ? "answered" : "cancelled"
9595
9625
  });
9596
9626
  return Promise.resolve({ accepted: true });
9597
- }
9627
+ },
9628
+ downloads: { sessionLog: () => Promise.resolve(new Response("fixture mode does not serve session export", { status: 404 })) }
9598
9629
  },
9599
9630
  rpc: { call(channel, endpoint, payload) {
9600
9631
  if (channel !== "/api") return Promise.reject(/* @__PURE__ */ new Error(`fixture connection RPC channel ${JSON.stringify(channel)} is unavailable`));
@@ -9916,17 +9947,49 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9916
9947
  const api = fixtureClient ?? new WebApiClient();
9917
9948
  const rpc = fixtureClient?.rpc ?? createWebConnectionRpc();
9918
9949
  let started = false;
9950
+ let description;
9951
+ const descriptionListeners = /* @__PURE__ */ new Set();
9952
+ const publishDescription = (next) => {
9953
+ if (Object.is(description, next)) return;
9954
+ description = next;
9955
+ for (const listener of [...descriptionListeners]) try {
9956
+ listener();
9957
+ } catch (error) {
9958
+ console.error("[web-runtime] host-description listener threw:", error);
9959
+ }
9960
+ };
9919
9961
  const handle = {
9920
9962
  api,
9921
9963
  isLoopback: pageLocation === void 0 || isLoopbackHostname(pageLocation.hostname),
9964
+ hostDescription: {
9965
+ getSnapshot: () => description,
9966
+ subscribe: (listener) => {
9967
+ descriptionListeners.add(listener);
9968
+ return () => {
9969
+ descriptionListeners.delete(listener);
9970
+ };
9971
+ }
9972
+ },
9922
9973
  rpc,
9923
9974
  start(sinks, config) {
9924
9975
  if (started) throw new Error("connection: the stream loop is already owned by another consumer");
9925
9976
  started = true;
9926
- const controller = new ConnectionController(api, sinks, config ?? {});
9977
+ const controller = new ConnectionController(api, {
9978
+ ...sinks,
9979
+ onConnected: (next) => {
9980
+ publishDescription(next);
9981
+ if (!Object.is(description, next)) return;
9982
+ sinks.onConnected?.(next);
9983
+ },
9984
+ onStateChange: (state) => {
9985
+ if (state === "reconnecting") publishDescription(void 0);
9986
+ sinks.onStateChange?.(state);
9987
+ }
9988
+ }, config ?? {});
9927
9989
  controller.start();
9928
9990
  return { stop: () => {
9929
9991
  controller.stop();
9992
+ publishDescription(void 0);
9930
9993
  } };
9931
9994
  }
9932
9995
  };
@@ -1,4 +1,4 @@
1
- export type { ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, PromptContentPart, HostApi, EventsApi, MuxFrame, HostFrame, ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView, DirectoryEntry, DirectoryListing, ResponseValue, WorkspaceApi, WorkspaceId, WorkspaceView, CommandsApi, CommandDescriptor, SkillsApi, SkillEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels, GoalsApi, GoalRef, SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView, CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi, SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, } from '@deepseek-ai/dsh-host-apiproxy/api';
1
+ export type { ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, PromptContentPart, HostApi, EventsApi, MuxFrame, HostFrame, ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView, DirectoryEntry, DirectoryListing, ResponseValue, WorkspaceApi, WorkspaceId, WorkspaceView, CommandsApi, CommandDescriptor, SkillsApi, SkillEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels, GoalsApi, GoalRef, SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView, CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi, SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, TaskView, } from '@deepseek-ai/dsh-host-apiproxy/api';
2
2
  export type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation';
3
3
  export type { RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode, ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt, } from '@deepseek-ai/dsh-host-apiproxy/api';
4
4
  export { RpcId, SESSION_SEARCH_RESULT_LIMIT, transportError, } from '@deepseek-ai/dsh-host-apiproxy/api';
@@ -1,4 +1,4 @@
1
- import type { IApiClient, HostFrame, MuxFrame, RpcRequest } from './api.ts';
1
+ import type { HostDescription, IApiClient, HostFrame, MuxFrame, RpcRequest } from './api.ts';
2
2
  /** Reconnect/backoff tunables (deployment-varying — no hardcoded tunables; these become the
3
3
  * future `ctx.connection` plugin's Config). All fields optional; defaults below. */
4
4
  export interface ConnectionConfig {
@@ -23,7 +23,7 @@ export interface ConnectionSinks {
23
23
  onMuxEnvelope?: (envelope: RpcRequest<MuxFrame>) => void;
24
24
  onHostEnvelope?: (envelope: RpcRequest<HostFrame>) => void;
25
25
  /** After each connection generation is established (both streams open + describe succeeded), first connect included. */
26
- onConnected?: () => void;
26
+ onConnected?: (description: HostDescription) => void;
27
27
  /** Coarse state transitions (deduplicated: fires only on change). The initial pre-connect
28
28
  * span reports nothing — the UI treats "no state yet" as connecting, not as an outage. */
29
29
  onStateChange?: (state: ConnectionState) => void;
@@ -52,6 +52,8 @@ export declare class ConnectionController {
52
52
  private backoffDelay;
53
53
  /** Read through a method: stop() flips the flag across awaits, so narrowing from the loop condition must not stick. */
54
54
  private isRunning;
55
+ /** Re-read both mutable liveness guards after a potentially reentrant sink. */
56
+ private isGenerationActive;
55
57
  private loop;
56
58
  /** Deduplicated state emission (sink isolation applies). */
57
59
  private emitState;
@@ -4,17 +4,24 @@
4
4
  * controller with its sinks.
5
5
  */
6
6
  import type { Context } from '@deepseek-ai/cordis';
7
- import type { IApiClient } from './api.ts';
7
+ import type { HostDescription, IApiClient } from './api.ts';
8
8
  import { type ConnectionConfig, type ConnectionSinks, type ConnectionState } from './connection.ts';
9
9
  import type { ClientConnectionRpc } from '../rpc.ts';
10
- export type { ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, PromptContentPart, HostApi, EventsApi, MuxFrame, HostFrame, ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView, DirectoryEntry, DirectoryListing, ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView, CommandsApi, CommandDescriptor, SkillsApi, SkillEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, MessageId, ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels, SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode, ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt, HostDescription, IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk, GoalsApi, GoalRef, SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView, CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi, } from './api.ts';
10
+ export type { ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, PromptContentPart, HostApi, EventsApi, MuxFrame, HostFrame, ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView, DirectoryEntry, DirectoryListing, ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView, CommandsApi, CommandDescriptor, SkillsApi, SkillEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, MessageId, ModelReasoningEffort, ModelSelection, QueueAction, QueuedInboxItem, SessionModels, SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, TaskView, RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode, ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt, HostDescription, IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk, GoalsApi, GoalRef, SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView, CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi, } from './api.ts';
11
11
  export { RpcId, AbstractApiClient, transportError, } from './api.ts';
12
12
  export type { ConnectionConfig, ConnectionSinks, ConnectionState };
13
13
  export type { ClientConnectionRpc } from '../rpc.ts';
14
+ /** Observable Host description published by each completed connection handshake. */
15
+ export interface HostDescriptionSource {
16
+ /** Latest connected-generation description; absent before connect and while reconnecting. */
17
+ getSnapshot(): HostDescription | undefined;
18
+ /** Subscribe to description replacement and connection loss. */
19
+ subscribe(listener: () => void): () => void;
20
+ }
14
21
  /** Required services (none — this is the wire root). */
15
22
  export declare const inject: string[];
16
23
  /**
17
- * The ctx.connection service surface: the api client plus a one-shot
24
+ * The ctx.connection service API: the API client plus a one-shot
18
25
  * controller starter (the runtime plugin supplies sinks when its object layer
19
26
  * is ready — connection stays consumer-agnostic).
20
27
  */
@@ -23,6 +30,8 @@ export interface ConnectionHandle {
23
30
  readonly api: IApiClient;
24
31
  /** Whether the current page authority is loopback; non-browser contexts default to true. */
25
32
  readonly isLoopback: boolean;
33
+ /** Generation-scoped Host facts, including native path-open capability. */
34
+ readonly hostDescription: HostDescriptionSource;
26
35
  /** Generic logical RPC channels over the same Connection transport. */
27
36
  readonly rpc: ClientConnectionRpc;
28
37
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-connection",
3
3
  "description": "Wire consumer layer: HTTP-up/WebSocket-down client, ConnectionController dual streams with reconnect, and fixture api",
4
- "version": "0.0.1-rc.1",
4
+ "version": "0.0.1-rc.2",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },
@@ -39,13 +39,13 @@
39
39
  "license": "BSD-3-Clause",
40
40
  "dependencies": {
41
41
  "ws": "^8.21.0",
42
- "@deepseek-ai/dsh-host-apiproxy": "^0.0.1-rc.1",
43
- "@deepseek-ai/dsh-commands": "^0.0.1-rc.1",
44
- "@deepseek-ai/dsh-attachment": "^0.0.1-rc.1",
45
- "@deepseek-ai/dsh-session": "^0.0.1-rc.1",
46
- "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
42
+ "@deepseek-ai/dsh-commands": "^0.0.1-rc.2",
43
+ "@deepseek-ai/dsh-attachment": "^0.0.1-rc.2",
44
+ "@deepseek-ai/dsh-host-apiproxy": "^0.0.1-rc.2",
45
+ "@deepseek-ai/dsh-session": "^0.0.1-rc.2",
46
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.2",
47
47
  "@deepseek-ai/schemastery": "^3.18.1-rc.1",
48
- "@deepseek-ai/dsh-llm": "^0.0.1-rc.1"
48
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.2"
49
49
  },
50
50
  "files": [
51
51
  "lib/index.js",
@@ -54,14 +54,14 @@
54
54
  "lib/types/**/*.d.ts"
55
55
  ],
56
56
  "peerDependencies": {
57
- "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
58
- "@deepseek-ai/dsh-host-webserver": "^0.0.1-rc.1",
57
+ "@deepseek-ai/dsh-host-webserver": "^0.0.1-rc.2",
58
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
59
59
  "@deepseek-ai/cordis": "^4.0.1-rc.1"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/ws": "^8.18.1",
63
- "@deepseek-ai/dsh-host-webserver": "^0.0.1-rc.1",
64
- "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
65
- "@deepseek-ai/cordis": "^4.0.1-rc.1"
63
+ "@deepseek-ai/dsh-host-webserver": "^0.0.1-rc.2",
64
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
65
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.2"
66
66
  }
67
67
  }