@gatherthread/dsh-host 0.1.0-beta.1 → 0.1.0-beta.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.md CHANGED
@@ -7,38 +7,40 @@ installation, GatherThread does not load in DSH or alter the GatherThread
7
7
  server, Web app, Codex bridge, or Claude adapter.
8
8
 
9
9
  The normal path is the published `@deepseek-ai/dsh` CLI, not a DSH source
10
- checkout. The verified distribution is `@deepseek-ai/dsh@0.1.2-rc.1` with the
11
- `web` profile and its public `sessionPersistence.list` recovery surface. The
12
- earlier source-only PoC remains fixed to `dsh-v0.1.3-alpha.1`, commit
13
- `d347e703908d0406b7a7ef80e3a0e594d86b2215`, and is only an advanced diagnostic
10
+ checkout. The verified distribution is `@deepseek-ai/dsh@0.1.7-rc.2` with the
11
+ `web` profile and its public `sessionPersistence` recovery surface. The
12
+ source-only path remains fixed to `dsh-v0.1.7-rc.2`, commit
13
+ `477b4f420553e8a52c2fbccc464d7561b239c443`, and is only an advanced diagnostic
14
14
  fallback. All version-sensitive Host calls remain inside `src/dsh-compat.ts`.
15
15
  The package does not install a DSH runtime dependency.
16
16
 
17
17
  ## Plugin-first connection
18
18
 
19
- After `@gatherthread/dsh-host` is published, the ordinary three-step path is:
19
+ Use **Connect DeepSeek Harness → Download packages for this server** in GatherThread to download its source-matched DSH plugin TGZ. Replace the example path below with the downloaded file's full path:
20
20
 
21
21
  ```text
22
- npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add @gatherthread/dsh-host@0.1.0-beta.1
23
- npx @deepseek-ai/dsh@0.1.2-rc.1 web
22
+ npx @deepseek-ai/dsh@0.1.7-rc.2 plugin --profile web add "/absolute/path/to/LOCAL_DSH_PLUGIN.tgz"
23
+ npx @deepseek-ai/dsh@0.1.7-rc.2 web
24
24
  # In DSH: Settings -> GatherThread / 共序 -> Sign in and pair
25
25
  ```
26
26
 
27
+ On Windows, use a quoted path such as `C:/Users/you/Downloads/your-plugin.tgz`. The registry's `@gatherthread/dsh-host@0.1.0-beta.1` is an older build and does not include the current message-deletion safeguards. If server downloads are unavailable, [build the matching source checkout](../../docs/DSH_CONNECT.md#source-checkout-fallback); do not substitute the old npm build.
28
+
27
29
  Check the active CLI with `npx @deepseek-ai/dsh --version`. The normal start
28
- command already pins `0.1.2-rc.1`. DSH 0.1.2 does not
30
+ command already pins `0.1.7-rc.2`. The pinned DSH does not
29
31
  expose the root CLI version as a stable Host service, so the package does not
30
32
  inspect npx cache paths or claim runtime-version introspection. The install
31
33
  command pins the exact verified CLI, while every required Host/Client service is
32
34
  validated structurally and fails closed before a request can be claimed.
33
35
 
34
- `dsh plugin --profile web add` is DSH 0.1.2's official package installation
36
+ `dsh plugin --profile web add` is the pinned DSH release's official package installation
35
37
  mechanism. It initializes a missing `web` profile on first use, invokes pnpm,
36
- and appends only packages declaring `dsh.bundle`. DSH 0.1.2 has no verified
38
+ and appends only packages declaring `dsh.bundle`. The pinned DSH has no verified
37
39
  public plugin marketplace, marketplace index, or registration workflow. This
38
40
  repository change prepares the package but does not publish it or modify a real
39
41
  DSH profile.
40
42
 
41
- Inside DSH, choose the future GatherThread public service or enter one custom
43
+ Inside DSH, choose the GatherThread public service or enter one custom
42
44
  server origin. The same field covers an HTTPS LAN host, self-hosted deployment,
