@blue-soda/dsh-remote 0.5.3 → 0.5.4

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
@@ -23,7 +23,7 @@ Continue DeepSeek Harness sessions and experimental Codex workspaces from anothe
23
23
  Add the current package version to the `web` profile, then restart Harness:
24
24
 
25
25
  ```sh
26
- dsh plugin --profile web add -w @blue-soda/dsh-remote@0.5.3
26
+ dsh plugin --profile web add -w @blue-soda/dsh-remote@0.5.4
27
27
  ```
28
28
 
29
29
  ### dsh-TUI Host
@@ -31,7 +31,7 @@ dsh plugin --profile web add -w @blue-soda/dsh-remote@0.5.3
31
31
  Remote can also run as a Host in a terminal-only [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) profile:
32
32
 
33
33
  ```sh
34
- dsh plugin --profile dsh-tui add -w @blue-soda/dsh-remote@0.5.3
34
+ dsh plugin --profile dsh-tui add -w @blue-soda/dsh-remote@0.5.4
35
35
  ```
36
36
 
37
37
  After starting dsh-TUI, manage Remote with `/remote`, `/remote login`, `/remote status`, and `/remote logout`.
@@ -265,7 +265,7 @@
265
265
  reconnectingAction: "Reconnecting\u2026",
266
266
  reconnectStarted: "Reconnect requested.",
267
267
  connectionAuthorizationExpired: "Authorization expired. Sign in again to authorize this device.",
268
- connectionReplaced: "Another instance is using this Host identity. Stop it or use a separate DSH_HOME before reconnecting.",
268
+ connectionReplaced: "Another instance is using this Host identity. Standing by: retrying every five minutes, and taking over as soon as it leaves.",
269
269
  connectionCredentialsBusy: "Credential refresh is locked. Stop other instances. After a crash, stop all instances before removing server-credentials.json.refresh-lock and authorizing again.",
270
270
  connectionDeviceRevoked: "This device was removed on the Server. Sign in again to add it back.",
271
271
  connectionOwnershipRequired: "The Server no longer recognizes this Host as an owned device.",
@@ -328,12 +328,12 @@
328
328
  exitRemote: "Exit",
329
329
  stopReconnecting: "Stop reconnecting",
330
330
  addRemoteWorkspace: "Add remote workspace",
331
- addCodexWorkspace: "Add CodeX workspace",
332
- noCodexWorkspaces: "No CodeX workspaces yet.",
331
+ addCodexWorkspace: "Add Codex workspace",
332
+ noCodexWorkspaces: "No Codex workspaces yet.",
333
333
  cancelAddWorkspace: "Cancel",
334
334
  confirmAddWorkspace: "Add and open",
335
335
  showAllWorkspaces: "Show all DSH workspaces",
336
- showAllCodexWorkspaces: "Show all CodeX workspaces",
336
+ showAllCodexWorkspaces: "Show all Codex workspaces",
337
337
  remoteModeDsh: "Remote DSH \xB7 ",
338
338
  remoteModeCodex: "Remote Codex \xB7 ",
339
339
  remoteNetworkP2p: "P2P",
@@ -430,7 +430,7 @@
430
430
  accountPasswordLogin: "Password",
431
431
  qrLoginExpired: "This QR code expired. Refresh it to continue.",
432
432
  refreshQrCode: "Refresh QR code",
433
- codexVirtualWorkspace: "CodeX virtual workspace",
433
+ codexVirtualWorkspace: "Codex virtual workspace",
434
434
  codexVirtualSessions: "Sessions"
