@camerontaylor/paseo-client 0.12.0-beta.1.fork.1 → 0.13.0-fork.1
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/dist/connection/index.d.ts +1 -0
- package/dist/connection/index.js +1 -0
- package/dist/daemon-client-relay-e2ee-transport.d.ts +24 -4
- package/dist/daemon-client-relay-e2ee-transport.js +94 -14
- package/dist/daemon-client-transport.d.ts +1 -1
- package/dist/daemon-client-transport.js +1 -1
- package/dist/daemon-client.d.ts +103 -9
- package/dist/daemon-client.js +333 -56
- package/dist/hosts/index.d.ts +22 -0
- package/dist/hosts/index.js +188 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +37 -6
- package/dist/node/buffering.d.ts +10 -0
- package/dist/node/buffering.js +28 -0
- package/dist/node/index.d.ts +13 -0
- package/dist/node/index.js +188 -0
- package/dist/node/ssh-tunnel.d.ts +9 -0
- package/dist/node/ssh-tunnel.js +79 -0
- package/package.json +12 -3
package/dist/daemon-client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HOST_ROUTE_OPCODE, encodeHostRouteFrame, decodeHostRouteFrame, } from "@camerontaylor/paseo-protocol/binary-frames/index";
|
|
1
2
|
import { messageTooLargeError } from "./daemon-client-transport-utils.js";
|
|
2
3
|
import { legacyUsageIcon } from "./legacy-usage-icons.js";
|
|
3
4
|
import { subscribeTimeline } from "./timeline-subscription/index.js";
|
|
@@ -10,7 +11,7 @@ import { AgentCreateFailedStatusPayloadSchema, AgentCreatedStatusPayloadSchema,
|
|
|
10
11
|
import { validateWSOutboundMessage } from "@camerontaylor/paseo-protocol/validation/ws-outbound";
|
|
11
12
|
import { isRelayClientWebSocketUrl } from "@camerontaylor/paseo-protocol/daemon-endpoints";
|
|
12
13
|
import { asUint8Array, decodeFileTransferFrame, encodeFileTransferFrame, decodeTerminalStreamFrame, FileTransferOpcode, TerminalStreamOpcode, } from "@camerontaylor/paseo-protocol/binary-frames/index";
|
|
13
|
-
import { createRelayE2eeTransportFactory,
|
|
14
|
+
import { createRelayE2eeTransportFactory, createRelayUpgradeTransportFactory, createWebSocketTransportFactory, decodeMessageData, defaultWebSocketFactory, describeEndpointSecurityClose, describeTransportClose, describeTransportError, } from "./daemon-client-transport.js";
|
|
14
15
|
import { DaemonClientRuntimeMetrics } from "./daemon-client-runtime-metrics.js";
|
|
15
16
|
import { normalizeListProviderModelsPayload, normalizeProviderSnapshotUpdateMessage, normalizeProvidersSnapshotPayload, } from "./compat/normalize-provider-models.js";
|
|
16
17
|
import { TerminalStreamRouter } from "./terminal-stream-router.js";
|
|
@@ -128,9 +129,11 @@ function normalizeListCommandsOptions(input, legacyOptions) {
|
|
|
128
129
|
return { agentId: input, ...legacyOptions };
|
|
129
130
|
}
|
|
130
131
|
export class DaemonConnectionError extends Error {
|
|
131
|
-
constructor(message, code = "DAEMON_CONNECTION_LOST") {
|
|
132
|
-
super(message);
|
|
132
|
+
constructor(message, code = "DAEMON_CONNECTION_LOST", context) {
|
|
133
|
+
super(message, context?.cause ? { cause: context.cause } : undefined);
|
|
133
134
|
this.code = code;
|
|
135
|
+
this.serverId = context?.serverId;
|
|
136
|
+
this.outcome = context?.outcome;
|
|
134
137
|
this.name = "DaemonConnectionError";
|
|
135
138
|
}
|
|
136
139
|
}
|
|
@@ -146,6 +149,8 @@ class DaemonRpcError extends Error {
|
|
|
146
149
|
this.requestId = params.requestId;
|
|
147
150
|
this.requestType = params.requestType;
|
|
148
151
|
this.code = params.code;
|
|
152
|
+
this.serverId = params.serverId;
|
|
153
|
+
this.userMessage = params.error;
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
class DaemonProtocolError extends Error {
|
|
@@ -261,15 +266,32 @@ function assertTransportFrameFits(frame) {
|
|
|
261
266
|
if (size > MAX_WEBSOCKET_MESSAGE_BYTES)
|
|
262
267
|
throw messageTooLargeError(size, MAX_WEBSOCKET_MESSAGE_BYTES);
|
|
263
268
|
}
|
|
264
|
-
function describeInboundTransportFrame(frame
|
|
269
|
+
function describeInboundTransportFrame(frame) {
|
|
265
270
|
if (typeof frame === "string") {
|
|
266
|
-
return { kind: "text", size: String(frame.length) };
|
|
271
|
+
return { kind: "text", size: String(frame.length), sizeUnit: "utf16-code-units" };
|
|
267
272
|
}
|
|
268
|
-
if (
|
|
269
|
-
return { kind: "binary", size: String(
|
|
273
|
+
if (frame instanceof ArrayBuffer || ArrayBuffer.isView(frame)) {
|
|
274
|
+
return { kind: "binary", size: String(frame.byteLength), sizeUnit: "bytes" };
|
|
270
275
|
}
|
|
271
276
|
return { kind: "unknown", size: "0" };
|
|
272
277
|
}
|
|
278
|
+
function describeRequestIdentity(value) {
|
|
279
|
+
if (!value || typeof value !== "object")
|
|
280
|
+
return {};
|
|
281
|
+
const message = "message" in value ? value.message : value;
|
|
282
|
+
if (!message || typeof message !== "object")
|
|
283
|
+
return {};
|
|
284
|
+
if ("requestId" in message && typeof message.requestId === "string")
|
|
285
|
+
return { requestId: message.requestId };
|
|
286
|
+
if ("payload" in message &&
|
|
287
|
+
message.payload &&
|
|
288
|
+
typeof message.payload === "object" &&
|
|
289
|
+
"requestId" in message.payload &&
|
|
290
|
+
typeof message.payload.requestId === "string") {
|
|
291
|
+
return { requestId: message.payload.requestId };
|
|
292
|
+
}
|
|
293
|
+
return {};
|
|
294
|
+
}
|
|
273
295
|
function hashForLog(value) {
|
|
274
296
|
let hash = 0;
|
|
275
297
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -371,6 +393,7 @@ export class DaemonClient {
|
|
|
371
393
|
this.connectTimeout = null;
|
|
372
394
|
this.pendingGenericTransportErrorTimeout = null;
|
|
373
395
|
this.reconnectAttempt = 0;
|
|
396
|
+
this.traceConnectionAttempt = 0;
|
|
374
397
|
this.shouldReconnect = true;
|
|
375
398
|
this.connectPromise = null;
|
|
376
399
|
this.connectResolve = null;
|
|
@@ -391,11 +414,14 @@ export class DaemonClient {
|
|
|
391
414
|
this.livenessHeartbeatTimer = null;
|
|
392
415
|
this.lastLivenessRttMs = null;
|
|
393
416
|
this.consecutiveLivenessFailures = 0;
|
|
417
|
+
this.hostFrameListeners = new Set();
|
|
394
418
|
// ============================================================================
|
|
395
419
|
// Agent Lifecycle
|
|
396
420
|
// ============================================================================
|
|
397
421
|
this.creations = new CreationClient({
|
|
398
|
-
supports: (feature) =>
|
|
422
|
+
supports: (feature) => {
|
|
423
|
+
return this.lastServerInfoMessage?.features?.[feature] === true;
|
|
424
|
+
},
|
|
399
425
|
requestId: () => this.createRequestId(),
|
|
400
426
|
request: (kind, input) => kind === "workspace"
|
|
401
427
|
? this.sendCorrelatedSessionRequest({
|
|
@@ -432,6 +458,7 @@ export class DaemonClient {
|
|
|
432
458
|
legacyWorkspace: (input) => this.createLegacyWorkspace(input, input.requestId),
|
|
433
459
|
});
|
|
434
460
|
this.logger = config.logger ?? consoleLogger;
|
|
461
|
+
this.relaySecurityFloorValue = config.e2ee?.minimumMode;
|
|
435
462
|
this.logConnectionPath = isRelayClientWebSocketUrl(this.config.url) ? "relay" : "direct";
|
|
436
463
|
let parsedUrlForLog = null;
|
|
437
464
|
try {
|
|
@@ -521,13 +548,14 @@ export class DaemonClient {
|
|
|
521
548
|
const baseTransportFactory = this.config.transportFactory ??
|
|
522
549
|
createWebSocketTransportFactory(this.config.webSocketFactory ?? defaultWebSocketFactory);
|
|
523
550
|
const isRelayTransport = isRelayClientWebSocketUrl(this.config.url);
|
|
524
|
-
const shouldUseRelayE2ee = this.config.e2ee?.enabled === true && isRelayTransport;
|
|
525
551
|
this.assertEncryptedRelayAuth(selected.helloAuth, isRelayTransport);
|
|
552
|
+
// Relay upgrades are routing only: credentials never reach the relay socket.
|
|
526
553
|
let transportFactory = isRelayTransport
|
|
527
|
-
?
|
|
554
|
+
? createRelayUpgradeTransportFactory(baseTransportFactory)
|
|
528
555
|
: baseTransportFactory;
|
|
529
|
-
|
|
530
|
-
|
|
556
|
+
// Endpoint security is selected by configuration, never inferred from the URL.
|
|
557
|
+
if (this.config.e2ee?.enabled === true) {
|
|
558
|
+
const daemonPublicKeyB64 = this.config.e2ee.daemonPublicKeyB64;
|
|
531
559
|
if (!daemonPublicKeyB64) {
|
|
532
560
|
throw new Error("daemonPublicKeyB64 is required for relay E2EE");
|
|
533
561
|
}
|
|
@@ -535,6 +563,12 @@ export class DaemonClient {
|
|
|
535
563
|
baseFactory: transportFactory,
|
|
536
564
|
daemonPublicKeyB64,
|
|
537
565
|
logger: this.logger,
|
|
566
|
+
...(this.relaySecurityFloorValue ? { minimumMode: this.relaySecurityFloorValue } : {}),
|
|
567
|
+
onSecurityMode: (mode) => {
|
|
568
|
+
if (mode === "protected")
|
|
569
|
+
this.relaySecurityFloorValue = "protected";
|
|
570
|
+
this.config.e2ee?.onSecurityMode?.(mode);
|
|
571
|
+
},
|
|
538
572
|
});
|
|
539
573
|
}
|
|
540
574
|
const transportUrl = this.resolveTransportUrlForAttempt();
|
|
@@ -543,6 +577,7 @@ export class DaemonClient {
|
|
|
543
577
|
headers: selected.headers,
|
|
544
578
|
...(selected.protocols ? { protocols: selected.protocols } : {}),
|
|
545
579
|
});
|
|
580
|
+
this.traceConnectionAttempt += 1;
|
|
546
581
|
this.transport = transport;
|
|
547
582
|
this.lastServerInfoMessage = null;
|
|
548
583
|
this.authFailureReasonValue = null;
|
|
@@ -566,6 +601,7 @@ export class DaemonClient {
|
|
|
566
601
|
}, timeoutMs);
|
|
567
602
|
this.transportCleanup = [
|
|
568
603
|
transport.onOpen(() => {
|
|
604
|
+
this.traceInstant("paseo.connection.open");
|
|
569
605
|
if (this.pendingGenericTransportErrorTimeout) {
|
|
570
606
|
clearTimeout(this.pendingGenericTransportErrorTimeout);
|
|
571
607
|
this.pendingGenericTransportErrorTimeout = null;
|
|
@@ -574,25 +610,34 @@ export class DaemonClient {
|
|
|
574
610
|
void this.sendHelloMessage();
|
|
575
611
|
}),
|
|
576
612
|
transport.onClose((event) => {
|
|
613
|
+
this.traceInstant("paseo.connection.close", {
|
|
614
|
+
code: String(event && typeof event === "object" && "code" in event ? event.code : "unknown"),
|
|
615
|
+
});
|
|
577
616
|
this.resetConnectTimeout();
|
|
578
617
|
if (this.pendingGenericTransportErrorTimeout) {
|
|
579
618
|
clearTimeout(this.pendingGenericTransportErrorTimeout);
|
|
580
619
|
this.pendingGenericTransportErrorTimeout = null;
|
|
581
620
|
}
|
|
582
621
|
this.authFailureReasonValue ?? (this.authFailureReasonValue = authFailureFromLegacyClose(event));
|
|
622
|
+
// An endpoint security policy close cannot be fixed by retrying.
|
|
623
|
+
const securityFailure = describeEndpointSecurityClose(event);
|
|
624
|
+
if (securityFailure)
|
|
625
|
+
this.setReconnectEnabled(false);
|
|
583
626
|
const reason = this.authFailureReasonValue
|
|
584
627
|
? new DaemonAuthenticationError(this.authFailureReasonValue).message
|
|
585
|
-
: describeTransportClose(event);
|
|
628
|
+
: (securityFailure ?? describeTransportClose(event));
|
|
586
629
|
if (reason) {
|
|
587
630
|
this.lastErrorValue = reason;
|
|
588
631
|
}
|
|
589
632
|
this.scheduleReconnect({
|
|
590
633
|
reason,
|
|
634
|
+
failure: event instanceof Error ? event : undefined,
|
|
591
635
|
event: "TRANSPORT_CLOSE",
|
|
592
636
|
reasonCode: "transport_closed",
|
|
593
637
|
});
|
|
594
638
|
}),
|
|
595
639
|
transport.onError((event) => {
|
|
640
|
+
this.traceInstant("paseo.connection.error");
|
|
596
641
|
this.resetConnectTimeout();
|
|
597
642
|
const reason = describeTransportError(event);
|
|
598
643
|
const isGeneric = reason === "Transport error";
|
|
@@ -609,6 +654,7 @@ export class DaemonClient {
|
|
|
609
654
|
this.lastErrorValue = reason;
|
|
610
655
|
this.scheduleReconnect({
|
|
611
656
|
reason,
|
|
657
|
+
failure: event instanceof Error ? event : undefined,
|
|
612
658
|
event: "TRANSPORT_ERROR",
|
|
613
659
|
reasonCode: "transport_error",
|
|
614
660
|
});
|
|
@@ -624,6 +670,7 @@ export class DaemonClient {
|
|
|
624
670
|
this.lastErrorValue = reason;
|
|
625
671
|
this.scheduleReconnect({
|
|
626
672
|
reason,
|
|
673
|
+
failure: event instanceof Error ? event : undefined,
|
|
627
674
|
event: "TRANSPORT_ERROR",
|
|
628
675
|
reasonCode: "transport_error",
|
|
629
676
|
});
|
|
@@ -673,9 +720,7 @@ export class DaemonClient {
|
|
|
673
720
|
return;
|
|
674
721
|
}
|
|
675
722
|
this.shouldReconnect = false;
|
|
676
|
-
this.
|
|
677
|
-
this.connectResolve = null;
|
|
678
|
-
this.connectReject = null;
|
|
723
|
+
this.rejectConnect(new Error("Daemon client closed"));
|
|
679
724
|
if (this.reconnectTimeout) {
|
|
680
725
|
clearTimeout(this.reconnectTimeout);
|
|
681
726
|
this.reconnectTimeout = null;
|
|
@@ -716,7 +761,7 @@ export class DaemonClient {
|
|
|
716
761
|
this.attemptConnect();
|
|
717
762
|
return;
|
|
718
763
|
}
|
|
719
|
-
void this.connect();
|
|
764
|
+
void this.connect().catch((error) => this.logger.debug({ err: error }, "Connection attempt ended"));
|
|
720
765
|
}
|
|
721
766
|
verifyConnection() {
|
|
722
767
|
const transport = this.transport;
|
|
@@ -742,6 +787,111 @@ export class DaemonClient {
|
|
|
742
787
|
this.connectionVerification = null;
|
|
743
788
|
});
|
|
744
789
|
}
|
|
790
|
+
requireConnectedHosts() {
|
|
791
|
+
if (!this.isConnected)
|
|
792
|
+
throw Object.assign(new Error("The managing daemon is offline"), {
|
|
793
|
+
code: "host_offline",
|
|
794
|
+
serverId: this.destinationServerId,
|
|
795
|
+
});
|
|
796
|
+
if (this.lastServerInfoMessage?.features?.connectedHosts !== true)
|
|
797
|
+
throw Object.assign(new Error("Update the managing daemon to use connected hosts"), {
|
|
798
|
+
code: "unsupported_capability",
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
async listHosts() {
|
|
802
|
+
this.requireConnectedHosts();
|
|
803
|
+
return (await this.sendCorrelatedSessionRequest({
|
|
804
|
+
message: { type: "hosts.list.request" },
|
|
805
|
+
responseType: "hosts.list.response",
|
|
806
|
+
})).directory;
|
|
807
|
+
}
|
|
808
|
+
observeHosts() {
|
|
809
|
+
this.requireConnectedHosts();
|
|
810
|
+
return this.owned.observe(async (accept) => {
|
|
811
|
+
const result = await this.sendCorrelatedSessionRequest({
|
|
812
|
+
message: { type: "hosts.list.request", observe: true },
|
|
813
|
+
responseType: "hosts.list.response",
|
|
814
|
+
});
|
|
815
|
+
if (!result.subscriptionId)
|
|
816
|
+
throw new Error("Host omitted subscription identity");
|
|
817
|
+
const snapshot = { ...result.directory, subscriptionId: result.subscriptionId };
|
|
818
|
+
accept(snapshot);
|
|
819
|
+
return snapshot;
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
async addHost(input) {
|
|
823
|
+
this.requireConnectedHosts();
|
|
824
|
+
return (await this.sendCorrelatedSessionRequest({
|
|
825
|
+
message: { type: "hosts.add.request", input },
|
|
826
|
+
responseType: "hosts.add.response",
|
|
827
|
+
})).host;
|
|
828
|
+
}
|
|
829
|
+
async updateHost(registrationId, patch) {
|
|
830
|
+
this.requireConnectedHosts();
|
|
831
|
+
return (await this.sendCorrelatedSessionRequest({
|
|
832
|
+
message: { type: "hosts.update.request", registrationId, patch },
|
|
833
|
+
responseType: "hosts.update.response",
|
|
834
|
+
})).host;
|
|
835
|
+
}
|
|
836
|
+
async removeHost(registrationId) {
|
|
837
|
+
this.requireConnectedHosts();
|
|
838
|
+
await this.sendCorrelatedSessionRequest({
|
|
839
|
+
message: { type: "hosts.remove.request", registrationId },
|
|
840
|
+
responseType: "hosts.remove.response",
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
async openHostRoute(serverId, scopeId) {
|
|
844
|
+
this.requireConnectedHosts();
|
|
845
|
+
try {
|
|
846
|
+
return (await this.sendCorrelatedSessionRequest({
|
|
847
|
+
message: { type: "hosts.route.open.request", serverId, scopeId },
|
|
848
|
+
responseType: "hosts.route.open.response",
|
|
849
|
+
})).routeId;
|
|
850
|
+
}
|
|
851
|
+
catch (error) {
|
|
852
|
+
throw Object.assign(error instanceof Error ? error : new Error("Host route failed"), {
|
|
853
|
+
serverId,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async openHostLink(channelId) {
|
|
858
|
+
this.requireConnectedHosts();
|
|
859
|
+
if (this.getLastServerInfoMessage()?.features?.hostMultiplexing !== true)
|
|
860
|
+
throw Object.assign(new Error("Update the destination daemon"), {
|
|
861
|
+
code: "unsupported_capability",
|
|
862
|
+
});
|
|
863
|
+
return (await this.sendCorrelatedSessionRequest({
|
|
864
|
+
message: { type: "hosts.link.open.request", channelId },
|
|
865
|
+
responseType: "hosts.link.open.response",
|
|
866
|
+
})).routeId;
|
|
867
|
+
}
|
|
868
|
+
sendHostFrame(routeId, data) {
|
|
869
|
+
if (typeof data === "string") {
|
|
870
|
+
this.sendSessionMessageStrict({
|
|
871
|
+
type: "hosts.route.frame.request",
|
|
872
|
+
routeId,
|
|
873
|
+
data,
|
|
874
|
+
binary: false,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
if (!this.isConnected || !this.transport)
|
|
879
|
+
throw this.unavailableError();
|
|
880
|
+
this.sendTransportFrame(encodeHostRouteFrame(routeId, data));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
subscribeHostFrames(handler) {
|
|
884
|
+
this.hostFrameListeners.add(handler);
|
|
885
|
+
return () => this.hostFrameListeners.delete(handler);
|
|
886
|
+
}
|
|
887
|
+
cancelHostRoute(scopeId) {
|
|
888
|
+
if (this.isConnected && this.transport)
|
|
889
|
+
this.sendSessionMessageStrict({ type: "hosts.route.close.request", scopeId });
|
|
890
|
+
}
|
|
891
|
+
closeHostRoute(routeId) {
|
|
892
|
+
if (this.isConnected && this.transport)
|
|
893
|
+
this.sendSessionMessageStrict({ type: "hosts.route.close.request", routeId });
|
|
894
|
+
}
|
|
745
895
|
getConnectionState() {
|
|
746
896
|
return this.connectionState;
|
|
747
897
|
}
|
|
@@ -758,6 +908,10 @@ export class DaemonClient {
|
|
|
758
908
|
get isConnecting() {
|
|
759
909
|
return this.connectionState.status === "connecting";
|
|
760
910
|
}
|
|
911
|
+
/** The relay endpoint security this client requires on its next attempt. */
|
|
912
|
+
get relaySecurityFloor() {
|
|
913
|
+
return this.relaySecurityFloorValue;
|
|
914
|
+
}
|
|
761
915
|
get lastError() {
|
|
762
916
|
return this.lastErrorValue;
|
|
763
917
|
}
|
|
@@ -833,7 +987,7 @@ export class DaemonClient {
|
|
|
833
987
|
if (!trace?.isEnabled()) {
|
|
834
988
|
return false;
|
|
835
989
|
}
|
|
836
|
-
trace.beginSection(name, args);
|
|
990
|
+
trace.beginSection(name, { ...args, connectionAttempt: String(this.traceConnectionAttempt) });
|
|
837
991
|
return true;
|
|
838
992
|
}
|
|
839
993
|
endTraceSection(isOpen) {
|
|
@@ -845,24 +999,50 @@ export class DaemonClient {
|
|
|
845
999
|
const isOpen = this.beginTraceSection(name, args);
|
|
846
1000
|
this.endTraceSection(isOpen);
|
|
847
1001
|
}
|
|
848
|
-
|
|
849
|
-
this.
|
|
850
|
-
|
|
851
|
-
|
|
1002
|
+
beginSessionDispatchTrace(message) {
|
|
1003
|
+
if (!this.config.trace?.isEnabled())
|
|
1004
|
+
return false;
|
|
1005
|
+
const fields = { messageType: message.type, ...describeRequestIdentity(message) };
|
|
1006
|
+
this.traceInstant("paseo.ws.message.inbound", { envelopeType: "session", ...fields });
|
|
1007
|
+
return this.beginTraceSection("paseo.ws.dispatch", fields);
|
|
1008
|
+
}
|
|
1009
|
+
sendJsonMessage(envelopeType, messageType, message, submitted) {
|
|
1010
|
+
const isOpen = this.config.trace?.isEnabled() === true &&
|
|
1011
|
+
this.beginTraceSection("paseo.ws.message.outbound", {
|
|
1012
|
+
envelopeType,
|
|
1013
|
+
messageType,
|
|
1014
|
+
...describeRequestIdentity(message),
|
|
1015
|
+
});
|
|
1016
|
+
try {
|
|
1017
|
+
this.sendTransportFrame(JSON.stringify(message), submitted);
|
|
1018
|
+
}
|
|
1019
|
+
finally {
|
|
1020
|
+
this.endTraceSection(isOpen);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
sendSessionPayload(payload) {
|
|
1024
|
+
this.sendJsonMessage("session", payload.type, { type: "session", message: payload }, () => {
|
|
1025
|
+
if (!("requestId" in payload) || typeof payload.requestId !== "string")
|
|
1026
|
+
return;
|
|
1027
|
+
for (const waiter of this.waiters) {
|
|
1028
|
+
if (waiter.requestId === payload.requestId)
|
|
1029
|
+
waiter.submitted = true;
|
|
1030
|
+
}
|
|
852
1031
|
});
|
|
853
|
-
this.sendTransportFrame(JSON.stringify(message));
|
|
854
1032
|
}
|
|
855
|
-
sendTransportFrame(frame) {
|
|
1033
|
+
sendTransportFrame(frame, submitted) {
|
|
856
1034
|
if (!this.transport) {
|
|
857
|
-
throw
|
|
1035
|
+
throw this.unavailableError();
|
|
858
1036
|
}
|
|
859
1037
|
// The daemon closes the connection on an oversized message, and creations resubmit on reconnect.
|
|
860
1038
|
assertTransportFrameFits(frame);
|
|
861
1039
|
const isOpen = this.beginTraceSection("paseo.ws.frame.outbound", {
|
|
862
1040
|
kind: typeof frame === "string" ? "text" : "binary",
|
|
863
1041
|
size: String(getTransportFrameSize(frame)),
|
|
1042
|
+
sizeUnit: typeof frame === "string" ? "utf16-code-units" : "bytes",
|
|
864
1043
|
});
|
|
865
1044
|
try {
|
|
1045
|
+
submitted?.();
|
|
866
1046
|
this.transport.send(frame);
|
|
867
1047
|
}
|
|
868
1048
|
finally {
|
|
@@ -883,7 +1063,7 @@ export class DaemonClient {
|
|
|
883
1063
|
}
|
|
884
1064
|
const payload = SessionInboundMessageSchema.parse(message);
|
|
885
1065
|
try {
|
|
886
|
-
this.
|
|
1066
|
+
this.sendSessionPayload(payload);
|
|
887
1067
|
}
|
|
888
1068
|
catch (error) {
|
|
889
1069
|
if (this.config.suppressSendErrors) {
|
|
@@ -922,7 +1102,7 @@ export class DaemonClient {
|
|
|
922
1102
|
sendSessionMessageOrThrow(message) {
|
|
923
1103
|
return this.sendWhenConnected(() => {
|
|
924
1104
|
const payload = SessionInboundMessageSchema.parse(message);
|
|
925
|
-
this.
|
|
1105
|
+
this.sendSessionPayload(payload);
|
|
926
1106
|
});
|
|
927
1107
|
}
|
|
928
1108
|
/** Resolves once connected, waiting out a connection that is still being established. */
|
|
@@ -951,7 +1131,7 @@ export class DaemonClient {
|
|
|
951
1131
|
});
|
|
952
1132
|
}
|
|
953
1133
|
// Not connected and not connecting - fail immediately
|
|
954
|
-
return Promise.reject(
|
|
1134
|
+
return Promise.reject(this.unavailableError());
|
|
955
1135
|
}
|
|
956
1136
|
/**
|
|
957
1137
|
* Flush pending send queue - called when connection is established.
|
|
@@ -995,6 +1175,7 @@ export class DaemonClient {
|
|
|
995
1175
|
kind: "error",
|
|
996
1176
|
error: new DaemonRpcError({
|
|
997
1177
|
requestId: msg.payload.requestId,
|
|
1178
|
+
serverId: this.destinationServerId,
|
|
998
1179
|
error: msg.payload.error,
|
|
999
1180
|
requestType: msg.payload.requestType,
|
|
1000
1181
|
code: msg.payload.code,
|
|
@@ -1070,13 +1251,19 @@ export class DaemonClient {
|
|
|
1070
1251
|
responseType,
|
|
1071
1252
|
});
|
|
1072
1253
|
}
|
|
1254
|
+
unavailableError() {
|
|
1255
|
+
return new DaemonConnectionError("Transport not connected", "DAEMON_CONNECTION_LOST", {
|
|
1256
|
+
serverId: this.destinationServerId,
|
|
1257
|
+
cause: this.connectionFailure,
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1073
1260
|
sendSessionMessageStrict(message) {
|
|
1074
1261
|
if (!this.transport || this.connectionState.status !== "connected") {
|
|
1075
|
-
throw
|
|
1262
|
+
throw this.unavailableError();
|
|
1076
1263
|
}
|
|
1077
1264
|
const payload = SessionInboundMessageSchema.parse(message);
|
|
1078
1265
|
try {
|
|
1079
|
-
this.
|
|
1266
|
+
this.sendSessionPayload(payload);
|
|
1080
1267
|
}
|
|
1081
1268
|
catch (error) {
|
|
1082
1269
|
throw error instanceof Error ? error : new Error(String(error));
|
|
@@ -1290,6 +1477,8 @@ export class DaemonClient {
|
|
|
1290
1477
|
let resetSource = false;
|
|
1291
1478
|
return this.owned.observeRequest(message, async (query, accept) => {
|
|
1292
1479
|
resetSource = false;
|
|
1480
|
+
if (query.type === "workspace.setup.subscribe.request")
|
|
1481
|
+
this.requireWorkspaceSetupObservation();
|
|
1293
1482
|
const requestId = this.createRequestId(options?.requestId);
|
|
1294
1483
|
const prepared = options?.prepare ? options.prepare({ ...query }) : query;
|
|
1295
1484
|
const request = SessionInboundMessageSchema.parse({ ...prepared, requestId });
|
|
@@ -1636,6 +1825,16 @@ export class DaemonClient {
|
|
|
1636
1825
|
responseType: "archive_workspace_response",
|
|
1637
1826
|
});
|
|
1638
1827
|
}
|
|
1828
|
+
observeWorkspaceSetup(workspaceId, options) {
|
|
1829
|
+
return this.observe("workspace.setup.subscribe.response", { type: "workspace.setup.subscribe.request", workspaceId }, options);
|
|
1830
|
+
}
|
|
1831
|
+
requireWorkspaceSetupObservation() {
|
|
1832
|
+
if (this.lastServerInfoMessage?.features?.workspaceSetupObservation !== true ||
|
|
1833
|
+
this.lastServerInfoMessage.features.ownedSubscriptions !== true ||
|
|
1834
|
+
this.config.capabilities?.[CLIENT_CAPS.workspaceSetupObservation] === false ||
|
|
1835
|
+
this.config.capabilities?.[CLIENT_CAPS.ownedSubscriptions] === false)
|
|
1836
|
+
throw new Error("Update the host to view workspace setup details");
|
|
1837
|
+
}
|
|
1639
1838
|
async fetchWorkspaceSetupStatus(workspaceId, requestId) {
|
|
1640
1839
|
return this.sendCorrelatedSessionRequest({
|
|
1641
1840
|
requestId,
|
|
@@ -1693,6 +1892,9 @@ export class DaemonClient {
|
|
|
1693
1892
|
return this.lastServerInfoMessage?.features?.backgroundWorkspaces === true;
|
|
1694
1893
|
}
|
|
1695
1894
|
async createAgent(options) {
|
|
1895
|
+
if (options.git !== undefined || options.worktreeName !== undefined) {
|
|
1896
|
+
throw new Error("Legacy git/worktreeName agent placement was removed. Use createWorkspace({ source, agent, idempotencyKey }) or create a workspace first and pass workspaceId.");
|
|
1897
|
+
}
|
|
1696
1898
|
if (options.background !== undefined)
|
|
1697
1899
|
this.requireBackgroundWorkspaces();
|
|
1698
1900
|
const result = await this.creations.createAgent({
|
|
@@ -2280,23 +2482,29 @@ export class DaemonClient {
|
|
|
2280
2482
|
// ============================================================================
|
|
2281
2483
|
// Agent Interaction
|
|
2282
2484
|
// ============================================================================
|
|
2283
|
-
async sendAgentMessage(agentId, text, options) {
|
|
2284
|
-
if (options
|
|
2485
|
+
async sendAgentMessage(agentId, text, options = {}) {
|
|
2486
|
+
if (options.sourceAgentId &&
|
|
2285
2487
|
this.lastServerInfoMessage?.features?.agentMessageProvenance !== true) {
|
|
2286
2488
|
throw new Error("Update the Paseo host to send messages with agent provenance.");
|
|
2287
2489
|
}
|
|
2490
|
+
if (options.sourceAgentServerId &&
|
|
2491
|
+
this.lastServerInfoMessage?.features?.agentMessageBatching !== true) {
|
|
2492
|
+
throw new Error("Update the Paseo host to send messages with a qualified agent address.");
|
|
2493
|
+
}
|
|
2288
2494
|
const requestId = this.createRequestId();
|
|
2289
|
-
const messageId = options
|
|
2495
|
+
const messageId = options.messageId ?? crypto.randomUUID();
|
|
2290
2496
|
const message = SessionInboundMessageSchema.parse({
|
|
2291
2497
|
type: "send_agent_message_request",
|
|
2292
2498
|
requestId,
|
|
2293
2499
|
agentId,
|
|
2294
2500
|
text,
|
|
2295
|
-
sourceAgentId: options
|
|
2501
|
+
sourceAgentId: options.sourceAgentId,
|
|
2502
|
+
sourceAgentServerId: options.sourceAgentServerId,
|
|
2503
|
+
sessionMode: options.sessionMode,
|
|
2296
2504
|
...(messageId ? { messageId } : {}),
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2505
|
+
activeTurnBehavior: options.activeTurnBehavior,
|
|
2506
|
+
images: options.images,
|
|
2507
|
+
attachments: options.attachments,
|
|
2300
2508
|
});
|
|
2301
2509
|
const payload = await this.sendRequest({
|
|
2302
2510
|
requestId,
|
|
@@ -2315,9 +2523,10 @@ export class DaemonClient {
|
|
|
2315
2523
|
if (!payload.accepted) {
|
|
2316
2524
|
throw new Error(payload.error ?? "sendAgentMessage rejected");
|
|
2317
2525
|
}
|
|
2526
|
+
return payload.deliveryStatus ? { deliveryStatus: payload.deliveryStatus } : {};
|
|
2318
2527
|
}
|
|
2319
2528
|
async sendMessage(agentId, text, options) {
|
|
2320
|
-
|
|
2529
|
+
return this.sendAgentMessage(agentId, text, options);
|
|
2321
2530
|
}
|
|
2322
2531
|
// ==========================================================================
|
|
2323
2532
|
// Durable agent message queue — see docs/queue-mirroring.md.
|
|
@@ -2958,12 +3167,12 @@ export class DaemonClient {
|
|
|
2958
3167
|
const trimmedBaseRef = compare.baseRef?.trim();
|
|
2959
3168
|
if (!trimmedBaseRef) {
|
|
2960
3169
|
return compare.ignoreWhitespace === true
|
|
2961
|
-
? { mode:
|
|
2962
|
-
: { mode:
|
|
3170
|
+
? { mode: compare.mode, ignoreWhitespace: true }
|
|
3171
|
+
: { mode: compare.mode };
|
|
2963
3172
|
}
|
|
2964
3173
|
return compare.ignoreWhitespace === true
|
|
2965
|
-
? { mode:
|
|
2966
|
-
: { mode:
|
|
3174
|
+
? { mode: compare.mode, baseRef: trimmedBaseRef, ignoreWhitespace: true }
|
|
3175
|
+
: { mode: compare.mode, baseRef: trimmedBaseRef };
|
|
2967
3176
|
}
|
|
2968
3177
|
async getCheckoutDiff(cwd, compare, requestId) {
|
|
2969
3178
|
return this.sendCorrelatedSessionRequest({
|
|
@@ -4126,6 +4335,10 @@ export class DaemonClient {
|
|
|
4126
4335
|
requestId,
|
|
4127
4336
|
message: { type: `plugin.${action}.request`, requestId, pluginId },
|
|
4128
4337
|
responseType: `plugin.${action}.response`,
|
|
4338
|
+
// Reload tears the plugin down and starts it again before the daemon answers, and it queues
|
|
4339
|
+
// behind other plugin lifecycle work. Wait for that answer instead of failing the reload at
|
|
4340
|
+
// the request deadline while the daemon keeps loading it.
|
|
4341
|
+
...(action === "reload" ? { timeout: 0 } : {}),
|
|
4129
4342
|
});
|
|
4130
4343
|
return payload.plugin;
|
|
4131
4344
|
}
|
|
@@ -4625,6 +4838,9 @@ export class DaemonClient {
|
|
|
4625
4838
|
const auth = this.helloAuth;
|
|
4626
4839
|
this.sendJsonMessage("hello", "hello", {
|
|
4627
4840
|
type: "hello",
|
|
4841
|
+
...(this.config.sessionPermissions
|
|
4842
|
+
? { sessionPermissions: this.config.sessionPermissions }
|
|
4843
|
+
: {}),
|
|
4628
4844
|
clientId: this.config.clientId,
|
|
4629
4845
|
clientType: this.config.clientType ?? "cli",
|
|
4630
4846
|
protocolVersion: 1,
|
|
@@ -4704,9 +4920,10 @@ export class DaemonClient {
|
|
|
4704
4920
|
});
|
|
4705
4921
|
return;
|
|
4706
4922
|
}
|
|
4707
|
-
const
|
|
4708
|
-
const isOpen = this.beginTraceSection("paseo.ws.frame.inbound", describeInboundTransportFrame(rawData, rawBytes));
|
|
4923
|
+
const isOpen = this.beginTraceSection("paseo.ws.frame.inbound", describeInboundTransportFrame(rawData));
|
|
4709
4924
|
try {
|
|
4925
|
+
// Record arrival before conversion: encoding a large text frame is JS work too.
|
|
4926
|
+
const rawBytes = asUint8Array(rawData);
|
|
4710
4927
|
if (rawBytes && this.tryHandleBinaryFrame(rawBytes)) {
|
|
4711
4928
|
return;
|
|
4712
4929
|
}
|
|
@@ -4726,18 +4943,28 @@ export class DaemonClient {
|
|
|
4726
4943
|
let parsedJson;
|
|
4727
4944
|
const parseTraceOpen = this.beginTraceSection("paseo.ws.json.parse", {
|
|
4728
4945
|
size: String(bytes),
|
|
4946
|
+
sizeUnit: rawBytesLength === undefined ? "utf16-code-units" : "bytes",
|
|
4729
4947
|
});
|
|
4730
4948
|
try {
|
|
4731
4949
|
parsedJson = JSON.parse(payload);
|
|
4732
4950
|
}
|
|
4733
4951
|
catch {
|
|
4952
|
+
this.traceInstant("paseo.ws.json.invalid");
|
|
4734
4953
|
return;
|
|
4735
4954
|
}
|
|
4736
4955
|
finally {
|
|
4737
4956
|
this.endTraceSection(parseTraceOpen);
|
|
4738
4957
|
}
|
|
4739
|
-
const
|
|
4958
|
+
const validationTrace = this.beginTraceSection("paseo.ws.validate");
|
|
4959
|
+
let parsed;
|
|
4960
|
+
try {
|
|
4961
|
+
parsed = validateWSOutboundMessage(parsedJson);
|
|
4962
|
+
}
|
|
4963
|
+
finally {
|
|
4964
|
+
this.endTraceSection(validationTrace);
|
|
4965
|
+
}
|
|
4740
4966
|
if (!parsed.success) {
|
|
4967
|
+
this.traceInstant("paseo.ws.validation.failed");
|
|
4741
4968
|
const responseIdentity = extractCorrelatedResponseIdentity(parsedJson);
|
|
4742
4969
|
const envelopeType = parsedJson != null &&
|
|
4743
4970
|
typeof parsedJson === "object" &&
|
|
@@ -4763,6 +4990,7 @@ export class DaemonClient {
|
|
|
4763
4990
|
return;
|
|
4764
4991
|
}
|
|
4765
4992
|
if (parsed.data.type === "hello.rejected") {
|
|
4993
|
+
this.traceInstant("paseo.connection.rejected", { reason: parsed.data.reason });
|
|
4766
4994
|
const reasonMessage = {
|
|
4767
4995
|
password_required: "Password required",
|
|
4768
4996
|
incorrect_password: "Incorrect password",
|
|
@@ -4773,11 +5001,13 @@ export class DaemonClient {
|
|
|
4773
5001
|
parsed.data.reason === "incompatible_protocol" ? null : parsed.data.reason;
|
|
4774
5002
|
return;
|
|
4775
5003
|
}
|
|
4776
|
-
this.
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
}
|
|
4780
|
-
|
|
5004
|
+
const dispatchTrace = this.beginSessionDispatchTrace(parsed.data.message);
|
|
5005
|
+
try {
|
|
5006
|
+
this.handleSessionMessage(parsed.data.message);
|
|
5007
|
+
}
|
|
5008
|
+
finally {
|
|
5009
|
+
this.endTraceSection(dispatchTrace);
|
|
5010
|
+
}
|
|
4781
5011
|
const msgType = parsed.data.message.type;
|
|
4782
5012
|
this.runtimeMetrics?.recordMessage(msgType, bytes, perfNow() - startMs);
|
|
4783
5013
|
if (parsed.data.message.type === "agent_stream") {
|
|
@@ -4785,12 +5015,24 @@ export class DaemonClient {
|
|
|
4785
5015
|
}
|
|
4786
5016
|
}
|
|
4787
5017
|
tryHandleBinaryFrame(rawBytes) {
|
|
5018
|
+
const hostFrame = decodeHostRouteFrame(rawBytes);
|
|
5019
|
+
if (rawBytes[0] === HOST_ROUTE_OPCODE && !hostFrame) {
|
|
5020
|
+
this.disposeTransport(1008, "Invalid host routing frame");
|
|
5021
|
+
this.scheduleReconnect({ reason: "Invalid host routing frame", event: "INVALID_HOST_FRAME" });
|
|
5022
|
+
return true;
|
|
5023
|
+
}
|
|
5024
|
+
if (hostFrame) {
|
|
5025
|
+
for (const handler of this.hostFrameListeners)
|
|
5026
|
+
handler(hostFrame);
|
|
5027
|
+
return true;
|
|
5028
|
+
}
|
|
4788
5029
|
const fileFrame = decodeFileTransferFrame(rawBytes);
|
|
4789
5030
|
if (fileFrame) {
|
|
4790
5031
|
this.traceInstant("paseo.ws.message.inbound", {
|
|
4791
5032
|
envelopeType: "binary",
|
|
4792
5033
|
messageType: "file",
|
|
4793
5034
|
opcode: String(fileFrame.opcode),
|
|
5035
|
+
requestId: fileFrame.requestId,
|
|
4794
5036
|
});
|
|
4795
5037
|
this.consecutiveLivenessFailures = 0;
|
|
4796
5038
|
this.handleFileTransferFrame(fileFrame);
|
|
@@ -4805,6 +5047,7 @@ export class DaemonClient {
|
|
|
4805
5047
|
envelopeType: "binary",
|
|
4806
5048
|
messageType: "terminal",
|
|
4807
5049
|
opcode: String(frame.opcode),
|
|
5050
|
+
slot: String(frame.slot),
|
|
4808
5051
|
});
|
|
4809
5052
|
this.consecutiveLivenessFailures = 0;
|
|
4810
5053
|
const binaryStartMs = perfNow();
|
|
@@ -4905,6 +5148,12 @@ export class DaemonClient {
|
|
|
4905
5148
|
const reasonFromNext = next.status === "disconnected" && typeof next.reason === "string" ? next.reason : null;
|
|
4906
5149
|
const reason = metadata?.reason ?? reasonFromNext;
|
|
4907
5150
|
const reasonCode = metadata?.reasonCode ?? toReasonCode(reason);
|
|
5151
|
+
this.traceInstant("paseo.connection.state", {
|
|
5152
|
+
from: previous.status,
|
|
5153
|
+
to: next.status,
|
|
5154
|
+
event: metadata?.event ?? "STATE_UPDATE",
|
|
5155
|
+
reasonCode: reasonCode ?? "none",
|
|
5156
|
+
});
|
|
4908
5157
|
this.logger.debug({
|
|
4909
5158
|
serverId: this.logServerId,
|
|
4910
5159
|
clientIdHash: this.logClientIdHash,
|
|
@@ -4946,9 +5195,14 @@ export class DaemonClient {
|
|
|
4946
5195
|
this.providerSnapshotUpdates.clear();
|
|
4947
5196
|
// Clear all pending waiters and queued sends since the connection was lost
|
|
4948
5197
|
// and responses from the previous connection will never arrive.
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
5198
|
+
const connectionError = new DaemonConnectionError(reason ?? "Connection lost", "DAEMON_CONNECTION_LOST", {
|
|
5199
|
+
serverId: this.destinationServerId,
|
|
5200
|
+
cause: input?.failure,
|
|
5201
|
+
});
|
|
5202
|
+
this.connectionFailure = input?.failure;
|
|
5203
|
+
this.clearWaiters(connectionError, true);
|
|
5204
|
+
this.rejectPendingSendQueue(connectionError);
|
|
5205
|
+
this.rejectPingProbe(connectionError);
|
|
4952
5206
|
this.terminalStreams.clearSlots();
|
|
4953
5207
|
this.lastServerInfoMessage = null;
|
|
4954
5208
|
if (this.authFailureReasonValue)
|
|
@@ -4961,7 +5215,7 @@ export class DaemonClient {
|
|
|
4961
5215
|
if (!this.shouldReconnect || this.config.reconnect?.enabled === false) {
|
|
4962
5216
|
this.rejectConnect(this.authFailureReasonValue
|
|
4963
5217
|
? new DaemonAuthenticationError(this.authFailureReasonValue)
|
|
4964
|
-
: new Error(reason ?? "Transport disconnected before connect"));
|
|
5218
|
+
: (input?.failure ?? new Error(reason ?? "Transport disconnected before connect")));
|
|
4965
5219
|
return;
|
|
4966
5220
|
}
|
|
4967
5221
|
this.armReconnectTimer();
|
|
@@ -4969,6 +5223,16 @@ export class DaemonClient {
|
|
|
4969
5223
|
emitDisconnectedStateForReconnect(reason, input) {
|
|
4970
5224
|
this.updateConnectionState({
|
|
4971
5225
|
status: "disconnected",
|
|
5226
|
+
...(input?.failure && "code" in input.failure && typeof input.failure.code === "string"
|
|
5227
|
+
? { failureCode: input.failure.code }
|
|
5228
|
+
: {}),
|
|
5229
|
+
...(this.shouldReconnect && this.config.reconnect?.enabled !== false
|
|
5230
|
+
? {
|
|
5231
|
+
nextRetryAt: new Date(Date.now() +
|
|
5232
|
+
Math.min((this.config.reconnect?.baseDelayMs ?? DEFAULT_RECONNECT_BASE_DELAY_MS) *
|
|
5233
|
+
2 ** this.reconnectAttempt, this.config.reconnect?.maxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS)).toISOString(),
|
|
5234
|
+
}
|
|
5235
|
+
: {}),
|
|
4972
5236
|
...(reason ? { reason } : {}),
|
|
4973
5237
|
}, {
|
|
4974
5238
|
event: input?.event ?? "TRANSPORT_CLOSE",
|
|
@@ -4981,6 +5245,10 @@ export class DaemonClient {
|
|
|
4981
5245
|
const baseDelay = this.config.reconnect?.baseDelayMs ?? DEFAULT_RECONNECT_BASE_DELAY_MS;
|
|
4982
5246
|
const maxDelay = this.config.reconnect?.maxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
4983
5247
|
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
|
|
5248
|
+
this.traceInstant("paseo.connection.reconnect", {
|
|
5249
|
+
attempt: String(attempt),
|
|
5250
|
+
delayMs: String(delay),
|
|
5251
|
+
});
|
|
4984
5252
|
this.reconnectAttempt = attempt + 1;
|
|
4985
5253
|
this.reconnectTimeout = setTimeout(() => {
|
|
4986
5254
|
this.reconnectTimeout = null;
|
|
@@ -5048,6 +5316,8 @@ export class DaemonClient {
|
|
|
5048
5316
|
const serverInfo = parseServerInfoStatusPayload(consumerMessage.payload);
|
|
5049
5317
|
if (serverInfo) {
|
|
5050
5318
|
this.lastServerInfoMessage = serverInfo;
|
|
5319
|
+
this.destinationServerId = serverInfo.serverId;
|
|
5320
|
+
this.connectionFailure = undefined;
|
|
5051
5321
|
if (this.connectionState.status === "connecting") {
|
|
5052
5322
|
this.resetConnectTimeout();
|
|
5053
5323
|
this.reconnectAttempt = 0;
|
|
@@ -5129,12 +5399,18 @@ export class DaemonClient {
|
|
|
5129
5399
|
waiter.reject(error);
|
|
5130
5400
|
}
|
|
5131
5401
|
}
|
|
5132
|
-
clearWaiters(error) {
|
|
5402
|
+
clearWaiters(error, connectionLost = false) {
|
|
5133
5403
|
for (const waiter of Array.from(this.waiters)) {
|
|
5134
5404
|
if (waiter.timeoutHandle) {
|
|
5135
5405
|
clearTimeout(waiter.timeoutHandle);
|
|
5136
5406
|
}
|
|
5137
|
-
waiter.reject(error
|
|
5407
|
+
waiter.reject(connectionLost && waiter.submitted && error instanceof DaemonConnectionError
|
|
5408
|
+
? new DaemonConnectionError(error.message, error.code, {
|
|
5409
|
+
serverId: error.serverId,
|
|
5410
|
+
outcome: "unknown",
|
|
5411
|
+
cause: error.cause instanceof Error ? error.cause : undefined,
|
|
5412
|
+
})
|
|
5413
|
+
: error);
|
|
5138
5414
|
}
|
|
5139
5415
|
this.waiters.clear();
|
|
5140
5416
|
}
|
|
@@ -5229,6 +5505,7 @@ export class DaemonClient {
|
|
|
5229
5505
|
reject: wrappedReject,
|
|
5230
5506
|
timeoutHandle,
|
|
5231
5507
|
requestId: options?.requestId,
|
|
5508
|
+
submitted: false,
|
|
5232
5509
|
};
|
|
5233
5510
|
this.waiters.add(waiter);
|
|
5234
5511
|
});
|