43
45
  or Tailscale address; plain HTTP is accepted only on loopback. The plugin opens
44
46
  an outbound HTTPS connection. A five-minute, single-use short code is confirmed
@@ -52,7 +54,7 @@ Before removal, use **Disconnect** inside the DSH panel to clear the local
52
54
  grant, then run:
53
55
 
54
56
  ```text
55
- npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web remove @gatherthread/dsh-host
57
+ npx @deepseek-ai/dsh@0.1.7-rc.2 plugin --profile web remove @gatherthread/dsh-host
56
58
  ```
57
59
 
58
60
  The official command removes only this package dependency and profile layer;
@@ -62,7 +64,7 @@ advanced/offline diagnostics, not the normal user path.
62
64
 
63
65
  ## GitHub code synchronization
64
66
 
65
- Beta 1 includes direct GitHub controls and requires matching server, Web app and native plugin releases. The project owner first connects an existing repository and base branch in Web. Install Git 2.38+ and GitHub CLI on the device, then open **Settings → GatherThread / 共序 → 项目代码 · GitHub(推荐)**. Click **连接 GitHub · 打开浏览器** to start local browser sign-in if needed, then authorize that exact project/repository configuration. `gh auth login --hostname github.com --git-protocol https --web` remains a local fallback. This consent is separate from GT Cloud and conversation sync; a new GitHub binding automatically uploads settled changes while idle and can be switched off. Configuration changes, including pause/resume, require renewed local authorization.
67
+ Beta 2 includes direct GitHub controls and requires matching server, Web app and native plugin releases. The project owner first connects an existing repository and base branch in Web. Install Git 2.38+ and GitHub CLI on the device, then open **Settings → GatherThread / 共序 → 项目代码 · GitHub(推荐)**. Click **连接 GitHub · 打开浏览器** to start local browser sign-in if needed, then authorize that exact project/repository configuration. `gh auth login --hostname github.com --git-protocol https --web` remains a local fallback. This consent is separate from GT Cloud and conversation sync; a new GitHub binding automatically uploads settled changes while idle and can be switched off. Configuration changes, including pause/resume, require renewed local authorization.
66
68
 
67
69
  The Web app may request code operations only on your selected online runtime and cannot grant this local consent. Code and GitHub credentials travel directly to GitHub. GitHub permissions remain independent of GT membership; removing a GT member or project does not revoke GitHub access or delete GitHub data. See [Project code collaboration](../../docs/CODE_SYNC.md) for setup, recovery, limits and GitHub workflow-trigger risks.
68
70
 
@@ -70,10 +72,13 @@ GitHub 接入仍为**未发布源码预览**,需使用匹配的服务端、网
70
72
 
71
73
  ## Security and recovery
72
74
 
73
- Only allowlisted public text/tool fields leave DSH. Reasoning blocks, raw model
74
- streams, request headers, replay state, tool-private metadata, and credentials
75
- are structurally unreachable from the upload mapper. Tool values are redacted
76
- and bounded. The connector holds one active request, persists a private atomic
75
+ Only allowlisted public text/tool fields leave DSH. An exact GatherThread-claimed
76
+ prompt can additionally stream native-exposed thinking and real plans. The live
77
+ listener verifies owned Agent, Session and execution; the npm fallback requires
78
+ DSH's genuine Agent-loop marker rather than a model-name match. Raw request
79
+ bodies, headers, replay state, tool-private metadata and unrelated native streams
80
+ stay outside the mapper. Tool arguments are parsed and redacted before upload;
81
+ incomplete JSON emits only a preparation marker. The connector holds one active request, persists a private atomic
77
82
  cursor/outbox, reuses DSH's official Session resume/repair path, and releases its
78
83
  listeners, timer, HTTP abort signal, Agent handle, and write ownership on unload.
