ds-harness-remote 0.5.11 → 0.5.13

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 (71) hide show
  1. package/dist/acp/adapters/isolated-process.d.ts.map +1 -1
  2. package/dist/acp/adapters/isolated-process.js +12 -3
  3. package/dist/acp/adapters/isolated-process.js.map +1 -1
  4. package/dist/acp/adapters/opencode-native-api.d.ts +4 -2
  5. package/dist/acp/adapters/opencode-native-api.d.ts.map +1 -1
  6. package/dist/acp/adapters/opencode-native-api.js +29 -3
  7. package/dist/acp/adapters/opencode-native-api.js.map +1 -1
  8. package/dist/acp/adapters/opencode-process.d.ts +2 -0
  9. package/dist/acp/adapters/opencode-process.d.ts.map +1 -1
  10. package/dist/acp/adapters/opencode-process.js +64 -24
  11. package/dist/acp/adapters/opencode-process.js.map +1 -1
  12. package/dist/acp/gateway.d.ts +3 -0
  13. package/dist/acp/gateway.d.ts.map +1 -1
  14. package/dist/acp/gateway.js +77 -13
  15. package/dist/acp/gateway.js.map +1 -1
  16. package/dist/acp/peer-bridge.d.ts.map +1 -1
  17. package/dist/acp/peer-bridge.js +2 -1
  18. package/dist/acp/peer-bridge.js.map +1 -1
  19. package/dist/acp/virtual-contract.d.ts +1 -0
  20. package/dist/acp/virtual-contract.d.ts.map +1 -1
  21. package/dist/acp/virtual-contract.js.map +1 -1
  22. package/dist/acp/virtual-harness.d.ts +1 -2
  23. package/dist/acp/virtual-harness.d.ts.map +1 -1
  24. package/dist/acp/virtual-harness.js +15 -23
  25. package/dist/acp/virtual-harness.js.map +1 -1
  26. package/dist/acp/virtual-live.d.ts +1 -0
  27. package/dist/acp/virtual-live.d.ts.map +1 -1
  28. package/dist/acp/virtual-live.js +6 -0
  29. package/dist/acp/virtual-live.js.map +1 -1
  30. package/dist/acp/virtual-titles.d.ts +17 -0
  31. package/dist/acp/virtual-titles.d.ts.map +1 -0
  32. package/dist/acp/virtual-titles.js +99 -0
  33. package/dist/acp/virtual-titles.js.map +1 -0
  34. package/dist/codex/app-server.d.ts +10 -0
  35. package/dist/codex/app-server.d.ts.map +1 -1
  36. package/dist/codex/app-server.js +24 -10
  37. package/dist/codex/app-server.js.map +1 -1
  38. package/dist/codex/domain.d.ts +3 -0
  39. package/dist/codex/domain.d.ts.map +1 -1
  40. package/dist/codex/domain.js +93 -17
  41. package/dist/codex/domain.js.map +1 -1
  42. package/dist/codex/peer-bridge.d.ts.map +1 -1
  43. package/dist/codex/peer-bridge.js +7 -5
  44. package/dist/codex/peer-bridge.js.map +1 -1
  45. package/dist/codex/virtual-harness.d.ts.map +1 -1
  46. package/dist/codex/virtual-harness.js +16 -3
  47. package/dist/codex/virtual-harness.js.map +1 -1
  48. package/dist/index.js +691 -146
  49. package/dist/index.js.map +4 -4
  50. package/dist/rpc-router.d.ts +4 -2
  51. package/dist/rpc-router.d.ts.map +1 -1
  52. package/dist/rpc-router.js +19 -2
  53. package/dist/rpc-router.js.map +1 -1
  54. package/dist/service.d.ts.map +1 -1
  55. package/dist/service.js +4 -2
  56. package/dist/service.js.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/dist/workspace-bridge.d.ts.map +1 -1
  60. package/dist/workspace-bridge.js +26 -4
  61. package/dist/workspace-bridge.js.map +1 -1
  62. package/dist/workspace-upload-authority.d.ts +4 -0
  63. package/dist/workspace-upload-authority.d.ts.map +1 -0
  64. package/dist/workspace-upload-authority.js +25 -0
  65. package/dist/workspace-upload-authority.js.map +1 -0
  66. package/dist/workspace-upload.d.ts +27 -0
  67. package/dist/workspace-upload.d.ts.map +1 -0
  68. package/dist/workspace-upload.js +226 -0
  69. package/dist/workspace-upload.js.map +1 -0
  70. package/dsh-plugin.json +1 -1
  71. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9574,6 +9574,10 @@ var controlFrameTypes = [
9574
9574
  "error"
9575
9575
  ];
