dsh-remote-local-plugin 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/goldenpigeon-dsh-remote-agent-0.2.3.tgz +0 -0
- package/lib/client.js +35 -12
- package/lib/client.js.map +2 -2
- package/lib/index.js +613 -42
- package/lib/types/host-api/contract.d.ts +5 -1
- package/lib/types/mirror/service.d.ts +10 -2
- package/lib/types/remote/restart.d.ts +7 -0
- package/lib/types/rpc/contract.d.ts +114 -0
- package/lib/types/rpc/handler.d.ts +12 -0
- package/lib/types/service.d.ts +13 -1
- package/node_modules/dsh-remote-terminal-controller/lib/client.js.map +7 -0
- package/package.json +1 -1
- package/assets/dsh-remote-agent-0.2.1.tgz +0 -0
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* dsh-remote-local-plugin@0.8.
|
|
1
|
+
/* dsh-remote-local-plugin@0.8.5 — 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();
|
|
@@ -295,6 +295,8 @@ async function dispatch(ctx, service, mirrors, endpoint, payload) {
|
|
|
295
295
|
return service.refresh(requireStringField(payload, "hostId"));
|
|
296
296
|
case "host/provision":
|
|
297
297
|
return service.provision(requireStringField(payload, "hostId"));
|
|
298
|
+
case "host/restart":
|
|
299
|
+
return service.restart(requireStringField(payload, "hostId"));
|
|
298
300
|
case "host/workspaces": {
|
|
299
301
|
const hostId = requireStringField(payload, "hostId");
|
|
300
302
|
const workspaces = await service.workspaces(hostId);
|
|
@@ -396,7 +398,6 @@ async function dispatch(ctx, service, mirrors, endpoint, payload) {
|
|
|
396
398
|
const files = ctx.get("workspaceFiles");
|
|
397
399
|
if (files === void 0) return { mirrored: false };
|
|
398
400
|
const scope = { sessionId, workspaceRoot: root };
|
|
399
|
-
const signal = new AbortController().signal;
|
|
400
401
|
if (endpoint === "host/files/list") return { mirrored: true, value: await files.list(scope, path, signal) };
|
|
401
402
|
if (endpoint === "host/files/stat") return { mirrored: true, value: await files.stat(scope, path, signal) };
|
|
402
403
|
if (endpoint === "host/files/bytes") return { mirrored: true, value: await files.readBytes(scope, path, {
|
|
@@ -574,6 +575,7 @@ var RemoteMirrorService = class {
|
|
|
574
575
|
#syncing = false;
|
|
575
576
|
#saveQueue = Promise.resolve();
|
|
576
577
|
#timer;
|
|
578
|
+
#selectionOff;
|
|
577
579
|
#disposed = false;
|
|
578
580
|
constructor(ctx, hosts, config) {
|
|
579
581
|
this.#ctx = ctx;
|
|
@@ -583,6 +585,9 @@ var RemoteMirrorService = class {
|
|
|
583
585
|
/** Load mappings and begin periodic discovery. */
|
|
584
586
|
async start() {
|
|
585
587
|
await this.#load();
|
|
588
|
+
this.#selectionOff = this.#ctx.on("session/event", ((session, event) => {
|
|
589
|
+
this.#onLocalSessionEvent(session, event);
|
|
590
|
+
}), { global: true });
|
|
586
591
|
if (!this.#config.mirrorSessions) return;
|
|
587
592
|
this.#timer = setInterval(() => {
|
|
588
593
|
void this.sync();
|
|
@@ -593,9 +598,13 @@ var RemoteMirrorService = class {
|
|
|
593
598
|
async dispose() {
|
|
594
599
|
this.#disposed = true;
|
|
595
600
|
if (this.#timer !== void 0) clearInterval(this.#timer);
|
|
601
|
+
this.#selectionOff?.();
|
|
602
|
+
this.#selectionOff = void 0;
|
|
596
603
|
for (const runtime of this.#runtimes.values()) {
|
|
597
604
|
runtime.stopped = true;
|
|
598
605
|
if (runtime.timer !== void 0) clearTimeout(runtime.timer);
|
|
606
|
+
runtime.stream?.abort();
|
|
607
|
+
runtime.assistant?.abort();
|
|
599
608
|
}
|
|
600
609
|
for (const dispose of this.#registrations.values()) dispose();
|
|
601
610
|
this.#registrations.clear();
|
|
@@ -674,18 +683,26 @@ var RemoteMirrorService = class {
|
|
|
674
683
|
const text = contentText(message);
|
|
675
684
|
if (text === "") return;
|
|
676
685
|
runtime.pendingUsers.push(message);
|
|
686
|
+
if (!runtime.running) {
|
|
687
|
+
runtime.awaitingTurn = runtime.latestTurn + 1;
|
|
688
|
+
runtime.awaitingStarted = false;
|
|
689
|
+
} else {
|
|
690
|
+
runtime.awaitingStarted = true;
|
|
691
|
+
}
|
|
677
692
|
this.#setRunning(runtime, true);
|
|
678
693
|
if (runtime.stopped || runtime.timer === void 0) {
|
|
679
694
|
runtime.stopped = false;
|
|
680
695
|
runtime.lastError = null;
|
|
681
696
|
this.#schedule(runtime);
|
|
682
697
|
}
|
|
698
|
+
const selection = this.#selectionFor(runtime);
|
|
699
|
+
if (selection !== void 0) Object.assign(runtime.agent.options, selection);
|
|
683
700
|
void this.#hosts.promptSession(
|
|
684
701
|
runtime.record.hostId,
|
|
685
702
|
runtime.record.remoteSessionId,
|
|
686
703
|
text,
|
|
687
704
|
mode,
|
|
688
|
-
|
|
705
|
+
selection
|
|
689
706
|
).catch((error) => {
|
|
690
707
|
runtime.lastError = error instanceof Error ? error.message : String(error);
|
|
691
708
|
runtime.pendingUsers.shift();
|
|
@@ -693,12 +710,32 @@ var RemoteMirrorService = class {
|
|
|
693
710
|
this.#setRunning(runtime, false);
|
|
694
711
|
});
|
|
695
712
|
}
|
|
696
|
-
/** Route cancellation to the remote session. */
|
|
713
|
+
/** Route cancellation to the remote session only after the remote accepts it. */
|
|
697
714
|
cancel(runtime) {
|
|
698
|
-
void this.#hosts.cancelSession(runtime.record.hostId, runtime.record.remoteSessionId).
|
|
699
|
-
|
|
715
|
+
void this.#hosts.cancelSession(runtime.record.hostId, runtime.record.remoteSessionId).then(() => {
|
|
716
|
+
runtime.awaitingTurn = null;
|
|
717
|
+
runtime.awaitingStarted = false;
|
|
718
|
+
this.#setRunning(runtime, false);
|
|
719
|
+
}).catch((error) => {
|
|
720
|
+
runtime.lastError = error instanceof Error ? error.message : String(error);
|
|
721
|
+
this.#ctx.logger.warn("dsh-remote-local: remote cancel failed: %s", runtime.lastError);
|
|
722
|
+
this.#setRunning(runtime, true);
|
|
700
723
|
});
|
|
701
|
-
|
|
724
|
+
}
|
|
725
|
+
#selectionFor(runtime) {
|
|
726
|
+
const projections = this.#ctx.get("sessionProjections");
|
|
727
|
+
const state = projections?.stateOf?.(runtime.session, "modelSelection");
|
|
728
|
+
const read = (candidate) => {
|
|
729
|
+
if (typeof candidate !== "object" || candidate === null) return void 0;
|
|
730
|
+
const record = candidate;
|
|
731
|
+
if (typeof record.provider !== "string" || typeof record.model !== "string") return void 0;
|
|
732
|
+
return {
|
|
733
|
+
provider: record.provider,
|
|
734
|
+
model: record.model,
|
|
735
|
+
...typeof record.reasoningEffort === "string" ? { reasoningEffort: record.reasoningEffort } : {}
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
return read(state?.pending) ?? latestSelection(runtime.session) ?? read(runtime.agent.options) ?? read(state?.lastUsed);
|
|
702
739
|
}
|
|
703
740
|
async #syncHost(host) {
|
|
704
741
|
this.#ensureApprovalStream(host);
|
|
@@ -890,18 +927,32 @@ var RemoteMirrorService = class {
|
|
|
890
927
|
async #ensureMirror(host, remote, workspace, _remotePath) {
|
|
891
928
|
const ref = key(host.id, remote.sessionId);
|
|
892
929
|
const existing = this.#state.sessions[ref];
|
|
930
|
+
let reuse;
|
|
893
931
|
if (existing !== void 0) {
|
|
894
932
|
const live = this.#sessions.get(existing.localSessionId);
|
|
895
|
-
const
|
|
896
|
-
if (live !== void 0 &&
|
|
933
|
+
const runtime = this.#runtimes.get(existing.localSessionId);
|
|
934
|
+
if (live !== void 0 && runtime !== void 0 && (!runtime.stopped || !remote.live)) {
|
|
897
935
|
await this.#attach(workspace, live.id);
|
|
936
|
+
if (remote.live) {
|
|
937
|
+
this.#startStream(runtime);
|
|
938
|
+
this.#startAssistantStream(runtime);
|
|
939
|
+
}
|
|
898
940
|
return existing;
|
|
899
941
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
942
|
+
const stalled = this.#runtimes.get(existing.localSessionId);
|
|
943
|
+
if (stalled !== void 0) {
|
|
944
|
+
stalled.stopped = true;
|
|
945
|
+
if (stalled.timer !== void 0) clearTimeout(stalled.timer);
|
|
946
|
+
stalled.stream?.abort();
|
|
947
|
+
stalled.assistant?.abort();
|
|
948
|
+
this.#runtimes.delete(existing.localSessionId);
|
|
904
949
|
}
|
|
950
|
+
const registration = this.#registrations.get(existing.localSessionId);
|
|
951
|
+
if (registration !== void 0) {
|
|
952
|
+
registration();
|
|
953
|
+
this.#registrations.delete(existing.localSessionId);
|
|
954
|
+
}
|
|
955
|
+
if (live !== void 0) reuse = live;
|
|
905
956
|
}
|
|
906
957
|
const snapshot = await this.#hosts.snapshot(host.id, remote.sessionId, this.#config.mirrorMaxEvents);
|
|
907
958
|
if (snapshot.truncated && (snapshot.events[0]?.seq ?? 0) > 0) {
|
|
@@ -914,11 +965,38 @@ var RemoteMirrorService = class {
|
|
|
914
965
|
}
|
|
915
966
|
const anchor = this.#anchorFor(workspace);
|
|
916
967
|
await mkdir(anchor, { recursive: true });
|
|
917
|
-
const
|
|
968
|
+
const filtered = snapshot.events.filter((event) => !isMirrorNoise(event.type));
|
|
969
|
+
const seqMap = new Map(filtered.map((event, index) => [event.seq, index]));
|
|
970
|
+
let latestTurn = 0;
|
|
971
|
+
for (const event of filtered) {
|
|
972
|
+
if (event.type !== "turn/start") continue;
|
|
973
|
+
const turn = event.data.turn;
|
|
974
|
+
if (typeof turn === "number") latestTurn = Math.max(latestTurn, turn);
|
|
975
|
+
}
|
|
976
|
+
const remoteToLocalSeq = /* @__PURE__ */ new Map();
|
|
977
|
+
for (const [index, event] of filtered.entries()) remoteToLocalSeq.set(event.seq, index);
|
|
978
|
+
if (reuse !== void 0) {
|
|
979
|
+
const localEvents = reuse.snapshotEvents();
|
|
980
|
+
for (let index = 0; index < Math.min(localEvents.length, filtered.length); index += 1) {
|
|
981
|
+
const localEvent = localEvents[index];
|
|
982
|
+
const remoteEvent = filtered[index];
|
|
983
|
+
if (localEvent !== void 0 && remoteEvent !== void 0) {
|
|
984
|
+
remoteToLocalSeq.set(remoteEvent.seq, localEvent.seq);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const record2 = existing;
|
|
988
|
+
await this.#attach(workspace, reuse.id);
|
|
989
|
+
await this.#mountRuntime(record2, reuse, snapshot, remoteToLocalSeq, latestTurn, remote.live);
|
|
990
|
+
this.#ctx.logger.info(
|
|
991
|
+
"dsh-remote-local: reattached remote session %s to local %s",
|
|
992
|
+
remote.sessionId,
|
|
993
|
+
reuse.id
|
|
994
|
+
);
|
|
995
|
+
return record2;
|
|
996
|
+
}
|
|
997
|
+
const localId = existing?.localSessionId ?? `session-${randomUUID()}`;
|
|
918
998
|
let session;
|
|
919
999
|
try {
|
|
920
|
-
const filtered = snapshot.events.filter((event) => !isMirrorNoise(event.type));
|
|
921
|
-
const seqMap = new Map(filtered.map((event, index) => [event.seq, index]));
|
|
922
1000
|
const seed = filtered.map((event, index) => toSeedEvent(event, index, seqMap));
|
|
923
1001
|
session = this.#sessions.create(localId, {
|
|
924
1002
|
...seed.length === 0 ? {} : { seed },
|
|
@@ -934,7 +1012,7 @@ var RemoteMirrorService = class {
|
|
|
934
1012
|
} catch (error) {
|
|
935
1013
|
this.#ctx.logger.debug("dsh-remote-local: mirror flush failed: %s", String(error));
|
|
936
1014
|
}
|
|
937
|
-
const record = {
|
|
1015
|
+
const record = existing === void 0 ? {
|
|
938
1016
|
hostId: host.id,
|
|
939
1017
|
remoteSessionId: remote.sessionId,
|
|
940
1018
|
localSessionId: localId,
|
|
@@ -942,32 +1020,53 @@ var RemoteMirrorService = class {
|
|
|
942
1020
|
anchor,
|
|
943
1021
|
lastSeq: snapshot.cursor,
|
|
944
1022
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1023
|
+
} : {
|
|
1024
|
+
...existing,
|
|
1025
|
+
remoteWorkspaceId: remote.workspaceId,
|
|
1026
|
+
anchor,
|
|
1027
|
+
lastSeq: snapshot.cursor,
|
|
1028
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
945
1029
|
};
|
|
946
1030
|
this.#state.sessions[ref] = record;
|
|
1031
|
+
await this.#mountRuntime(record, session, snapshot, remoteToLocalSeq, latestTurn, remote.live);
|
|
1032
|
+
this.#ctx.logger.info(
|
|
1033
|
+
"dsh-remote-local: mirrored remote session %s as local %s",
|
|
1034
|
+
remote.sessionId,
|
|
1035
|
+
localId
|
|
1036
|
+
);
|
|
1037
|
+
return record;
|
|
1038
|
+
}
|
|
1039
|
+
async #mountRuntime(record, session, snapshot, remoteToLocalSeq, latestTurn, live) {
|
|
947
1040
|
const runtime = {
|
|
948
1041
|
record,
|
|
949
1042
|
agent: null,
|
|
950
1043
|
session,
|
|
951
1044
|
timer: void 0,
|
|
1045
|
+
stream: void 0,
|
|
1046
|
+
assistant: void 0,
|
|
1047
|
+
remoteToLocalSeq,
|
|
1048
|
+
pendingAssistantEnds: /* @__PURE__ */ new Map(),
|
|
1049
|
+
latestTurn,
|
|
1050
|
+
awaitingTurn: null,
|
|
1051
|
+
awaitingStarted: false,
|
|
1052
|
+
applyingRemote: false,
|
|
952
1053
|
running: false,
|
|
953
1054
|
stopped: false,
|
|
954
1055
|
lastError: null,
|
|
955
1056
|
pendingUsers: [],
|
|
956
1057
|
waiters: []
|
|
957
1058
|
};
|
|
958
|
-
const options = modelOptions(snapshot.events);
|
|
1059
|
+
const options = this.#defaultSelection() ?? modelOptions(snapshot.events);
|
|
959
1060
|
const agent = new RemoteMirrorAgent(this, runtime, this.#ctx, options);
|
|
960
1061
|
runtime.agent = agent;
|
|
961
|
-
this.#runtimes.set(
|
|
962
|
-
this.#registrations.set(
|
|
1062
|
+
this.#runtimes.set(record.localSessionId, runtime);
|
|
1063
|
+
this.#registrations.set(record.localSessionId, this.#agents.enter(agent, void 0));
|
|
963
1064
|
await this.#save();
|
|
964
1065
|
this.#schedule(runtime);
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
);
|
|
970
|
-
return record;
|
|
1066
|
+
if (live) {
|
|
1067
|
+
this.#startStream(runtime);
|
|
1068
|
+
this.#startAssistantStream(runtime);
|
|
1069
|
+
}
|
|
971
1070
|
}
|
|
972
1071
|
#schedule(runtime) {
|
|
973
1072
|
if (runtime.stopped || this.#disposed) return;
|
|
@@ -981,6 +1080,8 @@ var RemoteMirrorService = class {
|
|
|
981
1080
|
const local = this.#sessions.get(runtime.session.id);
|
|
982
1081
|
if (local === void 0) {
|
|
983
1082
|
runtime.stopped = true;
|
|
1083
|
+
runtime.stream?.abort();
|
|
1084
|
+
runtime.assistant?.abort();
|
|
984
1085
|
return;
|
|
985
1086
|
}
|
|
986
1087
|
const snapshot = await this.#hosts.snapshot(
|
|
@@ -991,24 +1092,29 @@ var RemoteMirrorService = class {
|
|
|
991
1092
|
if (snapshot.truncated && (snapshot.events[0]?.seq ?? 0) > runtime.record.lastSeq) {
|
|
992
1093
|
runtime.lastError = "remote log grew past the mirror window";
|
|
993
1094
|
runtime.stopped = true;
|
|
1095
|
+
runtime.stream?.abort();
|
|
1096
|
+
runtime.assistant?.abort();
|
|
994
1097
|
return;
|
|
995
1098
|
}
|
|
996
|
-
let sawEnd = false;
|
|
997
1099
|
for (const event of snapshot.events) {
|
|
998
1100
|
if (event.seq < runtime.record.lastSeq) continue;
|
|
999
1101
|
if (event.seq !== runtime.record.lastSeq) {
|
|
1000
1102
|
throw new Error(`remote event gap at ${event.seq}, mirror ${runtime.record.lastSeq}`);
|
|
1001
1103
|
}
|
|
1002
|
-
|
|
1104
|
+
this.#appendEvent(runtime, event);
|
|
1003
1105
|
runtime.record.lastSeq = event.seq + 1;
|
|
1004
|
-
if (event.type === "turn/end") sawEnd = true;
|
|
1005
1106
|
}
|
|
1006
1107
|
runtime.record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1007
|
-
if (sawEnd) this.#setRunning(runtime, false);
|
|
1008
1108
|
if (!snapshot.live && runtime.record.lastSeq >= snapshot.cursor) {
|
|
1009
1109
|
runtime.stopped = true;
|
|
1110
|
+
runtime.stream?.abort();
|
|
1111
|
+
runtime.assistant?.abort();
|
|
1010
1112
|
}
|
|
1011
1113
|
await this.#save();
|
|
1114
|
+
if (snapshot.live && !runtime.stopped) {
|
|
1115
|
+
this.#startStream(runtime);
|
|
1116
|
+
this.#startAssistantStream(runtime);
|
|
1117
|
+
}
|
|
1012
1118
|
} catch (error) {
|
|
1013
1119
|
runtime.lastError = error instanceof Error ? error.message : String(error);
|
|
1014
1120
|
this.#ctx.logger.warn(
|
|
@@ -1019,12 +1125,204 @@ var RemoteMirrorService = class {
|
|
|
1019
1125
|
}
|
|
1020
1126
|
this.#schedule(runtime);
|
|
1021
1127
|
}
|
|
1128
|
+
#startStream(runtime) {
|
|
1129
|
+
if (runtime.stopped || this.#disposed || runtime.stream !== void 0) return;
|
|
1130
|
+
const controller = new AbortController();
|
|
1131
|
+
runtime.stream = controller;
|
|
1132
|
+
void this.#followStream(runtime, controller.signal).finally(() => {
|
|
1133
|
+
if (runtime.stream === controller) runtime.stream = void 0;
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
async #followStream(runtime, signal) {
|
|
1137
|
+
try {
|
|
1138
|
+
for await (const frame of this.#hosts.followSessionEvents(
|
|
1139
|
+
runtime.record.hostId,
|
|
1140
|
+
runtime.record.remoteSessionId,
|
|
1141
|
+
runtime.record.lastSeq,
|
|
1142
|
+
signal
|
|
1143
|
+
)) {
|
|
1144
|
+
if (runtime.stopped || this.#disposed || signal.aborted) return;
|
|
1145
|
+
if (frame.type === "end") return;
|
|
1146
|
+
if (frame.type !== "event") continue;
|
|
1147
|
+
const event = frame.event;
|
|
1148
|
+
if (event.seq < runtime.record.lastSeq) continue;
|
|
1149
|
+
if (event.seq !== runtime.record.lastSeq) {
|
|
1150
|
+
runtime.lastError = `remote event gap at ${event.seq}, mirror ${runtime.record.lastSeq}`;
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
this.#appendEvent(runtime, event);
|
|
1154
|
+
runtime.record.lastSeq = event.seq + 1;
|
|
1155
|
+
runtime.record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1156
|
+
await this.#save();
|
|
1157
|
+
}
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
if (signal.aborted || runtime.stopped || this.#disposed) return;
|
|
1160
|
+
runtime.lastError = error instanceof Error ? error.message : String(error);
|
|
1161
|
+
this.#ctx.logger.warn(
|
|
1162
|
+
"dsh-remote-local: mirror stream %s failed: %s",
|
|
1163
|
+
runtime.record.remoteSessionId,
|
|
1164
|
+
runtime.lastError
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
#appendEvent(runtime, event) {
|
|
1169
|
+
const before = runtime.session.seq;
|
|
1170
|
+
runtime.applyingRemote = true;
|
|
1171
|
+
try {
|
|
1172
|
+
if (!isMirrorNoise(event.type)) {
|
|
1173
|
+
appendRemoteEvent(runtime.session, remapRemoteEvent(event, runtime.remoteToLocalSeq), runtime);
|
|
1174
|
+
}
|
|
1175
|
+
} finally {
|
|
1176
|
+
runtime.applyingRemote = false;
|
|
1177
|
+
}
|
|
1178
|
+
const after = runtime.session.seq;
|
|
1179
|
+
if (after > before) {
|
|
1180
|
+
runtime.remoteToLocalSeq.set(event.seq, after - 1);
|
|
1181
|
+
this.#flushAssistantEnd(runtime, event.seq);
|
|
1182
|
+
}
|
|
1183
|
+
const turn = typeof event.data === "object" && event.data !== null ? event.data.turn : void 0;
|
|
1184
|
+
if (typeof turn !== "number") return;
|
|
1185
|
+
if (event.type === "turn/start") {
|
|
1186
|
+
runtime.latestTurn = Math.max(runtime.latestTurn, turn);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (event.type !== "turn/end") return;
|
|
1190
|
+
runtime.latestTurn = Math.max(runtime.latestTurn, turn);
|
|
1191
|
+
if (runtime.awaitingTurn === null || turn >= runtime.awaitingTurn) {
|
|
1192
|
+
runtime.awaitingTurn = null;
|
|
1193
|
+
runtime.awaitingStarted = false;
|
|
1194
|
+
this.#setRunning(runtime, false);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
#onLocalSessionEvent(session, event) {
|
|
1198
|
+
const candidate = event;
|
|
1199
|
+
if (candidate.type !== "model/selection") return;
|
|
1200
|
+
const runtime = [...this.#runtimes.values()].find((item) => item.session.id === session.id);
|
|
1201
|
+
if (runtime === void 0 || runtime.stopped || runtime.applyingRemote) return;
|
|
1202
|
+
const record = candidate.data;
|
|
1203
|
+
if (typeof record.provider !== "string" || typeof record.model !== "string") return;
|
|
1204
|
+
const selection = {
|
|
1205
|
+
provider: record.provider,
|
|
1206
|
+
model: record.model,
|
|
1207
|
+
...typeof record.reasoningEffort === "string" ? { reasoningEffort: record.reasoningEffort } : {}
|
|
1208
|
+
};
|
|
1209
|
+
Object.assign(runtime.agent.options, selection);
|
|
1210
|
+
void this.#hosts.selectModel(runtime.record.hostId, runtime.record.remoteSessionId, selection).catch((error) => {
|
|
1211
|
+
runtime.lastError = error instanceof Error ? error.message : String(error);
|
|
1212
|
+
this.#ctx.logger.warn("dsh-remote-local: remote model selection failed: %s", runtime.lastError);
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
#defaultSelection() {
|
|
1216
|
+
const defaults = this.#ctx.get("agentDefaultModel");
|
|
1217
|
+
const candidate = defaults?.currentSelection?.();
|
|
1218
|
+
if (typeof candidate !== "object" || candidate === null) return void 0;
|
|
1219
|
+
const record = candidate;
|
|
1220
|
+
if (typeof record.provider !== "string" || typeof record.model !== "string") return void 0;
|
|
1221
|
+
return {
|
|
1222
|
+
provider: record.provider,
|
|
1223
|
+
model: record.model,
|
|
1224
|
+
...typeof record.reasoningEffort === "string" ? { reasoningEffort: record.reasoningEffort } : {}
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
#flushAssistantEnd(runtime, remoteSeq) {
|
|
1228
|
+
const pending = runtime.pendingAssistantEnds.get(remoteSeq);
|
|
1229
|
+
if (pending === void 0) return;
|
|
1230
|
+
const localSeq = runtime.remoteToLocalSeq.get(remoteSeq);
|
|
1231
|
+
if (localSeq === void 0) return;
|
|
1232
|
+
runtime.pendingAssistantEnds.delete(remoteSeq);
|
|
1233
|
+
if (pending.outcome?.kind !== "committed") {
|
|
1234
|
+
this.#emitAssistant(runtime, pending);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
this.#emitAssistant(runtime, {
|
|
1238
|
+
...pending,
|
|
1239
|
+
outcome: { ...pending.outcome, seq: localSeq }
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
#emitAssistant(runtime, frame) {
|
|
1243
|
+
try {
|
|
1244
|
+
const emit = this.#ctx.emit;
|
|
1245
|
+
emit.call(this.#ctx, "agent/assistant-stream", { agent: runtime.agent, frame });
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
this.#ctx.logger.warn(
|
|
1248
|
+
"dsh-remote-local: local assistant frame %s failed: %s",
|
|
1249
|
+
runtime.record.remoteSessionId,
|
|
1250
|
+
String(error)
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
#startAssistantStream(runtime) {
|
|
1255
|
+
if (runtime.stopped || this.#disposed || runtime.assistant !== void 0) return;
|
|
1256
|
+
const controller = new AbortController();
|
|
1257
|
+
runtime.assistant = controller;
|
|
1258
|
+
void this.#followAssistant(runtime, controller.signal).finally(() => {
|
|
1259
|
+
if (runtime.assistant === controller) runtime.assistant = void 0;
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
async #followAssistant(runtime, signal) {
|
|
1263
|
+
try {
|
|
1264
|
+
for await (const frame of this.#hosts.followAssistantFrames(
|
|
1265
|
+
runtime.record.hostId,
|
|
1266
|
+
runtime.record.remoteSessionId,
|
|
1267
|
+
signal
|
|
1268
|
+
)) {
|
|
1269
|
+
if (runtime.stopped || this.#disposed || signal.aborted) return;
|
|
1270
|
+
if (frame.type === "start") {
|
|
1271
|
+
runtime.awaitingStarted = true;
|
|
1272
|
+
this.#setRunning(runtime, true);
|
|
1273
|
+
const { startedAfterSeq: _startedAfterSeq, ...rest } = frame;
|
|
1274
|
+
this.#emitAssistant(runtime, rest);
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (frame.type === "chunk") {
|
|
1278
|
+
const chunkType = frame.chunk?.type;
|
|
1279
|
+
if (chunkType !== "tool-call-delta") this.#emitAssistant(runtime, frame);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
const outcome = frame.outcome;
|
|
1283
|
+
if (outcome === void 0) return;
|
|
1284
|
+
if (outcome.kind !== "committed") {
|
|
1285
|
+
this.#emitAssistant(runtime, frame);
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
const localSeq = runtime.remoteToLocalSeq.get(outcome.seq);
|
|
1289
|
+
if (localSeq === void 0) {
|
|
1290
|
+
runtime.pendingAssistantEnds.set(outcome.seq, frame);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
this.#emitAssistant(runtime, {
|
|
1294
|
+
...frame,
|
|
1295
|
+
outcome: { ...outcome, seq: localSeq }
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
if (signal.aborted || runtime.stopped || this.#disposed) return;
|
|
1300
|
+
this.#ctx.logger.warn(
|
|
1301
|
+
"dsh-remote-local: assistant stream %s failed: %s",
|
|
1302
|
+
runtime.record.remoteSessionId,
|
|
1303
|
+
error instanceof Error ? error.message : String(error)
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1022
1307
|
#setRunning(runtime, running) {
|
|
1023
1308
|
if (runtime.running === running) return;
|
|
1024
1309
|
runtime.running = running;
|
|
1025
1310
|
if (!running) {
|
|
1026
1311
|
for (const waiter of runtime.waiters.splice(0)) waiter();
|
|
1027
1312
|
}
|
|
1313
|
+
try {
|
|
1314
|
+
const emit = this.#ctx.emit;
|
|
1315
|
+
emit.call(this.#ctx, "agent/status", {
|
|
1316
|
+
agent: runtime.agent,
|
|
1317
|
+
status: running ? "running" : "idle"
|
|
1318
|
+
});
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
this.#ctx.logger.warn(
|
|
1321
|
+
"dsh-remote-local: local agent status %s failed: %s",
|
|
1322
|
+
runtime.record.remoteSessionId,
|
|
1323
|
+
String(error)
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1028
1326
|
}
|
|
1029
1327
|
async #attach(workspace, sessionId) {
|
|
1030
1328
|
try {
|
|
@@ -1074,6 +1372,19 @@ var RemoteMirrorService = class {
|
|
|
1074
1372
|
return task;
|
|
1075
1373
|
}
|
|
1076
1374
|
};
|
|
1375
|
+
function remapRemoteEvent(event, remoteToLocal) {
|
|
1376
|
+
const sourceEventSeqs = event.sourceEventSeqs?.map((seq) => remoteToLocal.get(seq)).filter((seq) => seq !== void 0);
|
|
1377
|
+
const surfaceOp = typeof event.surfaceOp === "object" ? {
|
|
1378
|
+
op: "replace",
|
|
1379
|
+
startSeq: remoteToLocal.get(event.surfaceOp.startSeq) ?? 0,
|
|
1380
|
+
endSeq: remoteToLocal.get(event.surfaceOp.endSeq) ?? 0
|
|
1381
|
+
} : event.surfaceOp;
|
|
1382
|
+
return {
|
|
1383
|
+
...event,
|
|
1384
|
+
...sourceEventSeqs === void 0 || sourceEventSeqs.length === 0 ? {} : { sourceEventSeqs },
|
|
1385
|
+
...surfaceOp === void 0 ? {} : { surfaceOp }
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1077
1388
|
function isMirrorNoise(type) {
|
|
1078
1389
|
return type === "agent/inbox/spliced";
|
|
1079
1390
|
}
|
|
@@ -1189,6 +1500,7 @@ function safe(value) {
|
|
|
1189
1500
|
// src/remote/files.ts
|
|
1190
1501
|
import { spawn as spawn2 } from "node:child_process";
|
|
1191
1502
|
import { isAbsolute as isAbsolute2, join as join4, relative, resolve as resolve3, sep } from "node:path";
|
|
1503
|
+
var HELPER_TIMEOUT_MS = 15e3;
|
|
1192
1504
|
var HELPER = String.raw`
|
|
1193
1505
|
import sys, json, os, base64
|
|
1194
1506
|
req = json.load(sys.stdin)
|
|
@@ -1346,21 +1658,41 @@ function installRemoteWorkspaceFiles(ctx, mirrors, hosts) {
|
|
|
1346
1658
|
function targetOf(mirrors, workspaceRoot, path) {
|
|
1347
1659
|
const context = mirrors.remoteFileContext(workspaceRoot);
|
|
1348
1660
|
if (context === void 0) return void 0;
|
|
1349
|
-
const
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1661
|
+
const anchor = resolve3(workspaceRoot);
|
|
1662
|
+
const remoteRoot = resolve3(context.remoteRoot);
|
|
1663
|
+
const candidate = isAbsolute2(path) ? resolve3(path) : resolve3(anchor, path);
|
|
1664
|
+
const contains = (root, value) => {
|
|
1665
|
+
const rel = relative(root, value);
|
|
1666
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
1667
|
+
};
|
|
1668
|
+
if (isAbsolute2(path) && contains(remoteRoot, candidate)) {
|
|
1669
|
+
const rel = relative(remoteRoot, candidate);
|
|
1670
|
+
return {
|
|
1671
|
+
hostId: context.hostId,
|
|
1672
|
+
localPath: rel === "" ? anchor : resolve3(anchor, rel),
|
|
1673
|
+
remotePath: candidate,
|
|
1674
|
+
relativePath: rel.split(sep).join("/"),
|
|
1675
|
+
workspaceRoot: anchor
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
if (contains(anchor, candidate)) {
|
|
1679
|
+
const rel = relative(anchor, candidate);
|
|
1680
|
+
const remotePath = rel === "" ? remoteRoot : join4(remoteRoot, rel);
|
|
1353
1681
|
return {
|
|
1354
1682
|
hostId: context.hostId,
|
|
1355
|
-
localPath:
|
|
1683
|
+
localPath: candidate,
|
|
1356
1684
|
remotePath,
|
|
1357
1685
|
relativePath: rel.split(sep).join("/"),
|
|
1358
|
-
workspaceRoot
|
|
1686
|
+
workspaceRoot: anchor
|
|
1359
1687
|
};
|
|
1360
1688
|
}
|
|
1361
|
-
|
|
1689
|
+
throw Object.assign(
|
|
1690
|
+
new Error(`remote file path is outside the mirrored workspace: ${path}`),
|
|
1691
|
+
{ code: "workspace-file/not-found" }
|
|
1692
|
+
);
|
|
1362
1693
|
}
|
|
1363
1694
|
async function helper(hosts, hostId, request, signal) {
|
|
1695
|
+
if (signal.aborted) throw new Error("remote file helper aborted");
|
|
1364
1696
|
const ssh = hosts.sshFor(hostId);
|
|
1365
1697
|
const args = [
|
|
1366
1698
|
"-o",
|
|
@@ -1380,7 +1712,7 @@ async function helper(hosts, hostId, request, signal) {
|
|
|
1380
1712
|
const timer = setTimeout(() => {
|
|
1381
1713
|
child.kill("SIGKILL");
|
|
1382
1714
|
reject(new Error("remote file helper timed out"));
|
|
1383
|
-
},
|
|
1715
|
+
}, HELPER_TIMEOUT_MS);
|
|
1384
1716
|
const abort = () => {
|
|
1385
1717
|
child.kill("SIGKILL");
|
|
1386
1718
|
reject(new Error("remote file helper aborted"));
|
|
@@ -1394,6 +1726,11 @@ async function helper(hosts, hostId, request, signal) {
|
|
|
1394
1726
|
child.stderr.on("data", (chunk) => {
|
|
1395
1727
|
stderr += chunk;
|
|
1396
1728
|
});
|
|
1729
|
+
child.stdin.on("error", (error) => {
|
|
1730
|
+
clearTimeout(timer);
|
|
1731
|
+
signal.removeEventListener("abort", abort);
|
|
1732
|
+
reject(error);
|
|
1733
|
+
});
|
|
1397
1734
|
child.on("error", (error) => {
|
|
1398
1735
|
clearTimeout(timer);
|
|
1399
1736
|
signal.removeEventListener("abort", abort);
|
|
@@ -1840,6 +2177,35 @@ var RemoteProtocolError = class extends Error {
|
|
|
1840
2177
|
}
|
|
1841
2178
|
};
|
|
1842
2179
|
|
|
2180
|
+
// ../protocol/lib/types.js
|
|
2181
|
+
function isRemoteAssistantFrame(value) {
|
|
2182
|
+
if (typeof value !== "object" || value === null)
|
|
2183
|
+
return false;
|
|
2184
|
+
const frame = value;
|
|
2185
|
+
if (frame.type !== "start" && frame.type !== "chunk" && frame.type !== "end")
|
|
2186
|
+
return false;
|
|
2187
|
+
if (typeof frame.attemptId !== "string" || typeof frame.revision !== "number")
|
|
2188
|
+
return false;
|
|
2189
|
+
if (frame.type === "start") {
|
|
2190
|
+
return typeof frame.turn === "number" && typeof frame.step === "number";
|
|
2191
|
+
}
|
|
2192
|
+
if (frame.type === "chunk") {
|
|
2193
|
+
return typeof frame.index === "number" && typeof frame.time === "number" && "chunk" in frame;
|
|
2194
|
+
}
|
|
2195
|
+
if (typeof frame.index !== "number")
|
|
2196
|
+
return false;
|
|
2197
|
+
const outcome = frame.outcome;
|
|
2198
|
+
if (typeof outcome !== "object" || outcome === null)
|
|
2199
|
+
return false;
|
|
2200
|
+
const kind = outcome.kind;
|
|
2201
|
+
if (kind === "abandoned")
|
|
2202
|
+
return true;
|
|
2203
|
+
if (kind !== "committed")
|
|
2204
|
+
return false;
|
|
2205
|
+
const record = outcome;
|
|
2206
|
+
return typeof record.eventType === "string" && typeof record.seq === "number";
|
|
2207
|
+
}
|
|
2208
|
+
|
|
1843
2209
|
// ../protocol/lib/routes.js
|
|
1844
2210
|
var AGENT_ROUTES = {
|
|
1845
2211
|
health: `${AGENT_API_PREFIX}/health`,
|
|
@@ -1856,6 +2222,9 @@ function workspaceSessionsPath(workspaceId) {
|
|
|
1856
2222
|
function sessionPath(sessionId) {
|
|
1857
2223
|
return `${AGENT_API_PREFIX}/sessions/${encodeURIComponent(sessionId)}`;
|
|
1858
2224
|
}
|
|
2225
|
+
function sessionModelPath(sessionId) {
|
|
2226
|
+
return `${sessionPath(sessionId)}/model`;
|
|
2227
|
+
}
|
|
1859
2228
|
function sessionPromptPath(sessionId) {
|
|
1860
2229
|
return `${sessionPath(sessionId)}/prompt`;
|
|
1861
2230
|
}
|
|
@@ -1866,6 +2235,9 @@ function sessionEventsPath(sessionId, cursor) {
|
|
|
1866
2235
|
const base = `${sessionPath(sessionId)}/events`;
|
|
1867
2236
|
return cursor === void 0 ? base : `${base}?cursor=${encodeURIComponent(String(cursor))}`;
|
|
1868
2237
|
}
|
|
2238
|
+
function sessionAssistantEventsPath(sessionId) {
|
|
2239
|
+
return `${sessionPath(sessionId)}/assistant-events`;
|
|
2240
|
+
}
|
|
1869
2241
|
var SESSION_PREFIX = `${AGENT_API_PREFIX}/sessions/`;
|
|
1870
2242
|
var WORKSPACE_PREFIX = `${AGENT_API_PREFIX}/workspaces/`;
|
|
1871
2243
|
|
|
@@ -2108,6 +2480,13 @@ var RemoteAgentClient = class {
|
|
|
2108
2480
|
body: request
|
|
2109
2481
|
});
|
|
2110
2482
|
}
|
|
2483
|
+
/** Select the model and reasoning effort for one remote session. */
|
|
2484
|
+
async selectModel(sessionId, request) {
|
|
2485
|
+
await this.#request(sessionModelPath(sessionId), isAcceptedResult, {
|
|
2486
|
+
method: "POST",
|
|
2487
|
+
body: request
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2111
2490
|
/** Send a text prompt to a remote session. */
|
|
2112
2491
|
async promptSession(sessionId, request) {
|
|
2113
2492
|
await this.#request(sessionPromptPath(sessionId), isAcceptedResult, {
|
|
@@ -2192,6 +2571,38 @@ var RemoteAgentClient = class {
|
|
|
2192
2571
|
yield frame;
|
|
2193
2572
|
}
|
|
2194
2573
|
}
|
|
2574
|
+
/** Follow one session's dense process-local Assistant frames. */
|
|
2575
|
+
async *followAssistantFrames(sessionId, signal) {
|
|
2576
|
+
const response = await this.#openStream(sessionAssistantEventsPath(sessionId), signal);
|
|
2577
|
+
const body = response.body;
|
|
2578
|
+
if (body === null) {
|
|
2579
|
+
throw new RemoteProtocolError({
|
|
2580
|
+
code: "request/invalid",
|
|
2581
|
+
message: "remote agent returned an empty assistant stream"
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
for await (const message of parseSseStream(body)) {
|
|
2585
|
+
if (message.data === "")
|
|
2586
|
+
continue;
|
|
2587
|
+
let frame;
|
|
2588
|
+
try {
|
|
2589
|
+
frame = JSON.parse(message.data);
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
throw new RemoteProtocolError({
|
|
2592
|
+
code: "request/invalid",
|
|
2593
|
+
message: "remote agent sent malformed assistant JSON",
|
|
2594
|
+
cause: error
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
if (!isRemoteAssistantFrame(frame)) {
|
|
2598
|
+
throw new RemoteProtocolError({
|
|
2599
|
+
code: "request/invalid",
|
|
2600
|
+
message: "remote agent sent an unknown assistant frame"
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
yield frame;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2195
2606
|
/** Validate an event sequence against a cursor. */
|
|
2196
2607
|
static guardEvent(cursor, event) {
|
|
2197
2608
|
return cursor.accept(event);
|
|
@@ -2656,6 +3067,7 @@ function renderSystemdUnit(options) {
|
|
|
2656
3067
|
// $PATH is expanded by the bootstrap shell after nodePathPrelude(), so the
|
|
2657
3068
|
// systemd unit sees the discovered nvm/Volta/fnm Node bin directory too.
|
|
2658
3069
|
'Environment="PATH=$INSTALL_DIR/node/bin:$INSTALL_DIR/node_modules/.bin:/usr/local/bin:/usr/bin:/bin:$PATH"',
|
|
3070
|
+
"${PROXY_ENV_LINES}",
|
|
2659
3071
|
...options.modelBaseUrl === void 0 ? [] : ['Environment="DEEPSEEK_BASE_URL=$MODEL_BASE_URL"'],
|
|
2660
3072
|
...options.modelApiKey === void 0 ? [] : ['Environment="DEEPSEEK_API_KEY=$MODEL_API_KEY"'],
|
|
2661
3073
|
'ExecStart="$DSH_BIN" web --host 127.0.0.1 --port $PORT --no-open',
|
|
@@ -2795,6 +3207,9 @@ PNPM_VERSION=${shellLiteral(options.pnpmVersion)}
|
|
|
2795
3207
|
NODE_VERSION=${shellLiteral(options.nodeVersion)}
|
|
2796
3208
|
AUTO_INSTALL_NODE=${options.autoInstallNode ? "1" : "0"}
|
|
2797
3209
|
AGENT_TARBALL=${shellLiteral(options.agentTarball)}
|
|
3210
|
+
AGENT_NPM_SPEC=${options.agentPackageSpec === void 0 ? "''" : shellLiteral(options.agentPackageSpec)}
|
|
3211
|
+
AGENT_ARTIFACT_DIR="$INSTALL_DIR/artifacts"
|
|
3212
|
+
AGENT_ARTIFACT="$AGENT_ARTIFACT_DIR/$(basename "$AGENT_TARBALL")"
|
|
2798
3213
|
MODEL_BASE_URL=${options.modelBaseUrl === void 0 ? "''" : shellLiteral(options.modelBaseUrl)}
|
|
2799
3214
|
MODEL_API_KEY=${options.modelApiKey === void 0 ? "''" : shellLiteral(options.modelApiKey)}
|
|
2800
3215
|
PROXY_URL=${options.proxyUrl === void 0 ? "''" : shellLiteral(options.proxyUrl)}
|
|
@@ -2858,6 +3273,16 @@ if [ -n "$PROXY_URL" ]; then
|
|
|
2858
3273
|
fi
|
|
2859
3274
|
echo "dsh-remote: install proxy configured"
|
|
2860
3275
|
fi
|
|
3276
|
+
PROXY_ENV_LINES=""
|
|
3277
|
+
if [ -n "$PROXY_URL" ]; then
|
|
3278
|
+
PROXY_ENV_LINES="Environment=\\"HTTP_PROXY=$PROXY_URL\\"
|
|
3279
|
+
Environment=\\"HTTPS_PROXY=$PROXY_URL\\"
|
|
3280
|
+
Environment=\\"ALL_PROXY=$PROXY_URL\\""
|
|
3281
|
+
if [ -n "$PROXY_NO_PROXY" ]; then
|
|
3282
|
+
PROXY_ENV_LINES="$PROXY_ENV_LINES
|
|
3283
|
+
Environment=\\"NO_PROXY=$PROXY_NO_PROXY\\""
|
|
3284
|
+
fi
|
|
3285
|
+
fi
|
|
2861
3286
|
|
|
2862
3287
|
${nodePathPrelude()}
|
|
2863
3288
|
|
|
@@ -2898,7 +3323,14 @@ if [ ! -x "$DSH_BIN" ]; then
|
|
|
2898
3323
|
fi
|
|
2899
3324
|
|
|
2900
3325
|
echo "dsh-remote: adding plugin to profile $PROFILE"
|
|
2901
|
-
|
|
3326
|
+
mkdir -p "$AGENT_ARTIFACT_DIR"
|
|
3327
|
+
cp -f "$AGENT_TARBALL" "$AGENT_ARTIFACT"
|
|
3328
|
+
if [ -n "$AGENT_NPM_SPEC" ] && "$DSH_BIN" plugin --profile "$PROFILE" add "$AGENT_NPM_SPEC"; then
|
|
3329
|
+
echo "dsh-remote: installed agent from npm"
|
|
3330
|
+
else
|
|
3331
|
+
echo "dsh-remote: npm agent unavailable; installing the bundled agent" >&2
|
|
3332
|
+
"$DSH_BIN" plugin --profile "$PROFILE" add "$AGENT_ARTIFACT"
|
|
3333
|
+
fi
|
|
2902
3334
|
|
|
2903
3335
|
echo "dsh-remote: starting dsh web"
|
|
2904
3336
|
${startBlock}
|
|
@@ -3122,6 +3554,7 @@ async function provisionRemoteAgent(options) {
|
|
|
3122
3554
|
agentTarball: remoteTarball,
|
|
3123
3555
|
...options.modelBaseUrl === void 0 ? {} : { modelBaseUrl: options.modelBaseUrl },
|
|
3124
3556
|
...options.modelApiKey === void 0 ? {} : { modelApiKey: options.modelApiKey },
|
|
3557
|
+
...options.agentPackageSpec === void 0 ? {} : { agentPackageSpec: options.agentPackageSpec },
|
|
3125
3558
|
...options.proxyUrl === void 0 ? {} : { proxyUrl: options.proxyUrl },
|
|
3126
3559
|
...options.noProxy === void 0 ? {} : { noProxy: options.noProxy }
|
|
3127
3560
|
});
|
|
@@ -3283,6 +3716,8 @@ var RemoteProvisioner = class {
|
|
|
3283
3716
|
*/
|
|
3284
3717
|
async ensureInstalled(target, options = {}) {
|
|
3285
3718
|
const tarball = findAgentTarball();
|
|
3719
|
+
const version = /dsh-remote-agent-(.+)\.tgz$/u.exec(tarball)?.[1];
|
|
3720
|
+
const agentPackageSpec = version === void 0 ? void 0 : `@goldenpigeon/dsh-remote-agent@${version}`;
|
|
3286
3721
|
options.onProgress?.("\u6B63\u5728\u51C6\u5907\u8FDC\u7AEF\u5B89\u88C5\u73AF\u5883\u2026");
|
|
3287
3722
|
const proxyUrl = this.#config.remoteInstallProxy.trim() || void 0;
|
|
3288
3723
|
if (proxyUrl !== void 0) options.onProgress?.("\u6B63\u5728\u914D\u7F6E\u8FDC\u7AEF\u5B89\u88C5\u4EE3\u7406\u2026");
|
|
@@ -3290,6 +3725,7 @@ var RemoteProvisioner = class {
|
|
|
3290
3725
|
await provisionRemoteAgent({
|
|
3291
3726
|
target: target.canonical,
|
|
3292
3727
|
agentTarballPath: tarball,
|
|
3728
|
+
...agentPackageSpec === void 0 ? {} : { agentPackageSpec },
|
|
3293
3729
|
sshBinary: this.#config.sshBinary,
|
|
3294
3730
|
strictHostKeyChecking: this.#config.strictHostKeyChecking,
|
|
3295
3731
|
profile: "web",
|
|
@@ -3395,6 +3831,40 @@ function findAgentTarball() {
|
|
|
3395
3831
|
);
|
|
3396
3832
|
}
|
|
3397
3833
|
|
|
3834
|
+
// src/remote/restart.ts
|
|
3835
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
3836
|
+
import { promisify as promisify3 } from "node:util";
|
|
3837
|
+
var execFileAsync3 = promisify3(execFile4);
|
|
3838
|
+
var RESTART_COMMAND = [
|
|
3839
|
+
"if command -v systemctl >/dev/null 2>&1 && systemctl --user cat dsh-remote-agent.service >/dev/null 2>&1; then",
|
|
3840
|
+
" systemctl --user restart dsh-remote-agent.service",
|
|
3841
|
+
"else",
|
|
3842
|
+
' echo "dsh-remote: dsh-remote-agent.service was not found; use Reinstall or restart the remote DSH manually" >&2',
|
|
3843
|
+
" exit 4",
|
|
3844
|
+
"fi"
|
|
3845
|
+
].join("\n");
|
|
3846
|
+
async function restartRemoteAgent(target, options) {
|
|
3847
|
+
try {
|
|
3848
|
+
await execFileAsync3(
|
|
3849
|
+
options.sshBinary,
|
|
3850
|
+
[
|
|
3851
|
+
...baseSshArgs(target, { timeoutMs: 6e4, strictHostKeyChecking: options.strictHostKeyChecking }),
|
|
3852
|
+
target.destination,
|
|
3853
|
+
RESTART_COMMAND
|
|
3854
|
+
],
|
|
3855
|
+
{ timeout: 75e3, maxBuffer: 64 * 1024, encoding: "utf8", windowsHide: true }
|
|
3856
|
+
);
|
|
3857
|
+
} catch (error) {
|
|
3858
|
+
const failure = error;
|
|
3859
|
+
const detail = (failure.stderr ?? "").trim().split("\n")[0] ?? "";
|
|
3860
|
+
throw new LocalError(
|
|
3861
|
+
"agent/unavailable",
|
|
3862
|
+
failure.killed === true ? "remote DSH restart timed out" : detail !== "" ? `remote DSH restart failed: ${detail}` : `remote DSH restart failed: ${failure.message}`,
|
|
3863
|
+
{ details: { host: target.canonical }, retryable: true, cause: error }
|
|
3864
|
+
);
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3398
3868
|
// src/ssh/target.ts
|
|
3399
3869
|
var USER_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,63}$/u;
|
|
3400
3870
|
var HOST_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,252}[A-Za-z0-9])?$/u;
|
|
@@ -3858,6 +4328,60 @@ var RemoteHostsService = class {
|
|
|
3858
4328
|
}
|
|
3859
4329
|
return this.connect(id);
|
|
3860
4330
|
}
|
|
4331
|
+
/** Restart the remote DSH service and reconnect without reinstalling. */
|
|
4332
|
+
async restart(id) {
|
|
4333
|
+
this.#assertAlive();
|
|
4334
|
+
const record = this.#requireRecord(id);
|
|
4335
|
+
const target = parseSshTarget(record.sshTarget);
|
|
4336
|
+
this.#manualDisconnect.delete(id);
|
|
4337
|
+
this.#clearReconnect(id);
|
|
4338
|
+
await this.#stopTransport(id);
|
|
4339
|
+
this.#setRuntime(id, {
|
|
4340
|
+
...this.#runtimeFor(id),
|
|
4341
|
+
phase: "connecting",
|
|
4342
|
+
localPort: null,
|
|
4343
|
+
localUrl: null,
|
|
4344
|
+
hostInfo: null,
|
|
4345
|
+
connectedAt: null,
|
|
4346
|
+
error: null,
|
|
4347
|
+
errorCode: null,
|
|
4348
|
+
detail: "\u6B63\u5728\u91CD\u542F\u8FDC\u7AEF DSH\u2026"
|
|
4349
|
+
});
|
|
4350
|
+
try {
|
|
4351
|
+
await restartRemoteAgent(target, {
|
|
4352
|
+
sshBinary: this.config.sshBinary,
|
|
4353
|
+
strictHostKeyChecking: this.config.strictHostKeyChecking
|
|
4354
|
+
});
|
|
4355
|
+
} catch (error) {
|
|
4356
|
+
const failure2 = this.#markError(id, error, "agent/unavailable");
|
|
4357
|
+
throw failure2;
|
|
4358
|
+
}
|
|
4359
|
+
const deadline = Date.now() + 9e4;
|
|
4360
|
+
let lastError;
|
|
4361
|
+
while (Date.now() < deadline) {
|
|
4362
|
+
try {
|
|
4363
|
+
const discovery = await this.#discover(target);
|
|
4364
|
+
if (isAgentLive(discovery.state)) {
|
|
4365
|
+
await this.#saveRecord(id, {
|
|
4366
|
+
remoteAgentPort: discovery.state.port,
|
|
4367
|
+
token: discovery.state.token
|
|
4368
|
+
});
|
|
4369
|
+
return this.connect(id);
|
|
4370
|
+
}
|
|
4371
|
+
} catch (error) {
|
|
4372
|
+
lastError = error;
|
|
4373
|
+
}
|
|
4374
|
+
await new Promise((resolve4) => setTimeout(resolve4, 1e3));
|
|
4375
|
+
}
|
|
4376
|
+
const failure = new LocalError("agent/unavailable", "\u8FDC\u7AEF DSH \u91CD\u542F\u540E\u672A\u80FD\u5728 90 \u79D2\u5185\u5C31\u7EEA", {
|
|
4377
|
+
details: {
|
|
4378
|
+
host: target.canonical,
|
|
4379
|
+
reason: lastError instanceof Error ? lastError.message : String(lastError ?? "state file not ready")
|
|
4380
|
+
},
|
|
4381
|
+
retryable: true
|
|
4382
|
+
});
|
|
4383
|
+
throw this.#markError(id, failure, "agent/unavailable");
|
|
4384
|
+
}
|
|
3861
4385
|
/** List remote workspaces. */
|
|
3862
4386
|
async workspaces(id) {
|
|
3863
4387
|
const client = this.#requireClient(id);
|
|
@@ -3879,6 +4403,16 @@ var RemoteHostsService = class {
|
|
|
3879
4403
|
throw this.#remoteFailure(id, error);
|
|
3880
4404
|
}
|
|
3881
4405
|
}
|
|
4406
|
+
/** Follow a live remote session from a cursor as an SSE-backed stream. */
|
|
4407
|
+
async *followSessionEvents(id, sessionId, cursor, signal) {
|
|
4408
|
+
const client = this.#requireClient(id);
|
|
4409
|
+
try {
|
|
4410
|
+
yield* client.followSession(sessionId, cursor, signal);
|
|
4411
|
+
} catch (error) {
|
|
4412
|
+
if (signal.aborted) return;
|
|
4413
|
+
throw this.#remoteFailure(id, error);
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
3882
4416
|
/** Read one session snapshot. */
|
|
3883
4417
|
async snapshot(id, sessionId, limit) {
|
|
3884
4418
|
const client = this.#requireClient(id);
|
|
@@ -3888,6 +4422,16 @@ var RemoteHostsService = class {
|
|
|
3888
4422
|
throw this.#remoteFailure(id, error);
|
|
3889
4423
|
}
|
|
3890
4424
|
}
|
|
4425
|
+
/** Follow dense remote Assistant frames for one live Session. */
|
|
4426
|
+
async *followAssistantFrames(id, sessionId, signal) {
|
|
4427
|
+
const client = this.#requireClient(id);
|
|
4428
|
+
try {
|
|
4429
|
+
yield* client.followAssistantFrames(sessionId, signal);
|
|
4430
|
+
} catch (error) {
|
|
4431
|
+
if (signal.aborted) return;
|
|
4432
|
+
throw this.#remoteFailure(id, error);
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
3891
4435
|
/** Register a remote workspace. */
|
|
3892
4436
|
async createWorkspace(id, request) {
|
|
3893
4437
|
const client = this.#requireClient(id);
|
|
@@ -3908,6 +4452,15 @@ var RemoteHostsService = class {
|
|
|
3908
4452
|
throw this.#remoteFailure(id, error);
|
|
3909
4453
|
}
|
|
3910
4454
|
}
|
|
4455
|
+
/** Install a model/reasoning-effort selection on a remote session. */
|
|
4456
|
+
async selectModel(id, sessionId, selection) {
|
|
4457
|
+
const client = this.#requireClient(id);
|
|
4458
|
+
try {
|
|
4459
|
+
await client.selectModel(sessionId, selection);
|
|
4460
|
+
} catch (error) {
|
|
4461
|
+
throw this.#remoteFailure(id, error);
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
3911
4464
|
/** Send a prompt to a remote session. */
|
|
3912
4465
|
async promptSession(id, sessionId, text, mode, selection) {
|
|
3913
4466
|
const client = this.#requireClient(id);
|
|
@@ -4389,7 +4942,7 @@ var renderJson = (_args, value) => [
|
|
|
4389
4942
|
function toJsonObject(value) {
|
|
4390
4943
|
return JSON.parse(JSON.stringify(value));
|
|
4391
4944
|
}
|
|
4392
|
-
var HOST_WRITE_TOOLS = /* @__PURE__ */ new Set(["remote_host_add", "remote_host_connect", "remote_host_provision"]);
|
|
4945
|
+
var HOST_WRITE_TOOLS = /* @__PURE__ */ new Set(["remote_host_add", "remote_host_connect", "remote_host_provision", "remote_host_restart"]);
|
|
4393
4946
|
function installRemoteToolGuards(ctx) {
|
|
4394
4947
|
ctx.effect(() => ctx.on("tools/pre-execute", (exec, next) => {
|
|
4395
4948
|
if (!HOST_WRITE_TOOLS.has(exec.name)) return next();
|
|
@@ -4490,6 +5043,24 @@ function registerRemoteTools(ctx, service) {
|
|
|
4490
5043
|
return { host: toJsonObject(await service.provision(args.hostId)) };
|
|
4491
5044
|
}
|
|
4492
5045
|
}));
|
|
5046
|
+
ctx.tools.register(defineTool({
|
|
5047
|
+
name: "remote_host_restart",
|
|
5048
|
+
description: "Restart the remote DSH service without reinstalling it, preserving installed remote plugins, then reconnect. Requires the installer-owned systemd user unit; use remote_host_provision for nohup installs.",
|
|
5049
|
+
parameters: {
|
|
5050
|
+
hostId: { type: "string", required: true, description: "Registered host id." }
|
|
5051
|
+
},
|
|
5052
|
+
output: {
|
|
5053
|
+
schema: {
|
|
5054
|
+
type: "object",
|
|
5055
|
+
additionalProperties: false,
|
|
5056
|
+
properties: { host: { type: "object", additionalProperties: true, required: true } }
|
|
5057
|
+
},
|
|
5058
|
+
render: renderJson
|
|
5059
|
+
},
|
|
5060
|
+
async execute(args) {
|
|
5061
|
+
return { host: toJsonObject(await service.restart(args.hostId)) };
|
|
5062
|
+
}
|
|
5063
|
+
}));
|
|
4493
5064
|
ctx.tools.register(defineTool({
|
|
4494
5065
|
name: "remote_workspaces_list",
|
|
4495
5066
|
description: "List workspaces registered on a connected remote DSH host. Use the returned workspaceId with remote_sessions_list.",
|