79
84
 
@@ -114,9 +119,10 @@ cancels its queued permit before it can claim a request. Existing single-Session
114
119
  valid when `bindingMode` is omitted; both modes resolve the real current actor
115
120
  before applying Solo permissions.
116
121
 
117
- During an active prompt, newly durable DSH Host events queue a rate-limited,
118
- generic progress marker; event bursts coalesce. The marker carries no event text,
119
- reasoning, headers, or private stream data, but renews the exact claim attempt. Progress, completion,
122
+ During an active claimed prompt, selected native-visible answer, thinking, tool
123
+ and plan updates coalesce into bounded redacted snapshots. Generic activity
124
+ markers carry no native content and remain separate from presence. Accepted
125
+ progress renews the exact claim attempt. Progress, completion,
120
126
  and request-linked tool outbox entries are rebound to the renewed attempt after a
121
127
  crash recovery, while stale attempts remain fenced by the server.
122
128
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "name": "@gatherthread/dsh-host",
4
- "version": "0.1.0-beta.1",
4
+ "version": "0.1.0-beta.2",
5
5
  "license": "Apache-2.0",
6
6
  "defaultEnabled": false,
7
7
  "hostEntry": "@gatherthread/dsh-host",
@@ -15,15 +15,15 @@
15
15
  "primary": {
16
16
  "distribution": "npm",
17
17
  "package": "@deepseek-ai/dsh",
18
- "version": "0.1.2-rc.1",
18
+ "version": "0.1.7-rc.2",
19
19
  "profile": "web",
20
20
  "persistenceProbe": "list"
21
21
  },
22
22
  "sourceFallback": {
23
23
  "distribution": "source",
24
- "tag": "dsh-v0.1.3-alpha.1",
25
- "version": "0.1.3-alpha.1",
26
- "commit": "d347e703908d0406b7a7ef80e3a0e594d86b2215",
24
+ "tag": "dsh-v0.1.7-rc.2",
25
+ "version": "0.1.7-rc.2",
26
+ "commit": "477b4f420553e8a52c2fbccc464d7561b239c443",
27
27
  "verifiedProfiles": ["headless", "web"],
28
28
  "persistenceProbe": "stat"
29
29
  }
