@camerontaylor/paseo-client 0.12.0-fork.1 → 0.13.0-fork.2
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 +329 -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({
|
|
@@ -4629,6 +4838,9 @@ export class DaemonClient {
|
|
|
4629
4838
|
const auth = this.helloAuth;
|
|
4630
4839
|
this.sendJsonMessage("hello", "hello", {
|
|
4631
4840
|
type: "hello",
|
|
4841
|
+
...(this.config.sessionPermissions
|
|
4842
|
+
? { sessionPermissions: this.config.sessionPermissions }
|
|
4843
|
+
: {}),
|
|
4632
4844
|
clientId: this.config.clientId,
|
|
4633
4845
|
clientType: this.config.clientType ?? "cli",
|
|
4634
4846
|
protocolVersion: 1,
|
|
@@ -4708,9 +4920,10 @@ export class DaemonClient {
|
|
|
4708
4920
|
});
|
|
4709
4921
|
return;
|
|
4710
4922
|
}
|
|
4711
|
-
const
|
|
4712
|
-
const isOpen = this.beginTraceSection("paseo.ws.frame.inbound", describeInboundTransportFrame(rawData, rawBytes));
|
|
4923
|
+
const isOpen = this.beginTraceSection("paseo.ws.frame.inbound", describeInboundTransportFrame(rawData));
|
|
4713
4924
|
try {
|
|
4925
|
+
// Record arrival before conversion: encoding a large text frame is JS work too.
|
|
4926
|
+
const rawBytes = asUint8Array(rawData);
|
|
4714
4927
|
if (rawBytes && this.tryHandleBinaryFrame(rawBytes)) {
|
|
4715
4928
|
return;
|
|
4716
4929
|
}
|
|
@@ -4730,18 +4943,28 @@ export class DaemonClient {
|
|
|
4730
4943
|
let parsedJson;
|
|
4731
4944
|
const parseTraceOpen = this.beginTraceSection("paseo.ws.json.parse", {
|
|
4732
4945
|
size: String(bytes),
|
|
4946
|
+
sizeUnit: rawBytesLength === undefined ? "utf16-code-units" : "bytes",
|
|
4733
4947
|
});
|
|
4734
4948
|
try {
|
|
4735
4949
|
parsedJson = JSON.parse(payload);
|
|
4736
4950
|
}
|
|
4737
4951
|
catch {
|
|
4952
|
+
this.traceInstant("paseo.ws.json.invalid");
|
|
4738
4953
|
return;
|
|
4739
4954
|
}
|
|
4740
4955
|
finally {
|
|
4741
4956
|
this.endTraceSection(parseTraceOpen);
|
|
4742
4957
|
}
|
|
4743
|
-
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
|
+
}
|
|
4744
4966
|
if (!parsed.success) {
|
|
4967
|
+
this.traceInstant("paseo.ws.validation.failed");
|
|
4745
4968
|
const responseIdentity = extractCorrelatedResponseIdentity(parsedJson);
|
|
4746
4969
|
const envelopeType = parsedJson != null &&
|
|
4747
4970
|
typeof parsedJson === "object" &&
|
|
@@ -4767,6 +4990,7 @@ export class DaemonClient {
|
|
|
4767
4990
|
return;
|
|
4768
4991
|
}
|
|
4769
4992
|
if (parsed.data.type === "hello.rejected") {
|
|
4993
|
+
this.traceInstant("paseo.connection.rejected", { reason: parsed.data.reason });
|
|
4770
4994
|
const reasonMessage = {
|
|
4771
4995
|
password_required: "Password required",
|
|
4772
4996
|
incorrect_password: "Incorrect password",
|
|
@@ -4777,11 +5001,13 @@ export class DaemonClient {
|
|
|
4777
5001
|
parsed.data.reason === "incompatible_protocol" ? null : parsed.data.reason;
|
|
4778
5002
|
return;
|
|
4779
5003
|
}
|
|
4780
|
-
this.
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
}
|
|
4784
|
-
|
|
5004
|
+
const dispatchTrace = this.beginSessionDispatchTrace(parsed.data.message);
|
|
5005
|
+
try {
|
|
5006
|
+
this.handleSessionMessage(parsed.data.message);
|
|
5007
|
+
}
|
|
5008
|
+
finally {
|
|
5009
|
+
this.endTraceSection(dispatchTrace);
|
|
5010
|
+
}
|
|
4785
5011
|
const msgType = parsed.data.message.type;
|
|
4786
5012
|
this.runtimeMetrics?.recordMessage(msgType, bytes, perfNow() - startMs);
|
|
4787
5013
|
if (parsed.data.message.type === "agent_stream") {
|
|
@@ -4789,12 +5015,24 @@ export class DaemonClient {
|
|
|
4789
5015
|
}
|
|
4790
5016
|
}
|
|
4791
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
|
+
}
|
|
4792
5029
|
const fileFrame = decodeFileTransferFrame(rawBytes);
|
|
4793
5030
|
if (fileFrame) {
|
|
4794
5031
|
this.traceInstant("paseo.ws.message.inbound", {
|
|
4795
5032
|
envelopeType: "binary",
|
|
4796
5033
|
messageType: "file",
|
|
4797
5034
|
opcode: String(fileFrame.opcode),
|
|
5035
|
+
requestId: fileFrame.requestId,
|
|
4798
5036
|
});
|
|
4799
5037
|
this.consecutiveLivenessFailures = 0;
|
|
4800
5038
|
this.handleFileTransferFrame(fileFrame);
|
|
@@ -4809,6 +5047,7 @@ export class DaemonClient {
|
|
|
4809
5047
|
envelopeType: "binary",
|
|
4810
5048
|
messageType: "terminal",
|
|
4811
5049
|
opcode: String(frame.opcode),
|
|
5050
|
+
slot: String(frame.slot),
|
|
4812
5051
|
});
|
|
4813
5052
|
this.consecutiveLivenessFailures = 0;
|
|
4814
5053
|
const binaryStartMs = perfNow();
|
|
@@ -4909,6 +5148,12 @@ export class DaemonClient {
|
|
|
4909
5148
|
const reasonFromNext = next.status === "disconnected" && typeof next.reason === "string" ? next.reason : null;
|
|
4910
5149
|
const reason = metadata?.reason ?? reasonFromNext;
|
|
4911
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
|
+
});
|
|
4912
5157
|
this.logger.debug({
|
|
4913
5158
|
serverId: this.logServerId,
|
|
4914
5159
|
clientIdHash: this.logClientIdHash,
|
|
@@ -4950,9 +5195,14 @@ export class DaemonClient {
|
|
|
4950
5195
|
this.providerSnapshotUpdates.clear();
|
|
4951
5196
|
// Clear all pending waiters and queued sends since the connection was lost
|
|
4952
5197
|
// and responses from the previous connection will never arrive.
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
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);
|
|
4956
5206
|
this.terminalStreams.clearSlots();
|
|
4957
5207
|
this.lastServerInfoMessage = null;
|
|
4958
5208
|
if (this.authFailureReasonValue)
|
|
@@ -4965,7 +5215,7 @@ export class DaemonClient {
|
|
|
4965
5215
|
if (!this.shouldReconnect || this.config.reconnect?.enabled === false) {
|
|
4966
5216
|
this.rejectConnect(this.authFailureReasonValue
|
|
4967
5217
|
? new DaemonAuthenticationError(this.authFailureReasonValue)
|
|
4968
|
-
: new Error(reason ?? "Transport disconnected before connect"));
|
|
5218
|
+
: (input?.failure ?? new Error(reason ?? "Transport disconnected before connect")));
|
|
4969
5219
|
return;
|
|
4970
5220
|
}
|
|
4971
5221
|
this.armReconnectTimer();
|
|
@@ -4973,6 +5223,16 @@ export class DaemonClient {
|
|
|
4973
5223
|
emitDisconnectedStateForReconnect(reason, input) {
|
|
4974
5224
|
this.updateConnectionState({
|
|
4975
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
|
+
: {}),
|
|
4976
5236
|
...(reason ? { reason } : {}),
|
|
4977
5237
|
}, {
|
|
4978
5238
|
event: input?.event ?? "TRANSPORT_CLOSE",
|
|
@@ -4985,6 +5245,10 @@ export class DaemonClient {
|
|
|
4985
5245
|
const baseDelay = this.config.reconnect?.baseDelayMs ?? DEFAULT_RECONNECT_BASE_DELAY_MS;
|
|
4986
5246
|
const maxDelay = this.config.reconnect?.maxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
4987
5247
|
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
|
|
5248
|
+
this.traceInstant("paseo.connection.reconnect", {
|
|
5249
|
+
attempt: String(attempt),
|
|
5250
|
+
delayMs: String(delay),
|
|
5251
|
+
});
|
|
4988
5252
|
this.reconnectAttempt = attempt + 1;
|
|
4989
5253
|
this.reconnectTimeout = setTimeout(() => {
|
|
4990
5254
|
this.reconnectTimeout = null;
|
|
@@ -5052,6 +5316,8 @@ export class DaemonClient {
|
|
|
5052
5316
|
const serverInfo = parseServerInfoStatusPayload(consumerMessage.payload);
|
|
5053
5317
|
if (serverInfo) {
|
|
5054
5318
|
this.lastServerInfoMessage = serverInfo;
|
|
5319
|
+
this.destinationServerId = serverInfo.serverId;
|
|
5320
|
+
this.connectionFailure = undefined;
|
|
5055
5321
|
if (this.connectionState.status === "connecting") {
|
|
5056
5322
|
this.resetConnectTimeout();
|
|
5057
5323
|
this.reconnectAttempt = 0;
|
|
@@ -5133,12 +5399,18 @@ export class DaemonClient {
|
|
|
5133
5399
|
waiter.reject(error);
|
|
5134
5400
|
}
|
|
5135
5401
|
}
|
|
5136
|
-
clearWaiters(error) {
|
|
5402
|
+
clearWaiters(error, connectionLost = false) {
|
|
5137
5403
|
for (const waiter of Array.from(this.waiters)) {
|
|
5138
5404
|
if (waiter.timeoutHandle) {
|
|
5139
5405
|
clearTimeout(waiter.timeoutHandle);
|
|
5140
5406
|
}
|
|
5141
|
-
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);
|
|
5142
5414
|
}
|
|
5143
5415
|
this.waiters.clear();
|
|
5144
5416
|
}
|
|
@@ -5233,6 +5505,7 @@ export class DaemonClient {
|
|
|
5233
5505
|
reject: wrappedReject,
|
|
5234
5506
|
timeoutHandle,
|
|
5235
5507
|
requestId: options?.requestId,
|
|
5508
|
+
submitted: false,
|
|
5236
5509
|
};
|
|
5237
5510
|
this.waiters.add(waiter);
|
|
5238
5511
|
});
|