9576
9576
  var rpcMethods = [
9577
+ "workspace.upload.open",
9578
+ "workspace.upload.chunk",
9579
+ "workspace.upload.commit",
9580
+ "workspace.upload.abort",
9577
9581
  "harness.transport.describe",
9578
9582
  "harness.api.call",
9579
9583
  "harness.api.transfer.open",
@@ -11747,6 +11751,12 @@ var AcpLiveProjection = class {
11747
11751
  const update = isRecord3(params.update) ? params.update : params;
11748
11752
  const kind = string(update.sessionUpdate) ?? string(update.type);
11749
11753
  const session = this.context.sessions.get(follow.sessionId);
11754
+ if (kind === "session_info_update") {
11755
+ if (session && params.sessionId === follow.acpSessionId && typeof update.title === "string") {
11756
+ this.context.applySessionTitle(session, update.title);
11757
+ }
11758
+ return;
11759
+ }
11750
11760
  if (kind === "model_selection" && isRecord3(update.selected) && update.selected.provider === this.context.backend && typeof update.selected.model === "string") {
11751
11761
  this.context.selectedModels.set(follow.sessionId, update.selected);
11752
11762
  this.context.publishProjection(follow.sessionId, "modelSelection", this.context.modelSelectionProjection(follow.sessionId), this.context.nextProjectionSeq());
@@ -12111,6 +12121,86 @@ var AcpLiveProjection = class {
12111
12121
  }
12112
12122
  };
12113
12123
 
12124
+ // src/acp/virtual-titles.ts
12125
+ var OPENCODE_DEFAULT_TITLE = /^(?:New|Child) session - \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
12126
+ var AcpSessionTitles = class {
12127
+ constructor(backend, client, sessions, publish) {
12128
+ this.backend = backend;
12129
+ this.client = client;
12130
+ this.sessions = sessions;
12131
+ this.publish = publish;
12132
+ }
12133
+ pending = /* @__PURE__ */ new Map();
12134
+ closed = false;
12135
+ nativeTitle(title) {
12136
+ const next = title?.trim();
12137
+ if (!next || next === "Untitled Session" || this.backend === "opencode" && OPENCODE_DEFAULT_TITLE.test(next)) return void 0;
12138
+ return next;
12139
+ }
12140
+ applyNative(session, title) {
12141
+ const next = this.nativeTitle(title);
12142
+ if (this.closed || session.titleOverridden || !next || this.sessions.get(session.sessionId) !== session) return;
12143
+ session.titleFromPrompt = false;
12144
+ this.pending.get(session.sessionId)?.abort();
12145
+ if (next === session.title) return;
12146
+ session.title = next;
12147
+ this.publish(session.sessionId, next);
12148
+ }
12149
+ applyPrompt(session, text) {
12150
+ if (this.backend !== "opencode" || this.closed || session.titleOverridden || session.titleFromPrompt || !session.blank && this.nativeTitle(session.title) !== void 0) return;
12151
+ const next = Array.from(text?.replace(/\s+/g, " ").trim() ?? "").slice(0, 60).join("");
12152
+ if (!next) return;
12153
+ session.titleFromPrompt = true;
12154
+ session.title = next;
12155
+ this.publish(session.sessionId, next);
12156
+ }
12157
+ async refresh(session) {
12158
+ if (this.backend === "cursor" || this.closed || session.titleOverridden || !this.client.listSessions) return;
12159
+ this.pending.get(session.sessionId)?.abort();
12160
+ const controller = new AbortController();
12161
+ const signal = controller.signal;
12162
+ this.pending.set(session.sessionId, controller);
12163
+ const deadline = this.backend === "opencode" ? setTimeout(() => controller.abort(), 2e4) : void 0;
12164
+ try {
12165
+ for (const delay of this.backend === "opencode" ? [0, 1e3, 3e3, 6e3, 9e3] : [0]) {
12166
+ if (delay) await waitForTitle(delay, signal);
12167
+ if (signal.aborted || this.closed || session.titleOverridden) return;
12168
+ const previous = session.title;
12169
+ try {
12170
+ const items = await this.client.listSessions(session.cwd, this.backend, 30, signal);
12171
+ if (signal.aborted || this.sessions.get(session.sessionId) !== session || session.title !== previous) return;
12172
+ const current = items.find((item) => item.conversationId === session.acpSessionId);
12173
+ if (current && this.nativeTitle(current.title) !== void 0) {
12174
+ this.applyNative(session, current.title);
12175
+ return;
12176
+ }
12177
+ } catch {
12178
+ }
12179
+ }
12180
+ } finally {
12181
+ if (deadline !== void 0) clearTimeout(deadline);
12182
+ if (this.pending.get(session.sessionId) === controller) this.pending.delete(session.sessionId);
12183
+ }
12184
+ }
12185
+ close() {
12186
+ this.closed = true;
12187
+ for (const controller of this.pending.values()) controller.abort();
12188
+ this.pending.clear();
12189
+ }
12190
+ };
12191
+ function waitForTitle(delay, signal) {
12192
+ return new Promise((resolve6) => {
12193
+ const finish = () => {
12194
+ clearTimeout(timer);
12195
+ signal.removeEventListener("abort", finish);
12196
+ resolve6();
12197
+ };
12198
+ const timer = setTimeout(finish, delay);
12199
+ signal.addEventListener("abort", finish, { once: true });
12200
+ if (signal.aborted) finish();
12201
+ });
12202
+ }
12203
+
12114
12204
  // src/acp/virtual-harness.ts
12115
12205
  var AcpVirtualHarness = class _AcpVirtualHarness {
12116
12206
  constructor(client, host, backend = "cursor", hostCarrier) {
@@ -12142,7 +12232,8 @@ var AcpVirtualHarness = class _AcpVirtualHarness {
12142
12232
  },
12143
12233
  publishProjection: (...args) => owner.events.publishProjection(...args),
12144
12234
  nextProjectionSeq: (...args) => owner.nextProjectionSeq(...args),
12145
- refreshSessionTitle: (...args) => owner.refreshSessionTitle(...args),
12235
+ refreshSessionTitle: (...args) => owner.titles.refresh(...args),
12236
+ applySessionTitle: (...args) => owner.titles.applyNative(...args),
12146
12237
  publishWorkspaceBaseline: () => owner.publishWorkspaceBaseline(),
12147
12238
  get pendingApprovals() {
12148
12239
  return owner.pendingApprovals;
@@ -12190,6 +12281,12 @@ var AcpVirtualHarness = class _AcpVirtualHarness {
12190
12281
  },
12191
12282
  nextProjectionSeq: (...args) => owner.nextProjectionSeq(...args)
12192
12283
  });
12284
+ this.titles = new AcpSessionTitles(
12285
+ backend,
12286
+ client,
12287
+ this.sessions,
12288
+ (sessionId, title) => this.events.publishProjection(sessionId, "title", title)
12289
+ );
12193
12290
  this.api = this.events.createApiProxy();
12194
12291
  this.unsubscribeArchived = client.onSessionArchived?.((data2) => {
12195
12292
  if (this.closed || data2.backend !== this.backend) return;
@@ -12198,6 +12295,7 @@ var AcpVirtualHarness = class _AcpVirtualHarness {
12198
12295
  }
12199
12296
  live;
12200
12297
  events;
12298
+ titles;
12201
12299
  api;
12202
12300
  workspaceById = /* @__PURE__ */ new Map();
12203
12301
  sessions = /* @__PURE__ */ new Map();
@@ -12408,6 +12506,7 @@ var AcpVirtualHarness = class _AcpVirtualHarness {
12408
12506
  async close() {
12409
12507
  if (this.closed) return;
12410
12508
  this.closed = true;
12509
+ this.titles.close();
12411
12510
  this.unsubscribeArchived?.();
12412
12511
  for (const stream of this.workspaceStreams) stream.close();
12413
12512
  for (const stream of this.controlStreams) stream.close();
@@ -12563,6 +12662,7 @@ var AcpVirtualHarness = class _AcpVirtualHarness {
12563
12662
  source: requestId ? { kind: "user", rpcId: requestId } : { kind: "user" }
12564
12663
  });
12565
12664
  }
12665
+ this.titles.applyPrompt(session, text);
12566
12666
  session.blank = false;
12567
12667
  session.running = true;
12568
12668
  session.updatedAt = Date.now();
@@ -12639,6 +12739,8 @@ var AcpVirtualHarness = class _AcpVirtualHarness {
12639
12739
  }
12640
12740
  if (events.length > 0) {
12641
12741
  session.events = events;
12742
+ const firstUser = session.events.find((entry) => entry.event.type === "user/message");
12743
+ this.titles.applyPrompt(session, extractPromptText(array(record(firstUser?.event.data).content)));
12642
12744
  session.blank = false;
12643
12745
  for (const event of session.events) this.cacheImageAttachments(session.sessionId, array(record(event.event.data).content ?? record(record(event.event.data).message).content));
12644
12746
  }
@@ -12729,11 +12831,12 @@ var AcpVirtualHarness = class _AcpVirtualHarness {
12729
12831
  return success(await this.client.listDirectory(path, signal));
12730
12832
  }
12731
12833
  registerSession(acpSessionId, cwd2, title, createdAt = Date.now(), updatedAt = Date.now(), blank = true) {
12834
+ const nativeTitle = this.titles.nativeTitle(title);
12732
12835
  const session = {
12733
12836
  sessionId: `${sessionPrefix(this.backend)}${acpSessionId}`,
12734
12837
  acpSessionId,
12735
12838
  cwd: cwd2,
12736
- ...title === void 0 ? {} : { title },
12839
+ ...nativeTitle === void 0 ? {} : { title: nativeTitle },
12737
12840
  blank,
12738
12841
  running: false,
12739
12842
  createdAt,
@@ -12743,23 +12846,9 @@ var AcpVirtualHarness = class _AcpVirtualHarness {
12743
12846
  this.sessions.set(session.sessionId, session);
12744
12847
  return session;
12745
12848
  }
12746
- applyDiscoveredTitle(session, title) {
12747
- const next = title.trim();
12748
- if (this.closed || session.titleOverridden || !next || next === "Untitled Session" || next === session.title) return;
12749
- session.title = next;
12750
- this.events.publishProjection(session.sessionId, "title", next);
12751
- }
12752
- async refreshSessionTitle(session) {
12753
- if (this.backend === "cursor" || this.closed || session.titleOverridden || this.client.listSessions === void 0) return;
12754
- try {
12755
- const items = await this.client.listSessions(session.cwd, this.backend, 30);
12756
- const current = items.find((item) => item.conversationId === session.acpSessionId);
12757
- if (current !== void 0 && this.sessions.get(session.sessionId) === session) this.applyDiscoveredTitle(session, current.title);
12758
- } catch {
12759
- }
12760
- }
12761
12849
  async discoverAndAttachSessions(workspace) {
12762
12850
  if (this.backend === "cursor") return;
12851
+ const previousTitles = new Map([...this.sessions].map(([id5, session]) => [id5, session.title]));
12763
12852
  try {
12764
12853
  let discovered = [];
12765
12854
  if (this.client.listSessions !== void 0) {
@@ -12777,7 +12866,9 @@ var AcpVirtualHarness = class _AcpVirtualHarness {
12777
12866
  const existing = this.sessions.get(sessionId);
12778
12867
  if (existing !== void 0) {
12779
12868
  if (Number.isFinite(item.updatedAt)) existing.updatedAt = Math.max(existing.updatedAt, item.updatedAt);
12780
- this.applyDiscoveredTitle(existing, item.title);
12869
+ if (!previousTitles.has(sessionId) || existing.title === previousTitles.get(sessionId)) {
12870
+ this.titles.applyNative(existing, item.title);
12871
+ }
12781
12872
  }
12782
12873
  if (!this.archivedSessionIds.has(sessionId) && !workspace.sessionIds.includes(sessionId)) {
12783
12874
  workspace.sessionIds.push(sessionId);
@@ -14103,11 +14194,11 @@ function isLanCandidatePair(localType, remoteType, localScope, remoteScope) {
14103
14194
  return true;
14104
14195
  return localType === "host" && isLocalNetworkScope(localScope) && remoteType === "prflx" && remoteScope === "unknown" || remoteType === "host" && isLocalNetworkScope(remoteScope) && localType === "prflx" && localScope === "unknown";
14105
14196
  }
14106
- function isLocalNetworkScope(scope) {
14107
- return scope === "private" || scope === "link-local" || scope === "loopback";
14197
+ function isLocalNetworkScope(scope2) {
14198
+ return scope2 === "private" || scope2 === "link-local" || scope2 === "loopback";
14108
14199
  }
14109
- function isExplicitlyNonLanScope(scope) {
14110
- return scope === "public" || scope === "cgnat" || scope === "reserved";
14200
+ function isExplicitlyNonLanScope(scope2) {
14201
+ return scope2 === "public" || scope2 === "cgnat" || scope2 === "reserved";
14111
14202
  }
14112
14203
  function increment(map, key) {
14113
14204
  map[key] = (map[key] ?? 0) + 1;
@@ -25962,8 +26053,15 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
25962
26053
  }, signal));
25963
26054
  } catch (error) {
25964
26055
  if (!isLegacySessionHistoryUnsupported(error)) throw error;
25965
- const result = record4(await this.client.request("thread/read", { threadId, includeTurns: true }, signal));
25966
- const thread = record4(result.thread);
26056
+ let thread;
26057
+ try {
26058
+ const result = record4(await this.client.request("thread/read", { threadId, includeTurns: true }, signal));
26059
+ thread = record4(result.thread);
26060
+ } catch (historyError) {
26061
+ const code = historyError instanceof Error ? errorCode(historyError) : void 0;
26062
+ if (!this.blankThreads.has(threadId) || code !== "CODEX_UPSTREAM_ERROR") throw historyError;
26063
+ thread = { id: threadId, turns: [] };
26064
+ }
25967
26065
  if (string2(thread.id) !== threadId) throw new Error("CodeX returned an invalid Thread history.");
25968
26066
  value = paginateCodexNativeHistory(
25969
26067
  projectCodexNativeHistory(thread, `codex:${threadId}`, this.sessionGeneration),
@@ -26949,11 +27047,11 @@ function routeStreamEvent2(event, streamId, queue) {
26949
27047
  const data2 = event.data;
26950
27048
  if (data2.streamId !== streamId) return;
26951
27049
  if (data2.reason === "failed") {
26952
- const failure3 = data2.failure;
27050
+ const failure4 = data2.failure;
26953
27051
  queue.fail(remoteFailure({
26954
- code: typeof failure3?.code === "string" ? failure3.code : "internal",
26955
- message: typeof failure3?.message === "string" ? failure3.message : "The remote Harness stream failed.",
26956
- details: isRecord9(failure3?.details) ? failure3.details : {}
27052
+ code: typeof failure4?.code === "string" ? failure4.code : "internal",
27053
+ message: typeof failure4?.message === "string" ? failure4.message : "The remote Harness stream failed.",
27054
+ details: isRecord9(failure4?.details) ? failure4.details : {}
26957
27055
  }));
26958
27056
  } else {
26959
27057
  queue.close();
@@ -26970,11 +27068,11 @@ function parseRpcResult(value) {
26970
27068
  error: { code: value.error.code, message: value.error.message, details: value.error.details }
26971
27069
  };
26972
27070
  }
26973
- function remoteFailure(failure3) {
26974
- return Object.assign(new Error(failure3.message), {
27071
+ function remoteFailure(failure4) {
27072
+ return Object.assign(new Error(failure4.message), {
26975
27073
  isDSHRemoteError: true,
26976
- code: failure3.code,
26977
- details: failure3.details
27074
+ code: failure4.code,
27075
+ details: failure4.details
26978
27076
  });
26979
27077
  }
26980
27078
  function bytesToBase643(bytes) {
@@ -27306,7 +27404,7 @@ function normalizeServerUrl(value) {
27306
27404
  }
27307
27405
 
27308
27406
  // src/version.ts
27309
- var PLUGIN_VERSION = "0.5.11";
27407
+ var PLUGIN_VERSION = "0.5.13";
27310
27408
 
27311
27409
  // src/server-api.ts
27312
27410
  var TERMINAL_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/u;
@@ -28072,7 +28170,7 @@ var TypertGatewaySwitch = class {
28072
28170
  return { ok: false, error: this.failure(error) };
28073
28171
  }
28074
28172
  }) : (endpoint, payload, signal) => this.localDispatch(endpoint, payload, signal, peer);
28075
- const open = this.localOpen === void 0 ? (async (endpoint, payload, signal) => {
28173
+ const open3 = this.localOpen === void 0 ? (async (endpoint, payload, signal) => {
28076
28174
  if (this.localStream === void 0) throw new Error("The local Harness Gateway does not support Remote streams.");
28077
28175
  return this.localStream({ ...requestFromCarrier(endpoint, payload, signal), ...peer === void 0 ? {} : { peer } });
28078
28176
  }) : (endpoint, payload, signal, uplink) => this.localOpen(endpoint, payload, signal, uplink, peer);
@@ -28081,7 +28179,7 @@ var TypertGatewaySwitch = class {
28081
28179
  peer,
28082
28180
  ...this.localStream === void 0 ? {} : { stream: (request) => this.localStream({ ...request, ...peer === void 0 ? {} : { peer } }) },
28083
28181
  dispatch,
28084
- open,
28182
+ open: open3,
28085
28183
  failure: (error) => this.failure(error),
28086
28184
  supportsCarrier: this.localDispatch !== void 0 && this.localOpen !== void 0
28087
28185
  };
@@ -28108,12 +28206,12 @@ var TypertGatewaySwitch = class {
28108
28206
  };
28109
28207
  }
28110
28208
  if (this.originalOpen !== void 0) {
28111
- const open = this.originalOpen;
28112
- const rc1 = usesRc1Arity(open);
28209
+ const open3 = this.originalOpen;
28210
+ const rc1 = usesRc1Arity(open3);
28113
28211
  this.runtime.openWireStream = (...callArgs) => {
28114
28212
  const endpoint = callArgs[0];
28115
28213
  if (this.remoteTarget === void 0 || isLocalOnlyEndpoint(endpoint)) {
28116
- return rc1 ? Reflect.apply(open, this.runtime, callArgs) : open.call(this.runtime, endpoint, callArgs[1], callArgs[2]);
28214
+ return rc1 ? Reflect.apply(open3, this.runtime, callArgs) : open3.call(this.runtime, endpoint, callArgs[1], callArgs[2]);
28117
28215
  }
28118
28216
  const signal = rc1 ? callArgs[4] : callArgs[2];
28119
28217
  return this.openRemote(endpoint, callArgs[1], signal ?? new AbortController().signal, rc1 ? callArgs[2] : void 0);
@@ -28197,8 +28295,8 @@ function workspaceConnectionResult(endpoint, result) {
28197
28295
  ]
28198
28296
  };
28199
28297
  }
28200
- function usesRc1Arity(open) {
28201
- return open.length >= 5;
28298
+ function usesRc1Arity(open3) {
28299
+ return open3.length >= 5;
28202
28300
  }
28203
28301
  function endedUplink() {
28204
28302
  return {
@@ -28214,9 +28312,9 @@ function linkControl(signal) {
28214
28312
  return control;
28215
28313
  }
28216
28314
  function createLocalOpen(gateway, runtime) {
28217
- const open = runtime.openWireStream;
28218
- if (open !== void 0) {
28219
- return usesRc1Arity(open) ? (endpoint, payload, signal, uplink, peer) => open.call(runtime, endpoint, payload, uplink ?? endedUplink(), peer, signal, linkControl(signal)) : (endpoint, payload, signal) => open.call(runtime, endpoint, payload, signal);
28315
+ const open3 = runtime.openWireStream;
28316
+ if (open3 !== void 0) {
28317
+ return usesRc1Arity(open3) ? (endpoint, payload, signal, uplink, peer) => open3.call(runtime, endpoint, payload, uplink ?? endedUplink(), peer, signal, linkControl(signal)) : (endpoint, payload, signal) => open3.call(runtime, endpoint, payload, signal);
28220
28318
  }
28221
28319
  const wire = gateway.wireStream;
28222
28320
  if (wire === void 0) return void 0;
@@ -28359,10 +28457,10 @@ function isUsableExternalNode(candidate, requireFrom) {
28359
28457
  }
28360
28458
  function isExecutableFile(candidate) {
28361
28459
  try {
28362
- const stat8 = statSync(candidate);
28363
- if (!stat8.isFile()) return false;
28460
+ const stat9 = statSync(candidate);
28461
+ if (!stat9.isFile()) return false;
28364
28462
  if (process.platform === "win32") return true;
28365
- return (stat8.mode & 73) !== 0;
28463
+ return (stat9.mode & 73) !== 0;
28366
28464
  } catch {
28367
28465
  return false;
28368
28466
  }
@@ -30179,8 +30277,8 @@ var IdentityStore = class {
30179
30277
  };
30180
30278
  function serverStorageDirectory(root, serverUrl, role) {
30181
30279
  const origin = new URL(serverUrl).origin;
30182
- const scope = createHash("sha256").update(origin).digest("hex").slice(0, 24);
30183
- return join4(root, "servers", scope, role);
30280
+ const scope2 = createHash("sha256").update(origin).digest("hex").slice(0, 24);
30281
+ return join4(root, "servers", scope2, role);
30184
30282
  }
30185
30283
  function fingerprint(publicKey) {
30186
30284
  const compact = createHash("sha256").update(fromBase64Url2(publicKey)).digest("hex").slice(0, 12).toUpperCase();
@@ -30563,7 +30661,7 @@ var CodexAppServerClient = class {
30563
30661
  const pending = this.takePending(value.id);
30564
30662
  if (pending === void 0) return;
30565
30663
  if ("error" in value && value.error !== void 0) {
30566
- pending.reject(new CodexAppServerError("CODEX_UPSTREAM_ERROR", safeUpstreamError(value.error)));
30664
+ pending.reject(safeUpstreamError(value.error));
30567
30665
  } else {
30568
30666
  pending.resolve(value.result);
30569
30667
  }
@@ -30599,12 +30697,26 @@ var CodexAppServerClient = class {
30599
30697
  for (const id5 of [...this.pending.keys()]) this.takePending(id5)?.reject(error);
30600
30698
  }
30601
30699
  };
30700
+ var CODEX_THREAD_UNMATERIALIZED = "CODEX_THREAD_UNMATERIALIZED";
30701
+ var CODEX_UNMATERIALIZED_THREAD_MESSAGE = "Codex thread has no turns yet.";
30602
30702
  function safeUpstreamError(value) {
30603
- if (!isRecord12(value) || typeof value.message !== "string") return "Codex App Server rejected the request.";
30703
+ if (!isRecord12(value) || typeof value.message !== "string") {
30704
+ return new CodexAppServerError("CODEX_UPSTREAM_ERROR", "Codex App Server rejected the request.");
30705
+ }
30604
30706
  const message = value.message.toLowerCase();
30605
- if (message.includes("active writer")) return "Codex thread already has an active writer.";
30606
- if (message.includes("no rollout found")) return "Codex thread state is not persisted yet.";
30607
- return message.includes("not initialized") ? "Codex App Server is not initialized." : "Codex App Server rejected the request.";
30707
+ if (message.includes("active writer")) {
30708
+ return new CodexAppServerError("CODEX_UPSTREAM_ERROR", "Codex thread already has an active writer.");
30709
+ }
30710
+ if (message.includes("not materialized yet") || message.includes("before first user message")) {
30711
+ return new CodexAppServerError(CODEX_THREAD_UNMATERIALIZED, CODEX_UNMATERIALIZED_THREAD_MESSAGE);
30712
+ }
30713
+ if (message.includes("no rollout found")) {
30714
+ return new CodexAppServerError("CODEX_UPSTREAM_ERROR", "Codex thread state is not persisted yet.");
30715
+ }
30716
+ return new CodexAppServerError(
30717
+ "CODEX_UPSTREAM_ERROR",
30718
+ message.includes("not initialized") ? "Codex App Server is not initialized." : "Codex App Server rejected the request."
30719
+ );
30608
30720
  }
30609
30721
  function isRecord12(value) {
30610
30722
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -30788,6 +30900,7 @@ var CodexPeerBridge = class {
30788
30900
  method: "thread/read",
30789
30901
  params: { threadId: params.threadId, includeTurns: false }
30790
30902
  });
30903
+ this.requireOpen();
30791
30904
  this.streams.set(params.streamId, params.threadId);
30792
30905
  return { opened: true, streamId: params.streamId, threadId: params.threadId };
30793
30906
  }
@@ -30868,6 +30981,7 @@ var CodexPeerBridge = class {
30868
30981
  });
30869
30982
  throw error;
30870
30983
  }
30984
+ this.requireOpen();
30871
30985
  const responseBytes = new TextEncoder().encode(JSON.stringify(response));
30872
30986
  if (responseBytes.byteLength <= INLINE_TRANSFER_RESPONSE_BYTES) return { kind: "inline", response };
30873
30987
  if (responseBytes.byteLength > MAX_CODEX_APP_TRANSFER_BYTES) {
@@ -30950,11 +31064,11 @@ var CodexPeerBridge = class {
30950
31064
  this.streams.clear();
30951
31065
  this.incomingTransfers.clear();
30952
31066
  this.outgoingTransfers.clear();
30953
- await Promise.all(streamIds.map((streamId) => this.publish("codex.app.stream.closed", {
31067
+ const detach = this.domain.detachPeer(this.context.connectionId);
31068
+ await Promise.all([detach, ...streamIds.map((streamId) => this.publish("codex.app.stream.closed", {
30954
31069
  streamId,
30955
31070
  reason: "peer-disconnected"
30956
- }).catch(() => void 0)));
30957
- await this.domain.detachPeer(this.context.connectionId);
31071
+ }).catch(() => void 0))]);
30958
31072
  }
30959
31073
  pruneTransfers() {
30960
31074
  const staleBefore = Date.now() - TRANSFER_IDLE_MS;
@@ -31066,7 +31180,9 @@ var CodexRemoteDomain = class {
31066
31180
  async resolveThreadWorkspace(connectionId, threadId) {
31067
31181
  if (!this.peers.has(connectionId)) throw new RpcError("CODEX_THREAD_UNAVAILABLE", "The CodeX thread is not available on this connection.");
31068
31182
  try {
31183
+ const assertCurrent = this.captureCallScope(connectionId);
31069
31184
  const known = await this.readKnownThread(threadId);
31185
+ assertCurrent();
31070
31186
  return known.cwd;
31071
31187
  } catch (error) {
31072
31188
  if (error instanceof RpcError && error.code === "CODEX_THREAD_NOT_ALLOWED") {
@@ -31084,7 +31200,25 @@ var CodexRemoteDomain = class {
31084
31200
  async call(connectionId, input3) {
31085
31201
  const envelope = parseCallEnvelope(input3);
31086
31202
  const call = parseCodexCall(envelope.method, envelope.params);
31087
- this.requireAppServer();
31203
+ const assertCurrent = this.captureCallScope(connectionId);
31204
+ const result = await this.callInternal(connectionId, call, assertCurrent);
31205
+ assertCurrent();
31206
+ return result;
31207
+ }
31208
+ captureCallScope(connectionId) {
31209
+ const appServer = this.requireAppServer();
31210
+ const peer = this.peers.get(connectionId);
31211
+ return () => {
31212
+ if (this.closed || this.appServer !== appServer || !this.isAvailable()) {
31213
+ throw new RpcError("CODEX_UNAVAILABLE", "The Codex App Server changed while handling the request.");
31214
+ }
31215
+ if (peer !== void 0 && this.peers.get(connectionId) !== peer) {
31216
+ throw new RpcError("CODEX_CONNECTION_CLOSED", "The Codex connection is closed.");
31217
+ }
31218
+ };
31219
+ }
31220
+ async callInternal(connectionId, call, assertCurrent) {
31221
+ assertCurrent();
31088
31222
  if (call.method === "account/read") {
31089
31223
  return sanitizeAccount(await this.callUpstream(call.method, call.params));
31090
31224
  }
@@ -31094,6 +31228,7 @@ var CodexRemoteDomain = class {
31094
31228
  if (call.method === "project/create") {
31095
31229
  const roots = call.params.roots;
31096
31230
  const path = await this.requireNewCodexProjectPath(roots[0].path);
31231
+ assertCurrent();
31097
31232
  return sanitizeProjectCreate(await this.callUpstream(call.method, {
31098
31233
  name: call.params.name,
31099
31234
  roots: [{ path }],
@@ -31109,6 +31244,7 @@ var CodexRemoteDomain = class {
31109
31244
  }
31110
31245
  if (call.method === "thread/start") {
31111
31246
  const cwd2 = await this.requireCodexWorkspacePath(call.params.cwd);
31247
+ assertCurrent();
31112
31248
  const permission = codexPermission(call.params, "workspace-write");
31113
31249
  const result = await this.callUpstream(call.method, {
31114
31250
  ...permission.params,
@@ -31116,6 +31252,7 @@ var CodexRemoteDomain = class {
31116
31252
  ...codexThreadPermissionParams(permission),
31117
31253
  serviceName: "deepseek_harness_remote"
31118
31254
  });
31255
+ assertCurrent();
31119
31256
  if (extractThread(result)?.id === void 0) {
31120
31257
  throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid thread.");
31121
31258
  }
@@ -31154,12 +31291,13 @@ var CodexRemoteDomain = class {
31154
31291
  return this.listCodexDirectory(call.params.path);
31155
31292
  }
31156
31293
  const allowedThread = threadId === void 0 ? void 0 : await this.readKnownThread(threadId);
31294
+ assertCurrent();
31157
31295
  if (call.method === "thread/resume" && call.params.permissionPreset !== void 0) {
31158
31296
  const owner = this.turnOwners.get(threadId);
31159
31297
  if (owner !== void 0 && owner.connectionId !== connectionId) {
31160
31298
  throw new RpcError("CODEX_THREAD_BUSY", "Another Remote client is writing to this CodeX thread.");
31161
31299
  }
31162
- return this.changeThreadPermission(threadId, call.params, allowedThread);
31300
+ return this.changeThreadPermission(threadId, call.params, allowedThread, assertCurrent);
31163
31301
  }
31164
31302
  if (call.method === "thread/read") {
31165
31303
  return this.callUpstream(call.method, call.params);
@@ -31172,6 +31310,7 @@ var CodexRemoteDomain = class {
31172
31310
  }
31173
31311
  let claimed = false;
31174
31312
  let previousOwner;
31313
+ let claimedOwner;
31175
31314
  if (isThreadMutation(call.method) && threadId !== void 0) {
31176
31315
  const claim = this.claimTurn(
31177
31316
  threadId,
@@ -31181,6 +31320,7 @@ var CodexRemoteDomain = class {
31181
31320
  );
31182
31321
  claimed = claim.claimed;
31183
31322
  previousOwner = claim.previous;
31323
+ claimedOwner = this.turnOwners.get(threadId);
31184
31324
  }
31185
31325
  try {
31186
31326
  const permission = codexPermission(call.params);
@@ -31202,13 +31342,16 @@ var CodexRemoteDomain = class {
31202
31342
  try {
31203
31343
  result = await this.callUpstream(call.method, upstreamParams);
31204
31344
  } catch (error) {
31205
- if (call.method === "thread/resume" && allowedThread !== void 0 && error instanceof RpcError && error.code === "CODEX_UPSTREAM_ERROR" && call.params.permissionPreset === void 0) {
31345
+ assertCurrent();
31346
+ if (call.method === "thread/resume" && allowedThread !== void 0 && error instanceof RpcError && (error.code === "CODEX_UPSTREAM_ERROR" || error.code === CODEX_THREAD_UNMATERIALIZED) && call.params.permissionPreset === void 0) {
31206
31347
  return { thread: allowedThread.thread };
31207
31348
  }
31208
31349
  throw error;
31209
31350
  }
31351
+ assertCurrent();
31210
31352
  if (call.method === "thread/resume" || call.method === "thread/fork" || call.method === "thread/unarchive") {
31211
31353
  await this.assertResultThreadAllowed(result);
31354
+ assertCurrent();
31212
31355
  }
31213
31356
  if (call.method === "thread/resume" || call.method === "thread/fork") {
31214
31357
  const resultId = extractThread(result)?.id;
@@ -31225,7 +31368,7 @@ var CodexRemoteDomain = class {
31225
31368
  }
31226
31369
  return result;
31227
31370
  } catch (error) {
31228
- if (claimed && threadId !== void 0) {
31371
+ if (claimed && threadId !== void 0 && this.turnOwners.get(threadId) === claimedOwner) {
31229
31372
  if (previousOwner === void 0) this.turnOwners.delete(threadId);
31230
31373
  else this.turnOwners.set(threadId, previousOwner);
31231
31374
  }
@@ -31319,6 +31462,7 @@ var CodexRemoteDomain = class {
31319
31462
  const appServer = this.createAppServer(binary, this.logger);
31320
31463
  this.appServer = appServer;
31321
31464
  this.unsubscribeInbound = appServer.onInbound((message) => {
31465
+ if (this.appServer !== appServer) return;
31322
31466
  void this.handleInbound(message).catch((error) => {
31323
31467
  this.logger.warn("Codex inbound handling failed", { code: errorCode2(error) });
31324
31468
  });
@@ -31434,7 +31578,7 @@ var CodexRemoteDomain = class {
31434
31578
  if (preset === void 0) this.permissionPresets.delete(threadId);
31435
31579
  else this.permissionPresets.set(threadId, preset);
31436
31580
  }
31437
- async changeThreadPermission(threadId, params, allowed) {
31581
+ async changeThreadPermission(threadId, params, allowed, assertCurrent) {
31438
31582
  const permission = codexPermission(params);
31439
31583
  const settings = {
31440
31584
  ...codexTurnPermissionParams(permission, allowed.cwd),
@@ -31443,8 +31587,10 @@ var CodexRemoteDomain = class {
31443
31587
  let result;
31444
31588
  try {
31445
31589
  await this.callUpstream("thread/settings/update", { threadId, ...settings });
31590
+ assertCurrent();
31446
31591
  result = { thread: allowed.thread, ...settings, sandbox: settings.sandboxPolicy };
31447
31592
  } catch (error) {
31593
+ assertCurrent();
31448
31594
  if (isUnpersistedThreadError(error)) {
31449
31595
  return { thread: allowed.thread, ...settings, sandbox: settings.sandboxPolicy };
31450
31596
  }
@@ -31455,15 +31601,19 @@ var CodexRemoteDomain = class {
31455
31601
  ...codexThreadPermissionParams(permission),
31456
31602
  excludeTurns: true
31457
31603
  });
31604
+ assertCurrent();
31458
31605
  } catch (resumeError) {
31606
+ assertCurrent();
31459
31607
  if (isUnpersistedThreadError(resumeError)) {
31460
31608
  return { thread: allowed.thread, ...settings, sandbox: settings.sandboxPolicy };
31461
31609
  }
31462
31610
  throw resumeError;
31463
31611
  }
31464
31612
  await this.assertResultThreadAllowed(result);
31613
+ assertCurrent();
31465
31614
  if (codexPermissionPresetFromResponse(result) !== params.permissionPreset) {
31466
31615
  await this.callUpstream("thread/settings/update", { threadId, ...settings });
31616
+ assertCurrent();
31467
31617
  result = { ...isRecord14(result) ? result : {}, ...settings, sandbox: settings.sandboxPolicy };
31468
31618
  }
31469
31619
  }
@@ -31478,6 +31628,7 @@ var CodexRemoteDomain = class {
31478
31628
  async handleServerRequest(message) {
31479
31629
  const appServer = this.requireAppServer();
31480
31630
  await this.expireApprovals();
31631
+ if (this.appServer !== appServer || !this.isAvailable()) return;
31481
31632
  if (message.method !== "item/commandExecution/requestApproval" && message.method !== "item/fileChange/requestApproval") {
31482
31633
  await appServer.respondError(message.id, -32601, "This Remote client does not support the server request.");
31483
31634
  return;
@@ -31532,13 +31683,13 @@ var CodexRemoteDomain = class {
31532
31683
  async readThreadForHistory(connectionId, threadId) {
31533
31684
  const { thread: metadata } = await this.readKnownThread(threadId);
31534
31685
  try {
31535
- return { ...metadata, turns: await this.readThreadTurns(threadId, "full") };
31686
+ return { ...metadata, turns: await this.readThreadTurns(connectionId, threadId, "full") };
31536
31687
  } catch (fullError) {
31537
31688
  if (!isHistoryFormatFallback(fullError)) throw fullError;
31538
31689
  this.logHistoryFallback(connectionId, "turns-full", fullError);
31539
31690
  }
31540
31691
  try {
31541
- return { ...metadata, turns: await this.readThreadTurns(threadId, "summary") };
31692
+ return { ...metadata, turns: await this.readThreadTurns(connectionId, threadId, "summary") };
31542
31693
  } catch (summaryError) {
31543
31694
  if (!isHistoryFormatFallback(summaryError)) throw summaryError;
31544
31695
  this.logHistoryFallback(connectionId, "turns-summary", summaryError);
@@ -31554,22 +31705,35 @@ var CodexRemoteDomain = class {
31554
31705
  }
31555
31706
  return legacyThread;
31556
31707
  } catch (legacyError) {
31708
+ if (isUnmaterializedThreadError(legacyError)) {
31709
+ this.logEmptyUnmaterializedHistory(connectionId, "thread-read-full");
31710
+ return { ...metadata, turns: [] };
31711
+ }
31557
31712
  if (!isHistoryReadRecoverable(legacyError)) throw legacyError;
31558
31713
  this.logHistoryFallback(connectionId, "thread-read-full", legacyError);
31559
31714
  throw legacyError;
31560
31715
  }
31561
31716
  }
31562
- async readThreadTurns(threadId, itemsView) {
31717
+ async readThreadTurns(connectionId, threadId, itemsView) {
31563
31718
  const turns = [];
31564
31719
  let cursor2;
31565
31720
  for (let page = 0; page < MAX_CODEX_HISTORY_PAGES; page += 1) {
31566
- const result = await this.callUpstream("thread/turns/list", {
31567
- threadId,
31568
- limit: CODEX_HISTORY_PAGE_LIMIT,
31569
- sortDirection: "asc",
31570
- itemsView,
31571
- ...cursor2 === void 0 ? {} : { cursor: cursor2 }
31572
- });
31721
+ let result;
31722
+ try {
31723
+ result = await this.callUpstream("thread/turns/list", {
31724
+ threadId,
31725
+ limit: CODEX_HISTORY_PAGE_LIMIT,
31726
+ sortDirection: "asc",
31727
+ itemsView,
31728
+ ...cursor2 === void 0 ? {} : { cursor: cursor2 }
31729
+ });
31730
+ } catch (error) {
31731
+ if (page === 0 && isUnmaterializedThreadError(error)) {
31732
+ this.logEmptyUnmaterializedHistory(connectionId, `turns-${itemsView}`);
31733
+ return [];
31734
+ }
31735
+ throw error;
31736
+ }
31573
31737
  if (!isRecord14(result) || !Array.isArray(result.data)) {
31574
31738
  throw new RpcError("CODEX_INVALID_RESPONSE", "Codex App Server returned an invalid Turn page.");
31575
31739
  }
@@ -31622,6 +31786,12 @@ var CodexRemoteDomain = class {
31622
31786
  code: errorCode2(error)
31623
31787
  });
31624
31788
  }
31789
+ logEmptyUnmaterializedHistory(connectionId, stage) {
31790
+ this.logger.debug("Codex history projected as empty unmaterialized thread", {
31791
+ connectionId: maskId(connectionId),
31792
+ stage
31793
+ });
31794
+ }
31625
31795
  async assertResultThreadAllowed(result) {
31626
31796
  const thread = extractThread(result);
31627
31797
  if (thread === void 0 || typeof thread.id !== "string") {
@@ -31702,8 +31872,13 @@ var CodexRemoteDomain = class {
31702
31872
  return this.appServer;
31703
31873
  }
31704
31874
  async callUpstream(method, params) {
31875
+ const appServer = this.requireAppServer();
31705
31876
  try {
31706
- return await this.requireAppServer().call(method, params);
31877
+ const result = await appServer.call(method, params);
31878
+ if (this.closed || this.appServer !== appServer || !this.isAvailable()) {
31879
+ throw new RpcError("CODEX_UNAVAILABLE", "The Codex App Server changed while handling the request.");
31880
+ }
31881
+ return result;
31707
31882
  } catch (error) {
31708
31883
  throw mapAppServerError(error);
31709
31884
  }
@@ -32164,6 +32339,9 @@ function isActiveWriterMessage(message) {
32164
32339
  function isUnpersistedThreadError(error) {
32165
32340
  return error instanceof RpcError && error.code === "CODEX_UPSTREAM_ERROR" && error.message.toLowerCase().includes("not persisted yet");
32166
32341
  }
32342
+ function isUnmaterializedThreadError(error) {
32343
+ return error instanceof RpcError && error.code === CODEX_THREAD_UNMATERIALIZED;
32344
+ }
32167
32345
 
32168
32346
  // src/acp/adapters/session-models.ts
32169
32347
  var AcpSessionModelState = class {
@@ -32803,6 +32981,9 @@ var IsolatedAcpClient = class {
32803
32981
  if (typeof id6 !== "string" || !/^[A-Za-z0-9_-]{1,240}$/.test(id6) || this.clients.has(id6) || this.closed || method === "session/load" && typeof result.sessionId === "string" && result.sessionId !== id6) throw new CursorAcpError("ACP_INVALID_SESSION", `${this.label} returned an invalid session.`);
32804
32982
  this.clients.set(id6, client2);
32805
32983
  const config = { ...result };
32984
+ this.updateConfiguration(id6, { kind: "notification", method: "session/update", params: {
32985
+ update: { sessionUpdate: "config_option_update", configOptions: config.configOptions }
32986
+ } }, config);
32806
32987
  for (const key of ["models", "modes"]) {
32807
32988
  if (replayConfiguration[key]) config[key] = { ...config[key], ...replayConfiguration[key] };
32808
32989
  }
@@ -32831,10 +33012,12 @@ var IsolatedAcpClient = class {
32831
33012
  try {
32832
33013
  const modelResult = await client.call("session/set_config_option", { sessionId: id5, configId: "model", value: p.model });
32833
33014
  this.updateConfiguration(id5, { kind: "notification", method: "session/update", params: { update: { sessionUpdate: "config_option_update", configOptions: modelResult.configOptions } } });
33015
+ if (this.configurations.get(id5)?.models?.currentModelId !== p.model) throw new CursorAcpError("ACP_MODEL_NOT_CONFIRMED", `${this.label} did not confirm the model change.`);
32834
33016
  if (p.reasoningEffort !== void 0) {
32835
33017
  if (!this.configurations.get(id5)?.modes?.availableModes?.some((item) => item.id === p.reasoningEffort)) throw new CursorAcpError("INVALID_MESSAGE", "The selected model does not support this thinking level.");
32836
33018
  const thinkingResult = await client.call("session/set_config_option", { sessionId: id5, configId: "thought_level", value: p.reasoningEffort });
32837
33019
  this.updateConfiguration(id5, { kind: "notification", method: "session/update", params: { update: { sessionUpdate: "config_option_update", configOptions: thinkingResult.configOptions } } });
33020
+ if (this.configurations.get(id5)?.modes?.currentModeId !== p.reasoningEffort) throw new CursorAcpError("ACP_MODEL_NOT_CONFIRMED", `${this.label} did not confirm the thinking change.`);
32838
33021
  }
32839
33022
  return { selected: this.models(id5).current };
32840
33023
  } finally {
@@ -32860,14 +33043,15 @@ var IsolatedAcpClient = class {
32860
33043
  if (update.sessionUpdate === "current_mode_update" && typeof update.currentModeId === "string") config.modes = { ...config.modes, currentModeId: update.currentModeId };
32861
33044
  if (update.sessionUpdate === "config_option_update" && Array.isArray(update.configOptions)) {
32862
33045
  for (const option of update.configOptions) {
32863
- if (option.type !== "select" || !Array.isArray(option.options)) continue;
33046
+ if (option?.type !== "select" || !Array.isArray(option.options)) continue;
33047
+ const options = option.options.flatMap((item) => Array.isArray(item?.options) ? item.options : [item]).filter((item) => typeof item?.value === "string" && typeof item?.name === "string");
32864
33048
  if (option.id === "model") config.models = {
32865
33049
  currentModelId: option.currentValue,
32866
- availableModels: option.options.map((item) => ({ modelId: item.value, name: item.name }))
33050
+ availableModels: options.map((item) => ({ modelId: item.value, name: item.name }))
32867
33051
  };
32868
33052
  if (option.id === "thought_level") config.modes = {
32869
33053
  currentModeId: option.currentValue,
32870
- availableModes: option.options.map((item) => ({ id: item.value, name: item.name }))
33054
+ availableModes: options.map((item) => ({ id: item.value, name: item.name }))
32871
33055
  };
32872
33056
  }
32873
33057
  }
@@ -32956,7 +33140,7 @@ var IsolatedAcpClient = class {
32956
33140
  };
32957
33141
 
32958
33142
  // src/workspace-bridge.ts
32959
- import { realpath as realpath2, lstat, readdir as readdir2, readFile as readFile4, stat as stat4, watch } from "node:fs/promises";
33143
+ import { realpath as realpath2, lstat, readdir as readdir2, open, stat as stat4, watch } from "node:fs/promises";
32960
33144
  import { isAbsolute as isAbsolute4, join as join7, relative as relative2, resolve as resolve2 } from "node:path";
32961
33145
 
32962
33146
  // src/remote-terminal-controller.ts
@@ -33577,11 +33761,11 @@ var AsyncQueue = class {
33577
33761
  }
33578
33762
  };
33579
33763
  var WorkspaceBridge = class {
33580
- constructor(resolveCwd, terminalEnabled, state = new WorkspaceBridgeState(), spawnTerminal = pipeTerminalSpawner(), scope) {
33764
+ constructor(resolveCwd, terminalEnabled, state = new WorkspaceBridgeState(), spawnTerminal = pipeTerminalSpawner(), scope2) {
33581
33765
  this.resolveCwd = resolveCwd;
33582
33766
  this.terminalEnabled = terminalEnabled;
33583
33767
  this.state = state;
33584
- this.scope = scope;
33768
+ this.scope = scope2;
33585
33769
  this.terminal = new RemoteTerminalController(state, spawnTerminal, terminalEnabled, (code, message) => this.scopeError(code, message));
33586
33770
  }
33587
33771
  terminal;
@@ -33652,10 +33836,26 @@ var WorkspaceBridge = class {
33652
33836
  return { ok: true, value: { absolutePath, version, bytes: info.size } };
33653
33837
  }
33654
33838
  if (!info.isFile()) throw this.scopeError("WORKSPACE_INVALID_PATH", "The requested workspace path is not a file.");
33839
+ if (info.size > MAX_READ_BYTES) throw this.scopeError("WORKSPACE_TOO_LARGE", "The requested workspace file is too large.");
33655
33840
  const offset = readOffset(args);
33656
33841
  const limit = readLimit(args);
33657
- const bytes = await readFile4(target2);
33658
- if (bytes.byteLength > MAX_READ_BYTES) throw this.scopeError("WORKSPACE_TOO_LARGE", "The requested workspace file is too large.");
33842
+ const file = await open(target2, "r");
33843
+ let bytes;
33844
+ try {
33845
+ const buffer = Buffer.allocUnsafe(info.size + 1);
33846
+ let length = 0;
33847
+ while (length < buffer.length) {
33848
+ signal.throwIfAborted();
33849
+ const read = await file.read(buffer, length, buffer.length - length, length);
33850
+ if (read.bytesRead === 0) break;
33851
+ length += read.bytesRead;
33852
+ }
33853
+ if (length > MAX_READ_BYTES) throw this.scopeError("WORKSPACE_TOO_LARGE", "The requested workspace file is too large.");
33854
+ if (length !== info.size) throw this.scopeError("WORKSPACE_CHANGED", "The workspace file changed while reading it. Retry the request.");
33855
+ bytes = buffer.subarray(0, length);
33856
+ } finally {
33857
+ await file.close();
33858
+ }
33659
33859
  if (endpoint === "workspaceFiles/readBytes") {
33660
33860
  const slice2 = bytes.subarray(offset, Math.min(offset + limit, bytes.length));
33661
33861
  return {
@@ -34363,16 +34563,16 @@ var TranscriptWatcher = class extends EventEmitter {
34363
34563
  await this.readNewLines();
34364
34564
  }
34365
34565
  async readNewLines() {
34366
- let stat8;
34566
+ let stat9;
34367
34567
  try {
34368
- stat8 = await fs2.stat(this.transcriptPath);
34568
+ stat9 = await fs2.stat(this.transcriptPath);
34369
34569
  } catch {
34370
34570
  return;
34371
34571
  }
34372
- if (stat8.size <= this.offset) return;
34572
+ if (stat9.size <= this.offset) return;
34373
34573
  const handle = await fs2.open(this.transcriptPath, "r");
34374
34574
  try {
34375
- const bytesToRead = stat8.size - this.offset;
34575
+ const bytesToRead = stat9.size - this.offset;
34376
34576
  const buffer = Buffer.alloc(bytesToRead);
34377
34577
  const { bytesRead } = await handle.read(buffer, 0, bytesToRead, this.offset);
34378
34578
  this.offset += bytesRead;
@@ -35047,7 +35247,32 @@ import { realpath as realpath4 } from "node:fs/promises";
35047
35247
  import { randomBytes as randomBytes8 } from "node:crypto";
35048
35248
  import { createServer as createServer2 } from "node:net";
35049
35249
  import { realpath as realpath3 } from "node:fs/promises";
35250
+ import { execFile as execFile2 } from "node:child_process";
35251
+ import { promisify as promisify2, stripVTControlCharacters } from "node:util";
35050
35252
  var MAX_RESPONSE_BYTES = 64 * 1024;
35253
+ var execFileAsync = promisify2(execFile2);
35254
+ async function opencodeHasNativeApi(binary, args, cwd2, signal) {
35255
+ try {
35256
+ const { stdout, stderr } = await execFileAsync(binary, [...opencodePrivateArgs(args), "--help"], {
35257
+ cwd: cwd2,
35258
+ signal,
35259
+ timeout: 2e4,
35260
+ maxBuffer: MAX_RESPONSE_BYTES,
35261
+ windowsHide: true,
35262
+ killSignal: "SIGKILL"
35263
+ });
35264
+ const help = stripVTControlCharacters(`${stdout}
35265
+ ${stderr}`);
35266
+ const hasFlag = (name2) => new RegExp(`^\\s*(?:-[A-Za-z],?\\s+)?--${name2}(?=[\\s=,\\[]|$)`, "m").test(help);
35267
+ const network = ["hostname", "port", "mdns"].map(hasFlag);
35268
+ if (!/\bacp\b/i.test(help) || !hasFlag("help") || network.some(Boolean) && !network.every(Boolean)) throw new Error("Invalid ACP help");
35269
+ return network.every(Boolean);
35270
+ } catch (error) {
35271
+ if (signal.aborted) throw new RpcError("OPENCODE_CLOSED", "OpenCode ACP was closed.");
35272
+ if (error?.code === "ENOENT") throw new RpcError("OPENCODE_BINARY_UNAVAILABLE", "The Host OpenCode executable is unavailable.");
35273
+ throw new RpcError("OPENCODE_CLI_PROBE_FAILED", "Could not inspect OpenCode ACP options. Check opencode acp --help on the Host.");
35274
+ }
35275
+ }
35051
35276
  var OpencodeNativeApi = class {
35052
35277
  constructor(origin, request = fetch) {
35053
35278
  this.origin = origin;
@@ -35124,10 +35349,10 @@ function opencodePrivateArgs(args, port) {
35124
35349
  for (let i = 0; i < args.length; i++) {
35125
35350
  const arg = args[i];
35126
35351
  if (/^--(?:port|hostname|mdns|no-mdns)(?:=|$)/.test(arg)) {
35127
- if (arg === "--port" || arg === "--hostname" || arg === "--mdns" && /^(true|false)$/.test(args[i + 1] ?? "")) i++;
35352
+ if (arg === "--port" || arg === "--hostname" || (arg === "--mdns" || arg === "--no-mdns") && /^(true|false)$/.test(args[i + 1] ?? "")) i++;
35128
35353
  } else kept.push(arg);
35129
35354
  }
35130
- return [...kept, `--hostname=127.0.0.1`, `--port=${port}`, "--mdns=false"];
35355
+ return port === void 0 ? kept : [...kept, `--hostname=127.0.0.1`, `--port=${port}`, "--mdns=false"];
35131
35356
  }
35132
35357
  function unavailable() {
35133
35358
  return new RpcError("OPENCODE_ARCHIVE_UNAVAILABLE", "OpenCode did not confirm the archive operation. Check the Host OpenCode version and retry after refreshing its session list.");
@@ -35165,6 +35390,8 @@ var OpencodeAcpClient = class _OpencodeAcpClient {
35165
35390
  failureNotified = false;
35166
35391
  startPromise;
35167
35392
  historyBusy = false;
35393
+ historyStop;
35394
+ startupAbort = new AbortController();
35168
35395
  nativeApi;
35169
35396
  archivedSessions = /* @__PURE__ */ new Set();
35170
35397
  archivingSessions = /* @__PURE__ */ new Set();
@@ -35206,7 +35433,10 @@ var OpencodeAcpClient = class _OpencodeAcpClient {
35206
35433
  if (this.closed) throw new OpencodeAcpError("OPENCODE_CLOSED", "The OpenCode domain is closed.");
35207
35434
  if (this.historyBusy) throw new OpencodeAcpError("OPENCODE_HISTORY_BUSY", "An OpenCode history read is already in progress.");
35208
35435
  this.historyBusy = true;
35209
- const client = new _OpencodeAcpClient(this.binary, this.logger, void 0, { ...this.options, historyReader: true });
35436
+ const client = new _OpencodeAcpClient(this.binary, this.logger, this.spawnAcp, { ...this.options, historyReader: true });
35437
+ let stopping;
35438
+ const stop = () => stopping ??= client.close();
35439
+ this.historyStop = stop;
35210
35440
  const updates = [];
35211
35441
  let bytes = 0;
35212
35442
  let count = 0;
@@ -35219,18 +35449,29 @@ var OpencodeAcpClient = class _OpencodeAcpClient {
35219
35449
  if (message.method !== "session/update" || message.params?.sessionId !== sessionId || overflow) return;
35220
35450
  count++;
35221
35451
  bytes += Buffer7.byteLength(JSON.stringify(message));
35222
- if (bytes > 8 * 1024 * 1024 || count > 1e4) overflow = true;
35223
- else updates.push(message);
35452
+ if (bytes > 8 * 1024 * 1024 || count > 1e4) {
35453
+ overflow = true;
35454
+ void stop().catch(() => void 0);
35455
+ } else updates.push(message);
35224
35456
  });
35225
35457
  try {
35226
35458
  await client.start();
35459
+ if (this.closed) throw new OpencodeAcpError("OPENCODE_CLOSED", "OpenCode ACP was closed.");
35227
35460
  await client.call("session/load", { sessionId, cwd: cwd2, mcpServers: [] });
35461
+ if (this.closed) throw new OpencodeAcpError("OPENCODE_CLOSED", "OpenCode ACP was closed.");
35228
35462
  if (overflow) throw new OpencodeAcpError("OPENCODE_HISTORY_TOO_LARGE", "OpenCode history exceeds the transfer limit.");
35229
35463
  return acpHistoryEvents(updates, sessionId, "opencode");
35464
+ } catch (error) {
35465
+ if (overflow) throw new OpencodeAcpError("OPENCODE_HISTORY_TOO_LARGE", "OpenCode history exceeds the transfer limit.");
35466
+ throw error;
35230
35467
  } finally {
35231
35468
  unsubscribe();
35232
- await client.close().catch(() => void 0);
35233
- this.historyBusy = false;
35469
+ try {
35470
+ await stop();
35471
+ } finally {
35472
+ this.historyStop = void 0;
35473
+ this.historyBusy = false;
35474
+ }
35234
35475
  }
35235
35476
  }
35236
35477
  models = /* @__PURE__ */ new Map();
@@ -35340,14 +35581,19 @@ var OpencodeAcpClient = class _OpencodeAcpClient {
35340
35581
  async close() {
35341
35582
  if (this.closed) return;
35342
35583
  this.closed = true;
35584
+ this.startupAbort.abort();
35343
35585
  this.ready = false;
35344
35586
  this.models.clear();
35345
35587
  this.nativeApi?.close();
35346
35588
  this.failPending(new OpencodeAcpError("OPENCODE_CLOSED", "OpenCode ACP was closed."));
35589
+ const history = this.historyStop?.();
35347
35590
  const child = this.process;
35348
35591
  this.process = void 0;
35349
- if (child === void 0 || child.exitCode !== null || child.killed) return;
35350
- await new Promise((resolve6) => {
35592
+ if (child === void 0 || child.exitCode !== null || child.killed) {
35593
+ await history;
35594
+ return;
35595
+ }
35596
+ await Promise.all([history, new Promise((resolve6) => {
35351
35597
  const timer = setTimeout(() => {
35352
35598
  child.kill("SIGKILL");
35353
35599
  resolve6();
@@ -35358,7 +35604,7 @@ var OpencodeAcpClient = class _OpencodeAcpClient {
35358
35604
  resolve6();
35359
35605
  });
35360
35606
  child.kill("SIGTERM");
35361
- });
35607
+ })]);
35362
35608
  }
35363
35609
  async startOnce() {
35364
35610
  if (this.process !== void 0) {
@@ -35367,25 +35613,32 @@ var OpencodeAcpClient = class _OpencodeAcpClient {
35367
35613
  let child;
35368
35614
  if (this.spawnAcp) child = this.spawnAcp(this.binary);
35369
35615
  else {
35370
- const port = await opencodeLoopbackPort();
35616
+ const args = this.options.args ?? ["acp"];
35617
+ const nativeApiSupported = await opencodeHasNativeApi(this.binary, args, this.options.cwd, this.startupAbort.signal);
35618
+ const port = nativeApiSupported ? await opencodeLoopbackPort() : void 0;
35371
35619
  if (this.closed) throw new OpencodeAcpError("OPENCODE_CLOSED", "OpenCode ACP was closed.");
35372
- this.nativeApi = new OpencodeNativeApi(`http://127.0.0.1:${port}`);
35373
- child = spawn6(this.binary, opencodePrivateArgs(this.options.args ?? ["acp"], port), {
35620
+ if (port !== void 0) this.nativeApi = new OpencodeNativeApi(`http://127.0.0.1:${port}`);
35621
+ child = spawn6(this.binary, opencodePrivateArgs(args, port), {
35374
35622
  cwd: this.options.cwd,
35375
35623
  stdio: ["pipe", "pipe", "pipe"],
35376
35624
  windowsHide: true,
35377
- env: { ...process.env, OPENCODE_SERVER_USERNAME: "opencode", OPENCODE_SERVER_PASSWORD: this.nativeApi.password }
35625
+ env: { ...process.env, ...this.nativeApi ? { OPENCODE_SERVER_USERNAME: "opencode", OPENCODE_SERVER_PASSWORD: this.nativeApi.password } : {} }
35378
35626
  });
35379
35627
  }
35380
35628
  this.process = child;
35381
35629
  this.failureNotified = false;
35382
35630
  this.stdoutBuffer = Buffer7.alloc(0);
35383
35631
  this.stderrBytes = 0;
35384
- child.stdout.on("data", (chunk) => this.consumeStdout(Buffer7.from(chunk)));
35632
+ child.stdout.on("data", (chunk) => {
35633
+ if (this.process === child) this.consumeStdout(Buffer7.from(chunk));
35634
+ });
35385
35635
  child.stderr.on("data", (chunk) => {
35636
+ if (this.process !== child) return;
35386
35637
  this.stderrBytes = Math.min(MAX_STDERR_CAPTURE_BYTES4, this.stderrBytes + Buffer7.byteLength(chunk));
35387
35638
  });
35388
- child.on("error", (error) => this.handleProcessFailure("OPENCODE_BINARY_UNAVAILABLE", error));
35639
+ child.on("error", (error) => {
35640
+ if (this.process === child) this.handleProcessFailure("OPENCODE_BINARY_UNAVAILABLE", error);
35641
+ });
35389
35642
  child.on("exit", (code, signal) => {
35390
35643
  if (this.process !== child) return;
35391
35644
  this.process = void 0;
@@ -35393,6 +35646,7 @@ var OpencodeAcpClient = class _OpencodeAcpClient {
35393
35646
  this.nativeApi?.close();
35394
35647
  this.nativeApi = void 0;
35395
35648
  this.models.clear();
35649
+ void this.historyStop?.().catch(() => void 0);
35396
35650
  this.failPending(new OpencodeAcpError("OPENCODE_ACP_EXITED", "OpenCode ACP exited unexpectedly."));
35397
35651
  if (!this.closed) {
35398
35652
  this.logger?.warn("OpenCode ACP exited", {
@@ -35415,6 +35669,7 @@ var OpencodeAcpClient = class _OpencodeAcpClient {
35415
35669
  version: PLUGIN_VERSION
35416
35670
  }
35417
35671
  }, ACP_START_TIMEOUT_MS3);
35672
+ if (this.closed || this.process !== child) throw new OpencodeAcpError("OPENCODE_CLOSED", "OpenCode ACP was closed.");
35418
35673
  this.ready = true;
35419
35674
  this.logger?.info("OpenCode ACP ready");
35420
35675
  } catch (error) {
@@ -35502,6 +35757,7 @@ var OpencodeAcpClient = class _OpencodeAcpClient {
35502
35757
  this.publish(inbound);
35503
35758
  }
35504
35759
  handleProcessFailure(code, cause) {
35760
+ void this.historyStop?.().catch(() => void 0);
35505
35761
  this.nativeApi?.close();
35506
35762
  this.nativeApi = void 0;
35507
35763
  this.ready = false;
@@ -35778,15 +36034,15 @@ async function querySqliteJson(dbPath, sql, params = []) {
35778
36034
  } catch {
35779
36035
  }
35780
36036
  try {
35781
- const { execFile: execFile2 } = await import("node:child_process");
35782
- const { promisify: promisify2 } = await import("node:util");
35783
- const execFileAsync = promisify2(execFile2);
36037
+ const { execFile: execFile3 } = await import("node:child_process");
36038
+ const { promisify: promisify3 } = await import("node:util");
36039
+ const execFileAsync2 = promisify3(execFile3);
35784
36040
  let formattedSql = sql;
35785
36041
  for (const p of params) {
35786
36042
  const val = typeof p === "number" ? String(p) : `'${String(p).replace(/'/g, "''")}'`;
35787
36043
  formattedSql = formattedSql.replace("?", val);
35788
36044
  }
35789
- const { stdout } = await execFileAsync("sqlite3", [dbPath, "-json", formattedSql]);
36045
+ const { stdout } = await execFileAsync2("sqlite3", [dbPath, "-json", formattedSql]);
35790
36046
  return JSON.parse(stdout || "[]");
35791
36047
  } catch {
35792
36048
  return [];
@@ -35836,7 +36092,7 @@ async function readTranscriptSummary(baseDir, conversationId) {
35836
36092
  try {
35837
36093
  const path = await transcriptPath(baseDir, conversationId);
35838
36094
  if (path === void 0) return void 0;
35839
- const stat8 = await fs3.stat(path);
36095
+ const stat9 = await fs3.stat(path);
35840
36096
  const handle = await fs3.open(path, "r");
35841
36097
  try {
35842
36098
  const buffer = Buffer.alloc(4096);
@@ -35847,7 +36103,7 @@ async function readTranscriptSummary(baseDir, conversationId) {
35847
36103
  if (record9.type !== "USER_INPUT") return void 0;
35848
36104
  const title = cleanUserPrompt(record9.content).split("\n")[0]?.trim().slice(0, 40) || (agyImageReferences(record9.content ?? "").length > 0 ? "Image" : void 0);
35849
36105
  if (!title) return void 0;
35850
- return { conversationId, title, createdAt: record9.created_at ? new Date(record9.created_at).getTime() : stat8.mtimeMs, updatedAt: stat8.mtimeMs };
36106
+ return { conversationId, title, createdAt: record9.created_at ? new Date(record9.created_at).getTime() : stat9.mtimeMs, updatedAt: stat9.mtimeMs };
35851
36107
  } finally {
35852
36108
  await handle.close();
35853
36109
  }
@@ -36150,6 +36406,7 @@ var AcpPeerBridge = class {
36150
36406
  });
36151
36407
  throw error;
36152
36408
  }
36409
+ this.requireOpen();
36153
36410
  const responseBytes = new TextEncoder().encode(JSON.stringify(response));
36154
36411
  if (responseBytes.byteLength <= INLINE_TRANSFER_RESPONSE_BYTES2) return { kind: "inline", response };
36155
36412
  if (responseBytes.byteLength > MAX_AGENT_ACP_TRANSFER_BYTES) {
@@ -36243,11 +36500,11 @@ var AcpPeerBridge = class {
36243
36500
  this.toolControllers.clear();
36244
36501
  this.incomingTransfers.clear();
36245
36502
  this.outgoingTransfers.clear();
36503
+ this.domain.dropPeer(this.context.connectionId);
36246
36504
  await Promise.all(streamIds.map((streamId) => this.publish("agent.acp.stream.closed", {
36247
36505
  streamId,
36248
36506
  reason: "peer-disconnected"
36249
36507
  }).catch(() => void 0)));
36250
- this.domain.dropPeer(this.context.connectionId);
36251
36508
  }
36252
36509
  pruneTransfers() {
36253
36510
  const staleBefore = Date.now() - TRANSFER_IDLE_MS2;
@@ -36348,7 +36605,7 @@ var AcpRemoteGateway = class {
36348
36605
  /** Catch-up buffer for turns that finish while the Client is reconnecting. */
36349
36606
  recentFrames = /* @__PURE__ */ new Map();
36350
36607
  /** Per-prompt live updates; attached to prompt_completed when streaming was lossy. */
36351
- promptsInProgress = /* @__PURE__ */ new Set();
36608
+ promptsInProgress = /* @__PURE__ */ new Map();
36352
36609
  cancelledSessions = /* @__PURE__ */ new Set();
36353
36610
  /** Identity fences asynchronous Pi preparation from a later retry of the same session. */
36354
36611
  preparingPrompts = /* @__PURE__ */ new Map();
@@ -36436,13 +36693,16 @@ var AcpRemoteGateway = class {
36436
36693
  const call = parseAcpCall(envelope.method, envelope.params);
36437
36694
  const sessionId = sessionIdFromParams(call.method, call.params);
36438
36695
  const backend = call.params.backend ?? (sessionId ? this.sessionBackends.get(sessionId) ?? this.defaultBackend() : void 0);
36696
+ const assertCurrent = this.captureCallScope(connectionId, typeof backend === "string" ? backend : this.defaultBackend());
36439
36697
  const reserve = backend === "opencode" && sessionId !== void 0 && ["session/load", "session/prompt", "dsh/selectModel"].includes(call.method);
36440
36698
  if (reserve) {
36441
36699
  if (this.archivingSessions.has(sessionId)) throw new RpcError("ACP_SESSION_BUSY", "The OpenCode session is being archived.");
36442
36700
  this.opencodeOperations.set(sessionId, (this.opencodeOperations.get(sessionId) ?? 0) + 1);
36443
36701
  }
36444
36702
  try {
36445
- return await this.callInternal(connectionId, call);
36703
+ const result = await this.callInternal(connectionId, call, assertCurrent);
36704
+ assertCurrent();
36705
+ return result;
36446
36706
  } finally {
36447
36707
  if (reserve) {
36448
36708
  const remaining = (this.opencodeOperations.get(sessionId) ?? 1) - 1;
@@ -36451,7 +36711,20 @@ var AcpRemoteGateway = class {
36451
36711
  }
36452
36712
  }
36453
36713
  }
36454
- async callInternal(connectionId, call) {
36714
+ captureCallScope(connectionId, backend) {
36715
+ const peer = this.peers.get(connectionId);
36716
+ const instance = this.backendInstances.get(backend);
36717
+ return () => {
36718
+ if (this.closed || this.backendInstances.get(backend) !== instance || instance !== void 0 && this.requireAcp(backend) !== instance.client) {
36719
+ throw new RpcError("CURSOR_UNAVAILABLE", "The ACP backend changed while handling the request.");
36720
+ }
36721
+ if (peer !== void 0 && this.peers.get(connectionId) !== peer) {
36722
+ throw new RpcError("ACP_CONNECTION_CLOSED", "The ACP connection is closed.");
36723
+ }
36724
+ };
36725
+ }
36726
+ async callInternal(connectionId, call, assertCurrent) {
36727
+ assertCurrent();
36455
36728
  this.logger.info("ACP call received", {
36456
36729
  method: call.method,
36457
36730
  sessionId: typeof call.params?.sessionId === "string" ? shortSessionId(call.params.sessionId) : void 0
@@ -36482,6 +36755,7 @@ var AcpRemoteGateway = class {
36482
36755
  const client = this.requireAcp(backend);
36483
36756
  const instance = this.backendInstances.get(backend);
36484
36757
  const cwd2 = await this.catalogSessionCwd(backend, sessionId2);
36758
+ assertCurrent();
36485
36759
  if (this.closed || this.requireAcp(backend) !== client || this.backendInstances.get(backend) !== instance) throw new RpcError("OPENCODE_ARCHIVE_UNAVAILABLE", "The OpenCode backend changed. Refresh the session list.");
36486
36760
  if (this.promptsInProgress.has(sessionId2) || [...this.approvals.values()].some((item) => item.sessionId === sessionId2)) throw new RpcError("ACP_SESSION_BUSY", "Stop the OpenCode reply and resolve its approvals before archiving.");
36487
36761
  if (!client.archiveSession) throw new RpcError("OPENCODE_ARCHIVE_UNAVAILABLE", "The Host OpenCode adapter does not support native archiving.");
@@ -36511,6 +36785,7 @@ var AcpRemoteGateway = class {
36511
36785
  if (archived) throw new RpcError("ACP_SESSION_ARCHIVED", "This OpenCode session is archived.");
36512
36786
  }
36513
36787
  }
36788
+ assertCurrent();
36514
36789
  if (call.method === "dsh/directoryList") {
36515
36790
  return this.listDirectory(String(call.params.path));
36516
36791
  }
@@ -36576,11 +36851,13 @@ var AcpRemoteGateway = class {
36576
36851
  if (backend === "antigravity") {
36577
36852
  if (path.trim() !== "" && call.params.prewarm !== false) {
36578
36853
  const cwd2 = await this.requireExistingDirectory(path);
36854
+ assertCurrent();
36579
36855
  this.requireAcp("antigravity").prewarmSession?.(cwd2);
36580
36856
  }
36581
36857
  const items = await discoverAntigravitySessions(path, limit);
36582
36858
  if (path) {
36583
36859
  const cwd2 = await this.requireExistingDirectory(path);
36860
+ assertCurrent();
36584
36861
  for (const item of items) this.sessionCwds.set(item.conversationId, { backend, cwd: cwd2 });
36585
36862
  }
36586
36863
  this.logger.info("ACP session list fetched", { count: items.length });
@@ -36619,6 +36896,7 @@ var AcpRemoteGateway = class {
36619
36896
  }
36620
36897
  if (call.method === "session/new") {
36621
36898
  const cwd2 = await this.requireExistingDirectory(String(call.params.cwd));
36899
+ assertCurrent();
36622
36900
  const requestedBackend = typeof call.params.backend === "string" ? call.params.backend : this.defaultBackend();
36623
36901
  const acp = this.requireAcp(requestedBackend);
36624
36902
  const result = await acp.call("session/new", {
@@ -36626,6 +36904,7 @@ var AcpRemoteGateway = class {
36626
36904
  mcpServers: [],
36627
36905
  ...typeof call.params.mode === "string" ? { mode: call.params.mode } : {}
36628
36906
  });
36907
+ assertCurrent();
36629
36908
  const sessionId2 = readSessionId(result);
36630
36909
  if (sessionId2 !== void 0) {
36631
36910
  const bound = this.sessionBackends.get(sessionId2);
@@ -36661,9 +36940,12 @@ var AcpRemoteGateway = class {
36661
36940
  throw new RpcError("INVALID_MESSAGE", "The ACP session backend does not match.");
36662
36941
  }
36663
36942
  if (targetBackend === "pi" || targetBackend === "claude" || targetBackend === "opencode") await this.catalogSessionCwd(targetBackend, sessionId);
36943
+ assertCurrent();
36664
36944
  this.requireSessionOwner(connectionId, sessionId);
36665
36945
  const acp = this.requireAcp(targetBackend);
36946
+ this.sessionBackends.set(sessionId, targetBackend);
36666
36947
  const result = await acp.call(call.method, { ...call.params, ...sessionId && this.sessionCwds.get(sessionId) ? { cwd: this.sessionCwds.get(sessionId).cwd, mcpServers: [] } : {} });
36948
+ assertCurrent();
36667
36949
  const loadedId = readSessionId(result) ?? sessionId;
36668
36950
  if (loadedId !== void 0) {
36669
36951
  if (loadedId !== sessionId || this.sessionBackends.has(loadedId) && targetBackend !== void 0 && this.sessionBackends.get(loadedId) !== targetBackend) {
@@ -36681,7 +36963,9 @@ var AcpRemoteGateway = class {
36681
36963
  this.requireSessionOwner(connectionId, sessionId);
36682
36964
  const backend = call.params.backend;
36683
36965
  const cwd2 = await this.catalogSessionCwd(backend, sessionId);
36966
+ assertCurrent();
36684
36967
  await this.requireAcp(backend).call("session/load", { sessionId, cwd: cwd2, mcpServers: [] });
36968
+ assertCurrent();
36685
36969
  }
36686
36970
  if (call.method === "dsh/selectModel") {
36687
36971
  if (this.promptsInProgress.has(sessionId)) throw new RpcError("PROMPT_IN_PROGRESS", "Stop the ACP reply before changing its model.");
@@ -36690,12 +36974,15 @@ var AcpRemoteGateway = class {
36690
36974
  throw new RpcError("ACP_WORKSPACE_UNAVAILABLE", "The ACP session has no trusted workspace.");
36691
36975
  }
36692
36976
  const result = await this.requireAcp(session.backend).call(call.method, { ...call.params, cwd: session.cwd });
36977
+ assertCurrent();
36693
36978
  this.sessionBackends.set(sessionId, session.backend);
36694
36979
  return sanitizeSessionResult(result);
36695
36980
  }
36696
36981
  if (call.method === "session/prompt" && sessionId !== void 0) {
36697
36982
  if (this.promptsInProgress.has(sessionId)) throw new RpcError("PROMPT_IN_PROGRESS", "An ACP reply is already in progress.");
36698
- this.promptsInProgress.add(sessionId);
36983
+ const backend = this.sessionBackends.get(sessionId) ?? this.defaultBackend();
36984
+ const task = { backend, client: this.requireAcp(backend) };
36985
+ this.promptsInProgress.set(sessionId, task);
36699
36986
  this.cancelledSessions.delete(sessionId);
36700
36987
  const preparationBackend = this.sessionBackends.get(sessionId);
36701
36988
  if (preparationBackend === "pi" || preparationBackend === "claude") {
@@ -36704,16 +36991,18 @@ var AcpRemoteGateway = class {
36704
36991
  try {
36705
36992
  const cwd2 = await this.catalogSessionCwd(preparationBackend, sessionId);
36706
36993
  if (preparation.cancelled) return { accepted: false, stopReason: "cancelled" };
36707
- await this.requireAcp(preparationBackend).call("session/load", { sessionId, cwd: cwd2, mcpServers: [] });
36994
+ assertCurrent();
36995
+ await task.client.call("session/load", { sessionId, cwd: cwd2, mcpServers: [] });
36708
36996
  if (preparation.cancelled) return { accepted: false, stopReason: "cancelled" };
36997
+ assertCurrent();
36709
36998
  } catch (error) {
36710
36999
  if (preparation.cancelled) return { accepted: false, stopReason: "cancelled" };
36711
- this.promptsInProgress.delete(sessionId);
37000
+ if (this.promptsInProgress.get(sessionId) === task) this.promptsInProgress.delete(sessionId);
36712
37001
  throw error;
36713
37002
  } finally {
36714
37003
  if (this.preparingPrompts.get(sessionId) === preparation) {
36715
37004
  this.preparingPrompts.delete(sessionId);
36716
- if (preparation.cancelled) this.promptsInProgress.delete(sessionId);
37005
+ if (preparation.cancelled && this.promptsInProgress.get(sessionId) === task) this.promptsInProgress.delete(sessionId);
36717
37006
  }
36718
37007
  }
36719
37008
  }
@@ -36724,7 +37013,7 @@ var AcpRemoteGateway = class {
36724
37013
  });
36725
37014
  }
36726
37015
  this.turnCatchUp.set(sessionId, []);
36727
- void this.runPromptInBackground(sessionId, call.params);
37016
+ void this.runPromptInBackground(sessionId, call.params, task);
36728
37017
  return { accepted: true, stopReason: "in_progress" };
36729
37018
  }
36730
37019
  if (call.method === "session/cancel" && sessionId !== void 0) {
@@ -36747,12 +37036,12 @@ var AcpRemoteGateway = class {
36747
37036
  const sessionBackend = sessionId !== void 0 ? this.sessionBackends.get(sessionId) : void 0;
36748
37037
  return sanitizeSessionResult(await this.requireAcp(typeof call.params.backend === "string" ? call.params.backend : sessionBackend).call(call.method, call.params));
36749
37038
  }
36750
- async runPromptInBackground(sessionId, params) {
37039
+ async runPromptInBackground(sessionId, params, task) {
37040
+ const current = () => !this.closed && this.promptsInProgress.get(sessionId) === task && !this.pendingReconfiguration.has(task.backend) && this.backendInstances.get(task.backend)?.client === task.client;
36751
37041
  try {
36752
- const sessionBackend = this.sessionBackends.get(sessionId);
36753
- const acp = this.requireAcp(sessionBackend);
36754
- const result = await acp.call("session/prompt", params);
37042
+ const result = await task.client.call("session/prompt", params);
36755
37043
  await this.inboundChain;
37044
+ if (!current()) return;
36756
37045
  const stopReason = isRecord19(result) && typeof result.stopReason === "string" ? result.stopReason : "end_turn";
36757
37046
  const catchUp = takeTurnCatchUp(this.turnCatchUp, sessionId);
36758
37047
  this.logger.info("ACP prompt finished", {
@@ -36772,8 +37061,10 @@ var AcpRemoteGateway = class {
36772
37061
  }
36773
37062
  });
36774
37063
  } catch (error) {
37064
+ if (!current()) return;
36775
37065
  this.logger?.warn("ACP session/prompt failed", { code: errorCode3(error) });
36776
37066
  await this.inboundChain.catch(() => void 0);
37067
+ if (!current()) return;
36777
37068
  const catchUp = takeTurnCatchUp(this.turnCatchUp, sessionId);
36778
37069
  await this.publishToSession(sessionId, {
36779
37070
  method: "session/update",
@@ -36787,7 +37078,7 @@ var AcpRemoteGateway = class {
36787
37078
  }
36788
37079
  }).catch(() => void 0);
36789
37080
  } finally {
36790
- this.promptsInProgress.delete(sessionId);
37081
+ if (this.promptsInProgress.get(sessionId) === task) this.promptsInProgress.delete(sessionId);
36791
37082
  }
36792
37083
  }
36793
37084
  async respond(connectionId, input3) {
@@ -36832,6 +37123,18 @@ var AcpRemoteGateway = class {
36832
37123
  if (!tools) throw new RpcError("ACP_CONNECTION_CLOSED", "The ACP workspace connection is closed.");
36833
37124
  return tools;
36834
37125
  }
37126
+ /** Trusted, connection-scoped authority for the dedicated workspace uploader. */
37127
+ resolveUploadWorkspace(connectionId, publicId) {
37128
+ const match = /^(acp|cursor|pi|opencode|claude):(.+)$/.exec(publicId);
37129
+ const backend = match?.[1] === "acp" ? "antigravity" : match?.[1];
37130
+ const session = match ? this.sessionCwds.get(match[2]) : void 0;
37131
+ if (!this.peers.has(connectionId) || !session || session.backend !== backend || this.closed) {
37132
+ throw new RpcError("ACP_WORKSPACE_UNAVAILABLE", "The ACP session has no trusted workspace.");
37133
+ }
37134
+ this.requireAcp(backend);
37135
+ this.requireSessionOwner(connectionId, match[2]);
37136
+ return session.cwd;
37137
+ }
36835
37138
  async openToolStream(connectionId, sessionId, backend, endpoint, args, signal) {
36836
37139
  const session = this.sessionCwds.get(sessionId);
36837
37140
  if (!session || session.backend !== backend) throw new RpcError("ACP_WORKSPACE_UNAVAILABLE", "The ACP session has no trusted workspace.");
@@ -36898,6 +37201,10 @@ var AcpRemoteGateway = class {
36898
37201
  const terminalError = await this.workspaceState.dispose((sessionId) => changed.has(sessionId.startsWith("acp:") ? "antigravity" : sessionId.startsWith("pi:") ? "pi" : sessionId.startsWith("opencode:") ? "opencode" : "cursor")).then(() => void 0, () => new RpcError("TERMINAL_CLEANUP_FAILED", "Some ACP terminal processes could not be stopped. Save the backend settings again to retry."));
36899
37202
  for (const id5 of sessions) {
36900
37203
  this.sessionOwners.delete(id5);
37204
+ this.promptsInProgress.delete(id5);
37205
+ const preparation = this.preparingPrompts.get(id5);
37206
+ if (preparation) preparation.cancelled = true;
37207
+ this.preparingPrompts.delete(id5);
36901
37208
  this.sessionBackends.delete(id5);
36902
37209
  this.recentFrames.delete(id5);
36903
37210
  this.turnCatchUp.delete(id5);
@@ -36932,6 +37239,9 @@ var AcpRemoteGateway = class {
36932
37239
  this.sessionBackends.clear();
36933
37240
  this.recentFrames.clear();
36934
37241
  this.turnCatchUp.clear();
37242
+ this.promptsInProgress.clear();
37243
+ for (const preparation of this.preparingPrompts.values()) preparation.cancelled = true;
37244
+ this.preparingPrompts.clear();
36935
37245
  this.approvals.clear();
36936
37246
  const processes = await Promise.allSettled([...this.startingClients.keys()].map((client) => client.close()).concat(this.disposeAllInstances()));
36937
37247
  const terminalError = await terminals;
@@ -36969,8 +37279,8 @@ var AcpRemoteGateway = class {
36969
37279
  this.restartAttempt = 0;
36970
37280
  return;
36971
37281
  }
36972
- const failure3 = results.find((result) => result.status === "rejected");
36973
- throw failure3?.status === "rejected" ? failure3.reason : new CursorAcpError("CURSOR_BINARY_UNAVAILABLE", "No ACP backend is available.");
37282
+ const failure4 = results.find((result) => result.status === "rejected");
37283
+ throw failure4?.status === "rejected" ? failure4.reason : new CursorAcpError("CURSOR_BINARY_UNAVAILABLE", "No ACP backend is available.");
36974
37284
  }
36975
37285
  backendCurrent(backend) {
36976
37286
  return this.config.enabled && backend.enabled && !this.pendingReconfiguration.has(backend.id) && JSON.stringify(this.config.backends.find((item) => item.id === backend.id)) === JSON.stringify(backend);
@@ -37866,8 +38176,223 @@ function redact(value, key = "") {
37866
38176
  return Object.fromEntries(Object.entries(value).map(([childKey, child]) => [childKey, redact(child, childKey)]));
37867
38177
  }
37868
38178
 
37869
- // src/service.ts
38179
+ // src/workspace-upload.ts
37870
38180
  import { randomUUID as randomUUID5 } from "node:crypto";
38181
+ import { constants as constants4 } from "node:fs";
38182
+ import { link, lstat as lstat2, open as open2, realpath as realpath6, stat as stat6, unlink } from "node:fs/promises";
38183
+ import { extname, isAbsolute as isAbsolute7, join as join12 } from "node:path";
38184
+ var WORKSPACE_UPLOAD_BYTES = 20 * 1024 * 1024;
38185
+ var WORKSPACE_UPLOAD_CHUNK = 64 * 1024;
38186
+ var scope = external_exports.object({ sessionId: external_exports.string().min(1).max(512), uploadId: external_exports.string().uuid() }).strict();
38187
+ var openParams = scope.extend({ name: external_exports.string().min(1), totalBytes: external_exports.number().int().min(0).max(WORKSPACE_UPLOAD_BYTES) }).strict();
38188
+ var chunkParams = scope.extend({ offset: external_exports.number().int().min(0), data: external_exports.string().min(1).max(Math.ceil(WORKSPACE_UPLOAD_CHUNK / 3) * 4) }).strict();
38189
+ var WorkspaceUploadHost = class {
38190
+ constructor(resolveRoot, ttlMs = 12e4) {
38191
+ this.resolveRoot = resolveRoot;
38192
+ this.ttlMs = ttlMs;
38193
+ this.timer = setInterval(() => {
38194
+ void this.serialize(() => this.expire()).catch(() => void 0);
38195
+ }, Math.min(ttlMs, 3e4));
38196
+ this.timer.unref();
38197
+ }
38198
+ uploads = /* @__PURE__ */ new Map();
38199
+ completed = /* @__PURE__ */ new Map();
38200
+ queue = Promise.resolve();
38201
+ lifetime = new AbortController();
38202
+ timer;
38203
+ closed = false;
38204
+ call(method, input3) {
38205
+ return this.serialize(async () => {
38206
+ if (this.closed) throw failure3("UPLOAD_CLOSED");
38207
+ await this.expire();
38208
+ try {
38209
+ switch (method) {
38210
+ case "workspace.upload.open":
38211
+ return await this.begin(input3);
38212
+ case "workspace.upload.chunk":
38213
+ return await this.append(input3);
38214
+ case "workspace.upload.commit":
38215
+ return await this.commit(input3);
38216
+ case "workspace.upload.abort": {
38217
+ const p = scope.parse(input3);
38218
+ const upload = this.uploads.get(p.uploadId);
38219
+ if (upload && upload.sessionId !== p.sessionId) throw failure3("UPLOAD_NOT_FOUND");
38220
+ if (upload) await this.discard(p.uploadId, upload);
38221
+ return { aborted: true };
38222
+ }
38223
+ default:
38224
+ throw failure3("METHOD_NOT_FOUND");
38225
+ }
38226
+ } catch (error) {
38227
+ const p = scope.safeParse(input3 && typeof input3 === "object" ? { sessionId: input3.sessionId, uploadId: input3.uploadId } : input3);
38228
+ const upload = p.success ? this.uploads.get(p.data.uploadId) : void 0;
38229
+ if (p.success && upload?.sessionId === p.data.sessionId) await this.discard(p.data.uploadId, upload);
38230
+ if (error instanceof external_exports.ZodError || error instanceof RpcError) throw error;
38231
+ throw failure3("UPLOAD_FAILED");
38232
+ }
38233
+ });
38234
+ }
38235
+ closeAll() {
38236
+ this.closed = true;
38237
+ this.lifetime.abort();
38238
+ clearInterval(this.timer);
38239
+ return this.serialize(async () => {
38240
+ await Promise.all([...this.uploads].map(([id5, upload]) => this.discard(id5, upload)));
38241
+ this.completed.clear();
38242
+ });
38243
+ }
38244
+ serialize(operation) {
38245
+ const next = this.queue.then(operation);
38246
+ this.queue = next.catch(() => void 0);
38247
+ return next;
38248
+ }
38249
+ async begin(input3) {
38250
+ const p = openParams.parse(input3);
38251
+ if (!validName(p.name)) throw failure3("UPLOAD_INVALID_NAME");
38252
+ if (this.uploads.has(p.uploadId) || this.completed.has(p.uploadId)) throw failure3("UPLOAD_ID_IN_USE");
38253
+ if (this.uploads.size >= 2) throw failure3("UPLOAD_LIMIT");
38254
+ const sourceRoot = await this.sessionRoot(p.sessionId);
38255
+ this.assertLive();
38256
+ if (!sourceRoot || !isAbsolute7(sourceRoot)) throw failure3("UPLOAD_WORKSPACE_UNAVAILABLE");
38257
+ const root = await realpath6(sourceRoot);
38258
+ const directory = await stat6(root);
38259
+ if (!directory.isDirectory()) throw failure3("UPLOAD_WORKSPACE_UNAVAILABLE");
38260
+ const temp = join12(root, `.dsh-upload-${randomUUID5()}.part`);
38261
+ const handle = await open2(temp, constants4.O_CREAT | constants4.O_EXCL | constants4.O_RDWR | (constants4.O_NOFOLLOW ?? 0), 384);
38262
+ const upload = { ...p, received: 0, touched: Date.now(), root, sourceRoot, dev: directory.dev, ino: directory.ino, temp, handle };
38263
+ this.uploads.set(p.uploadId, upload);
38264
+ await this.validateRoot(upload);
38265
+ return { uploadId: p.uploadId, chunkBytes: WORKSPACE_UPLOAD_CHUNK };
38266
+ }
38267
+ async append(input3) {
38268
+ const p = chunkParams.parse(input3);
38269
+ const upload = this.requireUpload(p);
38270
+ const data2 = Buffer.from(p.data, "base64");
38271
+ if (!data2.length || data2.length > WORKSPACE_UPLOAD_CHUNK || data2.toString("base64") !== p.data || p.offset !== upload.received || upload.received + data2.length > upload.totalBytes) throw failure3("UPLOAD_INVALID_CHUNK");
38272
+ await this.validateRoot(upload);
38273
+ let written = 0;
38274
+ while (written < data2.length) {
38275
+ const result = await upload.handle.write(data2, written, data2.length - written, upload.received + written);
38276
+ if (!result.bytesWritten) throw failure3("UPLOAD_FAILED");
38277
+ written += result.bytesWritten;
38278
+ }
38279
+ this.assertLive();
38280
+ upload.received += data2.length;
38281
+ upload.touched = Date.now();
38282
+ return { uploadId: p.uploadId, receivedBytes: upload.received };
38283
+ }
38284
+ async commit(input3) {
38285
+ const p = scope.parse(input3);
38286
+ const prior = this.completed.get(p.uploadId);
38287
+ if (prior) {
38288
+ if (prior.sessionId !== p.sessionId) throw failure3("UPLOAD_NOT_FOUND");
38289
+ return prior.result;
38290
+ }
38291
+ const upload = this.requireUpload(p);
38292
+ if (upload.received !== upload.totalBytes) throw failure3("UPLOAD_INCOMPLETE");
38293
+ await this.validateRoot(upload);
38294
+ await upload.handle.sync();
38295
+ const original = await upload.handle.stat();
38296
+ const temp = await lstat2(upload.temp);
38297
+ if (!temp.isFile() || temp.dev !== original.dev || temp.ino !== original.ino || temp.size !== upload.totalBytes) throw failure3("UPLOAD_WORKSPACE_CHANGED");
38298
+ const extension = extname(upload.name);
38299
+ const stem = upload.name.slice(0, upload.name.length - extension.length);
38300
+ for (let suffix = 0; suffix < 1e4; suffix++) {
38301
+ await this.validateRoot(upload);
38302
+ const name2 = suffix === 0 ? upload.name : `${stem} (${suffix})${extension}`;
38303
+ try {
38304
+ await link(upload.temp, join12(upload.root, name2));
38305
+ } catch (error) {
38306
+ if (error.code === "EEXIST") continue;
38307
+ throw error;
38308
+ }
38309
+ const result = { name: name2, path: `./${name2}`, bytes: upload.totalBytes };
38310
+ this.completed.set(p.uploadId, { sessionId: p.sessionId, result, touched: Date.now() });
38311
+ if (this.completed.size > 64) this.completed.delete(this.completed.keys().next().value);
38312
+ await this.discard(p.uploadId, upload);
38313
+ return result;
38314
+ }
38315
+ throw failure3("UPLOAD_NAME_CONFLICT");
38316
+ }
38317
+ requireUpload(p) {
38318
+ const upload = this.uploads.get(p.uploadId);
38319
+ if (!upload || upload.sessionId !== p.sessionId) throw failure3("UPLOAD_NOT_FOUND");
38320
+ return upload;
38321
+ }
38322
+ assertLive() {
38323
+ if (this.closed) throw failure3("UPLOAD_CLOSED");
38324
+ }
38325
+ async sessionRoot(sessionId) {
38326
+ const signal = AbortSignal.any([this.lifetime.signal, AbortSignal.timeout(15e3)]);
38327
+ let onAbort;
38328
+ try {
38329
+ signal.throwIfAborted();
38330
+ return await Promise.race([
38331
+ this.resolveRoot(sessionId, signal),
38332
+ new Promise((_, reject) => {
38333
+ onAbort = () => reject(failure3(this.closed ? "UPLOAD_CLOSED" : "UPLOAD_TIMEOUT"));
38334
+ signal.addEventListener("abort", onAbort, { once: true });
38335
+ if (signal.aborted) onAbort();
38336
+ })
38337
+ ]);
38338
+ } finally {
38339
+ if (onAbort) signal.removeEventListener("abort", onAbort);
38340
+ }
38341
+ }
38342
+ async validateRoot(upload) {
38343
+ const current = await this.sessionRoot(upload.sessionId);
38344
+ this.assertLive();
38345
+ if (current !== upload.sourceRoot || await realpath6(current) !== upload.root || await realpath6(upload.root) !== upload.root) throw failure3("UPLOAD_WORKSPACE_CHANGED");
38346
+ const directory = await stat6(upload.root);
38347
+ if (!directory.isDirectory() || directory.dev !== upload.dev || directory.ino !== upload.ino) throw failure3("UPLOAD_WORKSPACE_CHANGED");
38348
+ }
38349
+ async discard(id5, upload) {
38350
+ this.uploads.delete(id5);
38351
+ const original = await upload.handle.stat().catch(() => void 0);
38352
+ await upload.handle.close().catch(() => void 0);
38353
+ try {
38354
+ const root = await lstat2(upload.root);
38355
+ const temp = await lstat2(upload.temp);
38356
+ if (original && root.isDirectory() && root.dev === upload.dev && root.ino === upload.ino && temp.isFile() && temp.dev === original.dev && temp.ino === original.ino) await unlink(upload.temp);
38357
+ } catch {
38358
+ }
38359
+ }
38360
+ async expire() {
38361
+ const cutoff = Date.now() - this.ttlMs;
38362
+ await Promise.all([...this.uploads].filter(([, u]) => u.touched <= cutoff).map(([id5, u]) => this.discard(id5, u)));
38363
+ for (const [id5, entry] of this.completed) if (entry.touched <= cutoff) this.completed.delete(id5);
38364
+ }
38365
+ };
38366
+ function failure3(code) {
38367
+ return new RpcError(code, "The workspace upload could not be completed.");
38368
+ }
38369
+ function validName(name2) {
38370
+ return Buffer.byteLength(name2, "utf8") <= 220 && name2 !== "." && name2 !== ".." && !/[/\\\x00-\x1f\x7f<>:"|?*]/u.test(name2) && !/[. ]$/u.test(name2) && !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(name2) && !name2.startsWith(".dsh-upload-");
38371
+ }
38372
+
38373
+ // src/workspace-upload-authority.ts
38374
+ function workspaceUploadRootResolver(gateway, supportsHarnessSessions, codex, acp) {
38375
+ return async (sessionId, signal) => {
38376
+ signal.throwIfAborted();
38377
+ if (sessionId.startsWith("codex:")) {
38378
+ const parsed = parseCodexSessionId(sessionId);
38379
+ if (!parsed) throw unavailable2();
38380
+ return codex(parsed.threadId);
38381
+ }
38382
+ if (/^(acp|cursor|pi|opencode|claude):/.test(sessionId)) return acp(sessionId);
38383
+ if (!gateway?.supportsCarrier || !supportsHarnessSessions()) throw unavailable2();
38384
+ const result = await gateway.dispatch("session/list", { args: { _request: {} } }, signal);
38385
+ signal.throwIfAborted();
38386
+ const value = result.ok ? result.value : void 0;
38387
+ return value?.items?.find((item) => item.sessionId === sessionId)?.cwd;
38388
+ };
38389
+ }
38390
+ function unavailable2() {
38391
+ return new RpcError("UPLOAD_WORKSPACE_UNAVAILABLE", "The session workspace is unavailable.");
38392
+ }
38393
+
38394
+ // src/service.ts
38395
+ import { randomUUID as randomUUID6 } from "node:crypto";
37871
38396
 
37872
38397
  // src/connection-controller.ts
37873
38398
  var ConnectionController = class {
@@ -38098,7 +38623,11 @@ var apiMethods = /* @__PURE__ */ new Set([
38098
38623
  "agent.acp.transfer.commit",
38099
38624
  "agent.acp.transfer.read",
38100
38625
  "agent.acp.transfer.close",
38101
- "plugin.route.call"
38626
+ "plugin.route.call",
38627
+ "workspace.upload.open",
38628
+ "workspace.upload.chunk",
38629
+ "workspace.upload.commit",
38630
+ "workspace.upload.abort"
38102
38631
  ]);
38103
38632
  var HOST_CAPABILITIES = [
38104
38633
  "harness.api.v1",
@@ -38106,13 +38635,14 @@ var HOST_CAPABILITIES = [
38106
38635
  "harness.remote.v1",
38107
38636
  "harness.remote.transfer.v1",
38108
38637
  "fileviewer.read.v1",
38638
+ "workspace.upload.v1",
38109
38639
  "codex.appserver.v1",
38110
38640
  "codex.appserver.transfer.v1",
38111
38641
  "agent.acp.v1",
38112
38642
  "agent.acp.transfer.v1"
38113
38643
  ];
38114
38644
  var RpcRouter = class {
38115
- constructor(harnessApi, maxPending = 128, logger, fileViewer, harnessRemote, capabilities = () => HOST_CAPABILITIES, codex, acp, loopback, workspaceTypes, pluginRoute) {
38645
+ constructor(harnessApi, maxPending = 128, logger, fileViewer, harnessRemote, capabilities = () => HOST_CAPABILITIES, codex, acp, loopback, workspaceTypes, pluginRoute, workspaceUpload) {
38116
38646
  this.harnessApi = harnessApi;
38117
38647
  this.maxPending = maxPending;
38118
38648
  this.logger = logger;
@@ -38124,6 +38654,7 @@ var RpcRouter = class {
38124
38654
  this.loopback = loopback;
38125
38655
  this.workspaceTypes = workspaceTypes;
38126
38656
  this.pluginRoute = pluginRoute;
38657
+ this.workspaceUpload = workspaceUpload;
38127
38658
  }
38128
38659
  active = 0;
38129
38660
  async closePeerStreams() {
@@ -38132,7 +38663,8 @@ var RpcRouter = class {
38132
38663
  this.harnessApi?.closeAll(),
38133
38664
  this.harnessRemote?.closeAll(),
38134
38665
  this.codex?.closeAll(),
38135
- this.acp?.closeAll()
38666
+ this.acp?.closeAll(),
38667
+ this.workspaceUpload?.closeAll()
38136
38668
  ]);
38137
38669
  }
38138
38670
  async handle(message) {
@@ -38161,7 +38693,7 @@ var RpcRouter = class {
38161
38693
  return createRpcResponse(request.id, result);
38162
38694
  } catch (error) {
38163
38695
  const response = errorResponse2(request.id, error);
38164
- const failedEndpoint = typeof request.payload.params?.endpoint === "string" ? String(request.payload.params.endpoint) : void 0;
38696
+ const failedEndpoint = !request.payload.method.startsWith("workspace.upload.") && typeof request.payload.params?.endpoint === "string" ? String(request.payload.params.endpoint) : void 0;
38165
38697
  this.logger?.warn("host rpc failed", {
38166
38698
  method: request.payload.method,
38167
38699
  ...failedEndpoint ? { endpoint: failedEndpoint } : {},
@@ -38272,6 +38804,13 @@ var RpcRouter = class {
38272
38804
  return this.requireAcp().readTransfer(params);
38273
38805
  case "agent.acp.transfer.close":
38274
38806
  return this.requireAcp().closeTransfer(params);
38807
+ case "workspace.upload.open":
38808
+ case "workspace.upload.chunk":
38809
+ case "workspace.upload.commit":
38810
+ case "workspace.upload.abort": {
38811
+ if (!this.workspaceUpload) throw new RpcError("FEATURE_NOT_SUPPORTED", "Workspace upload is unavailable on this Host.");
38812
+ return this.workspaceUpload.call(method, params);
38813
+ }
38275
38814
  case "plugin.route.call":
38276
38815
  return this.requirePluginRoute().call(params);
38277
38816
  default:
@@ -39357,22 +39896,22 @@ function closeCode(code) {
39357
39896
  }
39358
39897
 
39359
39898
  // src/remote-directory-browser.ts
39360
- import { readdir as readdir4, stat as stat6 } from "node:fs/promises";
39899
+ import { readdir as readdir4, stat as stat7 } from "node:fs/promises";
39361
39900
  import { homedir as homedir6, platform as platform2 } from "node:os";
39362
- import { basename as basename5, dirname as dirname5, isAbsolute as isAbsolute7, parse, resolve as resolve5 } from "node:path";
39901
+ import { basename as basename5, dirname as dirname5, isAbsolute as isAbsolute8, parse, resolve as resolve5 } from "node:path";
39363
39902
  var MAX_ENTRIES = 500;
39364
39903
  async function listRemoteDirectory(path, signal) {
39365
39904
  signal?.throwIfAborted();
39366
39905
  const home = resolve5(homedir6());
39367
39906
  const target2 = path === void 0 || path.trim() === "" ? home : resolve5(path);
39368
- if (!isAbsolute7(target2)) throw new Error("The remote directory path must be absolute.");
39907
+ if (!isAbsolute8(target2)) throw new Error("The remote directory path must be absolute.");
39369
39908
  const rows = await readdir4(target2, { withFileTypes: true });
39370
39909
  const directories = [];
39371
39910
  for (const row of rows) {
39372
39911
  signal?.throwIfAborted();
39373
39912
  const child = resolve5(target2, row.name);
39374
39913
  let directory = row.isDirectory();
39375
- if (!directory && row.isSymbolicLink()) directory = await stat6(child).then((value) => value.isDirectory()).catch(() => false);
39914
+ if (!directory && row.isSymbolicLink()) directory = await stat7(child).then((value) => value.isDirectory()).catch(() => false);
39376
39915
  if (!directory) continue;
39377
39916
  directories.push({ name: row.name, path: child, hidden: platform2() !== "win32" && row.name.startsWith(".") });
39378
39917
  }
@@ -40584,7 +41123,7 @@ var HarnessRemoteBridge = class {
40584
41123
  }
40585
41124
  async pump(streamId, source, signal) {
40586
41125
  let reason = "completed";
40587
- let failure3;
41126
+ let failure4;
40588
41127
  try {
40589
41128
  for await (const value of source) {
40590
41129
  if (signal.aborted) break;
@@ -40601,13 +41140,13 @@ var HarnessRemoteBridge = class {
40601
41140
  if (signal.aborted) reason = "cancelled";
40602
41141
  } catch (error) {
40603
41142
  reason = signal.aborted ? "cancelled" : "failed";
40604
- if (!signal.aborted) failure3 = this.gateway.failure(error);
41143
+ if (!signal.aborted) failure4 = this.gateway.failure(error);
40605
41144
  } finally {
40606
41145
  this.streams.delete(streamId);
40607
41146
  await this.publish("harness.remote.stream.closed", {
40608
41147
  streamId,
40609
41148
  reason,
40610
- ...failure3 === void 0 ? {} : { failure: failure3 }
41149
+ ...failure4 === void 0 ? {} : { failure: failure4 }
40611
41150
  }).catch(() => void 0);
40612
41151
  }
40613
41152
  }
@@ -40816,7 +41355,13 @@ var HostPluginRuntime = class {
40816
41355
  // Handles and their lifetime belong to this connection; only policy is shared.
40817
41356
  this.createLoopbackHost(),
40818
41357
  () => this.hostWorkspaceTypes(),
40819
- pluginRoute
41358
+ pluginRoute,
41359
+ new WorkspaceUploadHost(workspaceUploadRootResolver(
41360
+ peerGateway,
41361
+ () => harnessSessionGeneration(this.harnessVersion) === "v3",
41362
+ (threadId) => this.codex.resolveThreadWorkspace(context.connectionId, threadId),
41363
+ (sessionId) => this.acp.resolveUploadWorkspace(context.connectionId, sessionId)
41364
+ ))
40820
41365
  );
40821
41366
  }, this.logger);
40822
41367
  if (config.serverUrl !== void 0) {
@@ -41117,7 +41662,7 @@ var HostPluginRuntime = class {
41117
41662
  let reportedVersion;
41118
41663
  let errorCode6;
41119
41664
  try {
41120
- const response = await this.apiProxy?.host.describe({ rpcId: randomUUID5(), payload: {} });
41665
+ const response = await this.apiProxy?.host.describe({ rpcId: randomUUID6(), payload: {} });
41121
41666
  if (response === void 0) throw new Error("ApiProxy is unavailable");
41122
41667
  if (!response.result.ok) {
41123
41668
  errorCode6 = response.result.error.code;
@@ -41133,7 +41678,7 @@ var HostPluginRuntime = class {
41133
41678
  return void 0;
41134
41679
  }
41135
41680
  hostCapabilities() {
41136
- const capabilities = [];
41681
+ const capabilities = ["workspace.upload.v1"];
41137
41682
  if (this.loopbackPorts.length > 0) capabilities.push("loopback.http-ws.v1");
41138
41683
  if (this.localGateway?.supportsCarrier === true) {
41139
41684
  capabilities.push(
@@ -41290,9 +41835,9 @@ function withSignal(pending, signal) {
41290
41835
  }
41291
41836
 
41292
41837
  // src/cli.ts
41293
- import { stat as stat7 } from "node:fs/promises";
41838
+ import { stat as stat8 } from "node:fs/promises";
41294
41839
  import { hostname as hostname3 } from "node:os";
41295
- import { join as join12 } from "node:path";
41840
+ import { join as join13 } from "node:path";
41296
41841
  var QR_POLL_INTERVAL_MS = 2e3;
41297
41842
  var TERMINAL_QR_MARGIN = 4;
41298
41843
  async function runCli(args = process.argv.slice(2), dependencies = {}) {
@@ -41383,7 +41928,7 @@ async function status(args, runtime) {
41383
41928
  `Server: ${serverUrl}`,
41384
41929
  "Host control: enabled (dsh-TUI default)"
41385
41930
  ];
41386
- if (!await exists3(join12(directory, "device.json"))) {
41931
+ if (!await exists3(join13(directory, "device.json"))) {
41387
41932
  lines.push("Device: not initialized", "Authorization: logged out", "Credential: unavailable");
41388
41933
  write(runtime.stdout, `${lines.join("\n")}
41389
41934
 
@@ -41428,7 +41973,7 @@ async function logout(args, runtime) {
41428
41973
  const serverUrl = selectedServer();
41429
41974
  const root = new IdentityStore({ env: runtime.env }).directory;
41430
41975
  const directory = serverStorageDirectory(root, serverUrl, "host");
41431
- if (!await exists3(join12(directory, "device.json"))) {
41976
+ if (!await exists3(join13(directory, "device.json"))) {
41432
41977
  await new ServerCredentialStore(directory).clear();
41433
41978
  write(runtime.stdout, "This Host is already logged out.\n");
41434
41979
  return 0;
@@ -41554,7 +42099,7 @@ function terminalLink(url, target2) {
41554
42099
  }
41555
42100
  async function exists3(path) {
41556
42101
  try {
41557
- await stat7(path);
42102
+ await stat8(path);
41558
42103
  return true;
41559
42104
  } catch (error) {
41560
42105
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
@@ -42273,18 +42818,18 @@ async function createSettingsBinding(ctx, settings, readConfig, entryId) {
42273
42818
  }
42274
42819
  };
42275
42820
  }
42276
- const scope = settings.register(pluginSettingsNamespace, Config, {
42821
+ const scope2 = settings.register(pluginSettingsNamespace, Config, {
42277
42822
  base: readConfig(),
42278
42823
  applies: "restart",
42279
42824
  validate: (value) => {
42280
42825
  resolveConfig(value);
42281
42826
  }
42282
42827
  });
42283
- reportSettingsMigration(ctx, await migrateLegacySettings(settings, scope));
42828
+ reportSettingsMigration(ctx, await migrateLegacySettings(settings, scope2));
42284
42829
  return {
42285
- get: () => scope.get(),
42830
+ get: () => scope2.get(),
42286
42831
  replace: async (section) => {
42287
- await scope.replace(section);
42832
+ await scope2.replace(section);
42288
42833
  }
42289
42834
  };
42290
42835
  }