@blue-soda/dsh-remote 0.4.29 → 0.4.31

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 (54) hide show
  1. package/dist/client-runtime.d.ts +137 -1
  2. package/dist/client-runtime.d.ts.map +1 -1
  3. package/dist/client-runtime.js +399 -22
  4. package/dist/client-runtime.js.map +1 -1
  5. package/dist/client-secure-transport.d.ts.map +1 -1
  6. package/dist/client-secure-transport.js +2 -1
  7. package/dist/client-secure-transport.js.map +1 -1
  8. package/dist/client-target-store.d.ts +40 -0
  9. package/dist/client-target-store.d.ts.map +1 -0
  10. package/dist/client-target-store.js +79 -0
  11. package/dist/client-target-store.js.map +1 -0
  12. package/dist/client.github.js +63 -15
  13. package/dist/client.js +63 -15
  14. package/dist/codex/domain.d.ts +9 -0
  15. package/dist/codex/domain.d.ts.map +1 -1
  16. package/dist/codex/domain.js +48 -7
  17. package/dist/codex/domain.js.map +1 -1
  18. package/dist/codex/virtual-harness.d.ts.map +1 -1
  19. package/dist/codex/virtual-harness.js +35 -1
  20. package/dist/codex/virtual-harness.js.map +1 -1
  21. package/dist/harness-api-bridge.d.ts +2 -0
  22. package/dist/harness-api-bridge.d.ts.map +1 -1
  23. package/dist/harness-api-bridge.js +10 -0
  24. package/dist/harness-api-bridge.js.map +1 -1
  25. package/dist/harness-remote-bridge.d.ts +8 -0
  26. package/dist/harness-remote-bridge.d.ts.map +1 -1
  27. package/dist/harness-remote-bridge.js +12 -1
  28. package/dist/harness-remote-bridge.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +795 -184
  31. package/dist/index.js.map +4 -4
  32. package/dist/remote-api-proxy.d.ts.map +1 -1
  33. package/dist/remote-api-proxy.js +52 -1
  34. package/dist/remote-api-proxy.js.map +1 -1
  35. package/dist/remote-typert-gateway.d.ts.map +1 -1
  36. package/dist/remote-typert-gateway.js +5 -3
  37. package/dist/remote-typert-gateway.js.map +1 -1
  38. package/dist/rpc-binary-attachments.d.ts +33 -0
  39. package/dist/rpc-binary-attachments.d.ts.map +1 -0
  40. package/dist/rpc-binary-attachments.js +142 -0
  41. package/dist/rpc-binary-attachments.js.map +1 -0
  42. package/dist/service.d.ts +10 -0
  43. package/dist/service.d.ts.map +1 -1
  44. package/dist/service.js +17 -1
  45. package/dist/service.js.map +1 -1
  46. package/dist/tui-command.js +7 -5
  47. package/dist/tui-command.js.map +1 -1
  48. package/dist/typert-gateway-switch.d.ts.map +1 -1
  49. package/dist/typert-gateway-switch.js +148 -15
  50. package/dist/typert-gateway-switch.js.map +1 -1
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/dsh-plugin.json +1 -1
  54. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14579,7 +14579,7 @@ var ClientSecureTransport = class {
14579
14579
  }
14580
14580
  requireNoise() {
14581
14581
  if (this.noise === void 0 || !this.noise.complete || this.closed) {
14582
- throw new Error("The authenticated Noise channel is not connected.");
14582
+ throw Object.assign(new Error("The authenticated Noise channel is not connected."), { code: "TRANSPORT_CLOSED" });
14583
14583
  }
14584
14584
  return this.noise;
14585
14585
  }
@@ -14812,6 +14812,100 @@ async function readHarnessDistributionVersion(entrypoint = process.argv[1]) {
14812
14812
  }
14813
14813
  }
14814
14814
 
14815
+ // src/rpc-binary-attachments.ts
14816
+ function hydrateRpcAttachments(result) {
14817
+ if (!isRecord2(result)) return result;
14818
+ const envelope = result;
14819
+ if (!Array.isArray(envelope.attachments) || envelope.attachments.length === 0) return result;
14820
+ const { attachments, ...rest } = result;
14821
+ const base = Object.hasOwn(rest, "value") ? rest.value : void 0;
14822
+ for (const attachment of attachments) {
14823
+ const entry = attachment;
14824
+ const path = Array.isArray(entry.path) ? entry.path : void 0;
14825
+ if (path === void 0 || path.length === 0) {
14826
+ throw new Error("The remote Host returned a byte attachment without a path.");
14827
+ }
14828
+ assignAtPath(base, path, decodeAttachmentBytes(entry.bytes));
14829
+ }
14830
+ return rest;
14831
+ }
14832
+ function assignAtPath(base, path, bytes) {
14833
+ if (base === null || typeof base !== "object") {
14834
+ throw new Error("The remote Host returned a byte attachment that its result cannot hold.");
14835
+ }
14836
+ let cursor2 = base;
14837
+ for (let index = 0; index < path.length - 1; index += 1) {
14838
+ const key = pathKey(path[index]);
14839
+ const next = cursor2[key];
14840
+ if (next === null || typeof next !== "object") {
14841
+ throw new Error("The remote Host returned a byte attachment at an unknown result path.");
14842
+ }
14843
+ cursor2 = next;
14844
+ }
14845
+ cursor2[pathKey(path[path.length - 1])] = bytes;
14846
+ }
14847
+ function pathKey(segment) {
14848
+ if (typeof segment === "string") return segment;
14849
+ if (typeof segment === "number" && Number.isInteger(segment) && segment >= 0) return String(segment);
14850
+ throw new Error("The remote Host returned a byte attachment with an invalid path segment.");
14851
+ }
14852
+ function decodeAttachmentBytes(value) {
14853
+ if (value instanceof Uint8Array) return value;
14854
+ if (typeof value === "string") return decodeBase64(value);
14855
+ if (Array.isArray(value)) return Uint8Array.from(value);
14856
+ if (!isRecord2(value)) throw new Error("The remote Host returned invalid byte attachment data.");
14857
+ if (Array.isArray(value.data)) {
14858
+ return Uint8Array.from(value.data);
14859
+ }
14860
+ const keys = Object.keys(value);
14861
+ if (keys.every((key) => /^\d+$/u.test(key))) {
14862
+ const ordered = keys.map(Number).sort((left, right) => left - right);
14863
+ return Uint8Array.from(ordered.map((key) => value[String(key)]));
14864
+ }
14865
+ throw new Error("The remote Host returned invalid byte attachment data.");
14866
+ }
14867
+ function decodeBase64(value) {
14868
+ const binary = atob(value);
14869
+ const bytes = new Uint8Array(binary.length);
14870
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
14871
+ return bytes;
14872
+ }
14873
+ function isRecord2(value) {
14874
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14875
+ }
14876
+ function collectRpcAttachments(result) {
14877
+ if (!isRecord2(result) || !Object.hasOwn(result, "value")) return result;
14878
+ const attachments = [];
14879
+ const value = replaceBytes(result.value, [], attachments);
14880
+ if (attachments.length === 0) return result;
14881
+ return { ...result, value, attachments };
14882
+ }
14883
+ function replaceBytes(value, path, attachments) {
14884
+ if (value instanceof Uint8Array) {
14885
+ attachments.push({ path: [...path], bytes: encodeBase64(value) });
14886
+ return null;
14887
+ }
14888
+ if (Array.isArray(value)) return value.map((item, index) => replaceBytes(item, [...path, index], attachments));
14889
+ if (!isRecord2(value)) return value;
14890
+ const clone = {};
14891
+ for (const [key, item] of Object.entries(value)) clone[key] = replaceBytes(item, [...path, key], attachments);
14892
+ return clone;
14893
+ }
14894
+ function encodeBase64(bytes) {
14895
+ let binary = "";
14896
+ for (let offset = 0; offset < bytes.byteLength; offset += 32768) {
14897
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
14898
+ }
14899
+ return btoa(binary);
14900
+ }
14901
+ function decodeByteValue(value) {
14902
+ try {
14903
+ return decodeAttachmentBytes(value);
14904
+ } catch {
14905
+ return void 0;
14906
+ }
14907
+ }
14908
+
14815
14909
  // src/remote-api-proxy.ts
14816
14910
  var DIRECT_API_CALL_BYTES = 2 * 1024 * 1024;
14817
14911
  var AGENT_PRESET_SETTINGS_NS = "agent-presets";
@@ -14919,12 +15013,13 @@ var RemoteHarnessApiProxy = class {
14919
15013
  if (String(response.rpcId) !== String(request.rpcId) || typeof response.result !== "object" || response.result === null) {
14920
15014
  throw new Error("The remote Host returned an invalid Harness API response.");
14921
15015
  }
14922
- const normalized = normalizeLegacyResponse(method, response);
15016
+ const hydrated = { ...response, result: hydrateRpcAttachments(response.result) };
15017
+ const normalized = normalizeLegacyResponse(method, normalizeByteResult(method, hydrated));
14923
15018
  return this.normalizeLegacyWelcomeSettings(method, params.payload, normalized);
14924
15019
  }