435
435
  }, zh = {
436
436
  terminalRemote: "\u8FDC\u7A0B\u7EC8\u7AEF",
@@ -539,7 +539,7 @@
539
539
  reconnectingAction: "\u6B63\u5728\u91CD\u8FDE\u2026",
540
540
  reconnectStarted: "\u5DF2\u53D1\u8D77\u91CD\u8FDE\u3002",
541
541
  connectionAuthorizationExpired: "\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u4EE5\u91CD\u65B0\u6388\u6743\u6B64\u8BBE\u5907\u3002",
542
- connectionReplaced: "\u53E6\u4E00\u5B9E\u4F8B\u6B63\u5728\u4F7F\u7528\u6B64 Host \u8EAB\u4EFD\u3002\u8BF7\u505C\u6B62\u8BE5\u5B9E\u4F8B\uFF0C\u6216\u4F7F\u7528\u72EC\u7ACB\u7684 DSH_HOME \u540E\u91CD\u65B0\u8FDE\u63A5\u3002",
542
+ connectionReplaced: "\u53E6\u4E00\u5B9E\u4F8B\u6B63\u5728\u4F7F\u7528\u6B64 Host \u8EAB\u4EFD\u3002\u672C\u673A\u5DF2\u8FDB\u5165\u5F85\u547D\uFF1A\u6BCF 5 \u5206\u949F\u91CD\u8BD5\u4E00\u6B21\uFF0C\u5BF9\u65B9\u9000\u51FA\u540E\u4F1A\u81EA\u52A8\u63A5\u7BA1\u3002",
543
543
  connectionCredentialsBusy: "\u51ED\u636E\u5237\u65B0\u88AB\u9501\u5B9A\uFF0C\u8BF7\u505C\u6B62\u5176\u4ED6\u5B9E\u4F8B\u3002\u82E5\u6B64\u524D\u5F02\u5E38\u9000\u51FA\uFF0C\u8BF7\u505C\u6B62\u6240\u6709\u5B9E\u4F8B\u540E\u79FB\u9664 server-credentials.json.refresh-lock \u5E76\u91CD\u65B0\u6388\u6743\u3002",
544
544
  connectionDeviceRevoked: "\u6B64\u8BBE\u5907\u5DF2\u5728 Server \u4E0A\u88AB\u79FB\u9664\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u4EE5\u91CD\u65B0\u6DFB\u52A0\u3002",
545
545
  connectionOwnershipRequired: "Server \u5DF2\u4E0D\u518D\u5C06\u6B64 Host \u8BC6\u522B\u4E3A\u5F53\u524D\u8D26\u53F7\u7684\u8BBE\u5907\u3002",
@@ -602,12 +602,12 @@
602
602
  exitRemote: "\u9000\u51FA",
603
603
  stopReconnecting: "\u505C\u6B62\u91CD\u8FDE",
604
604
  addRemoteWorkspace: "\u6DFB\u52A0\u8FDC\u7A0B\u5DE5\u4F5C\u533A",
605
- addCodexWorkspace: "\u6DFB\u52A0 CodeX \u5DE5\u4F5C\u533A",
606
- noCodexWorkspaces: "\u8FD8\u6CA1\u6709 CodeX \u5DE5\u4F5C\u533A\u3002",
605
+ addCodexWorkspace: "\u6DFB\u52A0 Codex \u5DE5\u4F5C\u533A",
606
+ noCodexWorkspaces: "\u8FD8\u6CA1\u6709 Codex \u5DE5\u4F5C\u533A\u3002",
607
607
  cancelAddWorkspace: "\u53D6\u6D88",
608
608
  confirmAddWorkspace: "\u786E\u8BA4\u5E76\u6253\u5F00",
609
609
  showAllWorkspaces: "\u663E\u793A\u5168\u90E8 DSH \u5DE5\u4F5C\u533A",
610
- showAllCodexWorkspaces: "\u663E\u793A\u5168\u90E8 CodeX \u5DE5\u4F5C\u533A",
610
+ showAllCodexWorkspaces: "\u663E\u793A\u5168\u90E8 Codex \u5DE5\u4F5C\u533A",
611
611
  remoteModeDsh: "\u8FDC\u7A0BDSH \xB7 ",
612
612
  remoteModeCodex: "\u8FDC\u7A0BCodex \xB7 ",
613
613
  remoteNetworkP2p: "P2P",
@@ -704,7 +704,7 @@
704
704
  accountPasswordLogin: "\u8D26\u53F7\u5BC6\u7801",
705
705
  qrLoginExpired: "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002",
706
706
  refreshQrCode: "\u5237\u65B0\u4E8C\u7EF4\u7801",
707
- codexVirtualWorkspace: "CodeX \u5DE5\u4F5C\u533A",
707
+ codexVirtualWorkspace: "Codex \u5DE5\u4F5C\u533A",
708
708
  codexVirtualSessions: "Sessions"
709
709
  };
