dsh-remote-local-plugin 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /* dsh-remote-local-plugin@0.8.3 — bundled host entry */
1
+ /* dsh-remote-local-plugin@0.8.4 — bundled host entry */
2
2
 
3
3
  // src/index.ts
4
4
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
@@ -252,7 +252,7 @@ async function handleHostApi(ctx, service, mirrors, request) {
252
252
  });
253
253
  }
254
254
  try {
255
- const value = await dispatch(ctx, service, mirrors, body.endpoint, body.payload);
255
+ const value = await dispatch(ctx, service, mirrors, body.endpoint, body.payload, request.signal);
256
256
  return jsonResult({ ok: true, value });
257
257
  } catch (error) {
258
258
  const failure = asLocalError(error);
@@ -266,7 +266,7 @@ async function handleHostApi(ctx, service, mirrors, request) {
266
266
  });
267
267
  }
268
268
  }
269
- async function dispatch(ctx, service, mirrors, endpoint, payload) {
269
+ async function dispatch(ctx, service, mirrors, endpoint, payload, signal) {
270
270
  switch (endpoint) {
271
271
  case "state":
272
272
  return service.state();
@@ -396,7 +396,6 @@ async function dispatch(ctx, service, mirrors, endpoint, payload) {
396
396
  const files = ctx.get("workspaceFiles");
397
397
  if (files === void 0) return { mirrored: false };
398
398
  const scope = { sessionId, workspaceRoot: root };
399
- const signal = new AbortController().signal;
400
399
  if (endpoint === "host/files/list") return { mirrored: true, value: await files.list(scope, path, signal) };
401
400
  if (endpoint === "host/files/stat") return { mirrored: true, value: await files.stat(scope, path, signal) };
402
401
  if (endpoint === "host/files/bytes") return { mirrored: true, value: await files.readBytes(scope, path, {
@@ -574,6 +573,7 @@ var RemoteMirrorService = class {
574
573
  #syncing = false;
575
574
  #saveQueue = Promise.resolve();
576
575
  #timer;
576
+ #selectionOff;
577
577
  #disposed = false;
578
578
  constructor(ctx, hosts, config) {
579
579
  this.#ctx = ctx;
@@ -583,6 +583,9 @@ var RemoteMirrorService = class {
583
583
  /** Load mappings and begin periodic discovery. */
584
584
  async start() {
585
585
  await this.#load();
586
+ this.#selectionOff = this.#ctx.on("session/event", ((session, event) => {
587
+ this.#onLocalSessionEvent(session, event);
588
+ }), { global: true });
586
589
  if (!this.#config.mirrorSessions) return;
587
590
  this.#timer = setInterval(() => {
588
591
  void this.sync();
@@ -593,9 +596,13 @@ var RemoteMirrorService = class {
593
596
  async dispose() {
594
597
  this.#disposed = true;
595
598
  if (this.#timer !== void 0) clearInterval(this.#timer);
599
+ this.#selectionOff?.();
600
+ this.#selectionOff = void 0;
596
601
  for (const runtime of this.#runtimes.values()) {
597
602
  runtime.stopped = true;
598
603
  if (runtime.timer !== void 0) clearTimeout(runtime.timer);
604
+ runtime.stream?.abort();
605
+ runtime.assistant?.abort();
599
606
  }
600
607
  for (const dispose of this.#registrations.values()) dispose();
601
608
  this.#registrations.clear();
@@ -674,18 +681,26 @@ var RemoteMirrorService = class {
674
681
  const text = contentText(message);
675
682
  if (text === "") return;
676
683
  runtime.pendingUsers.push(message);
684
+ if (!runtime.running) {
685
+ runtime.awaitingTurn = runtime.latestTurn + 1;
686
+ runtime.awaitingStarted = false;
687
+ } else {
688
+ runtime.awaitingStarted = true;
689
+ }
677
690
  this.#setRunning(runtime, true);
678
691
  if (runtime.stopped || runtime.timer === void 0) {
679
692
  runtime.stopped = false;
680
693
  runtime.lastError = null;
681
694
  this.#schedule(runtime);
682
695
  }
696
+ const selection = this.#selectionFor(runtime);
697
+ if (selection !== void 0) Object.assign(runtime.agent.options, selection);
683
698
  void this.#hosts.promptSession(
684
699
  runtime.record.hostId,
685
700
  runtime.record.remoteSessionId,
686
701
  text,
687
702
  mode,
688
- latestSelection(runtime.session)
703
+ selection
689
704
  ).catch((error) => {
690
705
  runtime.lastError = error instanceof Error ? error.message : String(error);
691
706
  runtime.pendingUsers.shift();
@@ -693,12 +708,32 @@ var RemoteMirrorService = class {
693
708
  this.#setRunning(runtime, false);
694
709
  });
695
710
  }
696
- /** Route cancellation to the remote session. */
711
+ /** Route cancellation to the remote session only after the remote accepts it. */
697
712
  cancel(runtime) {
698
- void this.#hosts.cancelSession(runtime.record.hostId, runtime.record.remoteSessionId).catch((error) => {
699
- this.#ctx.logger.warn("dsh-remote-local: remote cancel failed: %s", String(error));
713
+ void this.#hosts.cancelSession(runtime.record.hostId, runtime.record.remoteSessionId).then(() => {
714
+ runtime.awaitingTurn = null;
715
+ runtime.awaitingStarted = false;
716
+ this.#setRunning(runtime, false);
717
+ }).catch((error) => {
718
+ runtime.lastError = error instanceof Error ? error.message : String(error);
719
+ this.#ctx.logger.warn("dsh-remote-local: remote cancel failed: %s", runtime.lastError);
720
+ this.#setRunning(runtime, true);
700
721
  });
701
- this.#setRunning(runtime, false);
722
+ }
723
+ #selectionFor(runtime) {
724
+ const projections = this.#ctx.get("sessionProjections");
725
+ const state = projections?.stateOf?.(runtime.session, "modelSelection");
726
+ const read = (candidate) => {
727
+ if (typeof candidate !== "object" || candidate === null) return void 0;
728
+ const record = candidate;
729
+ if (typeof record.provider !== "string" || typeof record.model !== "string") return void 0;
730
+ return {
731
+ provider: record.provider,
732
+ model: record.model,
733
+ ...typeof record.reasoningEffort === "string" ? { reasoningEffort: record.reasoningEffort } : {}
734
+ };
735
+ };
736
+ return read(state?.pending) ?? latestSelection(runtime.session) ?? read(runtime.agent.options) ?? read(state?.lastUsed);
702
737
  }
703
738
  async #syncHost(host) {
704
739
  this.#ensureApprovalStream(host);
@@ -890,18 +925,32 @@ var RemoteMirrorService = class {
890
925
  async #ensureMirror(host, remote, workspace, _remotePath) {
891
926
  const ref = key(host.id, remote.sessionId);
892
927
  const existing = this.#state.sessions[ref];
928
+ let reuse;
893
929
  if (existing !== void 0) {
894
930
  const live = this.#sessions.get(existing.localSessionId);
895
- const runtime2 = this.#runtimes.get(existing.localSessionId);
896
- if (live !== void 0 && runtime2 !== void 0) {
931
+ const runtime = this.#runtimes.get(existing.localSessionId);
932
+ if (live !== void 0 && runtime !== void 0 && (!runtime.stopped || !remote.live)) {
897
933
  await this.#attach(workspace, live.id);
934
+ if (remote.live) {
935
+ this.#startStream(runtime);
936
+ this.#startAssistantStream(runtime);
937
+ }
898
938
  return existing;
899
939
  }
900
- if (live !== void 0) {
901
- await this.#archive(existing.localSessionId);
902
- } else if (existing.localSessionId !== "") {
903
- await this.#archive(existing.localSessionId);
940
+ const stalled = this.#runtimes.get(existing.localSessionId);
941
+ if (stalled !== void 0) {
942
+ stalled.stopped = true;
943
+ if (stalled.timer !== void 0) clearTimeout(stalled.timer);
944
+ stalled.stream?.abort();
945
+ stalled.assistant?.abort();
946
+ this.#runtimes.delete(existing.localSessionId);
947
+ }
948
+ const registration = this.#registrations.get(existing.localSessionId);
949
+ if (registration !== void 0) {
950
+ registration();
951
+ this.#registrations.delete(existing.localSessionId);
904
952
  }
953
+ if (live !== void 0) reuse = live;
905
954
  }
906
955
  const snapshot = await this.#hosts.snapshot(host.id, remote.sessionId, this.#config.mirrorMaxEvents);
907
956
  if (snapshot.truncated && (snapshot.events[0]?.seq ?? 0) > 0) {
@@ -914,11 +963,38 @@ var RemoteMirrorService = class {
914
963
  }
915
964
  const anchor = this.#anchorFor(workspace);
916
965
  await mkdir(anchor, { recursive: true });
917
- const localId = `session-${randomUUID()}`;
966
+ const filtered = snapshot.events.filter((event) => !isMirrorNoise(event.type));
967
+ const seqMap = new Map(filtered.map((event, index) => [event.seq, index]));
968
+ let latestTurn = 0;
969
+ for (const event of filtered) {
970
+ if (event.type !== "turn/start") continue;
971
+ const turn = event.data.turn;
972
+ if (typeof turn === "number") latestTurn = Math.max(latestTurn, turn);
973
+ }
974
+ const remoteToLocalSeq = /* @__PURE__ */ new Map();
975
+ for (const [index, event] of filtered.entries()) remoteToLocalSeq.set(event.seq, index);
976
+ if (reuse !== void 0) {
977
+ const localEvents = reuse.snapshotEvents();
978
+ for (let index = 0; index < Math.min(localEvents.length, filtered.length); index += 1) {
979
+ const localEvent = localEvents[index];
980
+ const remoteEvent = filtered[index];
981
+ if (localEvent !== void 0 && remoteEvent !== void 0) {
982
+ remoteToLocalSeq.set(remoteEvent.seq, localEvent.seq);
983
+ }
984
+ }
985
+ const record2 = existing;
986
+ await this.#attach(workspace, reuse.id);
987
+ await this.#mountRuntime(record2, reuse, snapshot, remoteToLocalSeq, latestTurn, remote.live);
988
+ this.#ctx.logger.info(
989
+ "dsh-remote-local: reattached remote session %s to local %s",
990
+ remote.sessionId,
991
+ reuse.id
992
+ );
993
+ return record2;
994
+ }
995
+ const localId = existing?.localSessionId ?? `session-${randomUUID()}`;
918
996
  let session;
919
997
  try {
920
- const filtered = snapshot.events.filter((event) => !isMirrorNoise(event.type));
921
- const seqMap = new Map(filtered.map((event, index) => [event.seq, index]));
922
998
  const seed = filtered.map((event, index) => toSeedEvent(event, index, seqMap));
923
999
  session = this.#sessions.create(localId, {
924
1000
  ...seed.length === 0 ? {} : { seed },
@@ -934,7 +1010,7 @@ var RemoteMirrorService = class {
934
1010
  } catch (error) {
935
1011
  this.#ctx.logger.debug("dsh-remote-local: mirror flush failed: %s", String(error));
936
1012
  }
937
- const record = {
1013
+ const record = existing === void 0 ? {
938
1014
  hostId: host.id,
939
1015
  remoteSessionId: remote.sessionId,
940
1016
  localSessionId: localId,
@@ -942,32 +1018,53 @@ var RemoteMirrorService = class {
942
1018
  anchor,
943
1019
  lastSeq: snapshot.cursor,
944
1020
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1021
+ } : {
1022
+ ...existing,
1023
+ remoteWorkspaceId: remote.workspaceId,
1024
+ anchor,
1025
+ lastSeq: snapshot.cursor,
1026
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
945
1027
  };
946
1028
  this.#state.sessions[ref] = record;
1029
+ await this.#mountRuntime(record, session, snapshot, remoteToLocalSeq, latestTurn, remote.live);
1030
+ this.#ctx.logger.info(
1031
+ "dsh-remote-local: mirrored remote session %s as local %s",
1032
+ remote.sessionId,
1033
+ localId
1034
+ );
1035
+ return record;
1036
+ }
1037
+ async #mountRuntime(record, session, snapshot, remoteToLocalSeq, latestTurn, live) {
947
1038
  const runtime = {
948
1039
  record,
949
1040
  agent: null,
950
1041
  session,
951
1042
  timer: void 0,
1043
+ stream: void 0,
1044
+ assistant: void 0,
1045
+ remoteToLocalSeq,
1046
+ pendingAssistantEnds: /* @__PURE__ */ new Map(),
1047
+ latestTurn,
1048
+ awaitingTurn: null,
1049
+ awaitingStarted: false,
1050
+ applyingRemote: false,
952
1051
  running: false,
953
1052
  stopped: false,
954
1053
  lastError: null,
955
1054
  pendingUsers: [],
956
1055
  waiters: []
957
1056
  };
958
- const options = modelOptions(snapshot.events);
1057
+ const options = this.#defaultSelection() ?? modelOptions(snapshot.events);
959
1058
  const agent = new RemoteMirrorAgent(this, runtime, this.#ctx, options);
960
1059
  runtime.agent = agent;
961
- this.#runtimes.set(localId, runtime);
962
- this.#registrations.set(localId, this.#agents.enter(agent, void 0));
1060
+ this.#runtimes.set(record.localSessionId, runtime);
1061
+ this.#registrations.set(record.localSessionId, this.#agents.enter(agent, void 0));
963
1062
  await this.#save();
964
1063
  this.#schedule(runtime);
965
- this.#ctx.logger.info(
966
- "dsh-remote-local: mirrored remote session %s as local %s",
967
- remote.sessionId,
968
- localId
969
- );
970
- return record;
1064
+ if (live) {
1065
+ this.#startStream(runtime);
1066
+ this.#startAssistantStream(runtime);
1067
+ }
971
1068
  }
972
1069
  #schedule(runtime) {
973
1070
  if (runtime.stopped || this.#disposed) return;
@@ -981,6 +1078,8 @@ var RemoteMirrorService = class {
981
1078
  const local = this.#sessions.get(runtime.session.id);
982
1079
  if (local === void 0) {
983
1080
  runtime.stopped = true;
1081
+ runtime.stream?.abort();
1082
+ runtime.assistant?.abort();
984
1083
  return;
985
1084
  }
986
1085
  const snapshot = await this.#hosts.snapshot(
@@ -991,24 +1090,29 @@ var RemoteMirrorService = class {
991
1090
  if (snapshot.truncated && (snapshot.events[0]?.seq ?? 0) > runtime.record.lastSeq) {
992
1091
  runtime.lastError = "remote log grew past the mirror window";
993
1092
  runtime.stopped = true;
1093
+ runtime.stream?.abort();
1094
+ runtime.assistant?.abort();
994
1095
  return;
995
1096
  }
996
- let sawEnd = false;
997
1097
  for (const event of snapshot.events) {
998
1098
  if (event.seq < runtime.record.lastSeq) continue;
999
1099
  if (event.seq !== runtime.record.lastSeq) {
1000
1100
  throw new Error(`remote event gap at ${event.seq}, mirror ${runtime.record.lastSeq}`);
1001
1101
  }
1002
- if (!isMirrorNoise(event.type)) appendRemoteEvent(local, event, runtime);
1102
+ this.#appendEvent(runtime, event);
1003
1103
  runtime.record.lastSeq = event.seq + 1;
1004
- if (event.type === "turn/end") sawEnd = true;
1005
1104
  }
1006
1105
  runtime.record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1007
- if (sawEnd) this.#setRunning(runtime, false);
1008
1106
  if (!snapshot.live && runtime.record.lastSeq >= snapshot.cursor) {
1009
1107
  runtime.stopped = true;
1108
+ runtime.stream?.abort();
1109
+ runtime.assistant?.abort();
1010
1110
  }
1011
1111
  await this.#save();
1112
+ if (snapshot.live && !runtime.stopped) {
1113
+ this.#startStream(runtime);
1114
+ this.#startAssistantStream(runtime);
1115
+ }
1012
1116
  } catch (error) {
1013
1117
  runtime.lastError = error instanceof Error ? error.message : String(error);
1014
1118
  this.#ctx.logger.warn(
@@ -1019,12 +1123,204 @@ var RemoteMirrorService = class {
1019
1123
  }
1020
1124
  this.#schedule(runtime);
1021
1125
  }
1126
+ #startStream(runtime) {
1127
+ if (runtime.stopped || this.#disposed || runtime.stream !== void 0) return;
1128
+ const controller = new AbortController();
1129
+ runtime.stream = controller;
1130
+ void this.#followStream(runtime, controller.signal).finally(() => {
1131
+ if (runtime.stream === controller) runtime.stream = void 0;
1132
+ });
1133
+ }
1134
+ async #followStream(runtime, signal) {
1135
+ try {
1136
+ for await (const frame of this.#hosts.followSessionEvents(
1137
+ runtime.record.hostId,
1138
+ runtime.record.remoteSessionId,
1139
+ runtime.record.lastSeq,
1140
+ signal
1141
+ )) {
1142
+ if (runtime.stopped || this.#disposed || signal.aborted) return;
1143
+ if (frame.type === "end") return;
1144
+ if (frame.type !== "event") continue;
1145
+ const event = frame.event;
1146
+ if (event.seq < runtime.record.lastSeq) continue;
1147
+ if (event.seq !== runtime.record.lastSeq) {
1148
+ runtime.lastError = `remote event gap at ${event.seq}, mirror ${runtime.record.lastSeq}`;
1149
+ return;
1150
+ }
1151
+ this.#appendEvent(runtime, event);
1152
+ runtime.record.lastSeq = event.seq + 1;
1153
+ runtime.record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1154
+ await this.#save();
1155
+ }
1156
+ } catch (error) {
1157
+ if (signal.aborted || runtime.stopped || this.#disposed) return;
1158
+ runtime.lastError = error instanceof Error ? error.message : String(error);
1159
+ this.#ctx.logger.warn(
1160
+ "dsh-remote-local: mirror stream %s failed: %s",
1161
+ runtime.record.remoteSessionId,
1162
+ runtime.lastError
1163
+ );
1164
+ }
1165
+ }
1166
+ #appendEvent(runtime, event) {
1167
+ const before = runtime.session.seq;
1168
+ runtime.applyingRemote = true;
1169
+ try {
1170
+ if (!isMirrorNoise(event.type)) {
1171
+ appendRemoteEvent(runtime.session, remapRemoteEvent(event, runtime.remoteToLocalSeq), runtime);
1172
+ }
1173
+ } finally {
1174
+ runtime.applyingRemote = false;
1175
+ }
1176
+ const after = runtime.session.seq;
1177
+ if (after > before) {
1178
+ runtime.remoteToLocalSeq.set(event.seq, after - 1);
1179
+ this.#flushAssistantEnd(runtime, event.seq);
1180
+ }
1181
+ const turn = typeof event.data === "object" && event.data !== null ? event.data.turn : void 0;
1182
+ if (typeof turn !== "number") return;
1183
+ if (event.type === "turn/start") {
1184
+ runtime.latestTurn = Math.max(runtime.latestTurn, turn);
1185
+ return;
1186
+ }
1187
+ if (event.type !== "turn/end") return;
1188
+ runtime.latestTurn = Math.max(runtime.latestTurn, turn);
1189
+ if (runtime.awaitingTurn === null || turn >= runtime.awaitingTurn) {
1190
+ runtime.awaitingTurn = null;
1191
+ runtime.awaitingStarted = false;
1192
+ this.#setRunning(runtime, false);
1193
+ }
1194
+ }
1195
+ #onLocalSessionEvent(session, event) {
1196
+ const candidate = event;
1197
+ if (candidate.type !== "model/selection") return;
1198
+ const runtime = [...this.#runtimes.values()].find((item) => item.session.id === session.id);
1199
+ if (runtime === void 0 || runtime.stopped || runtime.applyingRemote) return;
1200
+ const record = candidate.data;
1201
+ if (typeof record.provider !== "string" || typeof record.model !== "string") return;
1202
+ const selection = {
1203
+ provider: record.provider,
1204
+ model: record.model,
1205
+ ...typeof record.reasoningEffort === "string" ? { reasoningEffort: record.reasoningEffort } : {}
1206
+ };
1207
+ Object.assign(runtime.agent.options, selection);
1208
+ void this.#hosts.selectModel(runtime.record.hostId, runtime.record.remoteSessionId, selection).catch((error) => {
1209
+ runtime.lastError = error instanceof Error ? error.message : String(error);
1210
+ this.#ctx.logger.warn("dsh-remote-local: remote model selection failed: %s", runtime.lastError);
1211
+ });
1212
+ }
1213
+ #defaultSelection() {
1214
+ const defaults = this.#ctx.get("agentDefaultModel");
1215
+ const candidate = defaults?.currentSelection?.();
1216
+ if (typeof candidate !== "object" || candidate === null) return void 0;
1217
+ const record = candidate;
1218
+ if (typeof record.provider !== "string" || typeof record.model !== "string") return void 0;
1219
+ return {
1220
+ provider: record.provider,
1221
+ model: record.model,
1222
+ ...typeof record.reasoningEffort === "string" ? { reasoningEffort: record.reasoningEffort } : {}
1223
+ };
1224
+ }
1225
+ #flushAssistantEnd(runtime, remoteSeq) {
1226
+ const pending = runtime.pendingAssistantEnds.get(remoteSeq);
1227
+ if (pending === void 0) return;
1228
+ const localSeq = runtime.remoteToLocalSeq.get(remoteSeq);
1229
+ if (localSeq === void 0) return;
1230
+ runtime.pendingAssistantEnds.delete(remoteSeq);
1231
+ if (pending.outcome?.kind !== "committed") {
1232
+ this.#emitAssistant(runtime, pending);
1233
+ return;
1234
+ }
1235
+ this.#emitAssistant(runtime, {
1236
+ ...pending,
1237
+ outcome: { ...pending.outcome, seq: localSeq }
1238
+ });
1239
+ }
1240
+ #emitAssistant(runtime, frame) {
1241
+ try {
1242
+ const emit = this.#ctx.emit;
1243
+ emit.call(this.#ctx, "agent/assistant-stream", { agent: runtime.agent, frame });
1244
+ } catch (error) {
1245
+ this.#ctx.logger.warn(
1246
+ "dsh-remote-local: local assistant frame %s failed: %s",
1247
+ runtime.record.remoteSessionId,
1248
+ String(error)
1249
+ );
1250
+ }
1251
+ }
1252
+ #startAssistantStream(runtime) {
1253
+ if (runtime.stopped || this.#disposed || runtime.assistant !== void 0) return;
1254
+ const controller = new AbortController();
1255
+ runtime.assistant = controller;
1256
+ void this.#followAssistant(runtime, controller.signal).finally(() => {
1257
+ if (runtime.assistant === controller) runtime.assistant = void 0;
1258
+ });
1259
+ }
1260
+ async #followAssistant(runtime, signal) {
1261
+ try {
1262
+ for await (const frame of this.#hosts.followAssistantFrames(
1263
+ runtime.record.hostId,
1264
+ runtime.record.remoteSessionId,
1265
+ signal
1266
+ )) {
1267
+ if (runtime.stopped || this.#disposed || signal.aborted) return;
1268
+ if (frame.type === "start") {
1269
+ runtime.awaitingStarted = true;
1270
+ this.#setRunning(runtime, true);
1271
+ const { startedAfterSeq: _startedAfterSeq, ...rest } = frame;
1272
+ this.#emitAssistant(runtime, rest);
1273
+ continue;
1274
+ }
1275
+ if (frame.type === "chunk") {
1276
+ const chunkType = frame.chunk?.type;
1277
+ if (chunkType !== "tool-call-delta") this.#emitAssistant(runtime, frame);
1278
+ continue;
1279
+ }
1280
+ const outcome = frame.outcome;
1281
+ if (outcome === void 0) return;
1282
+ if (outcome.kind !== "committed") {
1283
+ this.#emitAssistant(runtime, frame);
1284
+ continue;
1285
+ }
1286
+ const localSeq = runtime.remoteToLocalSeq.get(outcome.seq);
1287
+ if (localSeq === void 0) {
1288
+ runtime.pendingAssistantEnds.set(outcome.seq, frame);
1289
+ continue;
1290
+ }
1291
+ this.#emitAssistant(runtime, {
1292
+ ...frame,
1293
+ outcome: { ...outcome, seq: localSeq }
1294
+ });
1295
+ }
1296
+ } catch (error) {
1297
+ if (signal.aborted || runtime.stopped || this.#disposed) return;
1298
+ this.#ctx.logger.warn(
1299
+ "dsh-remote-local: assistant stream %s failed: %s",
1300
+ runtime.record.remoteSessionId,
1301
+ error instanceof Error ? error.message : String(error)
1302
+ );
1303
+ }
1304
+ }
1022
1305
  #setRunning(runtime, running) {
