@blue-soda/dsh-remote 0.4.29 → 0.4.30

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 (40) 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/index.d.ts.map +1 -1
  19. package/dist/index.js +662 -162
  20. package/dist/index.js.map +4 -4
  21. package/dist/remote-typert-gateway.d.ts.map +1 -1
  22. package/dist/remote-typert-gateway.js +5 -3
  23. package/dist/remote-typert-gateway.js.map +1 -1
  24. package/dist/rpc-binary-attachments.d.ts +17 -0
  25. package/dist/rpc-binary-attachments.d.ts.map +1 -0
  26. package/dist/rpc-binary-attachments.js +89 -0
  27. package/dist/rpc-binary-attachments.js.map +1 -0
  28. package/dist/service.d.ts +10 -0
  29. package/dist/service.d.ts.map +1 -1
  30. package/dist/service.js +17 -1
  31. package/dist/service.js.map +1 -1
  32. package/dist/tui-command.js +7 -5
  33. package/dist/tui-command.js.map +1 -1
  34. package/dist/typert-gateway-switch.d.ts.map +1 -1
  35. package/dist/typert-gateway-switch.js +56 -4
  36. package/dist/typert-gateway-switch.js.map +1 -1
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/dsh-plugin.json +1 -1
  40. 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
  }
@@ -15162,6 +15162,68 @@ function base64ToBytes2(value) {
15162
15162
  return bytes;
15163
15163
  }
15164
15164
 
15165
+ // src/rpc-binary-attachments.ts
15166
+ function hydrateRpcAttachments(result) {
15167
+ if (!isRecord3(result)) return result;
15168
+ const envelope = result;
15169
+ if (!Array.isArray(envelope.attachments) || envelope.attachments.length === 0) return result;
15170
+ const { attachments, ...rest } = result;
15171
+ const base = Object.hasOwn(rest, "value") ? rest.value : void 0;
15172
+ for (const attachment of attachments) {
15173
+ const entry = attachment;
15174
+ const path = Array.isArray(entry.path) ? entry.path : void 0;
15175
+ if (path === void 0 || path.length === 0) {
15176
+ throw new Error("The remote Host returned a byte attachment without a path.");
15177
+ }
15178
+ assignAtPath(base, path, decodeAttachmentBytes(entry.bytes));
15179
+ }
15180
+ return rest;
15181
+ }
15182
+ function assignAtPath(base, path, bytes) {
15183
+ if (base === null || typeof base !== "object") {
15184
+ throw new Error("The remote Host returned a byte attachment that its result cannot hold.");
15185
+ }
15186
+ let cursor2 = base;
15187
+ for (let index = 0; index < path.length - 1; index += 1) {
15188
+ const key = pathKey(path[index]);
15189
+ const next = cursor2[key];
15190
+ if (next === null || typeof next !== "object") {
15191
+ throw new Error("The remote Host returned a byte attachment at an unknown result path.");
15192
+ }
15193
+ cursor2 = next;
15194
+ }
15195
+ cursor2[pathKey(path[path.length - 1])] = bytes;
15196
+ }
15197
+ function pathKey(segment) {
15198
+ if (typeof segment === "string") return segment;
15199
+ if (typeof segment === "number" && Number.isInteger(segment) && segment >= 0) return String(segment);
15200
+ throw new Error("The remote Host returned a byte attachment with an invalid path segment.");
15201
+ }
15202
+ function decodeAttachmentBytes(value) {
15203
+ if (value instanceof Uint8Array) return value;
15204
+ if (typeof value === "string") return decodeBase64(value);
15205
+ if (Array.isArray(value)) return Uint8Array.from(value);
15206
+ if (!isRecord3(value)) throw new Error("The remote Host returned invalid byte attachment data.");
15207
+ if (Array.isArray(value.data)) {
15208
+ return Uint8Array.from(value.data);
15209
+ }
15210
+ const keys = Object.keys(value);
15211
+ if (keys.every((key) => /^\d+$/u.test(key))) {
15212
+ const ordered = keys.map(Number).sort((left, right) => left - right);
15213
+ return Uint8Array.from(ordered.map((key) => value[String(key)]));
15214
+ }
15215
+ throw new Error("The remote Host returned invalid byte attachment data.");
15216
+ }
15217
+ function decodeBase64(value) {
15218
+ const binary = atob(value);
15219
+ const bytes = new Uint8Array(binary.length);
15220
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
15221
+ return bytes;
15222
+ }
15223
+ function isRecord3(value) {
15224
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15225
+ }
15226
+
15165
15227
  // src/session-format-compat.ts