710
710
  function codexUnavailableKey(code) {
@@ -1514,7 +1514,12 @@
1514
1514
  targetDeviceId: host.deviceId
1515
1515
  }), codexReason, nextCodexWorkspaces = await props.control("codex.workspaces.list", {
1516
1516
  targetDeviceId: host.deviceId
1517
- }).catch((reason) => (codexReason = remoteErrorMessage(reason, t), [])), nextStatus = await props.control("status").catch(() => {
1517
+ }).catch(async (reason) => {
1518
+ let code = typeof reason?.code == "string" ? String(reason.code) : "";
1519
+ return ["TRANSPORT_CLOSED", "NOT_CONNECTED", "CONNECTION_FAILED", "RPC_TIMEOUT"].includes(code) ? await props.control("codex.workspaces.list", {
1520
+ targetDeviceId: host.deviceId
1521
+ }).catch((again) => (codexReason = remoteErrorMessage(again, t), [])) : (codexReason = remoteErrorMessage(reason, t), []);
1522
+ }), nextStatus = await props.control("status").catch(() => {
1518
1523
  });
1519
1524
  return nextStatus !== void 0 && setStatus(nextStatus), setCodexNotice(codexReason), { workspaces: nextWorkspaces, codexWorkspaces: nextCodexWorkspaces, status: nextStatus };
1520
1525
  },
@@ -2219,7 +2224,7 @@
2219
2224
  )
2220
2225
  ),
2221
2226
  notice === void 0 ? null : React.createElement("p", { className: "dshRemoteNotice", role: "status" }, notice),
2222
- error === void 0 ? null : React.createElement("p", { className: "dshRemoteError", role: "alert" }, error)
2227
+ error === void 0 || error === codexNotice ? null : React.createElement("p", { className: "dshRemoteError", role: "alert" }, error)
2223
2228
  )
2224
2229
  )) : null
2225
2230
  );
package/dist/client.js CHANGED
@@ -265,7 +265,7 @@
265
265
  reconnectingAction: "Reconnecting\u2026",
266
266
  reconnectStarted: "Reconnect requested.",
267
267
  connectionAuthorizationExpired: "Authorization expired. Sign in again to authorize this device.",
268
- connectionReplaced: "Another instance is using this Host identity. Stop it or use a separate DSH_HOME before reconnecting.",
268
+ connectionReplaced: "Another instance is using this Host identity. Standing by: retrying every five minutes, and taking over as soon as it leaves.",
269
269
  connectionCredentialsBusy: "Credential refresh is locked. Stop other instances. After a crash, stop all instances before removing server-credentials.json.refresh-lock and authorizing again.",
270
270
  connectionDeviceRevoked: "This device was removed on the Server. Sign in again to add it back.",
271
271
  connectionOwnershipRequired: "The Server no longer recognizes this Host as an owned device.",
@@ -328,12 +328,12 @@
328
328
  exitRemote: "Exit",
329
329
  stopReconnecting: "Stop reconnecting",
330
330
  addRemoteWorkspace: "Add remote workspace",
331
- addCodexWorkspace: "Add CodeX workspace",
332
- noCodexWorkspaces: "No CodeX workspaces yet.",
331
+ addCodexWorkspace: "Add Codex workspace",
332
+ noCodexWorkspaces: "No Codex workspaces yet.",
333
333
  cancelAddWorkspace: "Cancel",
334
334
  confirmAddWorkspace: "Add and open",