14925
15020
  normalizeLegacyWelcomeSettings(method, payload, response) {
14926
15021
  if (!isLegacyRemoteHost(this.harnessVersion)) return response;
14927
- if (method === "settings.describe" && response.result.ok && isRecord2(response.result.value)) {
15022
+ if (method === "settings.describe" && response.result.ok && isRecord3(response.result.value)) {
14928
15023
  this.settingsDescribeValue = response.result.value;
14929
15024
  return this.legacyWelcomeAcknowledged ? patchWelcomeDescribe(response) : response;
14930
15025
  }
@@ -15036,7 +15131,7 @@ function normalizeLegacyResponse(method, response) {
15036
15131
  };
15037
15132
  }
15038
15133
  function normalizeLegacyRequest(method, payload) {
15039
- if (!isRecord2(payload)) return payload;
15134
+ if (!isRecord3(payload)) return payload;
15040
15135
  if (method === "agentPreset.read") return replaceAgentPreset(payload, "agentPreset");
15041
15136
  if (method === "agentPreset.select") return replaceAgentPreset(payload, "agentPreset");
15042
15137
  if (method === "agentPreset.copy") return replaceAgentPreset(payload, "from");
@@ -15051,7 +15146,7 @@ function replaceAgentPreset(payload, key) {
15051
15146
  return replacement === void 0 ? payload : { ...payload, [key]: replacement };
15052
15147
  }
15053
15148
  function replaceAgentPresetSettings(payload) {
15054
- if (payload.ns !== AGENT_PRESET_SETTINGS_NS || !isRecord2(payload.patch)) return payload;
15149
+ if (payload.ns !== AGENT_PRESET_SETTINGS_NS || !isRecord3(payload.patch)) return payload;
15055
15150
  const patch = replaceAgentPreset(payload.patch, "default");
15056
15151
  return patch === payload.patch ? payload : { ...payload, patch };
15057
15152
  }
@@ -15061,11 +15156,11 @@ function isLegacyRemoteHost(version) {
15061
15156
  return match !== null && Number(match[1]) === 0 && Number(match[2]) === 1 && Number(match[3]) < 7;
15062
15157
  }
15063
15158
  function isWelcomeNoticeRequest(payload) {
15064
- if (!isRecord2(payload) || payload.ns !== WELCOME_NOTICE_NAMESPACE || !Array.isArray(payload.ops)) return false;
15065
- return payload.ops.some((operation) => isRecord2(operation) && operation.op === "set" && Array.isArray(operation.path) && operation.path.length === 1 && operation.path[0] === WELCOME_NOTICE_FIELD && operation.value === WELCOME_NOTICE_VERSION);
15159
+ if (!isRecord3(payload) || payload.ns !== WELCOME_NOTICE_NAMESPACE || !Array.isArray(payload.ops)) return false;
15160
+ return payload.ops.some((operation) => isRecord3(operation) && operation.op === "set" && Array.isArray(operation.path) && operation.path.length === 1 && operation.path[0] === WELCOME_NOTICE_FIELD && operation.value === WELCOME_NOTICE_VERSION);
15066
15161
  }
15067
15162
  function patchWelcomeDescribe(response) {
15068
- if (!response.result.ok || !isRecord2(response.result.value) || !Array.isArray(response.result.value.namespaces)) return response;
15163
+ if (!response.result.ok || !isRecord3(response.result.value) || !Array.isArray(response.result.value.namespaces)) return response;
15069
15164
  return {
15070
15165
  ...response,
15071
15166
  result: {
@@ -15073,8 +15168,8 @@ function patchWelcomeDescribe(response) {
15073
15168
  value: {
15074
15169
  ...response.result.value,
15075
15170
  namespaces: response.result.value.namespaces.map((namespace) => {
15076
- if (!isRecord2(namespace) || namespace.ns !== WELCOME_NOTICE_NAMESPACE) return namespace;
15077
- const value = isRecord2(namespace.value) ? namespace.value : {};
15171
+ if (!isRecord3(namespace) || namespace.ns !== WELCOME_NOTICE_NAMESPACE) return namespace;
15172
+ const value = isRecord3(namespace.value) ? namespace.value : {};
15078
15173
  return { ...namespace, value: { ...value, [WELCOME_NOTICE_FIELD]: WELCOME_NOTICE_VERSION } };
15079
15174
  })
15080
15175
  }
@@ -15088,8 +15183,8 @@ function patchWelcomeMutate(value, rpcId2) {
15088
15183
  };
15089
15184
  const namespaces = Array.isArray(value.namespaces) ? value.namespaces : [];
15090
15185
  response.result.value.namespaces = namespaces.map((namespace) => {
15091
- if (!isRecord2(namespace) || namespace.ns !== WELCOME_NOTICE_NAMESPACE) return namespace;
15092
- const current = isRecord2(namespace.value) ? namespace.value : {};
15186
+ if (!isRecord3(namespace) || namespace.ns !== WELCOME_NOTICE_NAMESPACE) return namespace;
15187
+ const current = isRecord3(namespace.value) ? namespace.value : {};
15093
15188
  return { ...namespace, value: { ...current, [WELCOME_NOTICE_FIELD]: WELCOME_NOTICE_VERSION } };
15094
15189
  });
15095
15190
  return response;
@@ -15097,7 +15192,7 @@ function patchWelcomeMutate(value, rpcId2) {
15097
15192
  function isRemoteDisconnect(error) {
15098
15193
  return error instanceof RemoteClientError && (error.code === "TRANSPORT_CLOSED" || error.code === "CLIENT_CLOSED");
15099
15194
  }
15100
- function isRecord2(value) {
15195
+ function isRecord3(value) {
15101
15196
  return typeof value === "object" && value !== null && !Array.isArray(value);
15102
15197
  }
15103
15198
  var AsyncFrameQueue = class {
@@ -15161,6 +15256,24 @@ function base64ToBytes2(value) {
15161
15256
  }
15162
15257
  return bytes;
15163
15258
  }
15259
+ function normalizeByteResult(method, response) {
15260
+ if (method !== "workspaceFiles.readBytes") return response;
15261
+ const result = response.result;
15262
+ if (result === void 0 || result.ok !== true) return response;
15263
+ const value = result.value;
15264
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return response;
15265
+ const raw = value.data;
15266
+ const data2 = raw instanceof Uint8Array ? raw : decodeByteValue(raw);
15267
+ if (data2 === void 0) return response;
15268
+ return {
15269
+ ...response,
15270
+ result: {
15271
+ ...result,
15272
+ value: { ...value, data: null },
15273
+ attachments: [{ path: ["data"], bytes: data2 }]
15274
+ }
15275
+ };
15276
+ }
15164
15277
 
15165
15278
  // src/session-format-compat.ts
15166
15279
  function normalizeLegacySessionGatewayValue(endpoint, value) {
@@ -15169,7 +15282,7 @@ function normalizeLegacySessionGatewayValue(endpoint, value) {
15169
15282
  return value;
15170
15283
  }
15171
15284
  function normalizeFollowFrame(value) {
15172
- if (!isRecord3(value)) return value;
15285
+ if (!isRecord4(value)) return value;
15173
15286
  if (value.type === "snapshot") {
15174
15287
  return {
15175
15288
  ...value,
@@ -15181,7 +15294,7 @@ function normalizeFollowFrame(value) {
15181
15294
  return normalizeEntry(value);
15182
15295
  }
15183
15296
  function normalizePage(value) {
15184
- if (!isRecord3(value)) return value;
15297
+ if (!isRecord4(value)) return value;
15185
15298
  return { ...value, records: normalizeRecords(value.records) };
15186
15299
  }
15187
15300
  function normalizeRecords(value) {
@@ -15226,10 +15339,10 @@ function eventTime(value) {
15226
15339
  return typeof value.event.time === "number" && Number.isSafeInteger(value.event.time) ? value.event.time : 0;
15227
15340
  }
15228
15341
  function isEventEntry(value) {
15229
- return isRecord3(value) && value.type === "event" && isRecord3(value.event);
15342
+ return isRecord4(value) && value.type === "event" && isRecord4(value.event);
15230
15343
  }
15231
15344
  function normalizeHeader(value) {
15232
- if (!isRecord3(value)) return value;
15345
+ if (!isRecord4(value)) return value;
15233
15346
  const next = {
15234
15347
  ...value,
15235
15348
  version: 3,
@@ -15238,7 +15351,7 @@ function normalizeHeader(value) {
15238
15351
  return next.agentPreset === "code" ? { ...next, agentPreset: "ptc" } : next;
15239
15352
  }
15240
15353
  function normalizeEvent(value) {
15241
- if (!isRecord3(value)) return value;
15354
+ if (!isRecord4(value)) return value;
15242
15355
  let next = value;
15243
15356
  const type = normalizeEventType(value.type);
15244
15357
  if (type !== value.type) next = { ...next, type };
@@ -15258,7 +15371,7 @@ function normalizeEventType(value) {
15258
15371
  return value;
15259
15372
  }
15260
15373
  function normalizeEventData(type, value) {
15261
- if (!isRecord3(value)) return value;
15374
+ if (!isRecord4(value)) return value;
15262
15375
  if (type === "request/header") return normalizeRequestHeaderData(value);
15263
15376
  if (type === "agent-preset/selected" && value.agentPreset === "code") return { ...value, agentPreset: "ptc" };
15264
15377
  if (type === "user/message") return normalizeMessage(value);
@@ -15271,7 +15384,7 @@ function normalizeEventData(type, value) {
15271
15384
  return value;
15272
15385
  }
15273
15386
  function normalizeRequestHeaderData(value) {
15274
- if (!isRecord3(value.header)) return value;
15387
+ if (!isRecord4(value.header)) return value;
15275
15388
  const header = normalizeRequestHeader(value.header);
15276
15389
  return header === value.header ? value : { ...value, header };
15277
15390
  }
@@ -15287,7 +15400,7 @@ function normalizeRequestHeader(value) {
15287
15400
  changed = true;
15288
15401
  continue;
15289
15402
  }
15290
- if (key === "adapterDefaults" && isRecord3(field) && Object.keys(field).length === 0) {
15403
+ if (key === "adapterDefaults" && isRecord4(field) && Object.keys(field).length === 0) {
15291
15404
  changed = true;
15292
15405
  continue;
15293
15406
  }
@@ -15296,12 +15409,12 @@ function normalizeRequestHeader(value) {
15296
15409
  return changed ? next : value;
15297
15410
  }
15298
15411
  function normalizeMessage(value) {
15299
- if (!isRecord3(value) || !isRecord3(value.source)) return value;
15412
+ if (!isRecord4(value) || !isRecord4(value.source)) return value;
15300
15413
  if (value.source.kind !== "plugin" || value.source.plugin !== "tools-code-mode") return value;
15301
15414
  return { ...value, source: { ...value.source, plugin: "tools-ptc" } };
15302
15415
  }
15303
15416
  function normalizeSurfaceOp(value) {
15304
- if (!isRecord3(value) || value.op !== "replace") return value;
15417
+ if (!isRecord4(value) || value.op !== "replace") return value;
15305
15418
  const { start, end, ...rest } = value;
15306
15419
  if (start === void 0 && end === void 0) return value;
15307
15420
  return {
@@ -15311,7 +15424,7 @@ function normalizeSurfaceOp(value) {
15311
15424
  ...end === void 0 ? {} : { endSeq: end }
15312
15425
  };
15313
15426
  }
15314
- function isRecord3(value) {
15427
+ function isRecord4(value) {
15315
15428
  return typeof value === "object" && value !== null && !Array.isArray(value);
15316
15429
  }
15317
15430
  function isEventSeq(value) {
@@ -15352,7 +15465,7 @@ var RemoteTypertGateway2 = class {
15352
15465
  response = await this.callTransferred(encoded, signal);
15353
15466
  }
15354
15467
  }
15355
- const result = parseRpcResult(response);
15468
+ const result = parseRpcResult(hydrateRpcAttachments(response));
15356
15469
  const settingsResult = this.normalizeLegacyWelcomeSettings(endpoint, payload, result);
15357
15470
  if (settingsResult !== void 0) return settingsResult;
15358
15471
  if (result.ok && this.compatibility === "legacy-to-v3") {
@@ -15365,11 +15478,11 @@ var RemoteTypertGateway2 = class {
15365
15478
  /** DSH <=0.1.6 cannot persist the 0.1.7 welcome acknowledgement remotely. */
15366
15479
  normalizeLegacyWelcomeSettings(endpoint, payload, result) {
15367
15480
  if (!isLegacyRemoteHost2(this.harnessVersion)) return void 0;
15368
- if (endpoint === "settings/describe" && result.ok && isRecord4(result.value)) {
15481
+ if (endpoint === "settings/describe" && result.ok && isRecord5(result.value)) {
15369
15482
  this.settingsDescribeValue = result.value;
15370
15483
  return this.legacyWelcomeAcknowledged ? { ...result, value: patchWelcomeDescribe2(result.value) } : void 0;
15371
15484
  }
15372
- if (endpoint !== "settings/mutate" || !isRecord4(payload) || !isRecord4(payload.args)) return void 0;
15485
+ if (endpoint !== "settings/mutate" || !isRecord5(payload) || !isRecord5(payload.args)) return void 0;
15373
15486
  const args = payload.args;
15374
15487
  if (args.ns !== WELCOME_NOTICE_NAMESPACE2 || !isWelcomeNoticeOperation(args.ops)) return void 0;
15375
15488
  const value = patchWelcomeDescribe2(this.settingsDescribeValue ?? { namespaces: [] });
@@ -15398,7 +15511,8 @@ var RemoteTypertGateway2 = class {
15398
15511
  async *iterate(streamId, endpoint, queue, unsubscribe, unsubscribeClose, signal, onAbort) {
15399
15512
  try {
15400
15513
  for await (const value of queue) {
15401
- yield this.compatibility === "legacy-to-v3" ? normalizeLegacySessionGatewayValue(endpoint, value) : value;
15514
+ const hydrated = hydrateRpcAttachments(value);
15515
+ yield this.compatibility === "legacy-to-v3" ? normalizeLegacySessionGatewayValue(endpoint, hydrated) : hydrated;
15402
15516
  }
15403
15517
  } finally {
15404
15518
  signal.removeEventListener("abort", onAbort);
@@ -15533,16 +15647,16 @@ function routeStreamEvent2(event, streamId, queue) {
15533
15647
  queue.fail(remoteFailure({
15534
15648
  code: typeof failure2?.code === "string" ? failure2.code : "internal",
15535
15649
  message: typeof failure2?.message === "string" ? failure2.message : "The remote Harness stream failed.",
15536
- details: isRecord4(failure2?.details) ? failure2.details : {}
15650
+ details: isRecord5(failure2?.details) ? failure2.details : {}
15537
15651
  }));
15538
15652
  } else {
15539
15653
  queue.close();
15540
15654
  }
15541
15655
  }
15542
15656
  function parseRpcResult(value) {
15543
- if (!isRecord4(value)) throw new Error("The remote Host returned an invalid Gateway result.");
15657
+ if (!isRecord5(value)) throw new Error("The remote Host returned an invalid Gateway result.");
15544
15658
  if (value.ok === true) return Object.hasOwn(value, "value") ? { ok: true, value: value.value } : { ok: true };
15545
- if (value.ok !== false || !isRecord4(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string" || !isRecord4(value.error.details)) {
15659
+ if (value.ok !== false || !isRecord5(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string" || !isRecord5(value.error.details)) {
15546
15660
  throw new Error("The remote Host returned an invalid Gateway failure.");
15547
15661
  }
15548
15662
  return {
@@ -15576,7 +15690,7 @@ function base64ToBytes3(value) {
15576
15690
  if (bytesToBase643(bytes) !== value) throw new Error("The remote Host returned non-canonical Harness Remote transfer data.");
15577
15691
  return bytes;
15578
15692
  }
15579
- function isRecord4(value) {
15693
+ function isRecord5(value) {
15580
15694
  return typeof value === "object" && value !== null && !Array.isArray(value);
15581
15695
  }
15582
15696
  function hasErrorCode(error, code) {
@@ -15589,13 +15703,13 @@ function isLegacyRemoteHost2(version) {
15589
15703
  return Number(match[1]) === 0 && Number(match[2]) === 1 && Number(match[3]) < 7;
15590
15704
  }
15591
15705
  function isWelcomeNoticeOperation(value) {
15592
- return Array.isArray(value) && value.some((operation) => isRecord4(operation) && operation.op === "set" && Array.isArray(operation.path) && operation.path.length === 1 && operation.path[0] === WELCOME_NOTICE_FIELD2 && operation.value === WELCOME_NOTICE_VERSION2);
15706
+ return Array.isArray(value) && value.some((operation) => isRecord5(operation) && operation.op === "set" && Array.isArray(operation.path) && operation.path.length === 1 && operation.path[0] === WELCOME_NOTICE_FIELD2 && operation.value === WELCOME_NOTICE_VERSION2);
15593
15707
  }
15594
15708
  function patchWelcomeDescribe2(value) {
15595
15709
  if (!Array.isArray(value.namespaces)) return value;
15596
15710
  const namespaces = value.namespaces.map((namespace) => {
15597
- if (!isRecord4(namespace) || namespace.ns !== WELCOME_NOTICE_NAMESPACE2) return namespace;
15598
- const current = isRecord4(namespace.value) ? namespace.value : {};
15711
+ if (!isRecord5(namespace) || namespace.ns !== WELCOME_NOTICE_NAMESPACE2) return namespace;
15712
+ const current = isRecord5(namespace.value) ? namespace.value : {};
15599
15713
  return { ...namespace, value: { ...current, [WELCOME_NOTICE_FIELD2]: WELCOME_NOTICE_VERSION2 } };
15600
15714
  });
15601
15715
  return { ...value, namespaces };
@@ -15757,11 +15871,11 @@ var DATA_IMAGE_URL2 = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/
15757
15871
  function string(value) {
15758
15872
  return typeof value === "string" ? value : void 0;
15759
15873
  }
15760
- function isRecord5(value) {
15874
+ function isRecord6(value) {
15761
15875
  return typeof value === "object" && value !== null && !Array.isArray(value);
15762
15876
  }
15763
15877
  function record2(value) {
15764
- return isRecord5(value) ? value : {};
15878
+ return isRecord6(value) ? value : {};
15765
15879
  }
15766
15880
  function toolOutputImageBlocks(item) {
15767
15881
  const candidates = [
@@ -15851,7 +15965,7 @@ function collectImageBlocks(value, output = []) {
15851
15965
  for (const item of value) collectImageBlocks(item, output);
15852
15966
  return output;
15853
15967
  }
15854
- if (!isRecord5(value)) return output;
15968
+ if (!isRecord6(value)) return output;
15855
15969
  if (value.type === "image") output.push(value);
15856
15970
  for (const [key, child] of Object.entries(value)) {
15857
15971
  if (key === "attachment") continue;
@@ -16110,7 +16224,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
16110
16224
  try {
16111
16225
  if (isHostWorkspaceEndpoint(endpoint)) {
16112
16226
  if (this.hostCarrier === void 0) return fail("method-not-found", `CodeX virtual Harness does not implement ${endpoint}.`);
16113
- return await this.hostCarrier.dispatch(endpoint, payload, signal);
16227
+ return normalizeByteResult2(endpoint, await this.hostCarrier.dispatch(endpoint, payload, signal));
16114
16228
  }
16115
16229
  const args = carrierArgs(payload);
16116
16230
  switch (endpoint) {
@@ -16932,7 +17046,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
16932
17046
  this.broadcastRcHost({ type: "host/session-status", sessionId, running: args[1] });
16933
17047
  } else if (event === "api-session/removed" && sessionId !== void 0) {
16934
17048
  this.broadcastRcHost({ type: "host/session-removed", sessionId });
16935
- } else if (event === "api-session/added" && isRecord6(args[0])) {
17049
+ } else if (event === "api-session/added" && isRecord7(args[0])) {
16936
17050
  const summary = args[0];
16937
17051
  this.broadcastRcHost({
16938
17052
  type: "host/session-added",
@@ -17322,7 +17436,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
17322
17436
  }
17323
17437
  createApiProxy() {
17324
17438
  const call = (endpoint) => async (request, signal) => {
17325
- const payload = endpoint === "session.prompt" && isRecord6(request.payload) ? { ...request.payload, requestId: String(request.rpcId) } : request.payload;
17439
+ const payload = endpoint === "session.prompt" && isRecord7(request.payload) ? { ...request.payload, requestId: String(request.rpcId) } : request.payload;
17326
17440
  const result = await this.dispatch(rcEndpoint(endpoint), { args: { request: payload } }, signal ?? new AbortController().signal);
17327
17441
  return {
17328
17442
  rpcId: request.rpcId,
@@ -17977,7 +18091,7 @@ function codexPromptInput(content) {
17977
18091
  if (content.length === 0 || content.length > MAX_CODEX_PROMPT_PARTS) return void 0;
17978
18092
  const input2 = [];
17979
18093
  for (const value of content) {
17980
- if (!isRecord6(value)) return void 0;
18094
+ if (!isRecord7(value)) return void 0;
17981
18095
  if (value.type === "text") {
17982
18096
  if (typeof value.text !== "string" || value.text.length === 0 || value.text.length > MAX_CODEX_PROMPT_TEXT) return void 0;
17983
18097
  input2.push({ type: "text", text: value.text });
@@ -18227,12 +18341,12 @@ function errorCode(error) {
18227
18341
  return "code" in error && typeof error.code === "string" ? error.code : void 0;
18228
18342
  }
18229
18343
  function errorDetails(error) {
18230
- return "details" in error && isRecord6(error.details) ? error.details : {};
18344
+ return "details" in error && isRecord7(error.details) ? error.details : {};
18231
18345
  }
18232
18346
  function record3(value) {
18233
- return isRecord6(value) ? value : {};
18347
+ return isRecord7(value) ? value : {};
18234
18348
  }
18235
- function isRecord6(value) {
18349
+ function isRecord7(value) {
18236
18350
  return typeof value === "object" && value !== null && !Array.isArray(value);
18237
18351
  }
18238
18352
  function array(value) {
@@ -18349,6 +18463,19 @@ var AsyncValueQueue2 = class {
18349
18463
  }
18350
18464
  }
18351
18465
  };
18466
+ function normalizeByteResult2(endpoint, result) {
18467
+ if (endpoint !== "workspaceFiles/readBytes" || !result.ok) return result;
18468
+ const value = result.value;
18469
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return result;
18470
+ const raw = value.data;
18471
+ const data2 = raw instanceof Uint8Array ? raw : decodeByteValue(raw);
18472
+ if (data2 === void 0) return result;
18473
+ return {
18474
+ ...result,
18475
+ value: { ...value, data: null },
18476
+ attachments: [{ path: ["data"], bytes: data2 }]
18477
+ };
18478
+ }
18352
18479
 
18353
18480
  // src/server-api.ts
18354
18481
  import { platform } from "node:os";
@@ -18629,7 +18756,7 @@ function normalizeServerUrl(value) {
18629
18756
  }
18630
18757
 
18631
18758
  // src/version.ts
18632
- var PLUGIN_VERSION = "0.4.29";
18759
+ var PLUGIN_VERSION = "0.4.31";
18633
18760
 
18634
18761
  // src/server-api.ts
18635
18762
  var ENABLED_QR_PROVIDERS = ["github"];
@@ -19104,6 +19231,17 @@ function requireRecord(value, name2) {
19104
19231
  // src/typert-gateway-switch.ts
19105
19232
  var REMOTE_COMMAND_METHODS = ["execute", "list"];
19106
19233
  var LOCAL_ONLY_NAMESPACES = /* @__PURE__ */ new Set(["dynamicCordisRunner"]);
19234
+ var LOCAL_FALLBACK_NAMESPACES = /* @__PURE__ */ new Set(["$events", "settings", "credentials", "dynamicCordisRunner"]);
19235
+ var PEER_GONE_CODES = /* @__PURE__ */ new Set([
19236
+ "TRANSPORT_CLOSED",
19237
+ "CLIENT_CLOSED",
19238
+ "RPC_TIMEOUT",
19239
+ "CONNECTION_FAILED",
19240
+ "CONNECTION_REPLACED",
19241
+ "NOT_CONNECTED",
19242
+ "UNAVAILABLE",
19243
+ "internal"
19244
+ ]);
19107
19245
  var ALL_REMOTE_COMMANDS = { execute: true, list: true };
19108
19246
  var TypertGatewaySwitch = class {
19109
19247
  runtime;
@@ -19165,25 +19303,39 @@ var TypertGatewaySwitch = class {
19165
19303
  install() {
19166
19304
  if (this.installed) return;
19167
19305
  this.runtime.invoke = (request) => this.selectInvoke(request);
19168
- if (this.originalStream !== void 0) {
19169
- this.runtime.stream = (request) => !this.routesToRemote(endpointOf(request)) ? this.localStream(request) : this.withLocalFallback(
19306
+ const previousStream = this.runtime.stream?.bind(this.runtime);
19307
+ const previousDispatch = this.runtime.dispatchRpc?.bind(this.runtime);
19308
+ const previousOpen = this.runtime.openWireStream;
19309
+ const forwards = (endpoint) => this.remoteTarget === void 0 && this.remoteAvailability() && !isLocalOnlyEndpoint(endpoint);
19310
+ if (previousStream !== void 0) {
19311
+ this.runtime.stream = (request) => forwards(endpointOf(request)) ? previousStream(request) : !this.routesToRemote(endpointOf(request)) ? this.localStream(request) : this.withLocalFallback(
19170
19312
  endpointOf(request),
19171
19313
  () => this.remoteTarget.open(endpointOf(request), { args: request.args }, request.signal ?? new AbortController().signal),
19172
19314
  () => this.localStream(request)
19173
19315
  );
19174
19316
  }
19175
- if (this.originalDispatch !== void 0) {
19176
- this.runtime.dispatchRpc = (endpoint, payload, signal) => !this.routesToRemote(endpoint) ? this.localDispatch(endpoint, payload, signal) : this.withLocalFallback(
19317
+ if (previousDispatch !== void 0) {
19318
+ this.runtime.dispatchRpc = (endpoint, payload, signal) => forwards(endpoint) ? Promise.resolve(previousDispatch(endpoint, payload, signal)).catch((error) => {
19319
+ console.warn("[dsh-remote] local call failed with no remote target selected", {
19320
+ endpoint,
19321
+ code: typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0,
19322
+ message: error instanceof Error ? error.message : String(error)
19323
+ });
19324
+ throw error;
19325
+ }) : !this.routesToRemote(endpoint) ? this.localDispatch(endpoint, payload, signal) : this.withLocalFallback(
19177
19326
  endpoint,
19178
19327
  () => Promise.resolve(this.remoteTarget.dispatch(endpoint, payload, signal)),
19179
19328
  () => this.localDispatch(endpoint, payload, signal)
19180
- );
19329
+ ).then((result) => normalizeByteResult3(endpoint, result));
19181
19330
  }
19182
- if (this.originalOpen !== void 0) {
19183
- const open = this.originalOpen;
19331
+ if (previousOpen !== void 0) {
19332
+ const open = previousOpen;
19184
19333
  const rc1 = usesRc1Arity(open);
19185
19334
  this.runtime.openWireStream = (...callArgs) => {
19186
19335
  const endpoint = callArgs[0];
19336
+ if (forwards(endpoint)) {
19337
+ return rc1 ? Reflect.apply(open, this.runtime, callArgs) : open.call(this.runtime, endpoint, callArgs[1], callArgs[2]);
19338
+ }
19187
19339
  if (!this.routesToRemote(endpoint)) {
19188
19340
  return rc1 ? Reflect.apply(open, this.runtime, callArgs) : open.call(this.runtime, endpoint, callArgs[1], callArgs[2]);
19189
19341
  }
@@ -19254,8 +19406,8 @@ var TypertGatewaySwitch = class {
19254
19406
  selectInvoke(request) {
19255
19407
  if (isLocalOnlyEndpoint(endpointOf(request))) return this.localInvoke(request);
19256
19408
  if (this.remoteTarget !== void 0 && this.remoteAvailability()) {
19257
- return this.remoteTarget.invoke(request).catch((error) => {
19258
- if (!isUnansweredByPeer(error)) throw error;
19409
+ return this.remoteTarget.invoke(request).then((result) => normalizeByteResult3(endpointOf(request), result)).catch((error) => {
19410
+ if (!localFallbackAllowed(endpointOf(request), error)) throw error;
19259
19411
  console.warn(`[dsh-remote] serving ${endpointOf(request)} locally: the peer did not answer it`, error);
19260
19412
  return this.localInvoke(request);
19261
19413
  });
@@ -19282,7 +19434,7 @@ var TypertGatewaySwitch = class {
19282
19434
  */
19283
19435
  withLocalFallback(endpoint, remote, local) {
19284
19436
  return remote().catch((error) => {
19285
- if (!isUnansweredByPeer(error)) throw error;
19437
+ if (!localFallbackAllowed(endpoint, error)) throw error;
19286
19438
  console.warn(`[dsh-remote] serving ${endpoint} locally: the peer did not answer it`, error);
19287
19439
  return local();
19288
19440
  });
@@ -19292,7 +19444,7 @@ var TypertGatewaySwitch = class {
19292
19444
  if (normalized !== void 0) return normalized;
19293
19445
  const source = error instanceof Error ? error : new Error("The Harness Gateway rejected the request.");
19294
19446
  const code = "code" in source && typeof source.code === "string" ? source.code : "internal";
19295
- const details = "details" in source && isRecord7(source.details) ? source.details : {};
19447
+ const details = "details" in source && isRecord8(source.details) ? source.details : {};
19296
19448
  return { code, message: source.message, details };
19297
19449
  }
19298
19450
  };
@@ -19332,7 +19484,7 @@ function requestFromCarrier(endpoint, payload, signal) {
19332
19484
  if (segments.length !== 2 || segments.some((segment) => segment.length === 0)) {
19333
19485
  throw new Error("The Harness Gateway endpoint is invalid.");
19334
19486
  }
19335
- if (!isRecord7(payload) || !isRecord7(payload.args)) {
19487
+ if (!isRecord8(payload) || !isRecord8(payload.args)) {
19336
19488
  throw new Error("The Harness Gateway payload is invalid.");
19337
19489
  }
19338
19490
  return { namespace: segments[0], method: segments[1], args: payload.args, signal };
@@ -19340,15 +19492,27 @@ function requestFromCarrier(endpoint, payload, signal) {
19340
19492
  function endpointOf(request) {
19341
19493
  return `${request.namespace}/${request.method}`;
19342
19494
  }
19343
- function isLocalOnlyEndpoint(endpoint) {
19495
+ function namespaceOf(endpoint) {
19344
19496
  const separator = endpoint.indexOf("/");
19345
- return separator > 0 && LOCAL_ONLY_NAMESPACES.has(endpoint.slice(0, separator));
19497
+ return separator > 0 ? endpoint.slice(0, separator) : void 0;
19498
+ }
19499
+ function isLocalOnlyEndpoint(endpoint) {
19500
+ const namespace = namespaceOf(endpoint);
19501
+ return namespace !== void 0 && LOCAL_ONLY_NAMESPACES.has(namespace);
19502
+ }
19503
+ function localFallbackAllowed(endpoint, error) {
19504
+ if (!isUnansweredByPeer(error)) return false;
19505
+ if (!isRecord8(error)) return true;
19506
+ const code = typeof error.code === "string" ? error.code : "";
19507
+ if (!PEER_GONE_CODES.has(code)) return true;
19508
+ const namespace = namespaceOf(endpoint);
19509
+ return namespace !== void 0 && LOCAL_FALLBACK_NAMESPACES.has(namespace);
19346
19510
  }
19347
19511
  function isRemoteCommandMethod(method) {
19348
19512
  return REMOTE_COMMAND_METHODS.includes(method);
19349
19513
  }
19350
19514
  function isUnansweredByPeer(error) {
19351
- if (!isRecord7(error)) return error instanceof Error;
19515
+ if (!isRecord8(error)) return error instanceof Error;
19352
19516
  const code = typeof error.code === "string" ? error.code : "";
19353
19517
  return code === "" || UNANSWERED_BY_PEER_CODES.has(code);
19354
19518
  }
@@ -19361,11 +19525,32 @@ var UNANSWERED_BY_PEER_CODES = /* @__PURE__ */ new Set([
19361
19525
  "CONNECTION_REPLACED",
19362
19526
  "NOT_CONNECTED",
19363
19527
  "UNAVAILABLE",
19528
+ // The client's own transport codes: the peer cannot answer a call it never received, so these are
19529
+ // "unanswered" rather than a refusal - and PEER_GONE_CODES above is what decides whether the local
19530
+ // shell may answer for it.
19531
+ "TRANSPORT_CLOSED",
19532
+ "CLIENT_CLOSED",
19533
+ "RPC_TIMEOUT",
19364
19534
  "internal"
19365
19535
  ]);
19366
- function isRecord7(value) {
19536
+ function isRecord8(value) {
19367
19537
  return typeof value === "object" && value !== null && !Array.isArray(value);
19368
19538
  }
19539
+ function normalizeByteResult3(endpoint, result) {
19540
+ if (endpoint !== "workspaceFiles/readBytes") return result;
19541
+ if (typeof result !== "object" || result === null || Array.isArray(result)) return result;
19542
+ const envelope = result;
19543
+ const value = envelope.value;
19544
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return result;
19545
+ const raw = value.data;
19546
+ const data2 = raw instanceof Uint8Array ? raw : decodeByteValue(raw);
19547
+ if (data2 === void 0) return result;
19548
+ return {
19549
+ ...envelope,
19550
+ value: { ...value, data: null },
19551
+ attachments: [{ path: ["data"], bytes: data2 }]
19552
+ };
19553
+ }
19369
19554
 
19370
19555
  // src/werift-rtc.ts
19371
19556
  import { createSocket } from "node:dgram";
@@ -20442,14 +20627,34 @@ var HOST_AUTHORIZATION_ERRORS = /* @__PURE__ */ new Set([
20442
20627
  "DEVICE_REVOKED",
20443
20628
  "TOKEN_EXPIRED"
20444
20629
  ]);
20630
+ var CREDENTIAL_FAILURE_CODES = /* @__PURE__ */ new Set([
20631
+ "AUTH_REQUIRED",
20632
+ "ACCOUNT_AUTH_REQUIRED",
20633
+ "AUTH_INVALID",
20634
+ "TOKEN_EXPIRED",
20635
+ "DEVICE_NOT_FOUND",
20636
+ "DEVICE_REVOKED",
20637
+ "MEMBERSHIP_REQUIRED"
20638
+ ]);
20639
+ var LIVENESS_INTERVAL_MS = 3e4;
20640
+ var FAST_RECONNECT_INTERVAL_MS = 5e3;
20641
+ var LIVENESS_TIMEOUT_MS = 4e3;
20642
+ var LIVENESS_TOLERATED_FAILURES = 2;
20643
+ var NO_ANSWER_CODES = /* @__PURE__ */ new Set(["RPC_TIMEOUT", "CLIENT_CLOSED", "TRANSPORT_CLOSED", "RPC_ABORTED"]);
20644
+ function livenessProbeLost(error) {
20645
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
20646
+ if (typeof code !== "string") return true;
20647
+ return NO_ANSWER_CODES.has(code);
20648
+ }
20445
20649
  var ClientModeRuntime = class {
20446
- constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory) {
20650
+ constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory, targetStore) {
20447
20651
  this.config = config;
20448
20652
  this.identities = identities;
20449
20653
  this.server = server;
20450
20654
  this.logger = logger;
20451
20655
  this.host = host;
20452
20656
  this.rtcFactoryProvider = rtcFactoryProvider;
20657
+ this.targetStore = targetStore;
20453
20658
  this.proxySwitch = apiProxy === void 0 ? void 0 : new ApiProxySwitch(apiProxy);
20454
20659
  this.gatewaySwitch = new TypertGatewaySwitch(typertGateway);
20455
20660
  this.gatewaySwitch.setRemoteAvailability(() => this.connected !== void 0);
@@ -20474,12 +20679,56 @@ var ClientModeRuntime = class {
20474
20679
  */
20475
20680
  remoteReconnectRun = 0;
20476
20681
  pendingWorkspaceSelection;
20682
+ /**
20683
+ * Transport a running fast reconnect is replacing.
20684
+ *
20685
+ * The rebuild opens its own control connection and the Server closes the older one for the same
20686
+ * device, so that close belongs to our own replacement. Reading it as a lost peer is what made the
20687
+ * fast level escalate itself into the fallback before it could ever succeed.
20688
+ */
20689
+ supersededClient;
20690
+ /**
20691
+ * True while a fast reconnect is rebuilding the link.
20692
+ *
20693
+ * The probe must not judge the link in the middle of its own replacement: the Server closes the
20694
+ * older connection for the device, so a check that ran then would read our replacement's side effect
20695
+ * as a second miss and escalate the level that is busy recovering. The rebuild's own outcome decides
20696
+ * first; only once it has settled does the cadence resume judging.
20697
+ */
20698
+ fastRebuildInFlight = false;
20699
+ /**
20700
+ * The last workspace the user opened for a Host.
20701
+ *
20702
+ * Kept so a reconnect can republish it: re-selecting the workspace is what makes the native UI
20703
+ * re-read its session list, and without it a list poisoned by the outage stays wrong even after
20704
+ * the link is back.
20705
+ */
20706
+ lastWorkspaceSelection;
20477
20707
  codexVirtual;
20478
20708
  proxySwitch;
20479
20709
  gatewaySwitch;
20480
20710
  codexStreams = /* @__PURE__ */ new Map();
20481
20711
  connectionProgress;
20482
20712
  connectionProgressRun = 0;
20713
+ /**
20714
+ * Host a boot is trying to restore, reported to the UI while the retry loop runs.
20715
+ *
20716
+ * Nothing is connected yet, so without this the window would show the local shell while the
20717
+ * user is still looking at the remote workspace they left.
20718
+ */
20719
+ /**
20720
+ * Recovery in progress, and which kind.
20721
+ *
20722
+ * `restore` is a start that is reconnecting to the recorded target, `fast` keeps the session on
20723
+ * screen while its link is rebuilt, and `fallback` is the last resort that returns the user to
20724
+ * the local shell. The UI shows all three, so a reconnect is never invisible, and the retry loop
20725
+ * stops as soon as the user asks for local.
20726
+ */
20727
+ reconnecting;
20728
+ /** Periodic proof of life for the live remote session; absent while nothing is connected. */
20729
+ livenessTimer;
20730
+ livenessInFlight = false;
20731
+ livenessFailures = 0;
20483
20732
  closed = false;
20484
20733
  async start() {
20485
20734
  if (this.closed) throw new Error("client remote-mode runtime is closed");
@@ -20540,6 +20789,7 @@ var ClientModeRuntime = class {
20540
20789
  ...targetStatus,
20541
20790
  connected: this.connected !== void 0,
20542
20791
  fellBackToLocal: this.fellBackToLocal,
20792
+ ...this.reconnecting === void 0 ? {} : { reconnecting: { ...this.reconnecting } },
20543
20793
  transport: this.connected?.client.getStats().mode ?? "Disconnected",
20544
20794
  connectedTargetDeviceId: this.connected?.target.deviceId,
20545
20795
  preferredTransports: this.config.forceRelay ? ["relay"] : ["lan", "p2p", "turn", "relay"],
@@ -20686,10 +20936,14 @@ var ClientModeRuntime = class {
20686
20936
  this.connected = void 0;
20687
20937
  this.connectionProgress = void 0;
20688
20938
  this.pendingWorkspaceSelection = void 0;
20939
+ this.lastWorkspaceSelection = void 0;
20689
20940
  await this.closeCodexStreams(previous2?.client);
20690
20941
  await previous2?.client.close().catch(() => void 0);
20691
20942
  this.fellBackToLocal = false;
20943
+ this.reconnecting = void 0;
20944
+ this.stopLivenessWatch();
20692
20945
  this.remoteReconnectRun += 1;
20946
+ await this.rememberTarget({ mode: "local" });
20693
20947
  this.logger.info("Harness target switched", { mode: "local" });
20694
20948
  return this.status();
20695
20949
  }
@@ -20712,11 +20966,191 @@ var ClientModeRuntime = class {
20712
20966
  await this.closeCodexVirtual();
20713
20967
  this.selectRemoteTarget(next);
20714
20968
  this.fellBackToLocal = false;
20969
+ this.reconnecting = void 0;
20970
+ await this.rememberTarget({ mode: "remote", hostDeviceId: next.target.deviceId, hostName: next.target.name });
20715
20971
  await this.closeCodexStreams(previous?.client);
20716
20972
  await previous?.client.close().catch(() => void 0);
20717
20973
  this.logger.info("Harness target switched", { mode: "remote", targetDeviceId: shortId(next.target.deviceId) });
20718
20974
  return this.status();
20719
20975
  }
20976
+ /**
20977
+ * Prove the live remote link still answers, and act on the result.
20978
+ *
20979
+ * Only a missing answer counts as loss: the peer's own error (a refusal, an unknown method) came
20980
+ * back over the same link and therefore proves it is there.
20981
+ *
20982
+ * The two recovery levels differ in what the user keeps. The first miss rebuilds the link in
20983
+ * place, so the session, its workspace selection and the remote carriers all stay; the second
20984
+ * gives up and returns to the local shell.
20985
+ */
20986
+ async verifyRemoteLiveness() {
20987
+ const connected = this.connected;
20988
+ if (connected === void 0 || this.livenessInFlight || this.fastRebuildInFlight) return;
20989
+ this.livenessInFlight = true;
20990
+ try {
20991
+ await connected.client.rpc("harness.transport.describe", {}, void 0, { timeoutMs: LIVENESS_TIMEOUT_MS });
20992
+ this.livenessFailures = 0;
20993
+ if (this.reconnecting?.phase === "fast") this.finishReconnect("the link answered again");
20994
+ } catch (error) {
20995
+ if (!livenessProbeLost(error)) {
20996
+ this.livenessFailures = 0;
20997
+ return;
20998
+ }
20999
+ this.livenessFailures += 1;
21000
+ this.logger.warn("remote Harness liveness check found no answer", {
21001
+ targetDeviceId: shortId(connected.target.deviceId),
21002
+ attempt: this.livenessFailures
21003
+ });
21004
+ if (this.livenessFailures === 1) {
21005
+ await this.enterFastReconnect(connected.target.deviceId, connected.target.name);
21006
+ return;
21007
+ }
21008
+ if (this.livenessFailures >= LIVENESS_TOLERATED_FAILURES) {
21009
+ this.handleRemoteTransportLost(connected.client, connected.target.deviceId, "unanswered-twice");
21010
+ }
21011
+ } finally {
21012
+ this.livenessInFlight = false;
21013
+ }
21014
+ }
21015
+ /**
21016
+ * Rebuild the link while the session stays on screen.
21017
+ *
21018
+ * This is what separates the two recovery levels: nothing is handed back to the local shell, so a
21019
+ * link that recovers does not cost the user the view they were working in.
21020
+ * @param targetDeviceId - the Host to rebuild the link to.
21021
+ */
21022
+ async enterFastReconnect(targetDeviceId, targetName) {
21023
+ if (this.reconnecting !== void 0) return;
21024
+ this.reconnecting = { targetDeviceId, ...targetName === void 0 ? {} : { targetName }, phase: "fast" };
21025
+ this.logger.warn("remote Harness link stopped answering; reconnecting in place", {
21026
+ targetDeviceId: shortId(targetDeviceId)
21027
+ });
21028
+ this.armLivenessWatch(FAST_RECONNECT_INTERVAL_MS);
21029
+ await this.reestablish(targetDeviceId);
21030
+ }
21031
+ /**
21032
+ * Build a new transport for the session already on screen.
21033
+ *
21034
+ * The carriers hold the previous client, so they are rebound to the new one before the old client
21035
+ * is closed; leaving it open would keep pointing remote calls at a dead transport.
21036
+ * @param targetDeviceId - the Host to reconnect to.
21037
+ * @returns true when a new session is in place.
21038
+ */
21039
+ async reestablish(targetDeviceId) {
21040
+ const previous = this.connected;
21041
+ this.supersededClient = previous?.client;
21042
+ this.fastRebuildInFlight = true;
21043
+ try {
21044
+ const next = await this.connect(targetDeviceId);
21045
+ if (previous === void 0) {
21046
+ this.supersededClient = void 0;
21047
+ await next.client.close().catch(() => void 0);
21048
+ return false;
21049
+ }
21050
+ await this.closePreview();
21051
+ this.connected = next;
21052
+ this.clearConnectionProgress(next.progressRunId);
21053
+ this.selectRemoteTarget(next);
21054
+ await this.closeCodexStreams(previous.client);
21055
+ await previous.client.close().catch(() => void 0);
21056
+ this.supersededClient = void 0;
21057
+ this.finishReconnect("link re-established");
21058
+ return true;
21059
+ } catch (error) {
21060
+ this.supersededClient = void 0;
21061
+ this.logger.warn("fast reconnect attempt failed", {
21062
+ targetDeviceId: shortId(targetDeviceId),
21063
+ code: safeErrorCode(error)
21064
+ });
21065
+ return false;
21066
+ } finally {
21067
+ this.fastRebuildInFlight = false;
21068
+ }
21069
+ }
21070
+ rememberWorkspaceSelection(selection) {
21071
+ this.pendingWorkspaceSelection = selection;
21072
+ this.lastWorkspaceSelection = { ...selection };
21073
+ }
21074
+ /**
21075
+ * Republish the workspace selection so the native UI re-reads its remote session list.
21076
+ *
21077
+ * The client half consumes status.workspaceSelection and reconnects that workspace; that refresh is
21078
+ * what replaces a list the outage had filled with local answers.
21079
+ * @param targetDeviceId - the Host the reconnect finished against.
21080
+ */
21081
+ restoreWorkspaceSelection(targetDeviceId) {
21082
+ const selection = this.lastWorkspaceSelection;
21083
+ if (selection === void 0 || selection.targetDeviceId !== targetDeviceId) return;
21084
+ this.pendingWorkspaceSelection = { ...selection };
21085
+ this.logger.info("republishing the workspace selection to re-read the remote session list", {
21086
+ targetDeviceId: shortId(targetDeviceId),
21087
+ workspaceId: selection.workspaceId
21088
+ });
21089
+ }
21090
+ finishReconnect(reason) {
21091
+ const target2 = this.reconnecting?.targetDeviceId;
21092
+ if (target2 === void 0) return;
21093
+ this.reconnecting = void 0;
21094
+ this.livenessFailures = 0;
21095
+ this.armLivenessWatch(LIVENESS_INTERVAL_MS);
21096
+ this.restoreWorkspaceSelection(target2);
21097
+ this.logger.info("remote Harness reconnect finished", { targetDeviceId: shortId(target2), reason });
21098
+ }
21099
+ /**
21100
+ * Check the live session now, outside the cadence.
21101
+ *
21102
+ * Used when the page becomes visible again, which is when a suspended client is most likely to be
21103
+ * holding a link that already ended.
21104
+ * @returns the status after the check.
21105
+ */
21106
+ async verifyRemoteConnection() {
21107
+ await this.verifyRemoteLiveness();
21108
+ return this.status();
21109
+ }
21110
+ armLivenessWatch(intervalMs) {
21111
+ if (this.livenessTimer !== void 0) clearInterval(this.livenessTimer);
21112
+ const timer = setInterval(() => {
21113
+ void this.verifyRemoteLiveness();
21114
+ }, intervalMs);
21115
+ timer.unref?.();
21116
+ this.livenessTimer = timer;
21117
+ }
21118
+ stopLivenessWatch() {
21119
+ if (this.livenessTimer !== void 0) clearInterval(this.livenessTimer);
21120
+ this.livenessTimer = void 0;
21121
+ this.livenessFailures = 0;
21122
+ this.livenessInFlight = false;
21123
+ }
21124
+ /**
21125
+ * Shared cleanup for a session whose transport is gone.
21126
+ *
21127
+ * A close event and an exhausted liveness check must leave exactly the same state behind, so both
21128
+ * paths run this. The session is gone for good here, which is why the phase becomes 'fallback' and
21129
+ * the retry loop keeps the UI saying that it is reconnecting.
21130
+ * @param client - the client that was connected.
21131
+ * @param targetDeviceId - the Host it was bound to.
21132
+ */
21133
+ handleRemoteTransportLost(client, targetDeviceId, reason) {
21134
+ if (this.connected?.client !== client) return;
21135
+ this.supersededClient = void 0;
21136
+ const targetName = this.connected.target.name;
21137
+ this.stopLivenessWatch();
21138
+ void this.closePreview();
21139
+ this.connected = void 0;
21140
+ this.connectionProgress = void 0;
21141
+ this.pendingWorkspaceSelection = void 0;
21142
+ void this.closeCodexVirtual();
21143
+ this.proxySwitch?.selectLocal();
21144
+ this.gatewaySwitch.selectLocal();
21145
+ this.fellBackToLocal = true;
21146
+ this.reconnecting = { targetDeviceId, ...targetName === void 0 ? {} : { targetName }, phase: "fallback" };
21147
+ void client.close().catch(() => void 0);
21148
+ this.logger.warn("remote Harness transport lost; reconnecting", {
21149
+ targetDeviceId: shortId(targetDeviceId),
21150
+ reason
21151
+ });
21152
+ void this.reconnectRemoteSession(targetDeviceId);
21153
+ }
20720
21154
  /**
20721
21155
  * Re-establish a remote session whose transport closed.
20722
21156
  *
@@ -20740,20 +21174,79 @@ var ClientModeRuntime = class {
20740
21174
  if (this.connected !== void 0) return;
20741
21175
  try {
20742
21176
  await this.setMode("remote", targetDeviceId);
21177
+ this.restoreWorkspaceSelection(targetDeviceId);
20743
21178
  this.logger.info("remote Harness session reconnected", { targetDeviceId: shortId(targetDeviceId) });
20744
21179
  return;
20745
21180
  } catch (error) {
21181
+ const code = safeErrorCode(error);
21182
+ if (code !== void 0 && CREDENTIAL_FAILURE_CODES.has(code)) {
21183
+ this.logger.warn("remote Harness reconnect stopped: authorization is required", {
21184
+ targetDeviceId: shortId(targetDeviceId),
21185
+ code
21186
+ });
21187
+ this.reconnecting = void 0;
21188
+ return;
21189
+ }
20746
21190
  if (attempt < 3 || attempt % 10 === 0) {
20747
21191
  this.logger.warn("remote Harness reconnect attempt failed", {
20748
21192
  targetDeviceId: shortId(targetDeviceId),
20749
21193
  attempt,
20750
- code: safeErrorCode(error)
21194
+ code
20751
21195
  });
20752
21196
  }
20753
21197
  }
20754
21198
  attempt += 1;
20755
21199
  }
20756
21200
  }
21201
+ /**
21202
+ * Persist the target a later boot may have to restore.
21203
+ *
21204
+ * A failure here must not fail a connect: the only cost is that a killed process comes back to
21205
+ * the local shell, which is where it would have been without this record.
21206
+ * @param target - the target to record.
21207
+ */
21208
+ async rememberTarget(target2) {
21209
+ if (this.targetStore === void 0) return;
21210
+ try {
21211
+ await this.targetStore.save({
21212
+ ...target2,
21213
+ ...this.config.serverUrl === void 0 ? {} : { serverUrl: this.config.serverUrl }
21214
+ });
21215
+ } catch (error) {
21216
+ this.logger.warn("could not record the remote target", { code: safeErrorCode(error) });
21217
+ }
21218
+ }
21219
+ /**
21220
+ * Reconnect to the target this device was last using, with the usual backoff.
21221
+ *
21222
+ * A resumed app can miss the transport close entirely - Android may reclaim the process - so
21223
+ * nothing would start the retry loop and the user would face a local shell behind a remote
21224
+ * workspace view. Restoring the recorded target gives that case the same loop a dropped
21225
+ * transport gets. A target recorded for another Server is left alone: it is not reachable
21226
+ * through the configured one.
21227
+ * @returns true when a retry loop was started.
21228
+ */
21229
+ async restoreLastTarget() {
21230
+ if (this.closed || this.connected !== void 0) return false;
21231
+ const record7 = await this.targetStore?.load();
21232
+ if (record7 === void 0 || record7.mode !== "remote" || record7.hostDeviceId === void 0) return false;
21233
+ if (record7.serverUrl !== void 0 && record7.serverUrl !== this.config.serverUrl) {
21234
+ this.logger.info("recorded remote target belongs to another Server; not restoring", {
21235
+ targetDeviceId: shortId(record7.hostDeviceId)
21236
+ });
21237
+ return false;
21238
+ }
21239
+ this.reconnecting = {
21240
+ targetDeviceId: record7.hostDeviceId,
21241
+ ...record7.hostName === void 0 ? {} : { targetName: record7.hostName },
21242
+ phase: "restore"
21243
+ };
21244
+ this.logger.info("restoring the remote target of the previous run", {
21245
+ targetDeviceId: shortId(record7.hostDeviceId)
21246
+ });
21247
+ void this.reconnectRemoteSession(record7.hostDeviceId);
21248
+ return true;
21249
+ }
20757
21250
  async listRemoteDirectory(targetDeviceId, path, signal) {
20758
21251
  const remote = await this.ensureConnected(targetDeviceId, signal);
20759
21252
  if (remote.features.remoteGateway) {
@@ -20808,7 +21301,7 @@ var ClientModeRuntime = class {
20808
21301
  await this.closeCodexVirtual();
20809
21302
  this.selectRemoteTarget(remote, transport);
20810
21303
  const workspaceId = workspaceRecordId(workspace.workspace);
20811
- this.pendingWorkspaceSelection = { targetDeviceId: remote.target.deviceId, workspaceId };
21304
+ this.rememberWorkspaceSelection({ targetDeviceId: remote.target.deviceId, workspaceId });
20812
21305
  this.logger.info("Remote workspace opened", { targetDeviceId: shortId(remote.target.deviceId) });
20813
21306
  return { ...this.status(), workspace };
20814
21307
  }
@@ -20842,12 +21335,12 @@ var ClientModeRuntime = class {
20842
21335
  this.codexVirtual = virtual;
20843
21336
  this.selectCodexTarget(virtual, remote);
20844
21337
  const preferredSessionId = await virtual.preferredSessionId(signal);
20845
- this.pendingWorkspaceSelection = {
21338
+ this.rememberWorkspaceSelection({
20846
21339
  targetDeviceId: remote.target.deviceId,
20847
21340
  workspaceId,
20848
21341
  backend: "codex",
20849
21342
  ...preferredSessionId === void 0 ? {} : { sessionId: preferredSessionId }
20850
- };
21343
+ });
20851
21344
  this.logger.info("CodeX virtual workspace opened", { targetDeviceId: shortId(remote.target.deviceId) });
20852
21345
  return { ...this.status(), workspace };
20853
21346
  }
@@ -20872,6 +21365,7 @@ var ClientModeRuntime = class {
20872
21365
  return this.openCodexWorkspace(targetDeviceId, codexProjectWorkspaceId(project.id), signal);
20873
21366
  }
20874
21367
  consumeWorkspaceSelection(selection) {
21368
+ this.lastWorkspaceSelection = { ...selection };
20875
21369
  const pending = this.pendingWorkspaceSelection;
20876
21370
  if (pending?.targetDeviceId === selection.targetDeviceId && pending.workspaceId === selection.workspaceId && (pending.backend ?? "harness") === (selection.backend ?? "harness")) {
20877
21371
  this.pendingWorkspaceSelection = void 0;
@@ -20882,6 +21376,7 @@ var ClientModeRuntime = class {
20882
21376
  await this.closePreview();
20883
21377
  if (this.closed) return;
20884
21378
  this.closed = true;
21379
+ this.stopLivenessWatch();
20885
21380
  this.proxySwitch?.selectLocal();
20886
21381
  await this.closePreview();
20887
21382
  this.gatewaySwitch.selectLocal();
@@ -20949,10 +21444,10 @@ var ClientModeRuntime = class {
20949
21444
  }
20950
21445
  }
20951
21446
  stream.unsubscribe = remote.client.onEvent((event) => {
20952
- if (event.event === "codex.app.frame" && isRecord8(event.data) && event.data.streamId === value.streamId) {
21447
+ if (event.event === "codex.app.frame" && isRecord9(event.data) && event.data.streamId === value.streamId) {
20953
21448
  this.appendCodexFrame(stream, event.data);
20954
21449
  }
20955
- if (event.event === "codex.app.stream.closed" && isRecord8(event.data) && event.data.streamId === value.streamId) {
21450
+ if (event.event === "codex.app.stream.closed" && isRecord9(event.data) && event.data.streamId === value.streamId) {
20956
21451
  stream.closed = typeof event.data.reason === "string" ? event.data.reason : "closed";
20957
21452
  stream.wake();
20958
21453
  }
@@ -20979,7 +21474,7 @@ var ClientModeRuntime = class {
20979
21474
  stream.wake();
20980
21475
  };
20981
21476
  appendCodexFrame(stream, data2) {
20982
- if (!isRecord8(data2) || !isRecord8(data2.frame) || typeof data2.frame.method !== "string") return;
21477
+ if (!isRecord9(data2) || !isRecord9(data2.frame) || typeof data2.frame.method !== "string") return;
20983
21478
  if (stream.frames.length >= 256) {
20984
21479
  stream.closed = "overflow";
20985
21480
  } else {
@@ -21185,21 +21680,15 @@ var ClientModeRuntime = class {
21185
21680
  connectedPreference === void 0 ? void 0 : [connectedPreference]
21186
21681
  );
21187
21682
  connectedClient.onClose(() => {
21188
- if (this.connected?.client !== connectedClient) return;
21189
- void this.closePreview();
21190
- this.connected = void 0;
21191
- this.connectionProgress = void 0;
21192
- this.pendingWorkspaceSelection = void 0;
21193
- void this.closeCodexVirtual();
21194
- this.proxySwitch?.selectLocal();
21195
- this.gatewaySwitch.selectLocal();
21196
- this.fellBackToLocal = true;
21197
- void connectedClient.close().catch(() => void 0);
21198
- this.logger.warn("remote Harness transport closed; reconnecting", {
21199
- targetDeviceId: shortId(target2.deviceId)
21200
- });
21201
- void this.reconnectRemoteSession(target2.deviceId);
21683
+ if (this.supersededClient === connectedClient) {
21684
+ this.logger.info("ignoring the close of the transport a reconnect is replacing", {
21685
+ targetDeviceId: shortId(target2.deviceId)
21686
+ });
21687
+ return;
21688
+ }
21689
+ this.handleRemoteTransportLost(connectedClient, target2.deviceId, "transport-closed");
21202
21690
  });
21691
+ this.armLivenessWatch(LIVENESS_INTERVAL_MS);
21203
21692
  const connectionDetails = await connectedTransport.connectionDetails().catch(() => void 0);
21204
21693
  this.logger.info("remote Harness transport ready", {
21205
21694
  targetDeviceId: shortId(target2.deviceId),
@@ -21270,6 +21759,7 @@ var ClientModeRuntime = class {
21270
21759
  async handleControl(endpoint, payload, signal) {
21271
21760
  try {
21272
21761
  if (endpoint === "status") return ok2(await this.detailedStatus());
21762
+ if (endpoint === "client.connection.verify") return ok2(await this.verifyRemoteConnection());
21273
21763
  if (endpoint === "devices") return ok2(await this.devices());
21274
21764
  if (endpoint === "client.account.login") {
21275
21765
  const value = record4(payload);
@@ -21483,7 +21973,7 @@ function record4(value) {
21483
21973
  }
21484
21974
  return value;
21485
21975
  }
21486
- function isRecord8(value) {
21976
+ function isRecord9(value) {
21487
21977
  return typeof value === "object" && value !== null && !Array.isArray(value);
21488
21978
  }
21489
21979
  function ok2(value) {
@@ -21508,11 +21998,11 @@ async function readRemoteWorkspaceBaseline(gateway, signal) {
21508
21998
  const iterator = source[Symbol.asyncIterator]();
21509
21999
  try {
21510
22000
  const first = await iterator.next();
21511
- if (first.done || !isRecord8(first.value) || first.value.type !== "baseline" || !isRecord8(first.value.value) || !Array.isArray(first.value.value.items)) {
22001
+ if (first.done || !isRecord9(first.value) || first.value.type !== "baseline" || !isRecord9(first.value.value) || !Array.isArray(first.value.value.items)) {
21512
22002
  throw new ClientModeError("INVALID_MESSAGE", "The remote Host returned an invalid Workspace baseline.");
21513
22003
  }
21514
22004
  return first.value.value.items.map((item) => {
21515
- if (!isRecord8(item) || typeof item.workspaceId !== "string" || typeof item.path !== "string" || typeof item.title !== "string") {
22005
+ if (!isRecord9(item) || typeof item.workspaceId !== "string" || typeof item.path !== "string" || typeof item.title !== "string") {
21516
22006
  throw new ClientModeError("INVALID_MESSAGE", "The remote Host returned an invalid Workspace row.");
21517
22007
  }
21518
22008
  return { workspaceId: item.workspaceId, path: item.path, title: item.title };
@@ -21578,7 +22068,7 @@ async function probeRemoteHostFeatures(client, clientVersion) {
21578
22068
  if (error instanceof Error && "code" in error && error.code === "METHOD_NOT_FOUND") return fallback;
21579
22069
  throw error;
21580
22070
  }
21581
- if (!isRecord8(value) || !Array.isArray(value.capabilities) || value.capabilities.some((capability) => typeof capability !== "string")) {
22071
+ if (!isRecord9(value) || !Array.isArray(value.capabilities) || value.capabilities.some((capability) => typeof capability !== "string")) {
21582
22072
  throw new ClientModeError("INVALID_MESSAGE", "The remote Host returned invalid transport capabilities.");
21583
22073
  }
21584
22074
  const capabilities = new Set(value.capabilities);
@@ -21663,6 +22153,67 @@ function iceServersForAttempt(attempt, iceServers) {
21663
22153
  return attempt === "direct" ? stunOnlyIceServers(iceServers) : iceServers;
21664
22154
  }
21665
22155
 
22156
+ // src/client-target-store.ts
22157
+ import { randomUUID as randomUUID2 } from "node:crypto";
22158
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
22159
+ import { join as join5 } from "node:path";
22160
+ var FILE_NAME = "client-target.json";
22161
+ var ClientTargetStore = class {
22162
+ constructor(directory) {
22163
+ this.directory = directory;
22164
+ }
22165
+ get file() {
22166
+ return join5(this.directory, FILE_NAME);
22167
+ }
22168
+ /** Read the record. A missing, unreadable or malformed file reads as "no record". */
22169
+ async load() {
22170
+ let raw;
22171
+ try {
22172
+ raw = await readFile3(this.file, "utf8");
22173
+ } catch {
22174
+ return void 0;
22175
+ }
22176
+ try {
22177
+ const value = JSON.parse(raw);
22178
+ if (typeof value !== "object" || value === null) return void 0;
22179
+ const record7 = value;
22180
+ if (record7.schemaVersion !== 1) return void 0;
22181
+ if (record7.mode !== "local" && record7.mode !== "remote") return void 0;
22182
+ if (record7.mode === "remote" && (typeof record7.hostDeviceId !== "string" || record7.hostDeviceId.length === 0)) {
22183
+ return void 0;
22184
+ }
22185
+ return {
22186
+ schemaVersion: 1,
22187
+ mode: record7.mode,
22188
+ ...typeof record7.serverUrl === "string" && record7.serverUrl.length > 0 ? { serverUrl: record7.serverUrl } : {},
22189
+ ...typeof record7.hostDeviceId === "string" && record7.hostDeviceId.length > 0 ? { hostDeviceId: record7.hostDeviceId } : {},
22190
+ ...typeof record7.hostName === "string" && record7.hostName.length > 0 ? { hostName: record7.hostName } : {},
22191
+ savedAt: typeof record7.savedAt === "number" ? record7.savedAt : 0
22192
+ };
22193
+ } catch {
22194
+ return void 0;
22195
+ }
22196
+ }
22197
+ /** Record the current target. Failures reach the caller so a boot can log them. */
22198
+ async save(target2) {
22199
+ const value = {
22200
+ schemaVersion: 1,
22201
+ mode: target2.mode,
22202
+ ...target2.serverUrl === void 0 ? {} : { serverUrl: target2.serverUrl },
22203
+ ...target2.hostDeviceId === void 0 ? {} : { hostDeviceId: target2.hostDeviceId },
22204
+ ...target2.hostName === void 0 ? {} : { hostName: target2.hostName },
22205
+ savedAt: Date.now()
22206
+ };
22207
+ await mkdir2(this.directory, { recursive: true, mode: 448 });
22208
+ await sweepStaleTemporaries(this.directory);
22209
+ const temporary = this.file + "." + String(process.pid) + "." + randomUUID2() + ".tmp";
22210
+ await writeFile2(temporary, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", mode: 384, flag: "wx" });
22211
+ await chmod2(temporary, 384);
22212
+ await replaceFile(temporary, this.file);
22213
+ await chmod2(this.file, 384);
22214
+ }
22215
+ };
22216
+
21666
22217
  // src/control-runtime.ts
21667
22218
  import { hostname as hostname2 } from "node:os";
21668
22219
  import { existsSync as existsSync3 } from "node:fs";
@@ -21670,9 +22221,9 @@ import { execFileSync } from "node:child_process";
21670
22221
 
21671
22222
  // src/identity-store.ts
21672
22223
  import { createHash } from "node:crypto";
21673
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, rm as rm3, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
22224
+ import { chmod as chmod3, mkdir as mkdir3, readFile as readFile4, rm as rm3, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
21674
22225
  import { homedir } from "node:os";
21675
- import { dirname as dirname4, join as join5 } from "node:path";
22226
+ import { dirname as dirname4, join as join6 } from "node:path";
21676
22227
  var identitySchema = external_exports.object({
21677
22228
  schemaVersion: external_exports.literal(1),
21678
22229
  deviceId: external_exports.string().uuid(),
@@ -21701,14 +22252,14 @@ var IdentityStore = class {
21701
22252
  peers = /* @__PURE__ */ new Map();
21702
22253
  constructor(options = {}) {
21703
22254
  const env = options.env ?? process.env;
21704
- const dshHome = env.DSH_HOME || join5(options.homeDirectory ?? homedir(), ".dsh");
21705
- this.directory = options.directory ?? join5(dshHome, "remote");
22255
+ const dshHome = env.DSH_HOME || join6(options.homeDirectory ?? homedir(), ".dsh");
22256
+ this.directory = options.directory ?? join6(dshHome, "remote");
21706
22257
  }
21707
22258
  async loadOrCreate(deviceName) {
21708
- await mkdir2(this.directory, { recursive: true, mode: 448 });
21709
- await chmod2(this.directory, 448);
21710
- const devicePath = join5(this.directory, "device.json");
21711
- const keyPath = join5(this.directory, "device.key");
22259
+ await mkdir3(this.directory, { recursive: true, mode: 448 });
22260
+ await chmod3(this.directory, 448);
22261
+ const devicePath = join6(this.directory, "device.json");
22262
+ const keyPath = join6(this.directory, "device.key");
21712
22263
  const [hasDevice, hasKey] = await Promise.all([exists2(devicePath), exists2(keyPath)]);
21713
22264
  if (hasDevice !== hasKey) {
21714
22265
  throw new IdentityInvalidError("device identity is incomplete; repair it explicitly before reconnecting");
@@ -21722,8 +22273,8 @@ var IdentityStore = class {
21722
22273
  }
21723
22274
  await assertPrivateMode2(keyPath);
21724
22275
  try {
21725
- let record7 = identitySchema.parse(JSON.parse(await readFile3(devicePath, "utf8")));
21726
- const privateKey = (await readFile3(keyPath, "utf8")).trim();
22276
+ let record7 = identitySchema.parse(JSON.parse(await readFile4(devicePath, "utf8")));
22277
+ const privateKey = (await readFile4(keyPath, "utf8")).trim();
21727
22278
  const regenerated = generateKeyPair(fromBase64Url2(privateKey));
21728
22279
  if (regenerated.publicKey !== record7.publicKey) {
21729
22280
  throw new IdentityInvalidError("device public and private keys do not match");
@@ -21777,11 +22328,11 @@ var IdentityStore = class {
21777
22328
  return removed;
21778
22329
  }
21779
22330
  async loadPeers() {
21780
- const path = join5(this.directory, "trusted-peers.json");
22331
+ const path = join6(this.directory, "trusted-peers.json");
21781
22332
  if (!await exists2(path)) {
21782
22333
  await atomicJsonWrite(path, { schemaVersion: 1, peers: [] }, 384);
21783
22334
  }
21784
- const parsed = trustedPeersSchema.parse(JSON.parse(await readFile3(path, "utf8")));
22335
+ const parsed = trustedPeersSchema.parse(JSON.parse(await readFile4(path, "utf8")));
21785
22336
  const peers = /* @__PURE__ */ new Map();
21786
22337
  for (const peer of parsed.peers) {
21787
22338
  if (peer.fingerprint !== fingerprint(peer.publicKey)) {
@@ -21793,7 +22344,7 @@ var IdentityStore = class {
21793
22344
  this.peers = peers;
21794
22345
  }
21795
22346
  async savePeers() {
21796
- await atomicJsonWrite(join5(this.directory, "trusted-peers.json"), {
22347
+ await atomicJsonWrite(join6(this.directory, "trusted-peers.json"), {
21797
22348
  schemaVersion: 1,
21798
22349
  peers: [...this.peers.values()]
21799
22350
  }, 384);
@@ -21802,7 +22353,7 @@ var IdentityStore = class {
21802
22353
  function serverStorageDirectory(root, serverUrl, role) {
21803
22354
  const origin = new URL(serverUrl).origin;
21804
22355
  const scope = createHash("sha256").update(origin).digest("hex").slice(0, 24);
21805
- return join5(root, "servers", scope, role);
22356
+ return join6(root, "servers", scope, role);
21806
22357
  }
21807
22358
  function fingerprint(publicKey) {
21808
22359
  const compact = createHash("sha256").update(fromBase64Url2(publicKey)).digest("hex").slice(0, 12).toUpperCase();
@@ -21821,13 +22372,13 @@ async function atomicJsonWrite(path, value, mode) {
21821
22372
  }
21822
22373
  async function atomicTextWrite(path, value, mode) {
21823
22374
  const directory = dirname4(path);
21824
- await mkdir2(directory, { recursive: true, mode: 448 });
22375
+ await mkdir3(directory, { recursive: true, mode: 448 });
21825
22376
  await sweepStaleTemporaries(directory);
21826
22377
  const temporary = `${path}.${process.pid}.${uuidV7()}.tmp`;
21827
- await writeFile2(temporary, value, { encoding: "utf8", mode, flag: "wx" });
21828
- await chmod2(temporary, mode);
22378
+ await writeFile3(temporary, value, { encoding: "utf8", mode, flag: "wx" });
22379
+ await chmod3(temporary, mode);
21829
22380
  await replaceFile(temporary, path);
21830
- await chmod2(path, mode);
22381
+ await chmod3(path, mode);
21831
22382
  }
21832
22383
  async function exists2(path) {
21833
22384
  try {
@@ -21989,11 +22540,11 @@ var ControlStatusStream = class {
21989
22540
  };
21990
22541
 
21991
22542
  // src/codex/domain.ts
21992
- import { randomUUID as randomUUID2 } from "node:crypto";
22543
+ import { randomUUID as randomUUID3 } from "node:crypto";
21993
22544
  import { accessSync, constants, existsSync as existsSync2, readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
21994
22545
  import { readdir as readdir2, realpath, stat as stat4 } from "node:fs/promises";
21995
22546
  import { homedir as homedir2 } from "node:os";
21996
- import { basename as basename2, isAbsolute as isAbsolute2, join as join6, relative, resolve } from "node:path";
22547
+ import { basename as basename2, isAbsolute as isAbsolute2, join as join7, posix, relative, resolve } from "node:path";
21997
22548
 
21998
22549
  // src/codex/app-server.ts
21999
22550
  import { spawn as spawn2 } from "node:child_process";
@@ -22179,7 +22730,7 @@ var CodexAppServerClient = class {
22179
22730
  this.handleProcessFailure("CODEX_INVALID_RESPONSE", new Error("Codex App Server emitted invalid JSON."));
22180
22731
  return;
22181
22732
  }
22182
- if (!isRecord9(value)) {
22733
+ if (!isRecord10(value)) {
22183
22734
  this.handleProcessFailure("CODEX_INVALID_RESPONSE", new Error("Codex App Server emitted an invalid message."));
22184
22735
  return;
22185
22736
  }
@@ -22224,12 +22775,12 @@ var CodexAppServerClient = class {
22224
22775
  }
22225
22776
  };
22226
22777
  function safeUpstreamError(value) {
22227
- if (!isRecord9(value) || typeof value.message !== "string") return "Codex App Server rejected the request.";
22778
+ if (!isRecord10(value) || typeof value.message !== "string") return "Codex App Server rejected the request.";
22228
22779
  const message = value.message.toLowerCase();
22229
22780
  if (message.includes("active writer")) return "Codex thread already has an active writer.";
22230
22781
  return message.includes("not initialized") ? "Codex App Server is not initialized." : "Codex App Server rejected the request.";
22231
22782
  }
22232
- function isRecord9(value) {
22783
+ function isRecord10(value) {
22233
22784
  return typeof value === "object" && value !== null && !Array.isArray(value);
22234
22785
  }
22235
22786
 
@@ -22489,15 +23040,15 @@ function decodeCanonicalBase64(value) {
22489
23040
  }
22490
23041
  return decoded;
22491
23042
  }
22492
- function isRecord10(value) {
23043
+ function isRecord11(value) {
22493
23044
  return typeof value === "object" && value !== null && !Array.isArray(value);
22494
23045
  }
22495
23046
  function safeErrorCode2(error) {
22496
- if (isRecord10(error) && typeof error.code === "string") return error.code;
23047
+ if (isRecord11(error) && typeof error.code === "string") return error.code;
22497
23048
  return "UNKNOWN";
22498
23049
  }
22499
23050
  function safeMethod(input2) {
22500
- return isRecord10(input2) && typeof input2.method === "string" ? input2.method : "invalid";
23051
+ return isRecord11(input2) && typeof input2.method === "string" ? input2.method : "invalid";
22501
23052
  }
22502
23053
  function concatChunks(chunks, totalBytes) {
22503
23054
  const output = new Uint8Array(totalBytes);
@@ -22512,6 +23063,7 @@ function concatChunks(chunks, totalBytes) {
22512
23063
  // src/codex/domain.ts
22513
23064
  var APPROVAL_TTL_MS = 5 * 6e4;
22514
23065
  var DEFAULT_RESTART_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
23066
+ var CODEX_START_BUDGET_MS = 5e3;
22515
23067
  var CODEX_PAGE_LIMIT2 = 100;
22516
23068
  var MAX_CODEX_PAGES2 = 32;
22517
23069
  var CODEX_HISTORY_PAGE_LIMIT = 25;
@@ -22544,7 +23096,7 @@ var CodexRemoteDomain = class {
22544
23096
  if (!this.config.enabled) return;
22545
23097
  try {
22546
23098
  this.state = "starting";
22547
- await this.launchAppServer();
23099
+ await this.launchWithinBudget();
22548
23100
  } catch (error) {
22549
23101
  this.available = false;
22550
23102
  this.state = "unavailable";
@@ -22775,6 +23327,32 @@ var CodexRemoteDomain = class {
22775
23327
  await this.appServer?.close();
22776
23328
  this.appServer = void 0;
22777
23329
  }
23330
+ /**
23331
+ * Launch the App Server within {@link CODEX_START_BUDGET_MS}.
23332
+ *
23333
+ * The losing side of the race keeps running until the disposal in start()'s catch stops it, so
23334
+ * it needs its own handler: an unhandled rejection here would surface as a process-level error
23335
+ * for what is only an optional feature being unavailable.
23336
+ * @returns nothing once a candidate is ready.
23337
+ */
23338
+ async launchWithinBudget() {
23339
+ let timer;
23340
+ const attempt = this.launchAppServer();
23341
+ attempt.catch(() => void 0);
23342
+ try {
23343
+ await Promise.race([
23344
+ attempt,
23345
+ new Promise((_resolve, reject) => {
23346
+ timer = setTimeout(() => {
23347
+ reject(new RpcError("CODEX_START_TIMEOUT", `Codex App Server did not start within ${CODEX_START_BUDGET_MS}ms.`));
23348
+ }, CODEX_START_BUDGET_MS);
23349
+ timer.unref?.();
23350
+ })
23351
+ ]);
23352
+ } finally {
23353
+ if (timer !== void 0) clearTimeout(timer);
23354
+ }
23355
+ }
22778
23356
  async launchAppServer() {
22779
23357
  let lastError;
22780
23358
  for (const binary of codexBinaryCandidates(this.config.binary)) {
@@ -22927,14 +23505,14 @@ var CodexRemoteDomain = class {
22927
23505
  await this.assertResultThreadAllowed(result);
22928
23506
  if (codexPermissionPresetFromResponse(result) !== params.permissionPreset) {
22929
23507
  await this.callUpstream("thread/settings/update", { threadId, ...settings });
22930
- result = { ...isRecord11(result) ? result : {}, ...settings, sandbox: settings.sandboxPolicy };
23508
+ result = { ...isRecord12(result) ? result : {}, ...settings, sandbox: settings.sandboxPolicy };
22931
23509
  }
22932
23510
  }
22933
23511
  await this.publishPermission(threadId, result);
22934
23512
  return result;
22935
23513
  }
22936
23514
  async publishPermission(threadId, value) {
22937
- const source = isRecord11(value) ? value : {};
23515
+ const source = isRecord12(value) ? value : {};
22938
23516
  const threadSettings = { approvalPolicy: source.approvalPolicy, sandboxPolicy: source.sandboxPolicy ?? source.sandbox };
22939
23517
  await this.handleInbound({ kind: "notification", method: "thread/settings/updated", params: { threadId, threadSettings } });
22940
23518
  }
@@ -22952,7 +23530,7 @@ var CodexRemoteDomain = class {
22952
23530
  await appServer.respond(message.id, { decision: "decline" });
22953
23531
  return;
22954
23532
  }
22955
- const requestHandle = randomUUID2();
23533
+ const requestHandle = randomUUID3();
22956
23534
  this.approvals.set(requestHandle, {
22957
23535
  upstreamId: message.id,
22958
23536
  connectionId: owner.connectionId,
@@ -23030,9 +23608,9 @@ var CodexRemoteDomain = class {
23030
23608
  itemsView,
23031
23609
  ...cursor2 === void 0 ? {} : { cursor: cursor2 }
23032
23610
  });
23033
- const pageResult = isRecord11(result) ? result : {};
23611
+ const pageResult = isRecord12(result) ? result : {};
23034
23612
  for (const rawTurn of array2(pageResult.data)) {
23035
- if (!isRecord11(rawTurn)) continue;
23613
+ if (!isRecord12(rawTurn)) continue;
23036
23614
  const turnId = typeof rawTurn.id === "string" ? rawTurn.id : void 0;
23037
23615
  const items = rawTurn.itemsView === "full" || turnId === void 0 ? array2(rawTurn.items) : await this.readThreadItems(connectionId, threadId, turnId, array2(rawTurn.items));
23038
23616
  turns.push({ ...rawTurn, items });
@@ -23054,9 +23632,9 @@ var CodexRemoteDomain = class {
23054
23632
  sortDirection: "asc",
23055
23633
  ...cursor2 === void 0 ? {} : { cursor: cursor2 }
23056
23634
  });
23057
- const pageResult = isRecord11(result) ? result : {};
23635
+ const pageResult = isRecord12(result) ? result : {};
23058
23636
  for (const entry of array2(pageResult.data)) {
23059
- if (isRecord11(entry) && entry.item !== void 0) items.push(entry.item);
23637
+ if (isRecord12(entry) && entry.item !== void 0) items.push(entry.item);
23060
23638
  }
23061
23639
  cursor2 = typeof pageResult.nextCursor === "string" && pageResult.nextCursor.length > 0 ? pageResult.nextCursor : void 0;
23062
23640
  if (cursor2 === void 0) break;
@@ -23120,7 +23698,7 @@ var CodexRemoteDomain = class {
23120
23698
  return [...this.peers.values()].some((peer) => peer.hasThreadSubscription(threadId));
23121
23699
  }
23122
23700
  resolveUpstreamApproval(params) {
23123
- if (!isRecord11(params) || typeof params.requestId !== "string" && typeof params.requestId !== "number") return;
23701
+ if (!isRecord12(params) || typeof params.requestId !== "string" && typeof params.requestId !== "number") return;
23124
23702
  for (const [handle, approval] of this.approvals) {
23125
23703
  if (approval.upstreamId === params.requestId) this.approvals.delete(handle);
23126
23704
  }
@@ -23334,15 +23912,15 @@ function codexBinaryCandidates(configured, hostPlatform = process.platform, user
23334
23912
  if (hostPlatform !== "darwin") return [configured];
23335
23913
  const bundledCandidates = [
23336
23914
  "/Applications/ChatGPT.app",
23337
- join6(userHome, "Applications", "ChatGPT.app")
23915
+ posix.join(userHome, "Applications", "ChatGPT.app")
23338
23916
  ].flatMap((chatGptApp) => {
23339
- const codexCli = join6(chatGptApp, "Contents", "Resources", "codex-cli");
23917
+ const codexCli = posix.join(chatGptApp, "Contents", "Resources", "codex-cli");
23340
23918
  try {
23341
- const manifest = JSON.parse(readFileSync(join6(codexCli, "codex-package.json"), "utf8"));
23342
- if (!isRecord11(manifest) || typeof manifest.entrypoint !== "string" || manifest.entrypoint.length === 0) {
23919
+ const manifest = JSON.parse(readFileSync(posix.join(codexCli, "codex-package.json"), "utf8"));
23920
+ if (!isRecord12(manifest) || typeof manifest.entrypoint !== "string" || manifest.entrypoint.length === 0) {
23343
23921
  return [];
23344
23922
  }
23345
- const candidate = join6(codexCli, manifest.entrypoint);
23923
+ const candidate = posix.join(codexCli, manifest.entrypoint);
23346
23924
  if (!existsSync2(candidate)) return [];
23347
23925
  accessSync(candidate, constants.X_OK);
23348
23926
  return [candidate];
@@ -23353,14 +23931,14 @@ function codexBinaryCandidates(configured, hostPlatform = process.platform, user
23353
23931
  return [.../* @__PURE__ */ new Set([
23354
23932
  ...bundledCandidates,
23355
23933
  "/Applications/ChatGPT.app/Contents/Resources/codex",
23356
- join6(userHome, "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
23934
+ posix.join(userHome, "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
23357
23935
  configured
23358
23936
  ])];
23359
23937
  }
23360
23938
  function bundledWindowsCodex(userHome) {
23361
- const bin = join6(userHome, "AppData", "Local", "OpenAI", "Codex", "bin");
23939
+ const bin = join7(userHome, "AppData", "Local", "OpenAI", "Codex", "bin");
23362
23940
  try {
23363
- const newest = readdirSync2(bin, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join6(bin, entry.name, "codex.exe")).filter((candidate2) => existsSync2(candidate2)).map((candidate2) => ({ candidate: candidate2, modified: statSync2(candidate2).mtimeMs })).sort((left, right) => right.modified - left.modified);
23941
+ const newest = readdirSync2(bin, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join7(bin, entry.name, "codex.exe")).filter((candidate2) => existsSync2(candidate2)).map((candidate2) => ({ candidate: candidate2, modified: statSync2(candidate2).mtimeMs })).sort((left, right) => right.modified - left.modified);
23364
23942
  const candidate = newest[0]?.candidate;
23365
23943
  return candidate === void 0 ? [] : [candidate];
23366
23944
  } catch {
@@ -23368,24 +23946,24 @@ function bundledWindowsCodex(userHome) {
23368
23946
  }
23369
23947
  }
23370
23948
  function parseCallEnvelope(input2) {
23371
- if (!isRecord11(input2) || typeof input2.method !== "string" || !("params" in input2) || Object.keys(input2).some((key) => key !== "method" && key !== "params")) {
23949
+ if (!isRecord12(input2) || typeof input2.method !== "string" || !("params" in input2) || Object.keys(input2).some((key) => key !== "method" && key !== "params")) {
23372
23950
  throw new RpcError("INVALID_MESSAGE", "The Codex call envelope is invalid.");
23373
23951
  }
23374
23952
  return { method: input2.method, params: input2.params };
23375
23953
  }
23376
23954
  function parseRespond(input2) {
23377
- if (!isRecord11(input2) || typeof input2.requestHandle !== "string" || !["accept", "decline", "cancel"].includes(String(input2.decision)) || Object.keys(input2).some((key) => key !== "requestHandle" && key !== "decision")) {
23955
+ if (!isRecord12(input2) || typeof input2.requestHandle !== "string" || !["accept", "decline", "cancel"].includes(String(input2.decision)) || Object.keys(input2).some((key) => key !== "requestHandle" && key !== "decision")) {
23378
23956
  throw new RpcError("INVALID_MESSAGE", "The Codex approval response is invalid.");
23379
23957
  }
23380
23958
  return input2;
23381
23959
  }
23382
23960
  function accountCanRun(result) {
23383
- if (!isRecord11(result) || typeof result.requiresOpenaiAuth !== "boolean") return false;
23384
- return result.requiresOpenaiAuth === false || isRecord11(result.account);
23961
+ if (!isRecord12(result) || typeof result.requiresOpenaiAuth !== "boolean") return false;
23962
+ return result.requiresOpenaiAuth === false || isRecord12(result.account);
23385
23963
  }
23386
23964
  function sanitizeAccount(result) {
23387
- if (!isRecord11(result)) throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned invalid account state.");
23388
- const account = isRecord11(result.account) ? result.account : void 0;
23965
+ if (!isRecord12(result)) throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned invalid account state.");
23966
+ const account = isRecord12(result.account) ? result.account : void 0;
23389
23967
  return {
23390
23968
  authenticated: account !== void 0 || result.requiresOpenaiAuth === false,
23391
23969
  requiresOpenaiAuth: result.requiresOpenaiAuth === true,
@@ -23398,11 +23976,11 @@ function sanitizeAccount(result) {
23398
23976
  };
23399
23977
  }
23400
23978
  function sanitizeThreadList(result) {
23401
- if (!isRecord11(result) || !Array.isArray(result.data)) {
23979
+ if (!isRecord12(result) || !Array.isArray(result.data)) {
23402
23980
  throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid thread list.");
23403
23981
  }
23404
23982
  const data2 = result.data.flatMap((value) => {
23405
- if (!isRecord11(value) || typeof value.id !== "string") return [];
23983
+ if (!isRecord12(value) || typeof value.id !== "string") return [];
23406
23984
  return [{
23407
23985
  id: value.id,
23408
23986
  ...typeof value.sessionId === "string" ? { sessionId: value.sessionId } : {},
@@ -23414,7 +23992,7 @@ function sanitizeThreadList(result) {
23414
23992
  ...typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) ? { updatedAt: value.updatedAt } : {},
23415
23993
  ...typeof value.archived === "boolean" ? { archived: value.archived } : {},
23416
23994
  ...typeof value.isPinned === "boolean" ? { isPinned: value.isPinned } : {},
23417
- ...isRecord11(value.status) ? { status: value.status } : {}
23995
+ ...isRecord12(value.status) ? { status: value.status } : {}
23418
23996
  }];
23419
23997
  });
23420
23998
  return {
@@ -23424,18 +24002,18 @@ function sanitizeThreadList(result) {
23424
24002
  };
23425
24003
  }
23426
24004
  function filterThreadListByWorkspaceAuthority(result, authority) {
23427
- if (!isRecord11(result) || !Array.isArray(result.data)) {
24005
+ if (!isRecord12(result) || !Array.isArray(result.data)) {
23428
24006
  throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid thread list.");
23429
24007
  }
23430
24008
  return {
23431
24009
  ...result,
23432
- data: result.data.map((record7) => isRecord11(record7) ? record7 : void 0).filter((thread) => thread !== void 0 && isThreadAllowedByWorkspaceAuthority(thread, authority))
24010
+ data: result.data.map((record7) => isRecord12(record7) ? record7 : void 0).filter((thread) => thread !== void 0 && isThreadAllowedByWorkspaceAuthority(thread, authority))
23433
24011
  };
23434
24012
  }
23435
24013
  function sanitizeProject(value) {
23436
- if (!isRecord11(value) || typeof value.id !== "string" || typeof value.name !== "string") return void 0;
24014
+ if (!isRecord12(value) || typeof value.id !== "string" || typeof value.name !== "string") return void 0;
23437
24015
  const roots = Array.isArray(value.roots) ? value.roots.flatMap((root) => {
23438
- const path = isRecord11(root) && typeof root.path === "string" && root.path.length > 0 ? root.path : void 0;
24016
+ const path = isRecord12(root) && typeof root.path === "string" && root.path.length > 0 ? root.path : void 0;
23439
24017
  return path === void 0 || !isAbsolute2(path) ? [] : [{ path }];
23440
24018
  }) : [];
23441
24019
  if (roots.length === 0) return void 0;
@@ -23449,7 +24027,7 @@ function sanitizeProject(value) {
23449
24027
  };
23450
24028
  }
23451
24029
  function sanitizeProjectList(result) {
23452
- if (!isRecord11(result) || !Array.isArray(result.data)) {
24030
+ if (!isRecord12(result) || !Array.isArray(result.data)) {
23453
24031
  throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid project list.");
23454
24032
  }
23455
24033
  const data2 = result.data.flatMap((value) => sanitizeProject(value) ?? []);
@@ -23459,7 +24037,7 @@ function sanitizeProjectList(result) {
23459
24037
  };
23460
24038
  }
23461
24039
  function sanitizeProjectCreate(result) {
23462
- const project = isRecord11(result) ? sanitizeProject(result.project) : void 0;
24040
+ const project = isRecord12(result) ? sanitizeProject(result.project) : void 0;
23463
24041
  if (project === void 0) {
23464
24042
  throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid created project.");
23465
24043
  }
@@ -23492,19 +24070,19 @@ function codexDirectoryCrumbs(root, path) {
23492
24070
  return crumbs2;
23493
24071
  }
23494
24072
  function extractThread(result) {
23495
- return isRecord11(result) && isRecord11(result.thread) ? result.thread : void 0;
24073
+ return isRecord12(result) && isRecord12(result.thread) ? result.thread : void 0;
23496
24074
  }
23497
24075
  function extractTurnId(result) {
23498
- if (!isRecord11(result)) return void 0;
24076
+ if (!isRecord12(result)) return void 0;
23499
24077
  if (typeof result.turnId === "string" && result.turnId.length > 0) return result.turnId;
23500
- if (isRecord11(result.turn) && typeof result.turn.id === "string" && result.turn.id.length > 0) return result.turn.id;
24078
+ if (isRecord12(result.turn) && typeof result.turn.id === "string" && result.turn.id.length > 0) return result.turn.id;
23501
24079
  return void 0;
23502
24080
  }
23503
24081
  function extractThreadId(params) {
23504
- if (!isRecord11(params)) return void 0;
24082
+ if (!isRecord12(params)) return void 0;
23505
24083
  if (typeof params.threadId === "string") return params.threadId;
23506
- if (isRecord11(params.thread) && typeof params.thread.id === "string") return params.thread.id;
23507
- if (isRecord11(params.turn) && typeof params.turn.threadId === "string") return params.turn.threadId;
24084
+ if (isRecord12(params.thread) && typeof params.thread.id === "string") return params.thread.id;
24085
+ if (isRecord12(params.turn) && typeof params.turn.threadId === "string") return params.turn.threadId;
23508
24086
  return void 0;
23509
24087
  }
23510
24088
  function optionalInteger2(value) {
@@ -23546,13 +24124,13 @@ function mapCodexImageInputs(params) {
23546
24124
  return {
23547
24125
  ...params,
23548
24126
  input: params.input.map((value) => {
23549
- if (!isRecord11(value) || value.type !== "image" || typeof value.mediaType !== "string" || typeof value.data !== "string") return value;
24127
+ if (!isRecord12(value) || value.type !== "image" || typeof value.mediaType !== "string" || typeof value.data !== "string") return value;
23550
24128
  return { type: "image", url: `data:${value.mediaType};base64,${value.data}` };
23551
24129
  })
23552
24130
  };
23553
24131
  }
23554
24132
  function sanitizeApprovalParams(params, requestHandle) {
23555
- if (!isRecord11(params)) return { requestHandle };
24133
+ if (!isRecord12(params)) return { requestHandle };
23556
24134
  const safe = { ...params };
23557
24135
  delete safe.proposedExecpolicyAmendment;
23558
24136
  delete safe.additionalPermissions;
@@ -23583,7 +24161,7 @@ function isProjectListFallbackError(error) {
23583
24161
  function canTryNextBinary(error) {
23584
24162
  return !(error instanceof RpcError) || !["CODEX_AUTH_REQUIRED", "CODEX_CLOSED"].includes(error.code);
23585
24163
  }
23586
- function isRecord11(value) {
24164
+ function isRecord12(value) {
23587
24165
  return typeof value === "object" && value !== null && !Array.isArray(value);
23588
24166
  }
23589
24167
  function array2(value) {
@@ -24019,11 +24597,11 @@ function editableConfig(config) {
24019
24597
  ...config.acp === void 0 ? {} : { acp: { enabled: config.acp.enabled, backends: config.acp.backends } }
24020
24598
  };
24021
24599
  }
24022
- function isRecord12(value) {
24600
+ function isRecord13(value) {
24023
24601
  return typeof value === "object" && value !== null && !Array.isArray(value);
24024
24602
  }
24025
24603
  function record5(value) {
24026
- if (!isRecord12(value)) throw new ClientModeError("INVALID_MESSAGE", "The control request payload is invalid.");
24604
+ if (!isRecord13(value)) throw new ClientModeError("INVALID_MESSAGE", "The control request payload is invalid.");
24027
24605
  return value;
24028
24606
  }
24029
24607
  function ok3(value) {
@@ -24148,7 +24726,7 @@ var TerminalPolicy = class {
24148
24726
  };
24149
24727
 
24150
24728
  // src/service.ts
24151
- import { randomUUID as randomUUID3 } from "node:crypto";
24729
+ import { randomUUID as randomUUID4 } from "node:crypto";
24152
24730
 
24153
24731
  // src/connection-controller.ts
24154
24732
  var ConnectionController = class {
@@ -25869,7 +26447,15 @@ var HarnessApiBridge = class {
25869
26447
  mux;
25870
26448
  host;
25871
26449
  answer;
26450
+ /** Answer one ApiProxy call, sending bytes in the form DSH's own connection layer expects. */
25872
26451
  async call(input2) {
26452
+ const response = await this.dispatchCall(input2);
26453
+ const result = response.result;
26454
+ if (result === void 0) return response;
26455
+ const tagged = collectRpcAttachments(result);
26456
+ return tagged === result ? response : { ...response, result: tagged };
26457
+ }
26458
+ async dispatchCall(input2) {
25873
26459
  const params = callSchema2.parse(input2);
25874
26460
  const signal = AbortSignal.timeout(NATIVE_CALL_TIMEOUT_MS);
25875
26461
  const method = this.methods.get(params.method);
@@ -26580,7 +27166,17 @@ var HarnessRemoteBridge = class {
26580
27166
  streams = /* @__PURE__ */ new Map();
26581
27167
  incomingTransfers = /* @__PURE__ */ new Map();
26582
27168
  outgoingTransfers = /* @__PURE__ */ new Map();
27169
+ /**
27170
+ * Answer one remote Gateway call.
27171
+ *
27172
+ * Bytes have to leave in DSH's own form: this result is JSON-encoded on its way to the client, and a
27173
+ * \`Uint8Array\` would arrive there as \`{"0":…}\` and fail the generated schema (the image-preview
27174
+ * failure). Tagging here, hydrating on the client, keeps one convention on both sides.
27175
+ */
26583
27176
  async call(input2) {
27177
+ return collectRpcAttachments(await this.dispatchCall(input2));
27178
+ }
27179
+ async dispatchCall(input2) {
26584
27180
  const params = callSchema3.parse(input2);
26585
27181
  this.assertAllowed(params.endpoint);
26586
27182
  if (TERMINAL_STREAMS.has(params.endpoint)) throw new RpcError("METHOD_NOT_ALLOWED", "Use a stream for this terminal endpoint.");
@@ -26800,7 +27396,7 @@ var HarnessRemoteBridge = class {
26800
27396
  await this.publish("harness.remote.frame", {
26801
27397
  streamId,
26802
27398
  hasValue: true,
26803
- ...value === void 0 ? {} : { value }
27399
+ ...value === void 0 ? {} : { value: collectRpcAttachments(value) }
26804
27400
  });
26805
27401
  }
26806
27402
  if (signal.aborted) reason = "cancelled";
@@ -26824,11 +27420,11 @@ var HarnessRemoteBridge = class {
26824
27420
  };
26825
27421
  function normalizeWorkspaceChangesPayload(endpoint, payload, harnessVersion) {
26826
27422
  if (endpoint !== "workspaceFiles/changes" || !requiresWorkspaceChangePath(harnessVersion)) return payload;
26827
- if (!isRecord13(payload) || !isRecord13(payload.args) || Object.hasOwn(payload.args, "path")) return payload;
27423
+ if (!isRecord14(payload) || !isRecord14(payload.args) || Object.hasOwn(payload.args, "path")) return payload;
26828
27424
  return { ...payload, args: { ...payload.args, path: "." } };
26829
27425
  }
26830
27426
  function normalizeWorkspaceRequestPayload(endpoint, payload, harnessVersion) {
26831
- if (!isRecord13(payload) || !isRecord13(payload.args)) return payload;
27427
+ if (!isRecord14(payload) || !isRecord14(payload.args)) return payload;
26832
27428
  const args = payload.args;
26833
27429
  if (endpoint === "workspaceFiles/readBytes" && requiresWorkspaceChangePath(harnessVersion)) {
26834
27430
  if (!Object.hasOwn(args, "range") || Object.hasOwn(args, "options")) return payload;
@@ -26896,13 +27492,13 @@ function needsDirectoryFallback(result) {
26896
27492
  }
26897
27493
  function requestArgs(payload) {
26898
27494
  const root = record6(payload);
26899
- const args = isRecord13(root.args) ? root.args : root;
27495
+ const args = isRecord14(root.args) ? root.args : root;
26900
27496
  return record6(args.request ?? args._request ?? args);
26901
27497
  }
26902
27498
  function record6(value) {
26903
- return isRecord13(value) ? value : {};
27499
+ return isRecord14(value) ? value : {};
26904
27500
  }
26905
- function isRecord13(value) {
27501
+ function isRecord14(value) {
26906
27502
  return typeof value === "object" && value !== null && !Array.isArray(value);
26907
27503
  }
26908
27504
 
@@ -27022,8 +27618,8 @@ import { execFileSync as execFileSync2 } from "node:child_process";
27022
27618
 
27023
27619
  // src/codex-workspace-bridge.ts
27024
27620
  import { spawn as spawn4 } from "node:child_process";
27025
- import { realpath as realpath2, lstat, readdir as readdir4, readFile as readFile4, stat as stat6, watch } from "node:fs/promises";
27026
- import { isAbsolute as isAbsolute4, join as join7, relative as relative2, resolve as resolve3 } from "node:path";
27621
+ import { realpath as realpath2, lstat, readdir as readdir4, readFile as readFile5, stat as stat6, watch } from "node:fs/promises";
27622
+ import { isAbsolute as isAbsolute4, join as join8, relative as relative2, resolve as resolve3 } from "node:path";
27027
27623
  var MAX_READ_BYTES = 4 * 1024 * 1024;
27028
27624
  var MAX_INPUT_BYTES = 64 * 1024;
27029
27625
  var MAX_COLS = 240;
@@ -27113,7 +27709,7 @@ var CodexWorkspaceBridge = class {
27113
27709
  const entries = await readdir4(target2, { withFileTypes: true });
27114
27710
  const result = [];
27115
27711
  for (const entry of entries.slice(0, 500)) {
27116
- const item = join7(target2, entry.name);
27712
+ const item = join8(target2, entry.name);
27117
27713
  const info2 = await lstat(item);
27118
27714
  if (info2.isSymbolicLink()) continue;
27119
27715
  result.push({ name: entry.name, type: info2.isDirectory() ? "directory" : info2.isFile() ? "file" : "other", ...info2.isFile() ? { size: info2.size } : {} });
@@ -27129,7 +27725,7 @@ var CodexWorkspaceBridge = class {
27129
27725
  if (!info.isFile()) throw new RpcError("CODEX_WORKSPACE_INVALID_PATH", "The requested workspace path is not a file.");
27130
27726
  const offset = readOffset(args);
27131
27727
  const limit = readLimit(args);
27132
- const bytes = await readFile4(target2);
27728
+ const bytes = await readFile5(target2);
27133
27729
  if (bytes.byteLength > MAX_READ_BYTES) throw new RpcError("CODEX_WORKSPACE_TOO_LARGE", "The requested workspace file is too large.");
27134
27730
  if (endpoint === "workspaceFiles/readBytes") {
27135
27731
  const slice2 = bytes.subarray(offset, Math.min(offset + limit, bytes.length));
@@ -27253,7 +27849,7 @@ var CodexWorkspaceBridge = class {
27253
27849
  throw new RpcError("METHOD_NOT_FOUND", "The requested terminal method does not exist.");
27254
27850
  }
27255
27851
  async createTerminal(sessionId, cwd, args) {
27256
- const request = isRecord14(args.request) ? args.request : {};
27852
+ const request = isRecord15(args.request) ? args.request : {};
27257
27853
  const id4 = stringId(request.id);
27258
27854
  if ([...this.terminals.values()].some((item) => item.sessionId === sessionId && item.id === id4)) throw new RpcError("REQUEST_CONFLICT", "The terminal id is already active.");
27259
27855
  if (this.terminals.size >= MAX_TERMINALS) throw new RpcError("RATE_LIMITED", "Too many remote terminals are active.", void 0, true);
@@ -27454,10 +28050,10 @@ function screenOf(terminal) {
27454
28050
  return (terminal.truncated ? "\x1Bc" : "") + terminal.screen.join("");
27455
28051
  }
27456
28052
  function argsOf(payload) {
27457
- const value = isRecord14(payload) ? payload : {};
27458
- return isRecord14(value.args) ? value.args : value;
28053
+ const value = isRecord15(payload) ? payload : {};
28054
+ return isRecord15(value.args) ? value.args : value;
27459
28055
  }
27460
- function isRecord14(value) {
28056
+ function isRecord15(value) {
27461
28057
  return typeof value === "object" && value !== null && !Array.isArray(value);
27462
28058
  }
27463
28059
  function stringId(value) {
@@ -27468,9 +28064,9 @@ function bounded(value, fallback, max) {
27468
28064
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? Math.min(value, max) : fallback;
27469
28065
  }
27470
28066
  function byteOrLineRange(args) {
27471
- const options = isRecord14(args.options) ? args.options : void 0;
27472
- if (options !== void 0 && isRecord14(options.range)) return options.range;
27473
- return isRecord14(args.range) ? args.range : args;
28067
+ const options = isRecord15(args.options) ? args.options : void 0;
28068
+ if (options !== void 0 && isRecord15(options.range)) return options.range;
28069
+ return isRecord15(args.range) ? args.range : args;
27474
28070
  }
27475
28071
  function readOffset(args) {
27476
28072
  const range = byteOrLineRange(args);
@@ -27561,6 +28157,14 @@ var HostPluginRuntime = class {
27561
28157
  * device identity, unlike clearing the authorization.
27562
28158
  */
27563
28159
  paused;
28160
+ /**
28161
+ * Whether start() has finished wiring the Server connection.
28162
+ *
28163
+ * The Codex domain is optional business that waits on an external binary, so it runs in the
28164
+ * background; without this flag a Host that is merely still starting would report itself as
28165
+ * offline and look broken to the user and to other clients.
28166
+ */
28167
+ starting = true;
27564
28168
  harnessVersion;
27565
28169
  closed = false;
27566
28170
  codex;
@@ -27588,7 +28192,6 @@ var HostPluginRuntime = class {
27588
28192
  fingerprint: this.identity.fingerprint,
27589
28193
  server: this.config.serverUrl ?? "not configured"
27590
28194
  });
27591
- await this.codex.start();
27592
28195
  if (this.serverApi !== void 0) {
27593
28196
  this.harnessVersion = await this.readHarnessVersion();
27594
28197
  this.serverApi.setHarnessVersion(this.harnessVersion);
@@ -27596,6 +28199,9 @@ var HostPluginRuntime = class {
27596
28199
  this.serverConnection = this.createServerConnection(this.identity);
27597
28200
  if (!this.paused) this.serverConnection.start();
27598
28201
  }
28202
+ this.starting = false;
28203
+ void this.codex.start().catch(() => {
28204
+ });
27599
28205
  }
27600
28206
  currentIdentity() {
27601
28207
  if (this.identity === void 0) throw new Error("remote runtime has not started");
@@ -27613,6 +28219,7 @@ var HostPluginRuntime = class {
27613
28219
  configured: this.serverApi !== void 0,
27614
28220
  online: this.serverConnection?.isOnline() ?? false,
27615
28221
  reconnecting: this.serverConnection?.isReconnecting() ?? false,
28222
+ starting: this.starting,
27616
28223
  ...this.serverConnection?.lastActivity() === void 0 ? {} : { lastActiveAt: this.serverConnection.lastActivity() },
27617
28224
  ...error === void 0 ? {} : { error },
27618
28225
  ...authorization?.account === void 0 ? {} : { account: authorization.account },
@@ -27818,7 +28425,7 @@ var HostPluginRuntime = class {
27818
28425
  let reportedVersion;
27819
28426
  let errorCode6;
27820
28427
  try {
27821
- const response = await this.apiProxy?.host.describe({ rpcId: randomUUID3(), payload: {} });
28428
+ const response = await this.apiProxy?.host.describe({ rpcId: randomUUID4(), payload: {} });
27822
28429
  if (response === void 0) throw new Error("ApiProxy is unavailable");
27823
28430
  if (!response.result.ok) {
27824
28431
  errorCode6 = response.result.error.code;
@@ -27888,7 +28495,7 @@ function isPlainRecord(value) {
27888
28495
  // src/cli.ts
27889
28496
  import { stat as stat7 } from "node:fs/promises";
27890
28497
  import { hostname as hostname3 } from "node:os";
27891
- import { join as join8 } from "node:path";
28498
+ import { join as join9 } from "node:path";
27892
28499
  var QR_POLL_INTERVAL_MS = 2e3;
27893
28500
  var TERMINAL_QR_MARGIN = 4;
27894
28501
  async function runCli(args = process.argv.slice(2), dependencies = {}) {
@@ -27976,7 +28583,7 @@ async function status(args, runtime) {
27976
28583
  `Server: ${serverUrl}`,
27977
28584
  "Host control: enabled (dsh-TUI default)"
27978
28585
  ];
27979
- if (!await exists3(join8(directory, "device.json"))) {
28586
+ if (!await exists3(join9(directory, "device.json"))) {
27980
28587
  lines.push("Device: not initialized", "Authorization: logged out", "Credential: unavailable");
27981
28588
  write(runtime.stdout, `${lines.join("\n")}
27982
28589
 
@@ -28020,7 +28627,7 @@ async function logout(args, runtime) {
28020
28627
  const serverUrl = selectedServer();
28021
28628
  const root = new IdentityStore({ env: runtime.env }).directory;
28022
28629
  const directory = serverStorageDirectory(root, serverUrl, "host");
28023
- if (!await exists3(join8(directory, "device.json"))) {
28630
+ if (!await exists3(join9(directory, "device.json"))) {
28024
28631
  await new ServerCredentialStore(directory).clear();
28025
28632
  write(runtime.stdout, "This Host is already logged out.\n");
28026
28633
  return 0;
@@ -28306,7 +28913,7 @@ function remoteStatusLines(target2) {
28306
28913
  const diagnostics = runtime.diagnostics();
28307
28914
  const codex = runtime.codexStatus();
28308
28915
  const capabilities = new Set(diagnostics.capabilities);
28309
- const connection = status2.online ? "online" : status2.reconnecting ? "reconnecting" : status2.accountRequired ? "authorization required" : "offline";
28916
+ const connection = status2.online ? "online" : status2.starting ? "starting" : status2.reconnecting ? "reconnecting" : status2.accountRequired ? "authorization required" : "offline";
28310
28917
  return [
28311
28918
  `Server: ${config.serverUrl ?? "not configured"}`,
28312
28919
  "Host control: enabled",
@@ -28537,7 +29144,7 @@ function createRemoteFileContentProvider(call, options = {}) {
28537
29144
  const offset = request.offset + received;
28538
29145
  const range = await call("fileviewer.readRange", { path: locator, offset, length }, request.signal);
28539
29146
  if (range.offset !== offset) throw new Error("The Remote Host returned a mismatched file range.");
28540
- const bytes = decodeBase64(range.data);
29147
+ const bytes = decodeBase642(range.data);
28541
29148
  if (bytes.byteLength > length) throw new Error("The Remote Host returned more file bytes than requested.");
28542
29149
  chunks.push(bytes);
28543
29150
  received += bytes.byteLength;
@@ -28569,7 +29176,7 @@ function currentSaveAsAllowed(value) {
28569
29176
  function currentSaveAsMaxBytes(value) {
28570
29177
  return typeof value === "function" ? value() : value ?? REMOTE_FILE_SAVE_AS_MAX_BYTES;
28571
29178
  }
28572
- function decodeBase64(value) {
29179
+ function decodeBase642(value) {
28573
29180
  const binary = atob(value);
28574
29181
  const bytes = new Uint8Array(binary.length);
28575
29182
  for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
@@ -28722,7 +29329,10 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
28722
29329
  apiProxy,
28723
29330
  nativeTypertGateway,
28724
29331
  logger,
28725
- hostControl
29332
+ hostControl,
29333
+ // The RTC factory keeps its default; the target store lives beside the plugin state.
29334
+ void 0,
29335
+ new ClientTargetStore(defaultIdentityDirectory)
28726
29336
  );
28727
29337
  }
28728
29338
  const controlRuntime = connection === void 0 ? void 0 : new PluginControlRuntime(config, defaultIdentityDirectory, settingsBinding, clientRuntime, hostControl, deepseekSession);
@@ -28737,6 +29347,7 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
28737
29347
  await runtime.start();
28738
29348
  if (clientRuntime !== void 0) {
28739
29349
  await clientRuntime.start();
29350
+ await clientRuntime.restoreLastTarget();
28740
29351
  } else {
28741
29352
  logger.warn("client remote mode is unavailable", {
28742
29353
  serverConfigured: config.serverUrl !== void 0,