15166
15228
  function normalizeLegacySessionGatewayValue(endpoint, value) {
15167
15229
  if (endpoint === "session/page") return normalizePage(value);
@@ -15169,7 +15231,7 @@ function normalizeLegacySessionGatewayValue(endpoint, value) {
15169
15231
  return value;
15170
15232
  }
15171
15233
  function normalizeFollowFrame(value) {
15172
- if (!isRecord3(value)) return value;
15234
+ if (!isRecord4(value)) return value;
15173
15235
  if (value.type === "snapshot") {
15174
15236
  return {
15175
15237
  ...value,
@@ -15181,7 +15243,7 @@ function normalizeFollowFrame(value) {
15181
15243
  return normalizeEntry(value);
15182
15244
  }
15183
15245
  function normalizePage(value) {
15184
- if (!isRecord3(value)) return value;
15246
+ if (!isRecord4(value)) return value;
15185
15247
  return { ...value, records: normalizeRecords(value.records) };
15186
15248
  }
15187
15249
  function normalizeRecords(value) {
@@ -15226,10 +15288,10 @@ function eventTime(value) {
15226
15288
  return typeof value.event.time === "number" && Number.isSafeInteger(value.event.time) ? value.event.time : 0;
15227
15289
  }
15228
15290
  function isEventEntry(value) {
15229
- return isRecord3(value) && value.type === "event" && isRecord3(value.event);
15291
+ return isRecord4(value) && value.type === "event" && isRecord4(value.event);
15230
15292
  }
15231
15293
  function normalizeHeader(value) {
15232
- if (!isRecord3(value)) return value;
15294
+ if (!isRecord4(value)) return value;
15233
15295
  const next = {
15234
15296
  ...value,
15235
15297
  version: 3,
@@ -15238,7 +15300,7 @@ function normalizeHeader(value) {
15238
15300
  return next.agentPreset === "code" ? { ...next, agentPreset: "ptc" } : next;
15239
15301
  }
15240
15302
  function normalizeEvent(value) {
15241
- if (!isRecord3(value)) return value;
15303
+ if (!isRecord4(value)) return value;
15242
15304
  let next = value;
15243
15305
  const type = normalizeEventType(value.type);
15244
15306
  if (type !== value.type) next = { ...next, type };
@@ -15258,7 +15320,7 @@ function normalizeEventType(value) {
15258
15320
  return value;
15259
15321
  }
15260
15322
  function normalizeEventData(type, value) {
15261
- if (!isRecord3(value)) return value;
15323
+ if (!isRecord4(value)) return value;
15262
15324
  if (type === "request/header") return normalizeRequestHeaderData(value);
15263
15325
  if (type === "agent-preset/selected" && value.agentPreset === "code") return { ...value, agentPreset: "ptc" };
15264
15326
  if (type === "user/message") return normalizeMessage(value);
@@ -15271,7 +15333,7 @@ function normalizeEventData(type, value) {
15271
15333
  return value;
15272
15334
  }
15273
15335
  function normalizeRequestHeaderData(value) {
15274
- if (!isRecord3(value.header)) return value;
15336
+ if (!isRecord4(value.header)) return value;
15275
15337
  const header = normalizeRequestHeader(value.header);
15276
15338
  return header === value.header ? value : { ...value, header };
15277
15339
  }
@@ -15287,7 +15349,7 @@ function normalizeRequestHeader(value) {
15287
15349
  changed = true;
15288
15350
  continue;
15289
15351
  }
15290
- if (key === "adapterDefaults" && isRecord3(field) && Object.keys(field).length === 0) {
15352
+ if (key === "adapterDefaults" && isRecord4(field) && Object.keys(field).length === 0) {
15291
15353
  changed = true;
15292
15354
  continue;
15293
15355
  }
@@ -15296,12 +15358,12 @@ function normalizeRequestHeader(value) {
15296
15358
  return changed ? next : value;
15297
15359
  }
15298
15360
  function normalizeMessage(value) {
15299
- if (!isRecord3(value) || !isRecord3(value.source)) return value;
15361
+ if (!isRecord4(value) || !isRecord4(value.source)) return value;
15300
15362
  if (value.source.kind !== "plugin" || value.source.plugin !== "tools-code-mode") return value;
15301
15363
  return { ...value, source: { ...value.source, plugin: "tools-ptc" } };
15302
15364
  }
15303
15365
  function normalizeSurfaceOp(value) {
15304
- if (!isRecord3(value) || value.op !== "replace") return value;
15366
+ if (!isRecord4(value) || value.op !== "replace") return value;
15305
15367
  const { start, end, ...rest } = value;
15306
15368
  if (start === void 0 && end === void 0) return value;
15307
15369
  return {
@@ -15311,7 +15373,7 @@ function normalizeSurfaceOp(value) {
15311
15373
  ...end === void 0 ? {} : { endSeq: end }
15312
15374
  };
15313
15375
  }
15314
- function isRecord3(value) {
15376
+ function isRecord4(value) {
15315
15377
  return typeof value === "object" && value !== null && !Array.isArray(value);
15316
15378
  }
15317
15379
  function isEventSeq(value) {
@@ -15352,7 +15414,7 @@ var RemoteTypertGateway2 = class {
15352
15414
  response = await this.callTransferred(encoded, signal);
15353
15415
  }
15354
15416
  }
15355
- const result = parseRpcResult(response);
15417
+ const result = parseRpcResult(hydrateRpcAttachments(response));
15356
15418
  const settingsResult = this.normalizeLegacyWelcomeSettings(endpoint, payload, result);
15357
15419
  if (settingsResult !== void 0) return settingsResult;
15358
15420
  if (result.ok && this.compatibility === "legacy-to-v3") {
@@ -15365,11 +15427,11 @@ var RemoteTypertGateway2 = class {
15365
15427
  /** DSH <=0.1.6 cannot persist the 0.1.7 welcome acknowledgement remotely. */
15366
15428
  normalizeLegacyWelcomeSettings(endpoint, payload, result) {
15367
15429
  if (!isLegacyRemoteHost2(this.harnessVersion)) return void 0;
15368
- if (endpoint === "settings/describe" && result.ok && isRecord4(result.value)) {
15430
+ if (endpoint === "settings/describe" && result.ok && isRecord5(result.value)) {
15369
15431
  this.settingsDescribeValue = result.value;
15370
15432
  return this.legacyWelcomeAcknowledged ? { ...result, value: patchWelcomeDescribe2(result.value) } : void 0;
15371
15433
  }
15372
- if (endpoint !== "settings/mutate" || !isRecord4(payload) || !isRecord4(payload.args)) return void 0;
15434
+ if (endpoint !== "settings/mutate" || !isRecord5(payload) || !isRecord5(payload.args)) return void 0;
15373
15435
  const args = payload.args;
15374
15436
  if (args.ns !== WELCOME_NOTICE_NAMESPACE2 || !isWelcomeNoticeOperation(args.ops)) return void 0;
15375
15437
  const value = patchWelcomeDescribe2(this.settingsDescribeValue ?? { namespaces: [] });
@@ -15398,7 +15460,8 @@ var RemoteTypertGateway2 = class {
15398
15460
  async *iterate(streamId, endpoint, queue, unsubscribe, unsubscribeClose, signal, onAbort) {
15399
15461
  try {
15400
15462
  for await (const value of queue) {
15401
- yield this.compatibility === "legacy-to-v3" ? normalizeLegacySessionGatewayValue(endpoint, value) : value;
15463
+ const hydrated = hydrateRpcAttachments(value);
15464
+ yield this.compatibility === "legacy-to-v3" ? normalizeLegacySessionGatewayValue(endpoint, hydrated) : hydrated;
15402
15465
  }
15403
15466
  } finally {
15404
15467
  signal.removeEventListener("abort", onAbort);
@@ -15533,16 +15596,16 @@ function routeStreamEvent2(event, streamId, queue) {
15533
15596
  queue.fail(remoteFailure({
15534
15597
  code: typeof failure2?.code === "string" ? failure2.code : "internal",
15535
15598
  message: typeof failure2?.message === "string" ? failure2.message : "The remote Harness stream failed.",
15536
- details: isRecord4(failure2?.details) ? failure2.details : {}
15599
+ details: isRecord5(failure2?.details) ? failure2.details : {}
15537
15600
  }));
15538
15601
  } else {
15539
15602
  queue.close();
15540
15603
  }
15541
15604
  }
15542
15605
  function parseRpcResult(value) {
15543
- if (!isRecord4(value)) throw new Error("The remote Host returned an invalid Gateway result.");
15606
+ if (!isRecord5(value)) throw new Error("The remote Host returned an invalid Gateway result.");
15544
15607
  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)) {
15608
+ if (value.ok !== false || !isRecord5(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string" || !isRecord5(value.error.details)) {
15546
15609
  throw new Error("The remote Host returned an invalid Gateway failure.");
15547
15610
  }
15548
15611
  return {
@@ -15576,7 +15639,7 @@ function base64ToBytes3(value) {
15576
15639
  if (bytesToBase643(bytes) !== value) throw new Error("The remote Host returned non-canonical Harness Remote transfer data.");
15577
15640
  return bytes;
15578
15641
  }
15579
- function isRecord4(value) {
15642
+ function isRecord5(value) {
15580
15643
  return typeof value === "object" && value !== null && !Array.isArray(value);
15581
15644
  }
15582
15645
  function hasErrorCode(error, code) {
@@ -15589,13 +15652,13 @@ function isLegacyRemoteHost2(version) {
15589
15652
  return Number(match[1]) === 0 && Number(match[2]) === 1 && Number(match[3]) < 7;
15590
15653
  }
15591
15654
  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);
15655
+ 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
15656
  }
15594
15657
  function patchWelcomeDescribe2(value) {
15595
15658
  if (!Array.isArray(value.namespaces)) return value;
15596
15659
  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 : {};
15660
+ if (!isRecord5(namespace) || namespace.ns !== WELCOME_NOTICE_NAMESPACE2) return namespace;
15661
+ const current = isRecord5(namespace.value) ? namespace.value : {};
15599
15662
  return { ...namespace, value: { ...current, [WELCOME_NOTICE_FIELD2]: WELCOME_NOTICE_VERSION2 } };
15600
15663
  });
15601
15664
  return { ...value, namespaces };
@@ -15757,11 +15820,11 @@ var DATA_IMAGE_URL2 = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/
15757
15820
  function string(value) {
15758
15821
  return typeof value === "string" ? value : void 0;
15759
15822
  }
15760
- function isRecord5(value) {
15823
+ function isRecord6(value) {
15761
15824
  return typeof value === "object" && value !== null && !Array.isArray(value);
15762
15825
  }
15763
15826
  function record2(value) {
15764
- return isRecord5(value) ? value : {};
15827
+ return isRecord6(value) ? value : {};
15765
15828
  }
15766
15829
  function toolOutputImageBlocks(item) {
15767
15830
  const candidates = [
@@ -15851,7 +15914,7 @@ function collectImageBlocks(value, output = []) {
15851
15914
  for (const item of value) collectImageBlocks(item, output);
15852
15915
  return output;
15853
15916
  }
15854
- if (!isRecord5(value)) return output;
15917
+ if (!isRecord6(value)) return output;
15855
15918
  if (value.type === "image") output.push(value);
15856
15919
  for (const [key, child] of Object.entries(value)) {
15857
15920
  if (key === "attachment") continue;
@@ -16932,7 +16995,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
16932
16995
  this.broadcastRcHost({ type: "host/session-status", sessionId, running: args[1] });
16933
16996
  } else if (event === "api-session/removed" && sessionId !== void 0) {
16934
16997
  this.broadcastRcHost({ type: "host/session-removed", sessionId });
16935
- } else if (event === "api-session/added" && isRecord6(args[0])) {
16998
+ } else if (event === "api-session/added" && isRecord7(args[0])) {
16936
16999
  const summary = args[0];
16937
17000
  this.broadcastRcHost({
16938
17001
  type: "host/session-added",
@@ -17322,7 +17385,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
17322
17385
  }
17323
17386
  createApiProxy() {
17324
17387
  const call = (endpoint) => async (request, signal) => {
17325
- const payload = endpoint === "session.prompt" && isRecord6(request.payload) ? { ...request.payload, requestId: String(request.rpcId) } : request.payload;
17388
+ const payload = endpoint === "session.prompt" && isRecord7(request.payload) ? { ...request.payload, requestId: String(request.rpcId) } : request.payload;
17326
17389
  const result = await this.dispatch(rcEndpoint(endpoint), { args: { request: payload } }, signal ?? new AbortController().signal);
17327
17390
  return {
17328
17391
  rpcId: request.rpcId,
@@ -17977,7 +18040,7 @@ function codexPromptInput(content) {
17977
18040
  if (content.length === 0 || content.length > MAX_CODEX_PROMPT_PARTS) return void 0;
17978
18041
  const input2 = [];
17979
18042
  for (const value of content) {
17980
- if (!isRecord6(value)) return void 0;
18043
+ if (!isRecord7(value)) return void 0;
17981
18044
  if (value.type === "text") {
17982
18045
  if (typeof value.text !== "string" || value.text.length === 0 || value.text.length > MAX_CODEX_PROMPT_TEXT) return void 0;
17983
18046
  input2.push({ type: "text", text: value.text });
@@ -18227,12 +18290,12 @@ function errorCode(error) {
18227
18290
  return "code" in error && typeof error.code === "string" ? error.code : void 0;
18228
18291
  }
18229
18292
  function errorDetails(error) {
18230
- return "details" in error && isRecord6(error.details) ? error.details : {};
18293
+ return "details" in error && isRecord7(error.details) ? error.details : {};
18231
18294
  }
18232
18295
  function record3(value) {
18233
- return isRecord6(value) ? value : {};
18296
+ return isRecord7(value) ? value : {};
18234
18297
  }
18235
- function isRecord6(value) {
18298
+ function isRecord7(value) {
18236
18299
  return typeof value === "object" && value !== null && !Array.isArray(value);
18237
18300
  }
18238
18301
  function array(value) {
@@ -18629,7 +18692,7 @@ function normalizeServerUrl(value) {
18629
18692
  }
18630
18693
 
18631
18694
  // src/version.ts
18632
- var PLUGIN_VERSION = "0.4.29";
18695
+ var PLUGIN_VERSION = "0.4.30";
18633
18696
 
18634
18697
  // src/server-api.ts
18635
18698
  var ENABLED_QR_PROVIDERS = ["github"];
@@ -19104,6 +19167,17 @@ function requireRecord(value, name2) {
19104
19167
  // src/typert-gateway-switch.ts
19105
19168
  var REMOTE_COMMAND_METHODS = ["execute", "list"];
19106
19169
  var LOCAL_ONLY_NAMESPACES = /* @__PURE__ */ new Set(["dynamicCordisRunner"]);
19170
+ var LOCAL_FALLBACK_NAMESPACES = /* @__PURE__ */ new Set(["$events", "settings", "credentials", "dynamicCordisRunner"]);
19171
+ var PEER_GONE_CODES = /* @__PURE__ */ new Set([
19172
+ "TRANSPORT_CLOSED",
19173
+ "CLIENT_CLOSED",
19174
+ "RPC_TIMEOUT",
19175
+ "CONNECTION_FAILED",
19176
+ "CONNECTION_REPLACED",
19177
+ "NOT_CONNECTED",
19178
+ "UNAVAILABLE",
19179
+ "internal"
19180
+ ]);
19107
19181
  var ALL_REMOTE_COMMANDS = { execute: true, list: true };
19108
19182
  var TypertGatewaySwitch = class {
19109
19183
  runtime;
@@ -19255,7 +19329,7 @@ var TypertGatewaySwitch = class {
19255
19329
  if (isLocalOnlyEndpoint(endpointOf(request))) return this.localInvoke(request);
19256
19330
  if (this.remoteTarget !== void 0 && this.remoteAvailability()) {
19257
19331
  return this.remoteTarget.invoke(request).catch((error) => {
19258
- if (!isUnansweredByPeer(error)) throw error;
19332
+ if (!localFallbackAllowed(endpointOf(request), error)) throw error;
19259
19333
  console.warn(`[dsh-remote] serving ${endpointOf(request)} locally: the peer did not answer it`, error);
19260
19334
  return this.localInvoke(request);
19261
19335
  });
@@ -19282,7 +19356,7 @@ var TypertGatewaySwitch = class {
19282
19356
  */
19283
19357
  withLocalFallback(endpoint, remote, local) {
19284
19358
  return remote().catch((error) => {
19285
- if (!isUnansweredByPeer(error)) throw error;
19359
+ if (!localFallbackAllowed(endpoint, error)) throw error;
19286
19360
  console.warn(`[dsh-remote] serving ${endpoint} locally: the peer did not answer it`, error);
19287
19361
  return local();
19288
19362
  });
@@ -19292,7 +19366,7 @@ var TypertGatewaySwitch = class {
19292
19366
  if (normalized !== void 0) return normalized;
19293
19367
  const source = error instanceof Error ? error : new Error("The Harness Gateway rejected the request.");
19294
19368
  const code = "code" in source && typeof source.code === "string" ? source.code : "internal";
19295
- const details = "details" in source && isRecord7(source.details) ? source.details : {};
19369
+ const details = "details" in source && isRecord8(source.details) ? source.details : {};
19296
19370
  return { code, message: source.message, details };
19297
19371
  }
19298
19372
  };
@@ -19332,7 +19406,7 @@ function requestFromCarrier(endpoint, payload, signal) {
19332
19406
  if (segments.length !== 2 || segments.some((segment) => segment.length === 0)) {
19333
19407
  throw new Error("The Harness Gateway endpoint is invalid.");
19334
19408
  }
19335
- if (!isRecord7(payload) || !isRecord7(payload.args)) {
19409
+ if (!isRecord8(payload) || !isRecord8(payload.args)) {
19336
19410
  throw new Error("The Harness Gateway payload is invalid.");
19337
19411
  }
19338
19412
  return { namespace: segments[0], method: segments[1], args: payload.args, signal };
@@ -19340,15 +19414,27 @@ function requestFromCarrier(endpoint, payload, signal) {
19340
19414
  function endpointOf(request) {
19341
19415
  return `${request.namespace}/${request.method}`;
19342
19416
  }
19343
- function isLocalOnlyEndpoint(endpoint) {
19417
+ function namespaceOf(endpoint) {
19344
19418
  const separator = endpoint.indexOf("/");
19345
- return separator > 0 && LOCAL_ONLY_NAMESPACES.has(endpoint.slice(0, separator));
19419
+ return separator > 0 ? endpoint.slice(0, separator) : void 0;
19420
+ }
19421
+ function isLocalOnlyEndpoint(endpoint) {
19422
+ const namespace = namespaceOf(endpoint);
19423
+ return namespace !== void 0 && LOCAL_ONLY_NAMESPACES.has(namespace);
19424
+ }
19425
+ function localFallbackAllowed(endpoint, error) {
19426
+ if (!isUnansweredByPeer(error)) return false;
19427
+ if (!isRecord8(error)) return true;
19428
+ const code = typeof error.code === "string" ? error.code : "";
19429
+ if (!PEER_GONE_CODES.has(code)) return true;
19430
+ const namespace = namespaceOf(endpoint);
19431
+ return namespace !== void 0 && LOCAL_FALLBACK_NAMESPACES.has(namespace);
19346
19432
  }
19347
19433
  function isRemoteCommandMethod(method) {
19348
19434
  return REMOTE_COMMAND_METHODS.includes(method);
19349
19435
  }
19350
19436
  function isUnansweredByPeer(error) {
19351
- if (!isRecord7(error)) return error instanceof Error;
19437
+ if (!isRecord8(error)) return error instanceof Error;
19352
19438
  const code = typeof error.code === "string" ? error.code : "";
19353
19439
  return code === "" || UNANSWERED_BY_PEER_CODES.has(code);
19354
19440
  }
@@ -19361,9 +19447,15 @@ var UNANSWERED_BY_PEER_CODES = /* @__PURE__ */ new Set([
19361
19447
  "CONNECTION_REPLACED",
19362
19448
  "NOT_CONNECTED",
19363
19449
  "UNAVAILABLE",
19450
+ // The client's own transport codes: the peer cannot answer a call it never received, so these are
19451
+ // "unanswered" rather than a refusal - and PEER_GONE_CODES above is what decides whether the local
19452
+ // shell may answer for it.
19453
+ "TRANSPORT_CLOSED",
19454
+ "CLIENT_CLOSED",
19455
+ "RPC_TIMEOUT",
19364
19456
  "internal"
19365
19457
  ]);
19366
- function isRecord7(value) {
19458
+ function isRecord8(value) {
19367
19459
  return typeof value === "object" && value !== null && !Array.isArray(value);
19368
19460
  }
19369
19461
 
@@ -20442,14 +20534,34 @@ var HOST_AUTHORIZATION_ERRORS = /* @__PURE__ */ new Set([
20442
20534
  "DEVICE_REVOKED",
20443
20535
  "TOKEN_EXPIRED"
20444
20536
  ]);
20537
+ var CREDENTIAL_FAILURE_CODES = /* @__PURE__ */ new Set([
20538
+ "AUTH_REQUIRED",
20539
+ "ACCOUNT_AUTH_REQUIRED",
20540
+ "AUTH_INVALID",
20541
+ "TOKEN_EXPIRED",
20542
+ "DEVICE_NOT_FOUND",
20543
+ "DEVICE_REVOKED",
20544
+ "MEMBERSHIP_REQUIRED"
20545
+ ]);
20546
+ var LIVENESS_INTERVAL_MS = 3e4;
20547
+ var FAST_RECONNECT_INTERVAL_MS = 5e3;
20548
+ var LIVENESS_TIMEOUT_MS = 4e3;
20549
+ var LIVENESS_TOLERATED_FAILURES = 2;
20550
+ var NO_ANSWER_CODES = /* @__PURE__ */ new Set(["RPC_TIMEOUT", "CLIENT_CLOSED", "TRANSPORT_CLOSED", "RPC_ABORTED"]);
20551
+ function livenessProbeLost(error) {
20552
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
20553
+ if (typeof code !== "string") return true;
20554
+ return NO_ANSWER_CODES.has(code);
20555
+ }
20445
20556
  var ClientModeRuntime = class {
20446
- constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory) {
20557
+ constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory, targetStore) {
20447
20558
  this.config = config;
20448
20559
  this.identities = identities;
20449
20560
  this.server = server;
20450
20561
  this.logger = logger;
20451
20562
  this.host = host;
20452
20563
  this.rtcFactoryProvider = rtcFactoryProvider;
20564
+ this.targetStore = targetStore;
20453
20565
  this.proxySwitch = apiProxy === void 0 ? void 0 : new ApiProxySwitch(apiProxy);
20454
20566
  this.gatewaySwitch = new TypertGatewaySwitch(typertGateway);
20455
20567
  this.gatewaySwitch.setRemoteAvailability(() => this.connected !== void 0);
@@ -20474,12 +20586,56 @@ var ClientModeRuntime = class {
20474
20586
  */
20475
20587
  remoteReconnectRun = 0;
20476
20588
  pendingWorkspaceSelection;
20589
+ /**
20590
+ * Transport a running fast reconnect is replacing.
20591
+ *
20592
+ * The rebuild opens its own control connection and the Server closes the older one for the same
20593
+ * device, so that close belongs to our own replacement. Reading it as a lost peer is what made the
20594
+ * fast level escalate itself into the fallback before it could ever succeed.
20595
+ */
20596
+ supersededClient;
20597
+ /**
20598
+ * True while a fast reconnect is rebuilding the link.
20599
+ *
20600
+ * The probe must not judge the link in the middle of its own replacement: the Server closes the
20601
+ * older connection for the device, so a check that ran then would read our replacement's side effect
20602
+ * as a second miss and escalate the level that is busy recovering. The rebuild's own outcome decides
20603
+ * first; only once it has settled does the cadence resume judging.
20604
+ */
20605
+ fastRebuildInFlight = false;
20606
+ /**
20607
+ * The last workspace the user opened for a Host.
20608
+ *
20609
+ * Kept so a reconnect can republish it: re-selecting the workspace is what makes the native UI
20610
+ * re-read its session list, and without it a list poisoned by the outage stays wrong even after
20611
+ * the link is back.
20612
+ */
20613
+ lastWorkspaceSelection;
20477
20614
  codexVirtual;
20478
20615
  proxySwitch;
20479
20616
  gatewaySwitch;
20480
20617
  codexStreams = /* @__PURE__ */ new Map();
20481
20618
  connectionProgress;
20482
20619
  connectionProgressRun = 0;
20620
+ /**
20621
+ * Host a boot is trying to restore, reported to the UI while the retry loop runs.
20622
+ *
20623
+ * Nothing is connected yet, so without this the window would show the local shell while the
20624
+ * user is still looking at the remote workspace they left.
20625
+ */
20626
+ /**
20627
+ * Recovery in progress, and which kind.
20628
+ *
20629
+ * `restore` is a start that is reconnecting to the recorded target, `fast` keeps the session on
20630
+ * screen while its link is rebuilt, and `fallback` is the last resort that returns the user to
20631
+ * the local shell. The UI shows all three, so a reconnect is never invisible, and the retry loop
20632
+ * stops as soon as the user asks for local.
20633
+ */
20634
+ reconnecting;
20635
+ /** Periodic proof of life for the live remote session; absent while nothing is connected. */
20636
+ livenessTimer;
20637
+ livenessInFlight = false;
20638
+ livenessFailures = 0;
20483
20639
  closed = false;
20484
20640
  async start() {
20485
20641
  if (this.closed) throw new Error("client remote-mode runtime is closed");
@@ -20540,6 +20696,7 @@ var ClientModeRuntime = class {
20540
20696
  ...targetStatus,
20541
20697
  connected: this.connected !== void 0,
20542
20698
  fellBackToLocal: this.fellBackToLocal,
20699
+ ...this.reconnecting === void 0 ? {} : { reconnecting: { ...this.reconnecting } },
20543
20700
  transport: this.connected?.client.getStats().mode ?? "Disconnected",
20544
20701
  connectedTargetDeviceId: this.connected?.target.deviceId,
20545
20702
  preferredTransports: this.config.forceRelay ? ["relay"] : ["lan", "p2p", "turn", "relay"],
@@ -20686,10 +20843,14 @@ var ClientModeRuntime = class {
20686
20843
  this.connected = void 0;
20687
20844
  this.connectionProgress = void 0;
20688
20845
  this.pendingWorkspaceSelection = void 0;
20846
+ this.lastWorkspaceSelection = void 0;
20689
20847
  await this.closeCodexStreams(previous2?.client);
20690
20848
  await previous2?.client.close().catch(() => void 0);
20691
20849
  this.fellBackToLocal = false;
20850
+ this.reconnecting = void 0;
20851
+ this.stopLivenessWatch();
20692
20852
  this.remoteReconnectRun += 1;
20853
+ await this.rememberTarget({ mode: "local" });
20693
20854
  this.logger.info("Harness target switched", { mode: "local" });
20694
20855
  return this.status();
20695
20856
  }
@@ -20712,11 +20873,191 @@ var ClientModeRuntime = class {
20712
20873
  await this.closeCodexVirtual();
20713
20874
  this.selectRemoteTarget(next);
20714
20875
  this.fellBackToLocal = false;
20876
+ this.reconnecting = void 0;
20877
+ await this.rememberTarget({ mode: "remote", hostDeviceId: next.target.deviceId, hostName: next.target.name });
20715
20878
  await this.closeCodexStreams(previous?.client);
20716
20879
  await previous?.client.close().catch(() => void 0);
20717
20880
  this.logger.info("Harness target switched", { mode: "remote", targetDeviceId: shortId(next.target.deviceId) });
20718
20881
  return this.status();
20719
20882
  }
20883
+ /**
20884
+ * Prove the live remote link still answers, and act on the result.
20885
+ *
20886
+ * Only a missing answer counts as loss: the peer's own error (a refusal, an unknown method) came
20887
+ * back over the same link and therefore proves it is there.
20888
+ *
20889
+ * The two recovery levels differ in what the user keeps. The first miss rebuilds the link in
20890
+ * place, so the session, its workspace selection and the remote carriers all stay; the second
20891
+ * gives up and returns to the local shell.
20892
+ */
20893
+ async verifyRemoteLiveness() {
20894
+ const connected = this.connected;
20895
+ if (connected === void 0 || this.livenessInFlight || this.fastRebuildInFlight) return;
20896
+ this.livenessInFlight = true;
20897
+ try {
20898
+ await connected.client.rpc("harness.transport.describe", {}, void 0, { timeoutMs: LIVENESS_TIMEOUT_MS });
20899
+ this.livenessFailures = 0;
20900
+ if (this.reconnecting?.phase === "fast") this.finishReconnect("the link answered again");
20901
+ } catch (error) {
20902
+ if (!livenessProbeLost(error)) {
20903
+ this.livenessFailures = 0;
20904
+ return;
20905
+ }
20906
+ this.livenessFailures += 1;
20907
+ this.logger.warn("remote Harness liveness check found no answer", {
20908
+ targetDeviceId: shortId(connected.target.deviceId),
20909
+ attempt: this.livenessFailures
20910
+ });
20911
+ if (this.livenessFailures === 1) {
20912
+ await this.enterFastReconnect(connected.target.deviceId, connected.target.name);
20913
+ return;
20914
+ }
20915
+ if (this.livenessFailures >= LIVENESS_TOLERATED_FAILURES) {
20916
+ this.handleRemoteTransportLost(connected.client, connected.target.deviceId, "unanswered-twice");
20917
+ }
20918
+ } finally {
20919
+ this.livenessInFlight = false;
20920
+ }
20921
+ }
20922
+ /**
20923
+ * Rebuild the link while the session stays on screen.
20924
+ *
20925
+ * This is what separates the two recovery levels: nothing is handed back to the local shell, so a
20926
+ * link that recovers does not cost the user the view they were working in.
20927
+ * @param targetDeviceId - the Host to rebuild the link to.
20928
+ */
20929
+ async enterFastReconnect(targetDeviceId, targetName) {
20930
+ if (this.reconnecting !== void 0) return;
20931
+ this.reconnecting = { targetDeviceId, ...targetName === void 0 ? {} : { targetName }, phase: "fast" };
20932
+ this.logger.warn("remote Harness link stopped answering; reconnecting in place", {
20933
+ targetDeviceId: shortId(targetDeviceId)
20934
+ });
20935
+ this.armLivenessWatch(FAST_RECONNECT_INTERVAL_MS);
20936
+ await this.reestablish(targetDeviceId);
20937
+ }
20938
+ /**
20939
+ * Build a new transport for the session already on screen.
20940
+ *
20941
+ * The carriers hold the previous client, so they are rebound to the new one before the old client
20942
+ * is closed; leaving it open would keep pointing remote calls at a dead transport.
20943
+ * @param targetDeviceId - the Host to reconnect to.
20944
+ * @returns true when a new session is in place.
20945
+ */
20946
+ async reestablish(targetDeviceId) {
20947
+ const previous = this.connected;
20948
+ this.supersededClient = previous?.client;
20949
+ this.fastRebuildInFlight = true;
20950
+ try {
20951
+ const next = await this.connect(targetDeviceId);
20952
+ if (previous === void 0) {
20953
+ this.supersededClient = void 0;
20954
+ await next.client.close().catch(() => void 0);
20955
+ return false;
20956
+ }
20957
+ await this.closePreview();
20958
+ this.connected = next;
20959
+ this.clearConnectionProgress(next.progressRunId);
20960
+ this.selectRemoteTarget(next);
20961
+ await this.closeCodexStreams(previous.client);
20962
+ await previous.client.close().catch(() => void 0);
20963
+ this.supersededClient = void 0;
20964
+ this.finishReconnect("link re-established");
20965
+ return true;
20966
+ } catch (error) {
20967
+ this.supersededClient = void 0;
20968
+ this.logger.warn("fast reconnect attempt failed", {
20969
+ targetDeviceId: shortId(targetDeviceId),
20970
+ code: safeErrorCode(error)
20971
+ });
20972
+ return false;
20973
+ } finally {
20974
+ this.fastRebuildInFlight = false;
20975
+ }
20976
+ }
20977
+ rememberWorkspaceSelection(selection) {
20978
+ this.pendingWorkspaceSelection = selection;
20979
+ this.lastWorkspaceSelection = { ...selection };
20980
+ }
20981
+ /**
20982
+ * Republish the workspace selection so the native UI re-reads its remote session list.
20983
+ *
20984
+ * The client half consumes status.workspaceSelection and reconnects that workspace; that refresh is
20985
+ * what replaces a list the outage had filled with local answers.
20986
+ * @param targetDeviceId - the Host the reconnect finished against.
20987
+ */
20988
+ restoreWorkspaceSelection(targetDeviceId) {
20989
+ const selection = this.lastWorkspaceSelection;
20990
+ if (selection === void 0 || selection.targetDeviceId !== targetDeviceId) return;
20991
+ this.pendingWorkspaceSelection = { ...selection };
20992
+ this.logger.info("republishing the workspace selection to re-read the remote session list", {
20993
+ targetDeviceId: shortId(targetDeviceId),
20994
+ workspaceId: selection.workspaceId
20995
+ });
20996
+ }
20997
+ finishReconnect(reason) {
20998
+ const target2 = this.reconnecting?.targetDeviceId;
20999
+ if (target2 === void 0) return;
21000
+ this.reconnecting = void 0;
21001
+ this.livenessFailures = 0;
21002
+ this.armLivenessWatch(LIVENESS_INTERVAL_MS);
21003
+ this.restoreWorkspaceSelection(target2);
21004
+ this.logger.info("remote Harness reconnect finished", { targetDeviceId: shortId(target2), reason });
21005
+ }
21006
+ /**
21007
+ * Check the live session now, outside the cadence.
21008
+ *
21009
+ * Used when the page becomes visible again, which is when a suspended client is most likely to be
21010
+ * holding a link that already ended.
21011
+ * @returns the status after the check.
21012
+ */
21013
+ async verifyRemoteConnection() {
21014
+ await this.verifyRemoteLiveness();
21015
+ return this.status();
21016
+ }
21017
+ armLivenessWatch(intervalMs) {
21018
+ if (this.livenessTimer !== void 0) clearInterval(this.livenessTimer);
21019
+ const timer = setInterval(() => {
21020
+ void this.verifyRemoteLiveness();
21021
+ }, intervalMs);
21022
+ timer.unref?.();
21023
+ this.livenessTimer = timer;
21024
+ }
21025
+ stopLivenessWatch() {
21026
+ if (this.livenessTimer !== void 0) clearInterval(this.livenessTimer);
21027
+ this.livenessTimer = void 0;
21028
+ this.livenessFailures = 0;
21029
+ this.livenessInFlight = false;
21030
+ }
21031
+ /**
21032
+ * Shared cleanup for a session whose transport is gone.
21033
+ *
21034
+ * A close event and an exhausted liveness check must leave exactly the same state behind, so both
21035
+ * paths run this. The session is gone for good here, which is why the phase becomes 'fallback' and
21036
+ * the retry loop keeps the UI saying that it is reconnecting.
21037
+ * @param client - the client that was connected.
21038
+ * @param targetDeviceId - the Host it was bound to.
21039
+ */
21040
+ handleRemoteTransportLost(client, targetDeviceId, reason) {
21041
+ if (this.connected?.client !== client) return;
21042
+ this.supersededClient = void 0;
21043
+ const targetName = this.connected.target.name;
21044
+ this.stopLivenessWatch();
21045
+ void this.closePreview();
21046
+ this.connected = void 0;
21047
+ this.connectionProgress = void 0;
21048
+ this.pendingWorkspaceSelection = void 0;
21049
+ void this.closeCodexVirtual();
21050
+ this.proxySwitch?.selectLocal();
21051
+ this.gatewaySwitch.selectLocal();
21052
+ this.fellBackToLocal = true;
21053
+ this.reconnecting = { targetDeviceId, ...targetName === void 0 ? {} : { targetName }, phase: "fallback" };
21054
+ void client.close().catch(() => void 0);
21055
+ this.logger.warn("remote Harness transport lost; reconnecting", {
21056
+ targetDeviceId: shortId(targetDeviceId),
21057
+ reason
21058
+ });
21059
+ void this.reconnectRemoteSession(targetDeviceId);
21060
+ }
20720
21061
  /**
20721
21062
  * Re-establish a remote session whose transport closed.
20722
21063
  *
@@ -20740,20 +21081,79 @@ var ClientModeRuntime = class {
20740
21081
  if (this.connected !== void 0) return;
20741
21082
  try {
20742
21083
  await this.setMode("remote", targetDeviceId);
21084
+ this.restoreWorkspaceSelection(targetDeviceId);
20743
21085
  this.logger.info("remote Harness session reconnected", { targetDeviceId: shortId(targetDeviceId) });
20744
21086
  return;
20745
21087
  } catch (error) {
21088
+ const code = safeErrorCode(error);
21089
+ if (code !== void 0 && CREDENTIAL_FAILURE_CODES.has(code)) {
21090
+ this.logger.warn("remote Harness reconnect stopped: authorization is required", {
21091
+ targetDeviceId: shortId(targetDeviceId),
21092
+ code
21093
+ });
21094
+ this.reconnecting = void 0;
21095
+ return;
21096
+ }
20746
21097
  if (attempt < 3 || attempt % 10 === 0) {
20747
21098
  this.logger.warn("remote Harness reconnect attempt failed", {
20748
21099
  targetDeviceId: shortId(targetDeviceId),
20749
21100
  attempt,
20750
- code: safeErrorCode(error)
21101
+ code
20751
21102
  });
20752
21103
  }
20753
21104
  }
20754
21105
  attempt += 1;
20755
21106
  }
20756
21107
  }
21108
+ /**
21109
+ * Persist the target a later boot may have to restore.
21110
+ *
21111
+ * A failure here must not fail a connect: the only cost is that a killed process comes back to
21112
+ * the local shell, which is where it would have been without this record.
21113
+ * @param target - the target to record.
21114
+ */
21115
+ async rememberTarget(target2) {
21116
+ if (this.targetStore === void 0) return;
21117
+ try {
21118
+ await this.targetStore.save({
21119
+ ...target2,
21120
+ ...this.config.serverUrl === void 0 ? {} : { serverUrl: this.config.serverUrl }
21121
+ });
21122
+ } catch (error) {
21123
+ this.logger.warn("could not record the remote target", { code: safeErrorCode(error) });
21124
+ }
21125
+ }
21126
+ /**
21127
+ * Reconnect to the target this device was last using, with the usual backoff.
21128
+ *
21129
+ * A resumed app can miss the transport close entirely - Android may reclaim the process - so
21130
+ * nothing would start the retry loop and the user would face a local shell behind a remote
21131
+ * workspace view. Restoring the recorded target gives that case the same loop a dropped
21132
+ * transport gets. A target recorded for another Server is left alone: it is not reachable
21133
+ * through the configured one.
21134
+ * @returns true when a retry loop was started.
21135
+ */
21136
+ async restoreLastTarget() {
21137
+ if (this.closed || this.connected !== void 0) return false;
21138
+ const record7 = await this.targetStore?.load();
21139
+ if (record7 === void 0 || record7.mode !== "remote" || record7.hostDeviceId === void 0) return false;
21140
+ if (record7.serverUrl !== void 0 && record7.serverUrl !== this.config.serverUrl) {
21141
+ this.logger.info("recorded remote target belongs to another Server; not restoring", {
21142
+ targetDeviceId: shortId(record7.hostDeviceId)
21143
+ });
21144
+ return false;
21145
+ }
21146
+ this.reconnecting = {
21147
+ targetDeviceId: record7.hostDeviceId,
21148
+ ...record7.hostName === void 0 ? {} : { targetName: record7.hostName },
21149
+ phase: "restore"
21150
+ };
21151
+ this.logger.info("restoring the remote target of the previous run", {
21152
+ targetDeviceId: shortId(record7.hostDeviceId)
21153
+ });
21154
+ void this.reconnectRemoteSession(record7.hostDeviceId);
21155
+ return true;
21156
+ }
20757
21157
  async listRemoteDirectory(targetDeviceId, path, signal) {
20758
21158
  const remote = await this.ensureConnected(targetDeviceId, signal);
20759
21159
  if (remote.features.remoteGateway) {
@@ -20808,7 +21208,7 @@ var ClientModeRuntime = class {
20808
21208
  await this.closeCodexVirtual();
20809
21209
  this.selectRemoteTarget(remote, transport);
20810
21210
  const workspaceId = workspaceRecordId(workspace.workspace);
20811
- this.pendingWorkspaceSelection = { targetDeviceId: remote.target.deviceId, workspaceId };
21211
+ this.rememberWorkspaceSelection({ targetDeviceId: remote.target.deviceId, workspaceId });
20812
21212
  this.logger.info("Remote workspace opened", { targetDeviceId: shortId(remote.target.deviceId) });
20813
21213
  return { ...this.status(), workspace };
20814
21214
  }
@@ -20842,12 +21242,12 @@ var ClientModeRuntime = class {
20842
21242
  this.codexVirtual = virtual;
20843
21243
  this.selectCodexTarget(virtual, remote);
20844
21244
  const preferredSessionId = await virtual.preferredSessionId(signal);
20845
- this.pendingWorkspaceSelection = {
21245
+ this.rememberWorkspaceSelection({
20846
21246
  targetDeviceId: remote.target.deviceId,
20847
21247
  workspaceId,
20848
21248
  backend: "codex",
20849
21249
  ...preferredSessionId === void 0 ? {} : { sessionId: preferredSessionId }
20850
- };
21250
+ });
20851
21251
  this.logger.info("CodeX virtual workspace opened", { targetDeviceId: shortId(remote.target.deviceId) });
20852
21252
  return { ...this.status(), workspace };
20853
21253
  }
@@ -20872,6 +21272,7 @@ var ClientModeRuntime = class {
20872
21272
  return this.openCodexWorkspace(targetDeviceId, codexProjectWorkspaceId(project.id), signal);
20873
21273
  }
20874
21274
  consumeWorkspaceSelection(selection) {
21275
+ this.lastWorkspaceSelection = { ...selection };
20875
21276
  const pending = this.pendingWorkspaceSelection;
20876
21277
  if (pending?.targetDeviceId === selection.targetDeviceId && pending.workspaceId === selection.workspaceId && (pending.backend ?? "harness") === (selection.backend ?? "harness")) {
20877
21278
  this.pendingWorkspaceSelection = void 0;
@@ -20882,6 +21283,7 @@ var ClientModeRuntime = class {
20882
21283
  await this.closePreview();
20883
21284
  if (this.closed) return;
20884
21285
  this.closed = true;
21286
+ this.stopLivenessWatch();
20885
21287
  this.proxySwitch?.selectLocal();
20886
21288
  await this.closePreview();
20887
21289
  this.gatewaySwitch.selectLocal();
@@ -20949,10 +21351,10 @@ var ClientModeRuntime = class {
20949
21351
  }
20950
21352
  }
20951
21353
  stream.unsubscribe = remote.client.onEvent((event) => {
20952
- if (event.event === "codex.app.frame" && isRecord8(event.data) && event.data.streamId === value.streamId) {
21354
+ if (event.event === "codex.app.frame" && isRecord9(event.data) && event.data.streamId === value.streamId) {
20953
21355
  this.appendCodexFrame(stream, event.data);
20954
21356
  }
20955
- if (event.event === "codex.app.stream.closed" && isRecord8(event.data) && event.data.streamId === value.streamId) {
21357
+ if (event.event === "codex.app.stream.closed" && isRecord9(event.data) && event.data.streamId === value.streamId) {
20956
21358
  stream.closed = typeof event.data.reason === "string" ? event.data.reason : "closed";
20957
21359
  stream.wake();
20958
21360
  }
@@ -20979,7 +21381,7 @@ var ClientModeRuntime = class {
20979
21381
  stream.wake();
20980
21382
  };
20981
21383
  appendCodexFrame(stream, data2) {
20982
- if (!isRecord8(data2) || !isRecord8(data2.frame) || typeof data2.frame.method !== "string") return;
21384
+ if (!isRecord9(data2) || !isRecord9(data2.frame) || typeof data2.frame.method !== "string") return;
20983
21385
  if (stream.frames.length >= 256) {
20984
21386
  stream.closed = "overflow";
20985
21387
  } else {
@@ -21185,21 +21587,15 @@ var ClientModeRuntime = class {
21185
21587
  connectedPreference === void 0 ? void 0 : [connectedPreference]
21186
21588
  );
21187
21589
  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);
21590
+ if (this.supersededClient === connectedClient) {
21591
+ this.logger.info("ignoring the close of the transport a reconnect is replacing", {
21592
+ targetDeviceId: shortId(target2.deviceId)
21593
+ });
21594
+ return;
21595
+ }
21596
+ this.handleRemoteTransportLost(connectedClient, target2.deviceId, "transport-closed");
21202
21597
  });
21598
+ this.armLivenessWatch(LIVENESS_INTERVAL_MS);
21203
21599
  const connectionDetails = await connectedTransport.connectionDetails().catch(() => void 0);
21204
21600
  this.logger.info("remote Harness transport ready", {
21205
21601
  targetDeviceId: shortId(target2.deviceId),
@@ -21270,6 +21666,7 @@ var ClientModeRuntime = class {
21270
21666
  async handleControl(endpoint, payload, signal) {
21271
21667
  try {
21272
21668
  if (endpoint === "status") return ok2(await this.detailedStatus());
21669
+ if (endpoint === "client.connection.verify") return ok2(await this.verifyRemoteConnection());
21273
21670
  if (endpoint === "devices") return ok2(await this.devices());
21274
21671
  if (endpoint === "client.account.login") {
21275
21672
  const value = record4(payload);
@@ -21483,7 +21880,7 @@ function record4(value) {
21483
21880
  }
21484
21881
  return value;
21485
21882
  }
21486
- function isRecord8(value) {
21883
+ function isRecord9(value) {
21487
21884
  return typeof value === "object" && value !== null && !Array.isArray(value);
21488
21885
  }
21489
21886
  function ok2(value) {
@@ -21508,11 +21905,11 @@ async function readRemoteWorkspaceBaseline(gateway, signal) {
21508
21905
  const iterator = source[Symbol.asyncIterator]();
21509
21906
  try {
21510
21907
  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)) {
21908
+ if (first.done || !isRecord9(first.value) || first.value.type !== "baseline" || !isRecord9(first.value.value) || !Array.isArray(first.value.value.items)) {
21512
21909
  throw new ClientModeError("INVALID_MESSAGE", "The remote Host returned an invalid Workspace baseline.");
21513
21910
  }
21514
21911
  return first.value.value.items.map((item) => {
21515
- if (!isRecord8(item) || typeof item.workspaceId !== "string" || typeof item.path !== "string" || typeof item.title !== "string") {
21912
+ if (!isRecord9(item) || typeof item.workspaceId !== "string" || typeof item.path !== "string" || typeof item.title !== "string") {
21516
21913
  throw new ClientModeError("INVALID_MESSAGE", "The remote Host returned an invalid Workspace row.");
21517
21914
  }
21518
21915
  return { workspaceId: item.workspaceId, path: item.path, title: item.title };
@@ -21578,7 +21975,7 @@ async function probeRemoteHostFeatures(client, clientVersion) {
21578
21975
  if (error instanceof Error && "code" in error && error.code === "METHOD_NOT_FOUND") return fallback;
21579
21976
  throw error;
21580
21977
  }
21581
- if (!isRecord8(value) || !Array.isArray(value.capabilities) || value.capabilities.some((capability) => typeof capability !== "string")) {
21978
+ if (!isRecord9(value) || !Array.isArray(value.capabilities) || value.capabilities.some((capability) => typeof capability !== "string")) {
21582
21979
  throw new ClientModeError("INVALID_MESSAGE", "The remote Host returned invalid transport capabilities.");
21583
21980
  }
21584
21981
  const capabilities = new Set(value.capabilities);
@@ -21663,6 +22060,67 @@ function iceServersForAttempt(attempt, iceServers) {
21663
22060
  return attempt === "direct" ? stunOnlyIceServers(iceServers) : iceServers;
21664
22061
  }
21665
22062
 
22063
+ // src/client-target-store.ts
22064
+ import { randomUUID as randomUUID2 } from "node:crypto";
22065
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
22066
+ import { join as join5 } from "node:path";
22067
+ var FILE_NAME = "client-target.json";
22068
+ var ClientTargetStore = class {
22069
+ constructor(directory) {
22070
+ this.directory = directory;
22071
+ }
22072
+ get file() {
22073
+ return join5(this.directory, FILE_NAME);
22074
+ }
22075
+ /** Read the record. A missing, unreadable or malformed file reads as "no record". */
22076
+ async load() {
22077
+ let raw;
22078
+ try {
22079
+ raw = await readFile3(this.file, "utf8");
22080
+ } catch {
22081
+ return void 0;
22082
+ }
22083
+ try {
22084
+ const value = JSON.parse(raw);
22085
+ if (typeof value !== "object" || value === null) return void 0;
22086
+ const record7 = value;
22087
+ if (record7.schemaVersion !== 1) return void 0;
22088
+ if (record7.mode !== "local" && record7.mode !== "remote") return void 0;
22089
+ if (record7.mode === "remote" && (typeof record7.hostDeviceId !== "string" || record7.hostDeviceId.length === 0)) {
22090
+ return void 0;
22091
+ }
22092
+ return {
22093
+ schemaVersion: 1,
22094
+ mode: record7.mode,
22095
+ ...typeof record7.serverUrl === "string" && record7.serverUrl.length > 0 ? { serverUrl: record7.serverUrl } : {},
22096
+ ...typeof record7.hostDeviceId === "string" && record7.hostDeviceId.length > 0 ? { hostDeviceId: record7.hostDeviceId } : {},
22097
+ ...typeof record7.hostName === "string" && record7.hostName.length > 0 ? { hostName: record7.hostName } : {},
22098
+ savedAt: typeof record7.savedAt === "number" ? record7.savedAt : 0
22099
+ };
22100
+ } catch {
22101
+ return void 0;
22102
+ }
22103
+ }
22104
+ /** Record the current target. Failures reach the caller so a boot can log them. */
22105
+ async save(target2) {
22106
+ const value = {
22107
+ schemaVersion: 1,
22108
+ mode: target2.mode,
22109
+ ...target2.serverUrl === void 0 ? {} : { serverUrl: target2.serverUrl },
22110
+ ...target2.hostDeviceId === void 0 ? {} : { hostDeviceId: target2.hostDeviceId },
22111
+ ...target2.hostName === void 0 ? {} : { hostName: target2.hostName },
22112
+ savedAt: Date.now()
22113
+ };
22114
+ await mkdir2(this.directory, { recursive: true, mode: 448 });
22115
+ await sweepStaleTemporaries(this.directory);
22116
+ const temporary = this.file + "." + String(process.pid) + "." + randomUUID2() + ".tmp";
22117
+ await writeFile2(temporary, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", mode: 384, flag: "wx" });
22118
+ await chmod2(temporary, 384);
22119
+ await replaceFile(temporary, this.file);
22120
+ await chmod2(this.file, 384);
22121
+ }
22122
+ };
22123
+
21666
22124
  // src/control-runtime.ts
21667
22125
  import { hostname as hostname2 } from "node:os";
21668
22126
  import { existsSync as existsSync3 } from "node:fs";
@@ -21670,9 +22128,9 @@ import { execFileSync } from "node:child_process";
21670
22128
 
21671
22129
  // src/identity-store.ts
21672
22130
  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";
22131
+ import { chmod as chmod3, mkdir as mkdir3, readFile as readFile4, rm as rm3, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
21674
22132
  import { homedir } from "node:os";
21675
- import { dirname as dirname4, join as join5 } from "node:path";
22133
+ import { dirname as dirname4, join as join6 } from "node:path";
21676
22134
  var identitySchema = external_exports.object({
21677
22135
  schemaVersion: external_exports.literal(1),
21678
22136
  deviceId: external_exports.string().uuid(),
@@ -21701,14 +22159,14 @@ var IdentityStore = class {
21701
22159
  peers = /* @__PURE__ */ new Map();
21702
22160
  constructor(options = {}) {
21703
22161
  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");
22162
+ const dshHome = env.DSH_HOME || join6(options.homeDirectory ?? homedir(), ".dsh");
22163
+ this.directory = options.directory ?? join6(dshHome, "remote");
21706
22164
  }
21707
22165
  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");
22166
+ await mkdir3(this.directory, { recursive: true, mode: 448 });
22167
+ await chmod3(this.directory, 448);
22168
+ const devicePath = join6(this.directory, "device.json");
22169
+ const keyPath = join6(this.directory, "device.key");
21712
22170
  const [hasDevice, hasKey] = await Promise.all([exists2(devicePath), exists2(keyPath)]);
21713
22171
  if (hasDevice !== hasKey) {
21714
22172
  throw new IdentityInvalidError("device identity is incomplete; repair it explicitly before reconnecting");
@@ -21722,8 +22180,8 @@ var IdentityStore = class {
21722
22180
  }
21723
22181
  await assertPrivateMode2(keyPath);
21724
22182
  try {
21725
- let record7 = identitySchema.parse(JSON.parse(await readFile3(devicePath, "utf8")));
21726
- const privateKey = (await readFile3(keyPath, "utf8")).trim();
22183
+ let record7 = identitySchema.parse(JSON.parse(await readFile4(devicePath, "utf8")));
22184
+ const privateKey = (await readFile4(keyPath, "utf8")).trim();
21727
22185
  const regenerated = generateKeyPair(fromBase64Url2(privateKey));
21728
22186
  if (regenerated.publicKey !== record7.publicKey) {
21729
22187
  throw new IdentityInvalidError("device public and private keys do not match");
@@ -21777,11 +22235,11 @@ var IdentityStore = class {
21777
22235
  return removed;
21778
22236
  }
21779
22237
  async loadPeers() {
21780
- const path = join5(this.directory, "trusted-peers.json");
22238
+ const path = join6(this.directory, "trusted-peers.json");
21781
22239
  if (!await exists2(path)) {
21782
22240
  await atomicJsonWrite(path, { schemaVersion: 1, peers: [] }, 384);
21783
22241
  }
21784
- const parsed = trustedPeersSchema.parse(JSON.parse(await readFile3(path, "utf8")));
22242
+ const parsed = trustedPeersSchema.parse(JSON.parse(await readFile4(path, "utf8")));
21785
22243
  const peers = /* @__PURE__ */ new Map();
21786
22244
  for (const peer of parsed.peers) {
21787
22245
  if (peer.fingerprint !== fingerprint(peer.publicKey)) {
@@ -21793,7 +22251,7 @@ var IdentityStore = class {
21793
22251
  this.peers = peers;
21794
22252
  }
21795
22253
  async savePeers() {
21796
- await atomicJsonWrite(join5(this.directory, "trusted-peers.json"), {
22254
+ await atomicJsonWrite(join6(this.directory, "trusted-peers.json"), {
21797
22255
  schemaVersion: 1,
21798
22256
  peers: [...this.peers.values()]
21799
22257
  }, 384);
@@ -21802,7 +22260,7 @@ var IdentityStore = class {
21802
22260
  function serverStorageDirectory(root, serverUrl, role) {
21803
22261
  const origin = new URL(serverUrl).origin;
21804
22262
  const scope = createHash("sha256").update(origin).digest("hex").slice(0, 24);
21805
- return join5(root, "servers", scope, role);
22263
+ return join6(root, "servers", scope, role);
21806
22264
  }
21807
22265
  function fingerprint(publicKey) {
21808
22266
  const compact = createHash("sha256").update(fromBase64Url2(publicKey)).digest("hex").slice(0, 12).toUpperCase();
@@ -21821,13 +22279,13 @@ async function atomicJsonWrite(path, value, mode) {
21821
22279
  }
21822
22280
  async function atomicTextWrite(path, value, mode) {
21823
22281
  const directory = dirname4(path);
21824
- await mkdir2(directory, { recursive: true, mode: 448 });
22282
+ await mkdir3(directory, { recursive: true, mode: 448 });
21825
22283
  await sweepStaleTemporaries(directory);
21826
22284
  const temporary = `${path}.${process.pid}.${uuidV7()}.tmp`;
21827
- await writeFile2(temporary, value, { encoding: "utf8", mode, flag: "wx" });
21828
- await chmod2(temporary, mode);
22285
+ await writeFile3(temporary, value, { encoding: "utf8", mode, flag: "wx" });
22286
+ await chmod3(temporary, mode);
21829
22287
  await replaceFile(temporary, path);
21830
- await chmod2(path, mode);
22288
+ await chmod3(path, mode);
21831
22289
  }
21832
22290
  async function exists2(path) {
21833
22291
  try {
@@ -21989,11 +22447,11 @@ var ControlStatusStream = class {
21989
22447
  };
21990
22448
 
21991
22449
  // src/codex/domain.ts
21992
- import { randomUUID as randomUUID2 } from "node:crypto";
22450
+ import { randomUUID as randomUUID3 } from "node:crypto";
21993
22451
  import { accessSync, constants, existsSync as existsSync2, readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
21994
22452
  import { readdir as readdir2, realpath, stat as stat4 } from "node:fs/promises";
21995
22453
  import { homedir as homedir2 } from "node:os";
21996
- import { basename as basename2, isAbsolute as isAbsolute2, join as join6, relative, resolve } from "node:path";
22454
+ import { basename as basename2, isAbsolute as isAbsolute2, join as join7, posix, relative, resolve } from "node:path";
21997
22455
 
21998
22456
  // src/codex/app-server.ts
21999
22457
  import { spawn as spawn2 } from "node:child_process";
@@ -22179,7 +22637,7 @@ var CodexAppServerClient = class {
22179
22637
  this.handleProcessFailure("CODEX_INVALID_RESPONSE", new Error("Codex App Server emitted invalid JSON."));
22180
22638
  return;
22181
22639
  }
22182
- if (!isRecord9(value)) {
22640
+ if (!isRecord10(value)) {
22183
22641
  this.handleProcessFailure("CODEX_INVALID_RESPONSE", new Error("Codex App Server emitted an invalid message."));
22184
22642
  return;
22185
22643
  }
@@ -22224,12 +22682,12 @@ var CodexAppServerClient = class {
22224
22682
  }
22225
22683
  };
22226
22684
  function safeUpstreamError(value) {
22227
- if (!isRecord9(value) || typeof value.message !== "string") return "Codex App Server rejected the request.";
22685
+ if (!isRecord10(value) || typeof value.message !== "string") return "Codex App Server rejected the request.";
22228
22686
  const message = value.message.toLowerCase();
22229
22687
  if (message.includes("active writer")) return "Codex thread already has an active writer.";
22230
22688
  return message.includes("not initialized") ? "Codex App Server is not initialized." : "Codex App Server rejected the request.";
22231
22689
  }
22232
- function isRecord9(value) {
22690
+ function isRecord10(value) {
22233
22691
  return typeof value === "object" && value !== null && !Array.isArray(value);
22234
22692
  }
22235
22693
 
@@ -22489,15 +22947,15 @@ function decodeCanonicalBase64(value) {
22489
22947
  }
22490
22948
  return decoded;
22491
22949
  }
22492
- function isRecord10(value) {
22950
+ function isRecord11(value) {
22493
22951
  return typeof value === "object" && value !== null && !Array.isArray(value);
22494
22952
  }
22495
22953
  function safeErrorCode2(error) {
22496
- if (isRecord10(error) && typeof error.code === "string") return error.code;
22954
+ if (isRecord11(error) && typeof error.code === "string") return error.code;
22497
22955
  return "UNKNOWN";
22498
22956
  }
22499
22957
  function safeMethod(input2) {
22500
- return isRecord10(input2) && typeof input2.method === "string" ? input2.method : "invalid";
22958
+ return isRecord11(input2) && typeof input2.method === "string" ? input2.method : "invalid";
22501
22959
  }
22502
22960
  function concatChunks(chunks, totalBytes) {
22503
22961
  const output = new Uint8Array(totalBytes);
@@ -22512,6 +22970,7 @@ function concatChunks(chunks, totalBytes) {
22512
22970
  // src/codex/domain.ts
22513
22971
  var APPROVAL_TTL_MS = 5 * 6e4;
22514
22972
  var DEFAULT_RESTART_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
22973
+ var CODEX_START_BUDGET_MS = 5e3;
22515
22974
  var CODEX_PAGE_LIMIT2 = 100;
22516
22975
  var MAX_CODEX_PAGES2 = 32;
22517
22976
  var CODEX_HISTORY_PAGE_LIMIT = 25;
@@ -22544,7 +23003,7 @@ var CodexRemoteDomain = class {
22544
23003
  if (!this.config.enabled) return;
22545
23004
  try {
22546
23005
  this.state = "starting";
22547
- await this.launchAppServer();
23006
+ await this.launchWithinBudget();
22548
23007
  } catch (error) {
22549
23008
  this.available = false;
22550
23009
  this.state = "unavailable";
@@ -22775,6 +23234,32 @@ var CodexRemoteDomain = class {
22775
23234
  await this.appServer?.close();
22776
23235
  this.appServer = void 0;
22777
23236
  }
23237
+ /**
23238
+ * Launch the App Server within {@link CODEX_START_BUDGET_MS}.
23239
+ *
23240
+ * The losing side of the race keeps running until the disposal in start()'s catch stops it, so
23241
+ * it needs its own handler: an unhandled rejection here would surface as a process-level error
23242
+ * for what is only an optional feature being unavailable.
23243
+ * @returns nothing once a candidate is ready.
23244
+ */
23245
+ async launchWithinBudget() {
23246
+ let timer;
23247
+ const attempt = this.launchAppServer();
23248
+ attempt.catch(() => void 0);
23249
+ try {
23250
+ await Promise.race([
23251
+ attempt,
23252
+ new Promise((_resolve, reject) => {
23253
+ timer = setTimeout(() => {
23254
+ reject(new RpcError("CODEX_START_TIMEOUT", `Codex App Server did not start within ${CODEX_START_BUDGET_MS}ms.`));
23255
+ }, CODEX_START_BUDGET_MS);
23256
+ timer.unref?.();
23257
+ })
23258
+ ]);
23259
+ } finally {
23260
+ if (timer !== void 0) clearTimeout(timer);
23261
+ }
23262
+ }
22778
23263
  async launchAppServer() {
22779
23264
  let lastError;
22780
23265
  for (const binary of codexBinaryCandidates(this.config.binary)) {
@@ -22927,14 +23412,14 @@ var CodexRemoteDomain = class {
22927
23412
  await this.assertResultThreadAllowed(result);
22928
23413
  if (codexPermissionPresetFromResponse(result) !== params.permissionPreset) {
22929
23414
  await this.callUpstream("thread/settings/update", { threadId, ...settings });
22930
- result = { ...isRecord11(result) ? result : {}, ...settings, sandbox: settings.sandboxPolicy };
23415
+ result = { ...isRecord12(result) ? result : {}, ...settings, sandbox: settings.sandboxPolicy };
22931
23416
  }
22932
23417
  }
22933
23418
  await this.publishPermission(threadId, result);
22934
23419
  return result;
22935
23420
  }
22936
23421
  async publishPermission(threadId, value) {
22937
- const source = isRecord11(value) ? value : {};
23422
+ const source = isRecord12(value) ? value : {};
22938
23423
  const threadSettings = { approvalPolicy: source.approvalPolicy, sandboxPolicy: source.sandboxPolicy ?? source.sandbox };
22939
23424
  await this.handleInbound({ kind: "notification", method: "thread/settings/updated", params: { threadId, threadSettings } });
22940
23425
  }
@@ -22952,7 +23437,7 @@ var CodexRemoteDomain = class {
22952
23437
  await appServer.respond(message.id, { decision: "decline" });
22953
23438
  return;
22954
23439
  }
22955
- const requestHandle = randomUUID2();
23440
+ const requestHandle = randomUUID3();
22956
23441
  this.approvals.set(requestHandle, {
22957
23442
  upstreamId: message.id,
22958
23443
  connectionId: owner.connectionId,
@@ -23030,9 +23515,9 @@ var CodexRemoteDomain = class {
23030
23515
  itemsView,
23031
23516
  ...cursor2 === void 0 ? {} : { cursor: cursor2 }
23032
23517
  });
23033
- const pageResult = isRecord11(result) ? result : {};
23518
+ const pageResult = isRecord12(result) ? result : {};
23034
23519
  for (const rawTurn of array2(pageResult.data)) {
23035
- if (!isRecord11(rawTurn)) continue;
23520
+ if (!isRecord12(rawTurn)) continue;
23036
23521
  const turnId = typeof rawTurn.id === "string" ? rawTurn.id : void 0;
23037
23522
  const items = rawTurn.itemsView === "full" || turnId === void 0 ? array2(rawTurn.items) : await this.readThreadItems(connectionId, threadId, turnId, array2(rawTurn.items));
23038
23523
  turns.push({ ...rawTurn, items });
@@ -23054,9 +23539,9 @@ var CodexRemoteDomain = class {
23054
23539
  sortDirection: "asc",
23055
23540
  ...cursor2 === void 0 ? {} : { cursor: cursor2 }
23056
23541
  });
23057
- const pageResult = isRecord11(result) ? result : {};
23542
+ const pageResult = isRecord12(result) ? result : {};
23058
23543
  for (const entry of array2(pageResult.data)) {
23059
- if (isRecord11(entry) && entry.item !== void 0) items.push(entry.item);
23544
+ if (isRecord12(entry) && entry.item !== void 0) items.push(entry.item);
23060
23545
  }
23061
23546
  cursor2 = typeof pageResult.nextCursor === "string" && pageResult.nextCursor.length > 0 ? pageResult.nextCursor : void 0;
23062
23547
  if (cursor2 === void 0) break;
@@ -23120,7 +23605,7 @@ var CodexRemoteDomain = class {
23120
23605
  return [...this.peers.values()].some((peer) => peer.hasThreadSubscription(threadId));
23121
23606
  }
23122
23607
  resolveUpstreamApproval(params) {
23123
- if (!isRecord11(params) || typeof params.requestId !== "string" && typeof params.requestId !== "number") return;
23608
+ if (!isRecord12(params) || typeof params.requestId !== "string" && typeof params.requestId !== "number") return;
23124
23609
  for (const [handle, approval] of this.approvals) {
23125
23610
  if (approval.upstreamId === params.requestId) this.approvals.delete(handle);
23126
23611
  }
@@ -23334,15 +23819,15 @@ function codexBinaryCandidates(configured, hostPlatform = process.platform, user
23334
23819
  if (hostPlatform !== "darwin") return [configured];
23335
23820
  const bundledCandidates = [
23336
23821
  "/Applications/ChatGPT.app",
23337
- join6(userHome, "Applications", "ChatGPT.app")
23822
+ posix.join(userHome, "Applications", "ChatGPT.app")
23338
23823
  ].flatMap((chatGptApp) => {
23339
- const codexCli = join6(chatGptApp, "Contents", "Resources", "codex-cli");
23824
+ const codexCli = posix.join(chatGptApp, "Contents", "Resources", "codex-cli");
23340
23825
  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) {
23826
+ const manifest = JSON.parse(readFileSync(posix.join(codexCli, "codex-package.json"), "utf8"));
23827
+ if (!isRecord12(manifest) || typeof manifest.entrypoint !== "string" || manifest.entrypoint.length === 0) {
23343
23828
  return [];
23344
23829
  }
23345
- const candidate = join6(codexCli, manifest.entrypoint);
23830
+ const candidate = posix.join(codexCli, manifest.entrypoint);
23346
23831
  if (!existsSync2(candidate)) return [];
23347
23832
  accessSync(candidate, constants.X_OK);
23348
23833
  return [candidate];
@@ -23353,14 +23838,14 @@ function codexBinaryCandidates(configured, hostPlatform = process.platform, user
23353
23838
  return [.../* @__PURE__ */ new Set([
23354
23839
  ...bundledCandidates,
23355
23840
  "/Applications/ChatGPT.app/Contents/Resources/codex",
23356
- join6(userHome, "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
23841
+ posix.join(userHome, "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
23357
23842
  configured
23358
23843
  ])];
23359
23844
  }
23360
23845
  function bundledWindowsCodex(userHome) {
23361
- const bin = join6(userHome, "AppData", "Local", "OpenAI", "Codex", "bin");
23846
+ const bin = join7(userHome, "AppData", "Local", "OpenAI", "Codex", "bin");
23362
23847
  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);
23848
+ 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
23849
  const candidate = newest[0]?.candidate;
23365
23850
  return candidate === void 0 ? [] : [candidate];
23366
23851
  } catch {
@@ -23368,24 +23853,24 @@ function bundledWindowsCodex(userHome) {
23368
23853
  }
23369
23854
  }
23370
23855
  function parseCallEnvelope(input2) {
23371
- if (!isRecord11(input2) || typeof input2.method !== "string" || !("params" in input2) || Object.keys(input2).some((key) => key !== "method" && key !== "params")) {
23856
+ if (!isRecord12(input2) || typeof input2.method !== "string" || !("params" in input2) || Object.keys(input2).some((key) => key !== "method" && key !== "params")) {
23372
23857
  throw new RpcError("INVALID_MESSAGE", "The Codex call envelope is invalid.");
23373
23858
  }
23374
23859
  return { method: input2.method, params: input2.params };
23375
23860
  }
23376
23861
  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")) {
23862
+ if (!isRecord12(input2) || typeof input2.requestHandle !== "string" || !["accept", "decline", "cancel"].includes(String(input2.decision)) || Object.keys(input2).some((key) => key !== "requestHandle" && key !== "decision")) {
23378
23863
  throw new RpcError("INVALID_MESSAGE", "The Codex approval response is invalid.");
23379
23864
  }
23380
23865
  return input2;
23381
23866
  }
23382
23867
  function accountCanRun(result) {
23383
- if (!isRecord11(result) || typeof result.requiresOpenaiAuth !== "boolean") return false;
23384
- return result.requiresOpenaiAuth === false || isRecord11(result.account);
23868
+ if (!isRecord12(result) || typeof result.requiresOpenaiAuth !== "boolean") return false;
23869
+ return result.requiresOpenaiAuth === false || isRecord12(result.account);
23385
23870
  }
23386
23871
  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;
23872
+ if (!isRecord12(result)) throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned invalid account state.");
23873
+ const account = isRecord12(result.account) ? result.account : void 0;
23389
23874
  return {
23390
23875
  authenticated: account !== void 0 || result.requiresOpenaiAuth === false,
23391
23876
  requiresOpenaiAuth: result.requiresOpenaiAuth === true,
@@ -23398,11 +23883,11 @@ function sanitizeAccount(result) {
23398
23883
  };
23399
23884
  }
23400
23885
  function sanitizeThreadList(result) {
23401
- if (!isRecord11(result) || !Array.isArray(result.data)) {
23886
+ if (!isRecord12(result) || !Array.isArray(result.data)) {
23402
23887
  throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid thread list.");
23403
23888
  }
23404
23889
  const data2 = result.data.flatMap((value) => {
23405
- if (!isRecord11(value) || typeof value.id !== "string") return [];
23890
+ if (!isRecord12(value) || typeof value.id !== "string") return [];
23406
23891
  return [{
23407
23892
  id: value.id,
23408
23893
  ...typeof value.sessionId === "string" ? { sessionId: value.sessionId } : {},
@@ -23414,7 +23899,7 @@ function sanitizeThreadList(result) {
23414
23899
  ...typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) ? { updatedAt: value.updatedAt } : {},
23415
23900
  ...typeof value.archived === "boolean" ? { archived: value.archived } : {},
23416
23901
  ...typeof value.isPinned === "boolean" ? { isPinned: value.isPinned } : {},
23417
- ...isRecord11(value.status) ? { status: value.status } : {}
23902
+ ...isRecord12(value.status) ? { status: value.status } : {}
23418
23903
  }];
23419
23904
  });
23420
23905
  return {
@@ -23424,18 +23909,18 @@ function sanitizeThreadList(result) {
23424
23909
  };
23425
23910
  }
23426
23911
  function filterThreadListByWorkspaceAuthority(result, authority) {
23427
- if (!isRecord11(result) || !Array.isArray(result.data)) {
23912
+ if (!isRecord12(result) || !Array.isArray(result.data)) {
23428
23913
  throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid thread list.");
23429
23914
  }
23430
23915
  return {
23431
23916
  ...result,
23432
- data: result.data.map((record7) => isRecord11(record7) ? record7 : void 0).filter((thread) => thread !== void 0 && isThreadAllowedByWorkspaceAuthority(thread, authority))
23917
+ data: result.data.map((record7) => isRecord12(record7) ? record7 : void 0).filter((thread) => thread !== void 0 && isThreadAllowedByWorkspaceAuthority(thread, authority))
23433
23918
  };
23434
23919
  }
23435
23920
  function sanitizeProject(value) {
23436
- if (!isRecord11(value) || typeof value.id !== "string" || typeof value.name !== "string") return void 0;
23921
+ if (!isRecord12(value) || typeof value.id !== "string" || typeof value.name !== "string") return void 0;
23437
23922
  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;
23923
+ const path = isRecord12(root) && typeof root.path === "string" && root.path.length > 0 ? root.path : void 0;
23439
23924
  return path === void 0 || !isAbsolute2(path) ? [] : [{ path }];
23440
23925
  }) : [];
23441
23926
  if (roots.length === 0) return void 0;
@@ -23449,7 +23934,7 @@ function sanitizeProject(value) {
23449
23934
  };
23450
23935
  }
23451
23936
  function sanitizeProjectList(result) {
23452
- if (!isRecord11(result) || !Array.isArray(result.data)) {
23937
+ if (!isRecord12(result) || !Array.isArray(result.data)) {
23453
23938
  throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid project list.");
23454
23939
  }
23455
23940
  const data2 = result.data.flatMap((value) => sanitizeProject(value) ?? []);
@@ -23459,7 +23944,7 @@ function sanitizeProjectList(result) {
23459
23944
  };
23460
23945
  }
23461
23946
  function sanitizeProjectCreate(result) {
23462
- const project = isRecord11(result) ? sanitizeProject(result.project) : void 0;
23947
+ const project = isRecord12(result) ? sanitizeProject(result.project) : void 0;
23463
23948
  if (project === void 0) {
23464
23949
  throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid created project.");
23465
23950
  }
@@ -23492,19 +23977,19 @@ function codexDirectoryCrumbs(root, path) {
23492
23977
  return crumbs2;
23493
23978
  }
23494
23979
  function extractThread(result) {
23495
- return isRecord11(result) && isRecord11(result.thread) ? result.thread : void 0;
23980
+ return isRecord12(result) && isRecord12(result.thread) ? result.thread : void 0;
23496
23981
  }
23497
23982
  function extractTurnId(result) {
23498
- if (!isRecord11(result)) return void 0;
23983
+ if (!isRecord12(result)) return void 0;
23499
23984
  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;
23985
+ if (isRecord12(result.turn) && typeof result.turn.id === "string" && result.turn.id.length > 0) return result.turn.id;
23501
23986
  return void 0;
23502
23987
  }
23503
23988
  function extractThreadId(params) {
23504
- if (!isRecord11(params)) return void 0;
23989
+ if (!isRecord12(params)) return void 0;
23505
23990
  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;
23991
+ if (isRecord12(params.thread) && typeof params.thread.id === "string") return params.thread.id;
23992
+ if (isRecord12(params.turn) && typeof params.turn.threadId === "string") return params.turn.threadId;
23508
23993
  return void 0;
23509
23994
  }
23510
23995
  function optionalInteger2(value) {
@@ -23546,13 +24031,13 @@ function mapCodexImageInputs(params) {
23546
24031
  return {
23547
24032
  ...params,
23548
24033
  input: params.input.map((value) => {
23549
- if (!isRecord11(value) || value.type !== "image" || typeof value.mediaType !== "string" || typeof value.data !== "string") return value;
24034
+ if (!isRecord12(value) || value.type !== "image" || typeof value.mediaType !== "string" || typeof value.data !== "string") return value;
23550
24035
  return { type: "image", url: `data:${value.mediaType};base64,${value.data}` };
23551
24036
  })
23552
24037
  };
23553
24038
  }
23554
24039
  function sanitizeApprovalParams(params, requestHandle) {
23555
- if (!isRecord11(params)) return { requestHandle };
24040
+ if (!isRecord12(params)) return { requestHandle };
23556
24041
  const safe = { ...params };
23557
24042
  delete safe.proposedExecpolicyAmendment;
23558
24043
  delete safe.additionalPermissions;
@@ -23583,7 +24068,7 @@ function isProjectListFallbackError(error) {
23583
24068
  function canTryNextBinary(error) {
23584
24069
  return !(error instanceof RpcError) || !["CODEX_AUTH_REQUIRED", "CODEX_CLOSED"].includes(error.code);
23585
24070
  }
23586
- function isRecord11(value) {
24071
+ function isRecord12(value) {
23587
24072
  return typeof value === "object" && value !== null && !Array.isArray(value);
23588
24073
  }
23589
24074
  function array2(value) {
@@ -24019,11 +24504,11 @@ function editableConfig(config) {
24019
24504
  ...config.acp === void 0 ? {} : { acp: { enabled: config.acp.enabled, backends: config.acp.backends } }
24020
24505
  };
24021
24506
  }
24022
- function isRecord12(value) {
24507
+ function isRecord13(value) {
24023
24508
  return typeof value === "object" && value !== null && !Array.isArray(value);
24024
24509
  }
24025
24510
  function record5(value) {
24026
- if (!isRecord12(value)) throw new ClientModeError("INVALID_MESSAGE", "The control request payload is invalid.");
24511
+ if (!isRecord13(value)) throw new ClientModeError("INVALID_MESSAGE", "The control request payload is invalid.");
24027
24512
  return value;
24028
24513
  }
24029
24514
  function ok3(value) {
@@ -24148,7 +24633,7 @@ var TerminalPolicy = class {
24148
24633
  };
24149
24634
 
24150
24635
  // src/service.ts
24151
- import { randomUUID as randomUUID3 } from "node:crypto";
24636
+ import { randomUUID as randomUUID4 } from "node:crypto";
24152
24637
 
24153
24638
  // src/connection-controller.ts
24154
24639
  var ConnectionController = class {
@@ -26824,11 +27309,11 @@ var HarnessRemoteBridge = class {
26824
27309
  };
26825
27310
  function normalizeWorkspaceChangesPayload(endpoint, payload, harnessVersion) {
26826
27311
  if (endpoint !== "workspaceFiles/changes" || !requiresWorkspaceChangePath(harnessVersion)) return payload;
26827
- if (!isRecord13(payload) || !isRecord13(payload.args) || Object.hasOwn(payload.args, "path")) return payload;
27312
+ if (!isRecord14(payload) || !isRecord14(payload.args) || Object.hasOwn(payload.args, "path")) return payload;
26828
27313
  return { ...payload, args: { ...payload.args, path: "." } };
26829
27314
  }
26830
27315
  function normalizeWorkspaceRequestPayload(endpoint, payload, harnessVersion) {
26831
- if (!isRecord13(payload) || !isRecord13(payload.args)) return payload;
27316
+ if (!isRecord14(payload) || !isRecord14(payload.args)) return payload;
26832
27317
  const args = payload.args;
26833
27318
  if (endpoint === "workspaceFiles/readBytes" && requiresWorkspaceChangePath(harnessVersion)) {
26834
27319
  if (!Object.hasOwn(args, "range") || Object.hasOwn(args, "options")) return payload;
@@ -26896,13 +27381,13 @@ function needsDirectoryFallback(result) {
26896
27381
  }
26897
27382
  function requestArgs(payload) {
26898
27383
  const root = record6(payload);
26899
- const args = isRecord13(root.args) ? root.args : root;
27384
+ const args = isRecord14(root.args) ? root.args : root;
26900
27385
  return record6(args.request ?? args._request ?? args);
26901
27386
  }
26902
27387
  function record6(value) {
26903
- return isRecord13(value) ? value : {};
27388
+ return isRecord14(value) ? value : {};
26904
27389
  }
26905
- function isRecord13(value) {
27390
+ function isRecord14(value) {
26906
27391
  return typeof value === "object" && value !== null && !Array.isArray(value);
26907
27392
  }
26908
27393
 
@@ -27022,8 +27507,8 @@ import { execFileSync as execFileSync2 } from "node:child_process";
27022
27507
 
27023
27508
  // src/codex-workspace-bridge.ts
27024
27509
  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";
27510
+ import { realpath as realpath2, lstat, readdir as readdir4, readFile as readFile5, stat as stat6, watch } from "node:fs/promises";
27511
+ import { isAbsolute as isAbsolute4, join as join8, relative as relative2, resolve as resolve3 } from "node:path";
27027
27512
  var MAX_READ_BYTES = 4 * 1024 * 1024;
27028
27513
  var MAX_INPUT_BYTES = 64 * 1024;
27029
27514
  var MAX_COLS = 240;
@@ -27113,7 +27598,7 @@ var CodexWorkspaceBridge = class {
27113
27598
  const entries = await readdir4(target2, { withFileTypes: true });
27114
27599
  const result = [];
27115
27600
  for (const entry of entries.slice(0, 500)) {
27116
- const item = join7(target2, entry.name);
27601
+ const item = join8(target2, entry.name);
27117
27602
  const info2 = await lstat(item);
27118
27603
  if (info2.isSymbolicLink()) continue;
27119
27604
  result.push({ name: entry.name, type: info2.isDirectory() ? "directory" : info2.isFile() ? "file" : "other", ...info2.isFile() ? { size: info2.size } : {} });
@@ -27129,7 +27614,7 @@ var CodexWorkspaceBridge = class {
27129
27614
  if (!info.isFile()) throw new RpcError("CODEX_WORKSPACE_INVALID_PATH", "The requested workspace path is not a file.");
27130
27615
  const offset = readOffset(args);
27131
27616
  const limit = readLimit(args);
27132
- const bytes = await readFile4(target2);
27617
+ const bytes = await readFile5(target2);
27133
27618
  if (bytes.byteLength > MAX_READ_BYTES) throw new RpcError("CODEX_WORKSPACE_TOO_LARGE", "The requested workspace file is too large.");
27134
27619
  if (endpoint === "workspaceFiles/readBytes") {
27135
27620
  const slice2 = bytes.subarray(offset, Math.min(offset + limit, bytes.length));
@@ -27253,7 +27738,7 @@ var CodexWorkspaceBridge = class {
27253
27738
  throw new RpcError("METHOD_NOT_FOUND", "The requested terminal method does not exist.");
27254
27739
  }
27255
27740
  async createTerminal(sessionId, cwd, args) {
27256
- const request = isRecord14(args.request) ? args.request : {};
27741
+ const request = isRecord15(args.request) ? args.request : {};
27257
27742
  const id4 = stringId(request.id);
27258
27743
  if ([...this.terminals.values()].some((item) => item.sessionId === sessionId && item.id === id4)) throw new RpcError("REQUEST_CONFLICT", "The terminal id is already active.");
27259
27744
  if (this.terminals.size >= MAX_TERMINALS) throw new RpcError("RATE_LIMITED", "Too many remote terminals are active.", void 0, true);
@@ -27454,10 +27939,10 @@ function screenOf(terminal) {
27454
27939
  return (terminal.truncated ? "\x1Bc" : "") + terminal.screen.join("");
27455
27940
  }
27456
27941
  function argsOf(payload) {
27457
- const value = isRecord14(payload) ? payload : {};
27458
- return isRecord14(value.args) ? value.args : value;
27942
+ const value = isRecord15(payload) ? payload : {};
27943
+ return isRecord15(value.args) ? value.args : value;
27459
27944
  }
27460
- function isRecord14(value) {
27945
+ function isRecord15(value) {
27461
27946
  return typeof value === "object" && value !== null && !Array.isArray(value);
27462
27947
  }
27463
27948
  function stringId(value) {
@@ -27468,9 +27953,9 @@ function bounded(value, fallback, max) {
27468
27953
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? Math.min(value, max) : fallback;
27469
27954
  }
27470
27955
  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;
27956
+ const options = isRecord15(args.options) ? args.options : void 0;
27957
+ if (options !== void 0 && isRecord15(options.range)) return options.range;
27958
+ return isRecord15(args.range) ? args.range : args;
27474
27959
  }
27475
27960
  function readOffset(args) {
27476
27961
  const range = byteOrLineRange(args);
@@ -27561,6 +28046,14 @@ var HostPluginRuntime = class {
27561
28046
  * device identity, unlike clearing the authorization.
27562
28047
  */
27563
28048
  paused;
28049
+ /**
28050
+ * Whether start() has finished wiring the Server connection.
28051
+ *
28052
+ * The Codex domain is optional business that waits on an external binary, so it runs in the
28053
+ * background; without this flag a Host that is merely still starting would report itself as
28054
+ * offline and look broken to the user and to other clients.
28055
+ */
28056
+ starting = true;
27564
28057
  harnessVersion;
27565
28058
  closed = false;
27566
28059
  codex;
@@ -27588,7 +28081,6 @@ var HostPluginRuntime = class {
27588
28081
  fingerprint: this.identity.fingerprint,
27589
28082
  server: this.config.serverUrl ?? "not configured"
27590
28083
  });
27591
- await this.codex.start();
27592
28084
  if (this.serverApi !== void 0) {
27593
28085
  this.harnessVersion = await this.readHarnessVersion();
27594
28086
  this.serverApi.setHarnessVersion(this.harnessVersion);
@@ -27596,6 +28088,9 @@ var HostPluginRuntime = class {
27596
28088
  this.serverConnection = this.createServerConnection(this.identity);
27597
28089
  if (!this.paused) this.serverConnection.start();
27598
28090
  }
28091
+ this.starting = false;
28092
+ void this.codex.start().catch(() => {
28093
+ });
27599
28094
  }
27600
28095
  currentIdentity() {
27601
28096
  if (this.identity === void 0) throw new Error("remote runtime has not started");
@@ -27613,6 +28108,7 @@ var HostPluginRuntime = class {
27613
28108
  configured: this.serverApi !== void 0,
27614
28109
  online: this.serverConnection?.isOnline() ?? false,
27615
28110
  reconnecting: this.serverConnection?.isReconnecting() ?? false,
28111
+ starting: this.starting,
27616
28112
  ...this.serverConnection?.lastActivity() === void 0 ? {} : { lastActiveAt: this.serverConnection.lastActivity() },
27617
28113
  ...error === void 0 ? {} : { error },
27618
28114
  ...authorization?.account === void 0 ? {} : { account: authorization.account },
@@ -27818,7 +28314,7 @@ var HostPluginRuntime = class {
27818
28314
  let reportedVersion;
27819
28315
  let errorCode6;
27820
28316
  try {
27821
- const response = await this.apiProxy?.host.describe({ rpcId: randomUUID3(), payload: {} });
28317
+ const response = await this.apiProxy?.host.describe({ rpcId: randomUUID4(), payload: {} });
27822
28318
  if (response === void 0) throw new Error("ApiProxy is unavailable");
27823
28319
  if (!response.result.ok) {
27824
28320
  errorCode6 = response.result.error.code;
@@ -27888,7 +28384,7 @@ function isPlainRecord(value) {
27888
28384
  // src/cli.ts
27889
28385
  import { stat as stat7 } from "node:fs/promises";
27890
28386
  import { hostname as hostname3 } from "node:os";
27891
- import { join as join8 } from "node:path";
28387
+ import { join as join9 } from "node:path";
27892
28388
  var QR_POLL_INTERVAL_MS = 2e3;
27893
28389
  var TERMINAL_QR_MARGIN = 4;
27894
28390
  async function runCli(args = process.argv.slice(2), dependencies = {}) {
@@ -27976,7 +28472,7 @@ async function status(args, runtime) {
27976
28472
  `Server: ${serverUrl}`,
27977
28473
  "Host control: enabled (dsh-TUI default)"
27978
28474
  ];
27979
- if (!await exists3(join8(directory, "device.json"))) {
28475
+ if (!await exists3(join9(directory, "device.json"))) {
27980
28476
  lines.push("Device: not initialized", "Authorization: logged out", "Credential: unavailable");
27981
28477
  write(runtime.stdout, `${lines.join("\n")}
27982
28478
 
@@ -28020,7 +28516,7 @@ async function logout(args, runtime) {
28020
28516
  const serverUrl = selectedServer();
28021
28517
  const root = new IdentityStore({ env: runtime.env }).directory;
28022
28518
  const directory = serverStorageDirectory(root, serverUrl, "host");
28023
- if (!await exists3(join8(directory, "device.json"))) {
28519
+ if (!await exists3(join9(directory, "device.json"))) {
28024
28520
  await new ServerCredentialStore(directory).clear();
28025
28521
  write(runtime.stdout, "This Host is already logged out.\n");
28026
28522
  return 0;
@@ -28306,7 +28802,7 @@ function remoteStatusLines(target2) {
28306
28802
  const diagnostics = runtime.diagnostics();
28307
28803
  const codex = runtime.codexStatus();
28308
28804
  const capabilities = new Set(diagnostics.capabilities);
28309
- const connection = status2.online ? "online" : status2.reconnecting ? "reconnecting" : status2.accountRequired ? "authorization required" : "offline";
28805
+ const connection = status2.online ? "online" : status2.starting ? "starting" : status2.reconnecting ? "reconnecting" : status2.accountRequired ? "authorization required" : "offline";
28310
28806
  return [
28311
28807
  `Server: ${config.serverUrl ?? "not configured"}`,
28312
28808
  "Host control: enabled",
@@ -28537,7 +29033,7 @@ function createRemoteFileContentProvider(call, options = {}) {
28537
29033
  const offset = request.offset + received;
28538
29034
  const range = await call("fileviewer.readRange", { path: locator, offset, length }, request.signal);
28539
29035
  if (range.offset !== offset) throw new Error("The Remote Host returned a mismatched file range.");
28540
- const bytes = decodeBase64(range.data);
29036
+ const bytes = decodeBase642(range.data);
28541
29037
  if (bytes.byteLength > length) throw new Error("The Remote Host returned more file bytes than requested.");
28542
29038
  chunks.push(bytes);
28543
29039
  received += bytes.byteLength;
@@ -28569,7 +29065,7 @@ function currentSaveAsAllowed(value) {
28569
29065
  function currentSaveAsMaxBytes(value) {
28570
29066
  return typeof value === "function" ? value() : value ?? REMOTE_FILE_SAVE_AS_MAX_BYTES;
28571
29067
  }
28572
- function decodeBase64(value) {
29068
+ function decodeBase642(value) {
28573
29069
  const binary = atob(value);
28574
29070
  const bytes = new Uint8Array(binary.length);
28575
29071
  for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
@@ -28722,7 +29218,10 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
28722
29218
  apiProxy,
28723
29219
  nativeTypertGateway,
28724
29220
  logger,
28725
- hostControl
29221
+ hostControl,
29222
+ // The RTC factory keeps its default; the target store lives beside the plugin state.
29223
+ void 0,
29224
+ new ClientTargetStore(defaultIdentityDirectory)
28726
29225
  );
28727
29226
  }
28728
29227
  const controlRuntime = connection === void 0 ? void 0 : new PluginControlRuntime(config, defaultIdentityDirectory, settingsBinding, clientRuntime, hostControl, deepseekSession);
@@ -28737,6 +29236,7 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
28737
29236
  await runtime.start();
28738
29237
  if (clientRuntime !== void 0) {
28739
29238
  await clientRuntime.start();
29239
+ await clientRuntime.restoreLastTarget();
28740
29240
  } else {
28741
29241
  logger.warn("client remote mode is unavailable", {
28742
29242
  serverConfigured: config.serverUrl !== void 0,