1023
1306
  if (runtime.running === running) return;
1024
1307
  runtime.running = running;
1025
1308
  if (!running) {
1026
1309
  for (const waiter of runtime.waiters.splice(0)) waiter();
1027
1310
  }
1311
+ try {
1312
+ const emit = this.#ctx.emit;
1313
+ emit.call(this.#ctx, "agent/status", {
1314
+ agent: runtime.agent,
1315
+ status: running ? "running" : "idle"
1316
+ });
1317
+ } catch (error) {
1318
+ this.#ctx.logger.warn(
1319
+ "dsh-remote-local: local agent status %s failed: %s",
1320
+ runtime.record.remoteSessionId,
1321
+ String(error)
1322
+ );
1323
+ }
1028
1324
  }
1029
1325
  async #attach(workspace, sessionId) {
1030
1326
  try {
@@ -1074,6 +1370,19 @@ var RemoteMirrorService = class {
1074
1370
  return task;
1075
1371
  }
1076
1372
  };
1373
+ function remapRemoteEvent(event, remoteToLocal) {
1374
+ const sourceEventSeqs = event.sourceEventSeqs?.map((seq) => remoteToLocal.get(seq)).filter((seq) => seq !== void 0);
1375
+ const surfaceOp = typeof event.surfaceOp === "object" ? {
1376
+ op: "replace",
1377
+ startSeq: remoteToLocal.get(event.surfaceOp.startSeq) ?? 0,
1378
+ endSeq: remoteToLocal.get(event.surfaceOp.endSeq) ?? 0
1379
+ } : event.surfaceOp;
1380
+ return {
1381
+ ...event,
1382
+ ...sourceEventSeqs === void 0 || sourceEventSeqs.length === 0 ? {} : { sourceEventSeqs },
1383
+ ...surfaceOp === void 0 ? {} : { surfaceOp }
1384
+ };
1385
+ }
1077
1386
  function isMirrorNoise(type) {
1078
1387
  return type === "agent/inbox/spliced";
1079
1388
  }