package/client/client.js CHANGED
@@ -35,6 +35,21 @@ function GatherThreadStatusPanel({ connection, sessions }) {
35
35
  const [provider, setProvider] = React.useState("");
36
36
  const [model, setModel] = React.useState("");
37
37
  const visibleSessionSignature = React.useRef("");
38
+ /** Recovery observed from both status polls and action responses. */
39
+ const lastRecoverableError = React.useRef(undefined);
40
+
41
+ const observeRouteRecovery = (next, configureResponse = false) => {
42
+ const recovery = next?.recoverableError;
43
+ // A failed configure is a new revoked choice even if the error code has not
44
+ // changed. Steady polls must leave an operator's new selection alone.
45
+ if (recovery === "route_unavailable"
46
+ && (configureResponse || lastRecoverableError.current !== "route_unavailable")) {
47
+ setCatalog(undefined);
48
+ setProvider("");
49
+ setModel("");
50
+ }
51
+ lastRecoverableError.current = recovery;
52
+ };
38
53
 
39
54
  React.useEffect(() => {
40
55
  let disposed = false;
@@ -60,6 +75,7 @@ function GatherThreadStatusPanel({ connection, sessions }) {
60
75
  }
61
76
  setSnapshot(result.snapshot);
62
77
  setNativeState(result.nativeState);
78
+ observeRouteRecovery(result.nativeState);
63
79
  setUnavailable(false);
64
80
  timer = setTimeout(refresh, POLL_INTERVAL_MS);
65
81
  }
@@ -95,10 +111,12 @@ function GatherThreadStatusPanel({ connection, sessions }) {
95
111
  const next = endpoint === "catalog/get" ? parseCatalog(value) : parseNativeState(value);
96
112
  if (endpoint === "catalog/get") {
97
113
  setCatalog(next);
98
- const firstProvider = next.providers[0];
99
- setProvider((current) => current || firstProvider?.id || "");
100
- setModel((current) => current || firstProvider?.models[0]?.id || "");
114
+ const selectedProvider = next.providers.find((entry) => entry.id === provider) ?? next.providers[0];
115
+ setProvider(selectedProvider?.id ?? "");
116
+ setModel((current) => selectedProvider?.models.some((entry) => entry.id === current)
117
+ ? current : selectedProvider?.models[0]?.id ?? "");
101
118
  } else {
119
+ observeRouteRecovery(next, endpoint === "connection/configure");
102
120
  setNativeState(next);
103
121
  setSnapshot(next.runtime);
104
122
  if (endpoint === "connection/configure") await sessions.refresh();
@@ -184,6 +202,7 @@ function GatherThreadStatusPanel({ connection, sessions }) {
184
202
  setDeviceName,
185
203
  setProvider,
186
204
  setModel,
205
+ setCatalog,
187
206
  runAction,
188
207
  }),
189
208
  nativeState === undefined || unavailable ? null : renderCodeSyncControls(nativeState, busy, runAction, true),
@@ -435,8 +454,9 @@ function renderNativeControls(input) {
435
454
  );
436
455
  } else if (state.route === undefined) {
437
456
  const providers = input.catalog?.providers ?? [];
438
- const selectedProvider = providers.find((entry) => entry.id === input.provider) ?? providers[0];
457
+ const selectedProvider = providers.find((entry) => entry.id === input.provider);
439
458
  const models = selectedProvider?.models ?? [];
459
+ const validSelection = models.some((entry) => entry.id === input.model);
440
460
  controls.push(
441
461
  connectionSummary(state),
442
462
  // Reaching this branch means authorization is already "paired": the
@@ -445,7 +465,12 @@ function renderNativeControls(input) {
445
465
  // is registered, so a bare stopped connection is indistinguishable from a
446
466
  // fresh install. State the remaining step explicitly.
447
467
  React.createElement("p", { key: "select-model-notice", style: styles.notice },
448
- "配对完成后,尚未选择 DSH Provider 与 Model。完成选择并点击下方按钮后,本机 DSH 运行时才会注册到 GatherThread;在此之前,GatherThread 网页无法发现这台 DSH。"),
468
+ // A dropped route is not the same situation as a fresh pairing, and saying
469
+ // so is the whole point of not repointing the model silently: the reader has
470
+ // to know their previous choice no longer exists before picking another.
471
+ state.recoverableError === "route_unavailable"
472
+ ? "DSH 当前已不再提供此前为该连接选择的模型,因此该连接已停止并取消注册。请重新选择 Provider 与 Model;在此之前的旧模型不会被自动替换。"
473
+ : "配对完成后,尚未选择 DSH Provider 与 Model。完成选择并点击下方按钮后,本机 DSH 运行时才会注册到 GatherThread;在此之前,GatherThread 网页无法发现这台 DSH。"),
449
474
  input.catalog === undefined
450
475
  ? React.createElement("button", {
451
476
  key: "load-catalog",
@@ -459,6 +484,7 @@ function renderNativeControls(input) {
459
484
  style: styles.form,
460
485
  onSubmit: (event) => {
461
486
  event.preventDefault();
487
+ if (input.busy || !validSelection) return;
462
488
  void input.runAction("connection/configure", {
463
489
  provider: input.provider,
464
490
  model: input.model,
@@ -487,7 +513,7 @@ function renderNativeControls(input) {
487
513
  )),
488
514
  React.createElement("button", {
489
515
  type: "submit",
490
- disabled: input.busy || !input.provider || !input.model,
516
+ disabled: input.busy || !validSelection,
491
517
  style: styles.primaryButton,
492
518
  }, input.busy ? "正在连接…" : "连接全部可访问项目")),
493
519
  disconnectButton(input),
@@ -540,11 +566,12 @@ function selectControl(entries, value, setValue, keyOf, labelOf) {
540
566
  return React.createElement("select", {
541
567
  required: true,
542
568
  value,
543
- style: styles.input,
569
+ style: styles.select,
544
570
  onChange: (event) => setValue(event.target.value),
545
571
  }, ...entries.map((entry) => React.createElement("option", {
546
572
  key: keyOf(entry),
547
573
  value: keyOf(entry),
574
+ style: styles.option,
548
575
  }, labelOf(entry))));
549
576
  }
550
577
 
@@ -563,7 +590,7 @@ function parseNativeState(value) {
563
590
  if (!AUTHORIZATION_STATES.has(value.authorization)) throw new Error("invalid native authorization state");
564
591
  exactObject(value.compatibility, ["package", "version", "profile"]);
565
592
  if (value.compatibility.package !== "@deepseek-ai/dsh"
566
- || value.compatibility.version !== "0.1.2-rc.1"
593
+ || value.compatibility.version !== "0.1.7-rc.2"
567
594
  || value.compatibility.profile !== "web") throw new Error("incompatible native DSH Host");
568
595
  const runtime = parseSnapshot(value.runtime);
569
596
  const optionalUrl = (url) => {
@@ -697,7 +724,8 @@ function parseNativeState(value) {
697
724
  }
698
725
  if (value.recoverableError !== undefined
699
726
  && value.recoverableError !== "pairing_failed"
700
- && value.recoverableError !== "connection_failed") throw new Error("invalid native recovery state");
727
+ && value.recoverableError !== "connection_failed"
728
+ && value.recoverableError !== "route_unavailable") throw new Error("invalid native recovery state");
701
729
  return {
702
730
  schemaVersion: 2,
703
731
  integration: "gatherthread",
@@ -845,6 +873,14 @@ const styles = {
845
873
  field: { display: "grid", gap: 6 },
846
874
  fieldLabel: { fontSize: 12, fontWeight: 600, opacity: 0.76 },
847
875
  input: { width: "100%", minHeight: 36, boxSizing: "border-box", padding: "7px 10px", borderRadius: 9, border: "1px solid rgba(127,127,127,.28)", background: "transparent", color: "inherit", font: "inherit" },
876
+ // A dropdown owns a popup it cannot inherit: the list surface is painted from the
877
+ // control's own background and each row from its option, while the text keeps
878
+ // following the panel. Sharing `input` therefore drew the model names in the
879
+ // panel's foreground on the UA's own surface — white on white under a dark harness
880
+ // theme. Both the control and the rows name the system field pair instead, which
881
+ // the UA resolves against the used color scheme, so neither can match the other.
882
+ select: { width: "100%", minHeight: 36, boxSizing: "border-box", padding: "7px 10px", borderRadius: 9, border: "1px solid rgba(127,127,127,.28)", background: "Field", color: "FieldText", font: "inherit" },
883
+ option: { background: "Field", color: "FieldText" },
848
884
  primaryButton: { minHeight: 36, padding: "8px 12px", borderRadius: 9, border: "1px solid Highlight", background: "Highlight", color: "HighlightText", WebkitTextFillColor: "HighlightText", font: "inherit", fontWeight: 600, cursor: "pointer" },
849
885
  secondaryButton: { minHeight: 36, padding: "8px 12px", borderRadius: 9, border: "1px solid rgba(127,127,127,.3)", background: "transparent", color: "inherit", font: "inherit", cursor: "pointer" },
850
886
  linkButton: { display: "inline-flex", alignItems: "center", justifyContent: "center", minHeight: 36, padding: "0 12px", borderRadius: 9, border: "1px solid rgba(127,127,127,.3)", color: "inherit", textDecoration: "none", fontWeight: 600 },