335
335
  showAllWorkspaces: "Show all DSH workspaces",
336
- showAllCodexWorkspaces: "Show all CodeX workspaces",
336
+ showAllCodexWorkspaces: "Show all Codex workspaces",
337
337
  remoteModeDsh: "Remote DSH \xB7 ",
338
338
  remoteModeCodex: "Remote Codex \xB7 ",
339
339
  remoteNetworkP2p: "P2P",
@@ -430,7 +430,7 @@
430
430
  accountPasswordLogin: "Password",
431
431
  qrLoginExpired: "This QR code expired. Refresh it to continue.",
432
432
  refreshQrCode: "Refresh QR code",
433
- codexVirtualWorkspace: "CodeX virtual workspace",
433
+ codexVirtualWorkspace: "Codex virtual workspace",
434
434
  codexVirtualSessions: "Sessions"
435
435
  }, zh = {
436
436
  terminalRemote: "\u8FDC\u7A0B\u7EC8\u7AEF",
@@ -539,7 +539,7 @@
539
539
  reconnectingAction: "\u6B63\u5728\u91CD\u8FDE\u2026",
540
540
  reconnectStarted: "\u5DF2\u53D1\u8D77\u91CD\u8FDE\u3002",
541
541
  connectionAuthorizationExpired: "\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u4EE5\u91CD\u65B0\u6388\u6743\u6B64\u8BBE\u5907\u3002",
542
- connectionReplaced: "\u53E6\u4E00\u5B9E\u4F8B\u6B63\u5728\u4F7F\u7528\u6B64 Host \u8EAB\u4EFD\u3002\u8BF7\u505C\u6B62\u8BE5\u5B9E\u4F8B\uFF0C\u6216\u4F7F\u7528\u72EC\u7ACB\u7684 DSH_HOME \u540E\u91CD\u65B0\u8FDE\u63A5\u3002",
542
+ connectionReplaced: "\u53E6\u4E00\u5B9E\u4F8B\u6B63\u5728\u4F7F\u7528\u6B64 Host \u8EAB\u4EFD\u3002\u672C\u673A\u5DF2\u8FDB\u5165\u5F85\u547D\uFF1A\u6BCF 5 \u5206\u949F\u91CD\u8BD5\u4E00\u6B21\uFF0C\u5BF9\u65B9\u9000\u51FA\u540E\u4F1A\u81EA\u52A8\u63A5\u7BA1\u3002",
543
543
  connectionCredentialsBusy: "\u51ED\u636E\u5237\u65B0\u88AB\u9501\u5B9A\uFF0C\u8BF7\u505C\u6B62\u5176\u4ED6\u5B9E\u4F8B\u3002\u82E5\u6B64\u524D\u5F02\u5E38\u9000\u51FA\uFF0C\u8BF7\u505C\u6B62\u6240\u6709\u5B9E\u4F8B\u540E\u79FB\u9664 server-credentials.json.refresh-lock \u5E76\u91CD\u65B0\u6388\u6743\u3002",
544
544
  connectionDeviceRevoked: "\u6B64\u8BBE\u5907\u5DF2\u5728 Server \u4E0A\u88AB\u79FB\u9664\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u4EE5\u91CD\u65B0\u6DFB\u52A0\u3002",
545
545
  connectionOwnershipRequired: "Server \u5DF2\u4E0D\u518D\u5C06\u6B64 Host \u8BC6\u522B\u4E3A\u5F53\u524D\u8D26\u53F7\u7684\u8BBE\u5907\u3002",
@@ -602,12 +602,12 @@
602
602
  exitRemote: "\u9000\u51FA",
603
603
  stopReconnecting: "\u505C\u6B62\u91CD\u8FDE",
604
604
  addRemoteWorkspace: "\u6DFB\u52A0\u8FDC\u7A0B\u5DE5\u4F5C\u533A",
605
- addCodexWorkspace: "\u6DFB\u52A0 CodeX \u5DE5\u4F5C\u533A",
606
- noCodexWorkspaces: "\u8FD8\u6CA1\u6709 CodeX \u5DE5\u4F5C\u533A\u3002",
605
+ addCodexWorkspace: "\u6DFB\u52A0 Codex \u5DE5\u4F5C\u533A",
606
+ noCodexWorkspaces: "\u8FD8\u6CA1\u6709 Codex \u5DE5\u4F5C\u533A\u3002",
607
607
  cancelAddWorkspace: "\u53D6\u6D88",
608
608
  confirmAddWorkspace: "\u786E\u8BA4\u5E76\u6253\u5F00",
609
609
  showAllWorkspaces: "\u663E\u793A\u5168\u90E8 DSH \u5DE5\u4F5C\u533A",
610
- showAllCodexWorkspaces: "\u663E\u793A\u5168\u90E8 CodeX \u5DE5\u4F5C\u533A",
610
+ showAllCodexWorkspaces: "\u663E\u793A\u5168\u90E8 Codex \u5DE5\u4F5C\u533A",
611
611
  remoteModeDsh: "\u8FDC\u7A0BDSH \xB7 ",
612
612
  remoteModeCodex: "\u8FDC\u7A0BCodex \xB7 ",
613
613
  remoteNetworkP2p: "P2P",
@@ -704,7 +704,7 @@
704
704
  accountPasswordLogin: "\u8D26\u53F7\u5BC6\u7801",
705
705
  qrLoginExpired: "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002",
706
706
  refreshQrCode: "\u5237\u65B0\u4E8C\u7EF4\u7801",
707
- codexVirtualWorkspace: "CodeX \u5DE5\u4F5C\u533A",
707
+ codexVirtualWorkspace: "Codex \u5DE5\u4F5C\u533A",
708
708
  codexVirtualSessions: "Sessions"
709
709
  };