@@ -1189,6 +1498,7 @@ function safe(value) {
1189
1498
  // src/remote/files.ts
1190
1499
  import { spawn as spawn2 } from "node:child_process";
1191
1500
  import { isAbsolute as isAbsolute2, join as join4, relative, resolve as resolve3, sep } from "node:path";
1501
+ var HELPER_TIMEOUT_MS = 15e3;
1192
1502
  var HELPER = String.raw`
1193
1503
  import sys, json, os, base64
1194
1504
  req = json.load(sys.stdin)
@@ -1346,21 +1656,41 @@ function installRemoteWorkspaceFiles(ctx, mirrors, hosts) {
1346
1656
  function targetOf(mirrors, workspaceRoot, path) {
1347
1657
  const context = mirrors.remoteFileContext(workspaceRoot);
1348
1658
  if (context === void 0) return void 0;
1349
- const localPath = isAbsolute2(path) ? resolve3(path) : resolve3(workspaceRoot, path);
1350
- const rel = relative(workspaceRoot, localPath);
1351
- if (rel === "" || !rel.startsWith("..") && !isAbsolute2(rel)) {
1352
- const remotePath = rel === "" ? context.remoteRoot : join4(context.remoteRoot, rel);
1659
+ const anchor = resolve3(workspaceRoot);
1660
+ const remoteRoot = resolve3(context.remoteRoot);
1661
+ const candidate = isAbsolute2(path) ? resolve3(path) : resolve3(anchor, path);
1662
+ const contains = (root, value) => {
1663
+ const rel = relative(root, value);
1664
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
1665
+ };
1666
+ if (isAbsolute2(path) && contains(remoteRoot, candidate)) {
1667
+ const rel = relative(remoteRoot, candidate);
1353
1668
  return {
1354
1669
  hostId: context.hostId,
1355
- localPath: localPath === "" ? workspaceRoot : localPath,
1670
+ localPath: rel === "" ? anchor : resolve3(anchor, rel),
1671
+ remotePath: candidate,
1672
+ relativePath: rel.split(sep).join("/"),
1673
+ workspaceRoot: anchor
1674
+ };
1675
+ }
1676
+ if (contains(anchor, candidate)) {
1677
+ const rel = relative(anchor, candidate);
1678
+ const remotePath = rel === "" ? remoteRoot : join4(remoteRoot, rel);
1679
+ return {
1680
+ hostId: context.hostId,
1681
+ localPath: candidate,
1356
1682
  remotePath,
1357
1683
  relativePath: rel.split(sep).join("/"),
1358
- workspaceRoot
1684
+ workspaceRoot: anchor
1359
1685
  };
1360
1686
  }
1361
- return void 0;
1687
+ throw Object.assign(
1688
+ new Error(`remote file path is outside the mirrored workspace: ${path}`),
1689
+ { code: "workspace-file/not-found" }
1690
+ );
1362
1691
  }
1363
1692
  async function helper(hosts, hostId, request, signal) {
1693
+ if (signal.aborted) throw new Error("remote file helper aborted");
1364
1694
  const ssh = hosts.sshFor(hostId);
1365
1695
  const args = [
1366
1696
  "-o",
@@ -1380,7 +1710,7 @@ async function helper(hosts, hostId, request, signal) {
1380
1710
  const timer = setTimeout(() => {
1381
1711
  child.kill("SIGKILL");
1382
1712
  reject(new Error("remote file helper timed out"));
1383
- }, 3e4);
1713
+ }, HELPER_TIMEOUT_MS);
1384
1714
  const abort = () => {
1385
1715
  child.kill("SIGKILL");
1386
1716
  reject(new Error("remote file helper aborted"));
@@ -1394,6 +1724,11 @@ async function helper(hosts, hostId, request, signal) {
1394
1724
  child.stderr.on("data", (chunk) => {
1395
1725
  stderr += chunk;
1396
1726
  });
1727
+ child.stdin.on("error", (error) => {
1728
+ clearTimeout(timer);
1729
+ signal.removeEventListener("abort", abort);
1730
+ reject(error);
1731
+ });
1397
1732
  child.on("error", (error) => {
1398
1733
  clearTimeout(timer);
1399
1734
  signal.removeEventListener("abort", abort);
@@ -1840,6 +2175,35 @@ var RemoteProtocolError = class extends Error {
1840
2175
  }
1841
2176
  };
1842
2177
 
2178
+ // ../protocol/lib/types.js
2179
+ function isRemoteAssistantFrame(value) {
2180
+ if (typeof value !== "object" || value === null)
2181
+ return false;
2182
+ const frame = value;
2183
+ if (frame.type !== "start" && frame.type !== "chunk" && frame.type !== "end")
2184
+ return false;
2185
+ if (typeof frame.attemptId !== "string" || typeof frame.revision !== "number")
2186
+ return false;
2187
+ if (frame.type === "start") {
2188
+ return typeof frame.turn === "number" && typeof frame.step === "number";
2189
+ }
2190
+ if (frame.type === "chunk") {
2191
+ return typeof frame.index === "number" && typeof frame.time === "number" && "chunk" in frame;
2192
+ }
2193
+ if (typeof frame.index !== "number")
2194
+ return false;
2195
+ const outcome = frame.outcome;
2196
+ if (typeof outcome !== "object" || outcome === null)
2197
+ return false;
2198
+ const kind = outcome.kind;
2199
+ if (kind === "abandoned")
2200
+ return true;
2201
+ if (kind !== "committed")
2202
+ return false;
2203
+ const record = outcome;
2204
+ return typeof record.eventType === "string" && typeof record.seq === "number";
2205
+ }
2206
+
1843
2207
  // ../protocol/lib/routes.js
1844
2208
  var AGENT_ROUTES = {
1845
2209
  health: `${AGENT_API_PREFIX}/health`,
@@ -1856,6 +2220,9 @@ function workspaceSessionsPath(workspaceId) {
1856
2220
  function sessionPath(sessionId) {
1857
2221
  return `${AGENT_API_PREFIX}/sessions/${encodeURIComponent(sessionId)}`;
1858
2222
  }
2223
+ function sessionModelPath(sessionId) {
2224
+ return `${sessionPath(sessionId)}/model`;
2225
+ }
1859
2226
  function sessionPromptPath(sessionId) {
1860
2227
  return `${sessionPath(sessionId)}/prompt`;
1861
2228
  }
@@ -1866,6 +2233,9 @@ function sessionEventsPath(sessionId, cursor) {
1866
2233
  const base = `${sessionPath(sessionId)}/events`;
1867
2234
  return cursor === void 0 ? base : `${base}?cursor=${encodeURIComponent(String(cursor))}`;
1868
2235
  }
2236
+ function sessionAssistantEventsPath(sessionId) {
2237
+ return `${sessionPath(sessionId)}/assistant-events`;
2238
+ }
1869
2239
  var SESSION_PREFIX = `${AGENT_API_PREFIX}/sessions/`;
1870
2240
  var WORKSPACE_PREFIX = `${AGENT_API_PREFIX}/workspaces/`;
1871
2241
 
@@ -2108,6 +2478,13 @@ var RemoteAgentClient = class {
2108
2478
  body: request
2109
2479
  });
2110
2480
  }
2481
+ /** Select the model and reasoning effort for one remote session. */
2482
+ async selectModel(sessionId, request) {
2483
+ await this.#request(sessionModelPath(sessionId), isAcceptedResult, {
2484
+ method: "POST",
2485
+ body: request
2486
+ });
2487
+ }
2111
2488
  /** Send a text prompt to a remote session. */
2112
2489
  async promptSession(sessionId, request) {
2113
2490
  await this.#request(sessionPromptPath(sessionId), isAcceptedResult, {
@@ -2192,6 +2569,38 @@ var RemoteAgentClient = class {
2192
2569
  yield frame;
2193
2570
  }
2194
2571
  }
2572
+ /** Follow one session's dense process-local Assistant frames. */
2573
+ async *followAssistantFrames(sessionId, signal) {
2574
+ const response = await this.#openStream(sessionAssistantEventsPath(sessionId), signal);
2575
+ const body = response.body;
2576
+ if (body === null) {
2577
+ throw new RemoteProtocolError({
2578
+ code: "request/invalid",
2579
+ message: "remote agent returned an empty assistant stream"
2580
+ });
2581
+ }
2582
+ for await (const message of parseSseStream(body)) {
2583
+ if (message.data === "")
2584
+ continue;
2585
+ let frame;
2586
+ try {
2587
+ frame = JSON.parse(message.data);
2588
+ } catch (error) {
2589
+ throw new RemoteProtocolError({
2590
+ code: "request/invalid",
2591
+ message: "remote agent sent malformed assistant JSON",
2592
+ cause: error
2593
+ });
2594
+ }
2595
+ if (!isRemoteAssistantFrame(frame)) {
2596
+ throw new RemoteProtocolError({
2597
+ code: "request/invalid",
2598
+ message: "remote agent sent an unknown assistant frame"
2599
+ });
2600
+ }
2601
+ yield frame;
2602
+ }
2603
+ }
2195
2604
  /** Validate an event sequence against a cursor. */
2196
2605
  static guardEvent(cursor, event) {
2197
2606
  return cursor.accept(event);
@@ -2656,6 +3065,7 @@ function renderSystemdUnit(options) {
2656
3065
  // $PATH is expanded by the bootstrap shell after nodePathPrelude(), so the
2657
3066
  // systemd unit sees the discovered nvm/Volta/fnm Node bin directory too.
2658
3067
  'Environment="PATH=$INSTALL_DIR/node/bin:$INSTALL_DIR/node_modules/.bin:/usr/local/bin:/usr/bin:/bin:$PATH"',
3068
+ "${PROXY_ENV_LINES}",
2659
3069
  ...options.modelBaseUrl === void 0 ? [] : ['Environment="DEEPSEEK_BASE_URL=$MODEL_BASE_URL"'],
2660
3070
  ...options.modelApiKey === void 0 ? [] : ['Environment="DEEPSEEK_API_KEY=$MODEL_API_KEY"'],
2661
3071
  'ExecStart="$DSH_BIN" web --host 127.0.0.1 --port $PORT --no-open',
@@ -2858,6 +3268,16 @@ if [ -n "$PROXY_URL" ]; then
2858
3268
  fi
2859
3269
  echo "dsh-remote: install proxy configured"
2860
3270
  fi
3271
+ PROXY_ENV_LINES=""
3272
+ if [ -n "$PROXY_URL" ]; then
3273
+ PROXY_ENV_LINES="Environment=\\"HTTP_PROXY=$PROXY_URL\\"
3274
+ Environment=\\"HTTPS_PROXY=$PROXY_URL\\"
3275
+ Environment=\\"ALL_PROXY=$PROXY_URL\\""
3276
+ if [ -n "$PROXY_NO_PROXY" ]; then
3277
+ PROXY_ENV_LINES="$PROXY_ENV_LINES
3278
+ Environment=\\"NO_PROXY=$PROXY_NO_PROXY\\""
3279
+ fi
3280
+ fi
2861
3281
 
2862
3282
  ${nodePathPrelude()}
2863
3283
 
@@ -3879,6 +4299,16 @@ var RemoteHostsService = class {
3879
4299
  throw this.#remoteFailure(id, error);
3880
4300
  }
3881
4301
  }
4302
+ /** Follow a live remote session from a cursor as an SSE-backed stream. */
4303
+ async *followSessionEvents(id, sessionId, cursor, signal) {
4304
+ const client = this.#requireClient(id);
4305
+ try {
4306
+ yield* client.followSession(sessionId, cursor, signal);
4307
+ } catch (error) {
4308
+ if (signal.aborted) return;
4309
+ throw this.#remoteFailure(id, error);
4310
+ }
4311
+ }
3882
4312
  /** Read one session snapshot. */
3883
4313
  async snapshot(id, sessionId, limit) {
3884
4314
  const client = this.#requireClient(id);
@@ -3888,6 +4318,16 @@ var RemoteHostsService = class {
3888
4318
  throw this.#remoteFailure(id, error);
3889
4319
  }
3890
4320
  }
4321
+ /** Follow dense remote Assistant frames for one live Session. */
4322
+ async *followAssistantFrames(id, sessionId, signal) {
4323
+ const client = this.#requireClient(id);
4324
+ try {
4325
+ yield* client.followAssistantFrames(sessionId, signal);
4326
+ } catch (error) {
4327
+ if (signal.aborted) return;
4328
+ throw this.#remoteFailure(id, error);
4329
+ }
4330
+ }
3891
4331
  /** Register a remote workspace. */
3892
4332
  async createWorkspace(id, request) {
3893
4333
  const client = this.#requireClient(id);
@@ -3908,6 +4348,15 @@ var RemoteHostsService = class {
3908
4348
  throw this.#remoteFailure(id, error);
3909
4349
  }
3910
4350
  }
4351
+ /** Install a model/reasoning-effort selection on a remote session. */
4352
+ async selectModel(id, sessionId, selection) {
4353
+ const client = this.#requireClient(id);
4354
+ try {
4355
+ await client.selectModel(sessionId, selection);
4356
+ } catch (error) {
4357
+ throw this.#remoteFailure(id, error);
4358
+ }
4359
+ }
3911
4360
  /** Send a prompt to a remote session. */
3912
4361
  async promptSession(id, sessionId, text, mode, selection) {
3913
4362
  const client = this.#requireClient(id);