710
710
  function codexUnavailableKey(code) {
@@ -1514,7 +1514,12 @@
1514
1514
  targetDeviceId: host.deviceId
1515
1515
  }), codexReason, nextCodexWorkspaces = await props.control("codex.workspaces.list", {
1516
1516
  targetDeviceId: host.deviceId
1517
- }).catch((reason) => (codexReason = remoteErrorMessage(reason, t), [])), nextStatus = await props.control("status").catch(() => {
1517
+ }).catch(async (reason) => {
1518
+ let code = typeof reason?.code == "string" ? String(reason.code) : "";
1519
+ return ["TRANSPORT_CLOSED", "NOT_CONNECTED", "CONNECTION_FAILED", "RPC_TIMEOUT"].includes(code) ? await props.control("codex.workspaces.list", {
1520
+ targetDeviceId: host.deviceId
1521
+ }).catch((again) => (codexReason = remoteErrorMessage(again, t), [])) : (codexReason = remoteErrorMessage(reason, t), []);
1522
+ }), nextStatus = await props.control("status").catch(() => {
1518
1523
  });
1519
1524
  return nextStatus !== void 0 && setStatus(nextStatus), setCodexNotice(codexReason), { workspaces: nextWorkspaces, codexWorkspaces: nextCodexWorkspaces, status: nextStatus };
1520
1525
  },
@@ -2219,7 +2224,7 @@
2219
2224
  )
2220
2225
  ),
2221
2226
  notice === void 0 ? null : React.createElement("p", { className: "dshRemoteNotice", role: "status" }, notice),
2222
- error === void 0 ? null : React.createElement("p", { className: "dshRemoteError", role: "alert" }, error)
2227
+ error === void 0 || error === codexNotice ? null : React.createElement("p", { className: "dshRemoteError", role: "alert" }, error)
2223
2228
  )
2224
2229
  )) : null
2225
2230
  );
package/dist/index.js CHANGED
@@ -13480,7 +13480,12 @@ function resolveConfig(input2 = {}, env = process.env) {
13480
13480
  enabled: parsed.reconnect !== false,
13481
13481
  initialDelayMs,
13482
13482
  maxDelayMs,
13483
- jitter: reconnect.jitter ?? 0.2
13483
+ jitter: reconnect.jitter ?? 0.2,
13484
+ // A displaced connection is a standby: another instance holds this device identity, so it waits five
13485
+ // minutes - well outside the Server's 40-65 s reap window - and then tries again, taking over as soon
13486
+ // as that instance leaves. Giving up for good is how the device stayed offline after the winner left.
13487
+ // The fixture matrix needs a short interval to watch a standby take over, so an operator can override it.
13488
+ replacedRetryMs: reconnect.replacedRetryMs ?? (Number.isFinite(Number(env.DSH_REMOTE_RECONNECT_REPLACED_RETRY_MS)) ? Number(env.DSH_REMOTE_RECONNECT_REPLACED_RETRY_MS) : 3e5)
13484
13489
  },
13485
13490
  codex: {
13486
13491
  // Off by default (2026-10-14). Carrying another agent's workspaces is a capability the user opts into, not
@@ -13516,7 +13521,7 @@ function normalizeServerUrl(value) {
13516
13521
  }
13517
13522
 
13518
13523
  // src/version.ts
13519
- var PLUGIN_VERSION = "0.5.3";
13524
+ var PLUGIN_VERSION = "0.5.4";
13520
13525
 
13521
13526
  // src/server-api.ts
13522
13527
  var ENABLED_QR_PROVIDERS = ["github"];
@@ -14080,15 +14085,21 @@ var HostServerConnection = class {
14080
14085
  }
14081
14086
  async run() {
14082
14087
  let delayMs = this.config.reconnect.initialDelayMs;
14088
+ let replacedRetryMs;
14089
+ let replacedAttempts = 0;
14083
14090
  this.authRecoveryAttempted = false;
14084
14091
  while (!this.stopped) {
14085
14092
  try {
14086
14093
  await this.connectOnce();
14087
14094
  delayMs = this.config.reconnect.initialDelayMs;
14095
+ replacedRetryMs = void 0;
14096
+ replacedAttempts = 0;
14088
14097
  } catch (error) {
14089
14098
  const code = errorCode2(error);
14090
14099
  if (code === "CREDENTIALS_REFRESHED") continue;
14091
14100
  this.terminalError = code;
14101
+ replacedRetryMs = code === "CONNECTION_REPLACED" ? replacedAttempts === 0 ? Math.min(this.config.reconnect.replacedRetryMs, REPLACED_FIRST_RETRY_MS) : this.config.reconnect.replacedRetryMs : void 0;
14102
+ replacedAttempts = code === "CONNECTION_REPLACED" ? replacedAttempts + 1 : 0;
14092
14103
  this.logger.warn("server control connection failed", {
14093
14104
  code,
14094
14105
  retryable: isRetryable(error),
@@ -14096,7 +14107,7 @@ var HostServerConnection = class {
14096
14107
  ...connectionFailureDetail(error)
14097
14108
  });
14098
14109
  if (TERMINAL_AUTH_ERRORS.has(code)) {
14099
- this.logger.warn(code === "CONNECTION_REPLACED" ? "Another instance is using this Host identity. Stop it or use a separate DSH_HOME; automatic reconnect is paused." : code === "SERVER_CREDENTIALS_BUSY" ? "Credential refresh is locked. Stop other instances; after a crash, stop all instances before removing server-credentials.json.refresh-lock and authorizing again." : "Host authorization failed. Run /remote login or authorize this Host again in Remote settings.");
14110
+ this.logger.warn(code === "CONNECTION_REPLACED" ? "Another instance is using this Host identity. Standing by: retrying in five minutes, and taking over as soon as it leaves." : code === "SERVER_CREDENTIALS_BUSY" ? "Credential refresh is locked. Stop other instances; after a crash, stop all instances before removing server-credentials.json.refresh-lock and authorizing again." : "Host authorization failed. Run /remote login or authorize this Host again in Remote settings.");
14100
14111
  }
14101
14112
  if (code === "DEVICE_REVOKED") {
14102
14113
  try {
@@ -14114,7 +14125,7 @@ var HostServerConnection = class {
14114
14125
  continue;
14115
14126
  }
14116
14127
  if (!this.config.reconnect.enabled) return;
14117
- await this.waitBeforeRetry(delayMs);
14128
+ await (replacedRetryMs === void 0 ? this.waitBeforeRetry(delayMs) : this.waitBeforeRetry(replacedRetryMs, true));
14118
14129
  if (this.reconnectRequested) {
14119
14130
  this.reconnectRequested = false;
14120
14131
  delayMs = this.config.reconnect.initialDelayMs;
@@ -14678,8 +14689,8 @@ var HostServerConnection = class {
14678
14689
  tunnel.noise.destroy();
14679
14690
  }
14680
14691
  }
14681
- waitBeforeRetry(baseDelay) {
14682
- const spread = baseDelay * this.config.reconnect.jitter;
14692
+ waitBeforeRetry(baseDelay, exact = false) {
14693
+ const spread = exact ? 0 : baseDelay * this.config.reconnect.jitter;
14683
14694
  const delay = Math.max(0, Math.round(baseDelay - spread + Math.random() * spread * 2));
14684
14695
  return new Promise((resolve4) => {
14685
14696
  const timer = setTimeout(() => {
@@ -14693,8 +14704,8 @@ var HostServerConnection = class {
14693
14704
  });
14694
14705
  }
14695
14706
  };
14707
+ var REPLACED_FIRST_RETRY_MS = 65e3;
14696
14708
  var TERMINAL_AUTH_ERRORS = /* @__PURE__ */ new Set([
14697
- "CONNECTION_REPLACED",
14698
14709
  "SERVER_CREDENTIALS_BUSY",
14699
14710
  "ACCOUNT_AUTH_REQUIRED",
14700
14711
  "AUTH_INVALID",
@@ -22077,9 +22088,26 @@ var ClientModeRuntime = class {
22077
22088
  }
22078
22089
  async setHostAuthorization(enabled) {
22079
22090
  if (this.host === void 0) throw new ClientModeError("METHOD_NOT_ALLOWED", "This plugin is not running as a Host.");
22080
- await this.host.setHostControl?.(enabled);
22081
- if (enabled) await this.host.resumeHostConnection?.();
22082
- else await this.host.pauseHostConnection?.();
22091
+ if (!enabled) {
22092
+ await this.host.setHostControl?.(false);
22093
+ await this.host.pauseHostConnection?.();
22094
+ return this.status();
22095
+ }
22096
+ let flagError;
22097
+ try {
22098
+ await this.host.setHostControl?.(true);
22099
+ } catch (error) {
22100
+ flagError = error;
22101
+ }
22102
+ await this.host.resumeHostConnection?.();
22103
+ try {
22104
+ await this.host.setHostControl?.(true);
22105
+ } catch (error) {
22106
+ this.logger.warn("could not tell the Server that this Host accepts control; retrying when it reconnects", {
22107
+ code: safeErrorCode(error),
22108
+ ...flagError === void 0 ? {} : { firstCode: safeErrorCode(flagError) }
22109
+ });
22110
+ }
22083
22111
  return this.status();
22084
22112
  }
22085
22113
  async setMode(mode, targetDeviceId, signal) {
@@ -22515,6 +22543,22 @@ var ClientModeRuntime = class {
22515
22543
  return;
22516
22544
  }
22517
22545
  const transportLost = reason !== "codex-closed";
22546
+ if (String(reason) === "CONNECTION_REPLACED" && this.reconnecting === void 0) {
22547
+ const run = this.remoteReconnectRun;
22548
+ this.reconnecting = { targetDeviceId, ...targetName === void 0 ? {} : { targetName }, phase: "fast" };
22549
+ this.logger.info("the link was closed by this client's own connection replacement; retrying in place", {
22550
+ targetDeviceId: shortId2(targetDeviceId),
22551
+ kind
22552
+ });
22553
+ const rescued = await this.quickReconnectSession(targetDeviceId, target2, transportLost, run);
22554
+ if (run !== this.remoteReconnectRun) return;
22555
+ if (rescued) {
22556
+ this.finishRecovery(targetDeviceId, "replacement", kind);
22557
+ return;
22558
+ }
22559
+ void this.reconnectRemoteSession(targetDeviceId, target2);
22560
+ return;
22561
+ }
22518
22562
  if (this.reconnecting === void 0) {
22519
22563
  const run = this.remoteReconnectRun;
22520
22564
  this.reconnecting = { targetDeviceId, ...targetName === void 0 ? {} : { targetName }, phase: "fast" };
@@ -23227,7 +23271,26 @@ var ClientModeRuntime = class {
23227
23271
  throw error;
23228
23272
  }
23229
23273
  }
23274
+ /**
23275
+ * One attempt at a time per target: the panel's refresh and the automatic reconnect can ask at the same moment, and
23276
+ * two control connections in flight make the Server close the older one with 4003 (`CONNECTION_REPLACED`), which
23277
+ * also kills the link that was being built on it - what the user saw as being kicked out of the card.
23278
+ */
23279
+ inFlightConnect;
23230
23280
  async connect(targetDeviceId, signal, forceRelay = false) {
23281
+ const key = `${targetDeviceId}|${forceRelay ? "relay" : "auto"}`;
23282
+ const inFlight = this.inFlightConnect;
23283
+ if (inFlight?.key === key) {
23284
+ signal?.throwIfAborted();
23285
+ return await inFlight.promise;
23286
+ }
23287
+ const promise = this.connectNow(targetDeviceId, signal, forceRelay).finally(() => {
23288
+ if (this.inFlightConnect?.promise === promise) this.inFlightConnect = void 0;
23289
+ });
23290
+ this.inFlightConnect = { key, promise };
23291
+ return await promise;
23292
+ }
23293
+ async connectNow(targetDeviceId, signal, forceRelay = false) {
23231
23294
  signal?.throwIfAborted();
23232
23295
  const progressRunId = this.connectionProgressRun + 1;
23233
23296
  this.connectionProgressRun = progressRunId;
@@ -29994,7 +30057,7 @@ function remoteStatusLines(target2) {
29994
30057
  `Authorization: ${status2.authorized ? status2.account === void 0 ? "logged in" : `logged in (${status2.account})` : "logged out"}`,
29995
30058
  `Server connection: ${connection}`,
29996
30059
  ...status2.error === void 0 ? [] : [`Connection error: ${status2.error}`],
29997
- ...status2.error === "CONNECTION_REPLACED" ? ["Another instance is using this Host identity. Stop it or use a separate DSH_HOME before reconnecting."] : status2.error === "SERVER_CREDENTIALS_BUSY" ? ["Credential refresh is locked. Stop all instances before removing an orphaned server-credentials.json.refresh-lock and authorizing again."] : status2.accountRequired ? ["Authorize again with /remote login [github|zhihu]."] : [],
30060
+ ...status2.error === "CONNECTION_REPLACED" ? ["Another instance is using this Host identity. Standing by: retrying every five minutes, and taking over as soon as it leaves."] : status2.error === "SERVER_CREDENTIALS_BUSY" ? ["Credential refresh is locked. Stop all instances before removing an orphaned server-credentials.json.refresh-lock and authorizing again."] : status2.accountRequired ? ["Authorize again with /remote login [github|zhihu]."] : [],
29998
30061
  `Harness Remote API: ${capabilities.has("harness.api.v1") ? "available (ApiProxy)" : capabilities.has("harness.remote.v3") ? "available (Typert Remote Session V3)" : capabilities.has("harness.remote.v1") ? "available (Typert Remote)" : "unavailable"}`,
29999
30062
  `Remote clients: ${diagnostics.activeConnections}`,
30000
30063
  `Codex Remote: ${codex.enabled ? codex.state : "disabled"}`,