@camerontaylor/paseo-client 0.8.0-fork.1 → 0.9.0-beta.2.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 +28 -21
- package/dist/connection/index.js +90 -85
- package/dist/connection/legacy-workspaces.d.ts +19 -0
- package/dist/connection/legacy-workspaces.js +160 -0
- package/dist/connection/legacy.d.ts +31 -0
- package/dist/connection/legacy.js +245 -0
- package/dist/connection/owned.d.ts +36 -0
- package/dist/connection/owned.js +210 -0
- package/dist/creation/index.d.ts +2 -9
- package/dist/creation/index.js +37 -56
- package/dist/daemon-client.d.ts +104 -43
- package/dist/daemon-client.js +408 -376
- package/dist/index.d.ts +37 -3
- package/dist/index.js +235 -104
- package/dist/provider-snapshots/index.d.ts +1 -3
- package/dist/provider-snapshots/index.js +23 -27
- package/dist/terminal-stream-router.d.ts +7 -10
- package/dist/terminal-stream-router.js +41 -88
- package/dist/timeline-subscription/index.d.ts +25 -0
- package/dist/timeline-subscription/index.js +56 -0
- package/package.json +7 -3
package/dist/daemon-client.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import { subscribeTimeline } from "./timeline-subscription/index.js";
|
|
1
2
|
import { ProviderSnapshotUpdates } from "./provider-snapshots/index.js";
|
|
2
3
|
import { ConnectionSubscriptions, DEFAULT_CLIENT_CAPABILITIES, } from "./connection/index.js";
|
|
3
4
|
import { CreationClient } from "./creation/index.js";
|
|
4
5
|
import { CLIENT_CAPS } from "@camerontaylor/paseo-protocol/client-capabilities";
|
|
5
|
-
import { parsePluginSourceReference } from "@camerontaylor/paseo-protocol/plugin-source-reference";
|
|
6
6
|
import { AgentCreateFailedStatusPayloadSchema, AgentCreatedStatusPayloadSchema, AgentRefreshedStatusPayloadSchema, AgentResumedStatusPayloadSchema, parseServerInfoStatusPayload, RestartRequestedStatusPayloadSchema, ShutdownRequestedStatusPayloadSchema, DaemonUpdateResponseSchema, SessionInboundMessageSchema, } from "@camerontaylor/paseo-protocol/messages";
|
|
7
7
|
import { validateWSOutboundMessage } from "@camerontaylor/paseo-protocol/validation/ws-outbound";
|
|
8
8
|
import { isRelayClientWebSocketUrl } from "@camerontaylor/paseo-protocol/daemon-endpoints";
|
|
9
|
-
import { terminalSubscriptionKey } from "@camerontaylor/paseo-protocol/terminal-subscription-key";
|
|
10
9
|
import { asUint8Array, decodeFileTransferFrame, encodeFileTransferFrame, decodeTerminalStreamFrame, FileTransferOpcode, TerminalStreamOpcode, } from "@camerontaylor/paseo-protocol/binary-frames/index";
|
|
11
10
|
import { createRelayE2eeTransportFactory, createWebSocketTransportFactory, decodeMessageData, defaultWebSocketFactory, describeTransportClose, describeTransportError, } from "./daemon-client-transport.js";
|
|
12
11
|
import { DaemonClientRuntimeMetrics } from "./daemon-client-runtime-metrics.js";
|
|
@@ -76,6 +75,13 @@ function normalizeListCommandsOptions(input, legacyOptions) {
|
|
|
76
75
|
}
|
|
77
76
|
return { agentId: input, ...legacyOptions };
|
|
78
77
|
}
|
|
78
|
+
export class DaemonConnectionError extends Error {
|
|
79
|
+
constructor(message, code = "DAEMON_CONNECTION_LOST") {
|
|
80
|
+
super(message);
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.name = "DaemonConnectionError";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
79
85
|
class DaemonRpcError extends Error {
|
|
80
86
|
constructor(params) {
|
|
81
87
|
const parts = [params.error];
|
|
@@ -232,14 +238,29 @@ export class DaemonClient {
|
|
|
232
238
|
constructor(config) {
|
|
233
239
|
this.config = config;
|
|
234
240
|
this.providerSnapshotUpdates = new ProviderSnapshotUpdates({
|
|
241
|
+
active: (message) => this.owned.owns(message),
|
|
235
242
|
fetch: (cwd) => this.requestProvidersSnapshot({ cwd }),
|
|
236
243
|
emit: (message) => this.deliverSessionMessage(message),
|
|
237
244
|
failed: (error) => this.logger.error({ err: error }, "Failed to resolve provider snapshot"),
|
|
238
245
|
});
|
|
239
|
-
this.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
246
|
+
this.owned = new ConnectionSubscriptions({
|
|
247
|
+
send: (message) => this.sendSessionMessageOrThrow(SessionInboundMessageSchema.parse({ ...message, requestId: this.createRequestId() })),
|
|
248
|
+
release: async (subscriptionId) => {
|
|
249
|
+
if (!this.isConnected)
|
|
250
|
+
return;
|
|
251
|
+
try {
|
|
252
|
+
await this.sendCorrelatedSessionRequest({
|
|
253
|
+
message: { type: "subscription.release.request", subscriptionId },
|
|
254
|
+
responseType: "subscription.release.response",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
this.disposeTransport(1001, "Subscription release failed");
|
|
259
|
+
this.scheduleReconnect({ reason: "Subscription release failed" });
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
failed: (error) => this.logger.error({ err: error }, "Subscription failed"),
|
|
243
264
|
});
|
|
244
265
|
this.transport = null;
|
|
245
266
|
this.transportCleanup = [];
|
|
@@ -259,9 +280,6 @@ export class DaemonClient {
|
|
|
259
280
|
this.connectReject = null;
|
|
260
281
|
this.lastErrorValue = null;
|
|
261
282
|
this.connectionState = { status: "idle" };
|
|
262
|
-
this.checkoutDiffSubscriptions = new Map();
|
|
263
|
-
this.terminalDirectorySubscriptions = new Map();
|
|
264
|
-
this.fileSubscriptions = new Map();
|
|
265
283
|
this.terminalStreams = new TerminalStreamRouter();
|
|
266
284
|
this.pendingBinaryFileReads = new Map();
|
|
267
285
|
this.activeBinaryFileTransfers = new Map();
|
|
@@ -271,6 +289,7 @@ export class DaemonClient {
|
|
|
271
289
|
this.runtimeMetricsInterval = null;
|
|
272
290
|
this.runtimeMetrics = null;
|
|
273
291
|
this.pingProbe = null;
|
|
292
|
+
this.connectionVerification = null;
|
|
274
293
|
this.livenessHeartbeatTimer = null;
|
|
275
294
|
this.lastLivenessRttMs = null;
|
|
276
295
|
this.consecutiveLivenessFailures = 0;
|
|
@@ -278,8 +297,7 @@ export class DaemonClient {
|
|
|
278
297
|
// Agent Lifecycle
|
|
279
298
|
// ============================================================================
|
|
280
299
|
this.creations = new CreationClient({
|
|
281
|
-
supports: () => this.lastServerInfoMessage?.features?.
|
|
282
|
-
connected: () => this.connectionState.status === "connected",
|
|
300
|
+
supports: (feature) => this.lastServerInfoMessage?.features?.[feature] === true,
|
|
283
301
|
requestId: () => this.createRequestId(),
|
|
284
302
|
request: (kind, input) => kind === "workspace"
|
|
285
303
|
? this.sendCorrelatedSessionRequest({
|
|
@@ -294,18 +312,26 @@ export class DaemonClient {
|
|
|
294
312
|
responseType: "agent.create.response",
|
|
295
313
|
timeout: 0,
|
|
296
314
|
}),
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
315
|
+
observe: (kind, idempotencyKey, next, error) => {
|
|
316
|
+
const observation = this.observe("creation.subscribe.response", {
|
|
317
|
+
type: "creation.subscribe.request",
|
|
318
|
+
kind,
|
|
319
|
+
idempotencyKey,
|
|
301
320
|
});
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
321
|
+
observation.subscribe({
|
|
322
|
+
snapshot: (result) => next(result.snapshot),
|
|
323
|
+
update: (message) => {
|
|
324
|
+
if (message.type === "workspace.create.update" || message.type === "agent.create.update")
|
|
325
|
+
next(message.payload);
|
|
326
|
+
},
|
|
327
|
+
error,
|
|
328
|
+
});
|
|
329
|
+
return () => {
|
|
330
|
+
void observation.release().catch(error);
|
|
331
|
+
};
|
|
305
332
|
},
|
|
306
333
|
legacyAgent: (input) => this.createLegacyAgent(input),
|
|
307
334
|
legacyWorkspace: (input) => this.createLegacyWorkspace(input, input.requestId),
|
|
308
|
-
sendMessage: (id, text, options) => this.sendMessage(id, text, options),
|
|
309
335
|
});
|
|
310
336
|
this.logger = config.logger ?? consoleLogger;
|
|
311
337
|
this.logConnectionPath = isRelayClientWebSocketUrl(this.config.url) ? "relay" : "direct";
|
|
@@ -379,6 +405,12 @@ export class DaemonClient {
|
|
|
379
405
|
if (this.connectionState.status === "connecting") {
|
|
380
406
|
return;
|
|
381
407
|
}
|
|
408
|
+
// This attempt supersedes any retry the last disconnect scheduled. Left
|
|
409
|
+
// armed, that retry would tear down the connection this attempt opens.
|
|
410
|
+
if (this.reconnectTimeout) {
|
|
411
|
+
clearTimeout(this.reconnectTimeout);
|
|
412
|
+
this.reconnectTimeout = null;
|
|
413
|
+
}
|
|
382
414
|
const headers = {};
|
|
383
415
|
const password = normalizePassword(this.config.password);
|
|
384
416
|
if (password) {
|
|
@@ -494,7 +526,10 @@ export class DaemonClient {
|
|
|
494
526
|
reasonCode: "transport_error",
|
|
495
527
|
});
|
|
496
528
|
}),
|
|
497
|
-
transport.onMessage((data) =>
|
|
529
|
+
transport.onMessage((data) => {
|
|
530
|
+
if (this.transport === transport)
|
|
531
|
+
this.handleTransportMessage(data);
|
|
532
|
+
}),
|
|
498
533
|
];
|
|
499
534
|
}
|
|
500
535
|
catch (error) {
|
|
@@ -530,7 +565,6 @@ export class DaemonClient {
|
|
|
530
565
|
return;
|
|
531
566
|
}
|
|
532
567
|
this.shouldReconnect = false;
|
|
533
|
-
this.subscriptions.close();
|
|
534
568
|
this.connectPromise = null;
|
|
535
569
|
this.connectResolve = null;
|
|
536
570
|
this.connectReject = null;
|
|
@@ -542,11 +576,11 @@ export class DaemonClient {
|
|
|
542
576
|
this.disposeTransport(1000, "Client closed");
|
|
543
577
|
this.providerSnapshotUpdates.clear();
|
|
544
578
|
this.clearWaiters(new Error("Daemon client closed"));
|
|
579
|
+
await this.owned.close();
|
|
545
580
|
this.creations.close();
|
|
546
581
|
this.rejectPendingSendQueue(new Error("Daemon client closed"));
|
|
547
582
|
this.rejectPingProbe(new Error("Daemon client closed"));
|
|
548
583
|
this.terminalStreams.clearSlots();
|
|
549
|
-
this.fileSubscriptions.clear();
|
|
550
584
|
this.lastServerInfoMessage = null;
|
|
551
585
|
if (this.runtimeMetricsInterval) {
|
|
552
586
|
clearInterval(this.runtimeMetricsInterval);
|
|
@@ -556,27 +590,50 @@ export class DaemonClient {
|
|
|
556
590
|
}
|
|
557
591
|
this.updateConnectionState({ status: "disposed" }, { event: "DISPOSE", reason: "Client closed", reasonCode: "disposed" });
|
|
558
592
|
}
|
|
559
|
-
ensureConnected() {
|
|
593
|
+
ensureConnected(options) {
|
|
560
594
|
if (this.connectionState.status === "disposed") {
|
|
561
595
|
return;
|
|
562
596
|
}
|
|
563
597
|
if (!this.shouldReconnect) {
|
|
564
598
|
this.shouldReconnect = true;
|
|
565
599
|
}
|
|
566
|
-
if (this.connectionState.status === "connected"
|
|
567
|
-
|
|
600
|
+
if (this.connectionState.status === "connected") {
|
|
601
|
+
if (options?.verify)
|
|
602
|
+
this.verifyConnection();
|
|
568
603
|
return;
|
|
569
604
|
}
|
|
570
|
-
if (this.
|
|
571
|
-
|
|
572
|
-
this.reconnectTimeout = null;
|
|
573
|
-
}
|
|
605
|
+
if (this.connectionState.status === "connecting")
|
|
606
|
+
return;
|
|
574
607
|
if (this.connectPromise) {
|
|
575
608
|
this.attemptConnect();
|
|
576
609
|
return;
|
|
577
610
|
}
|
|
578
611
|
void this.connect();
|
|
579
612
|
}
|
|
613
|
+
verifyConnection() {
|
|
614
|
+
const transport = this.transport;
|
|
615
|
+
if (!transport || this.connectionVerification === transport)
|
|
616
|
+
return;
|
|
617
|
+
this.connectionVerification = transport;
|
|
618
|
+
// A session probe has its own deadline, independent of a heartbeat that the OS
|
|
619
|
+
// may have suspended. A successful response also proves the session can serve RPCs.
|
|
620
|
+
void this.ping({ timeoutMs: 3000 })
|
|
621
|
+
.catch((error) => {
|
|
622
|
+
if (this.transport !== transport || this.connectionState.status !== "connected")
|
|
623
|
+
return;
|
|
624
|
+
this.disposeTransport(1001, "Connection verification failed");
|
|
625
|
+
this.scheduleReconnect({
|
|
626
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
627
|
+
event: "CONNECTION_VERIFICATION_FAILED",
|
|
628
|
+
reasonCode: "liveness_timeout",
|
|
629
|
+
});
|
|
630
|
+
this.ensureConnected();
|
|
631
|
+
})
|
|
632
|
+
.finally(() => {
|
|
633
|
+
if (this.connectionVerification === transport)
|
|
634
|
+
this.connectionVerification = null;
|
|
635
|
+
});
|
|
636
|
+
}
|
|
580
637
|
getConnectionState() {
|
|
581
638
|
return this.connectionState;
|
|
582
639
|
}
|
|
@@ -604,10 +661,8 @@ export class DaemonClient {
|
|
|
604
661
|
// ============================================================================
|
|
605
662
|
subscribe(handler) {
|
|
606
663
|
this.eventListeners.add(handler);
|
|
607
|
-
this.updateEventSubscriptions();
|
|
608
664
|
return () => {
|
|
609
665
|
this.eventListeners.delete(handler);
|
|
610
|
-
this.updateEventSubscriptions();
|
|
611
666
|
};
|
|
612
667
|
}
|
|
613
668
|
subscribeRawMessages(handler) {
|
|
@@ -626,7 +681,6 @@ export class DaemonClient {
|
|
|
626
681
|
this.messageHandlers.set(type, new Set());
|
|
627
682
|
}
|
|
628
683
|
this.messageHandlers.get(type).add(handler);
|
|
629
|
-
this.updateEventSubscriptions();
|
|
630
684
|
return () => {
|
|
631
685
|
const handlers = this.messageHandlers.get(type);
|
|
632
686
|
if (!handlers) {
|
|
@@ -636,7 +690,6 @@ export class DaemonClient {
|
|
|
636
690
|
if (handlers.size === 0) {
|
|
637
691
|
this.messageHandlers.delete(type);
|
|
638
692
|
}
|
|
639
|
-
this.updateEventSubscriptions();
|
|
640
693
|
};
|
|
641
694
|
}
|
|
642
695
|
onAgentAttentionRequired(handler) {
|
|
@@ -690,7 +743,7 @@ export class DaemonClient {
|
|
|
690
743
|
}
|
|
691
744
|
sendTransportFrame(frame) {
|
|
692
745
|
if (!this.transport) {
|
|
693
|
-
throw new
|
|
746
|
+
throw new DaemonConnectionError("Transport not connected");
|
|
694
747
|
}
|
|
695
748
|
const isOpen = this.beginTraceSection("paseo.ws.frame.outbound", {
|
|
696
749
|
kind: typeof frame === "string" ? "text" : "binary",
|
|
@@ -770,13 +823,13 @@ export class DaemonClient {
|
|
|
770
823
|
if (idx !== -1) {
|
|
771
824
|
this.pendingSendQueue.splice(idx, 1);
|
|
772
825
|
}
|
|
773
|
-
reject(new
|
|
826
|
+
reject(new DaemonConnectionError("Timed out waiting for connection to send message", "DAEMON_REQUEST_TIMEOUT"));
|
|
774
827
|
}, DEFAULT_SEND_QUEUE_TIMEOUT_MS);
|
|
775
828
|
this.pendingSendQueue.push({ message, resolve, reject, timeoutHandle });
|
|
776
829
|
});
|
|
777
830
|
}
|
|
778
831
|
// Not connected and not connecting - fail immediately
|
|
779
|
-
return Promise.reject(new
|
|
832
|
+
return Promise.reject(new DaemonConnectionError(`Transport not connected (status: ${status})`));
|
|
780
833
|
}
|
|
781
834
|
/**
|
|
782
835
|
* Flush pending send queue - called when connection is established.
|
|
@@ -793,7 +846,7 @@ export class DaemonClient {
|
|
|
793
846
|
pending.resolve();
|
|
794
847
|
}
|
|
795
848
|
else {
|
|
796
|
-
pending.reject(new
|
|
849
|
+
pending.reject(new DaemonConnectionError("Connection lost before message could be sent"));
|
|
797
850
|
}
|
|
798
851
|
}
|
|
799
852
|
catch (error) {
|
|
@@ -813,6 +866,7 @@ export class DaemonClient {
|
|
|
813
866
|
}
|
|
814
867
|
}
|
|
815
868
|
async sendRequest(params) {
|
|
869
|
+
const wire = this.owned.prepareRequest(params.message);
|
|
816
870
|
const timeout = params.timeout ?? DEFAULT_SESSION_RPC_TIMEOUT_MS;
|
|
817
871
|
const { promise, cancel } = this.waitForWithCancel((msg) => {
|
|
818
872
|
if (msg.type === "rpc_error" && msg.payload.requestId === params.requestId) {
|
|
@@ -826,14 +880,14 @@ export class DaemonClient {
|
|
|
826
880
|
}),
|
|
827
881
|
};
|
|
828
882
|
}
|
|
829
|
-
const value = params.select(msg);
|
|
883
|
+
const value = params.select(wire.receive(msg));
|
|
830
884
|
if (value === null) {
|
|
831
885
|
return null;
|
|
832
886
|
}
|
|
833
887
|
return { kind: "ok", value };
|
|
834
888
|
}, timeout, { ...params.options, requestId: params.requestId });
|
|
835
889
|
try {
|
|
836
|
-
await this.sendSessionMessageOrThrow(
|
|
890
|
+
await this.sendSessionMessageOrThrow(wire.message);
|
|
837
891
|
}
|
|
838
892
|
catch (error) {
|
|
839
893
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -841,11 +895,15 @@ export class DaemonClient {
|
|
|
841
895
|
void promise.catch(() => undefined);
|
|
842
896
|
throw err;
|
|
843
897
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
898
|
+
try {
|
|
899
|
+
const result = await promise;
|
|
900
|
+
if (result.kind === "error")
|
|
901
|
+
throw result.error;
|
|
902
|
+
return result.value;
|
|
903
|
+
}
|
|
904
|
+
finally {
|
|
905
|
+
await wire.finish();
|
|
847
906
|
}
|
|
848
|
-
return result.value;
|
|
849
907
|
}
|
|
850
908
|
async sendCorrelatedRequest(params) {
|
|
851
909
|
return this.sendRequest({
|
|
@@ -893,7 +951,7 @@ export class DaemonClient {
|
|
|
893
951
|
}
|
|
894
952
|
sendSessionMessageStrict(message) {
|
|
895
953
|
if (!this.transport || this.connectionState.status !== "connected") {
|
|
896
|
-
throw new
|
|
954
|
+
throw new DaemonConnectionError("Transport not connected");
|
|
897
955
|
}
|
|
898
956
|
const payload = SessionInboundMessageSchema.parse(message);
|
|
899
957
|
try {
|
|
@@ -1107,7 +1165,69 @@ export class DaemonClient {
|
|
|
1107
1165
|
// ============================================================================
|
|
1108
1166
|
// Agent RPCs (requestId-correlated)
|
|
1109
1167
|
// ============================================================================
|
|
1168
|
+
observe(responseType, message, options) {
|
|
1169
|
+
let resetSource = false;
|
|
1170
|
+
return this.owned.observeRequest(message, async (query, accept) => {
|
|
1171
|
+
resetSource = false;
|
|
1172
|
+
const requestId = this.createRequestId(options?.requestId);
|
|
1173
|
+
const request = SessionInboundMessageSchema.parse({ ...query, requestId });
|
|
1174
|
+
try {
|
|
1175
|
+
return await this.sendCorrelatedRequest({
|
|
1176
|
+
message: request,
|
|
1177
|
+
responseType,
|
|
1178
|
+
requestId,
|
|
1179
|
+
timeout: options?.timeout,
|
|
1180
|
+
options: { skipQueue: true },
|
|
1181
|
+
selectPayload: (payload) => {
|
|
1182
|
+
if ("error" in payload && typeof payload.error === "string")
|
|
1183
|
+
throw new DaemonRpcError({
|
|
1184
|
+
requestId: payload.requestId,
|
|
1185
|
+
requestType: message.type,
|
|
1186
|
+
error: payload.error,
|
|
1187
|
+
});
|
|
1188
|
+
accept(payload);
|
|
1189
|
+
return payload;
|
|
1190
|
+
},
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
catch (error) {
|
|
1194
|
+
resetSource = !(error instanceof DaemonRpcError) && this.isConnected;
|
|
1195
|
+
throw error;
|
|
1196
|
+
}
|
|
1197
|
+
}, options?.signal, () => {
|
|
1198
|
+
// Reject and release this handle before closing a source with an unknown bootstrap outcome.
|
|
1199
|
+
// Closing first would classify the failure as a reconnect and replay the failed request.
|
|
1200
|
+
if (resetSource) {
|
|
1201
|
+
this.disposeTransport(1001, "Subscription request failed");
|
|
1202
|
+
this.scheduleReconnect({ reason: "Subscription request failed" });
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
registerBrowserHost(registration, options) {
|
|
1207
|
+
return this.observe("browser.host.register.response", { type: "browser.host.register.request", ...registration }, options);
|
|
1208
|
+
}
|
|
1209
|
+
observeEvents(events, options) {
|
|
1210
|
+
return this.observe("session.events.set_subscription.response", {
|
|
1211
|
+
type: "session.events.set_subscription.request",
|
|
1212
|
+
events,
|
|
1213
|
+
notifications: options?.notifications,
|
|
1214
|
+
}, options);
|
|
1215
|
+
}
|
|
1216
|
+
observeAgents(options = {}) {
|
|
1217
|
+
const { signal, requestId, timeout, ...query } = options;
|
|
1218
|
+
return this.observe("fetch_agents_response", { ...query, type: "fetch_agents_request", subscribe: {} }, { signal, requestId, timeout });
|
|
1219
|
+
}
|
|
1220
|
+
observeWorkspaces(options = {}) {
|
|
1221
|
+
const { signal, requestId, ...query } = options;
|
|
1222
|
+
return this.observe("fetch_workspaces_response", { ...query, type: "fetch_workspaces_request", subscribe: {} }, { signal, requestId });
|
|
1223
|
+
}
|
|
1110
1224
|
async fetchAgents(options) {
|
|
1225
|
+
if (options?.subscribe) {
|
|
1226
|
+
if (options.subscribe.subscriptionId !== undefined)
|
|
1227
|
+
throw new Error("Subscription IDs are assigned by the host");
|
|
1228
|
+
const subscription = this.observeAgents(options);
|
|
1229
|
+
return { ...(await subscription.ready), subscription };
|
|
1230
|
+
}
|
|
1111
1231
|
const resolvedRequestId = this.createRequestId(options?.requestId);
|
|
1112
1232
|
const message = SessionInboundMessageSchema.parse({
|
|
1113
1233
|
type: "fetch_agents_request",
|
|
@@ -1187,6 +1307,12 @@ export class DaemonClient {
|
|
|
1187
1307
|
});
|
|
1188
1308
|
}
|
|
1189
1309
|
async fetchWorkspaces(options) {
|
|
1310
|
+
if (options?.subscribe) {
|
|
1311
|
+
if (options.subscribe.subscriptionId !== undefined)
|
|
1312
|
+
throw new Error("Subscription IDs are assigned by the host");
|
|
1313
|
+
const subscription = this.observeWorkspaces(options);
|
|
1314
|
+
return { ...(await subscription.ready), subscription };
|
|
1315
|
+
}
|
|
1190
1316
|
const resolvedRequestId = this.createRequestId(options?.requestId);
|
|
1191
1317
|
const message = SessionInboundMessageSchema.parse({
|
|
1192
1318
|
type: "fetch_workspaces_request",
|
|
@@ -1212,16 +1338,18 @@ export class DaemonClient {
|
|
|
1212
1338
|
},
|
|
1213
1339
|
});
|
|
1214
1340
|
}
|
|
1215
|
-
listWorkspaceLabels(options) {
|
|
1341
|
+
listWorkspaceLabels(options = {}) {
|
|
1216
1342
|
return this.sendNamespacedCorrelatedSessionRequest({
|
|
1217
1343
|
requestId: options.requestId,
|
|
1218
1344
|
message: {
|
|
1219
1345
|
type: "workspace.label.list.request",
|
|
1220
|
-
subscribe: { subscriptionId: options.subscriptionId },
|
|
1221
1346
|
...(options.sync ? { sync: options.sync } : {}),
|
|
1222
1347
|
},
|
|
1223
1348
|
});
|
|
1224
1349
|
}
|
|
1350
|
+
observeWorkspaceLabels(options) {
|
|
1351
|
+
return this.observe("workspace.label.list.response", { type: "workspace.label.list.request", subscribe: {} }, options);
|
|
1352
|
+
}
|
|
1225
1353
|
setWorkspaceLabel(options) {
|
|
1226
1354
|
return this.sendNamespacedCorrelatedSessionRequest({
|
|
1227
1355
|
requestId: options.requestId,
|
|
@@ -1422,50 +1550,6 @@ export class DaemonClient {
|
|
|
1422
1550
|
}
|
|
1423
1551
|
return { agent: payload.agent, project: payload.project ?? null };
|
|
1424
1552
|
}
|
|
1425
|
-
resubscribeCheckoutDiffSubscriptions() {
|
|
1426
|
-
if (this.checkoutDiffSubscriptions.size === 0) {
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
for (const [subscriptionId, subscription] of this.checkoutDiffSubscriptions) {
|
|
1430
|
-
const message = SessionInboundMessageSchema.parse({
|
|
1431
|
-
type: "subscribe_checkout_diff_request",
|
|
1432
|
-
subscriptionId,
|
|
1433
|
-
cwd: subscription.cwd,
|
|
1434
|
-
compare: subscription.compare,
|
|
1435
|
-
requestId: this.createRequestId(),
|
|
1436
|
-
});
|
|
1437
|
-
this.sendSessionMessage(message);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
resubscribeTerminalDirectorySubscriptions() {
|
|
1441
|
-
if (this.terminalDirectorySubscriptions.size === 0) {
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
for (const subscription of this.terminalDirectorySubscriptions.values()) {
|
|
1445
|
-
this.sendSessionMessage({
|
|
1446
|
-
type: "subscribe_terminals_request",
|
|
1447
|
-
cwd: subscription.cwd,
|
|
1448
|
-
...(subscription.workspaceId !== undefined
|
|
1449
|
-
? { workspaceId: subscription.workspaceId }
|
|
1450
|
-
: {}),
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
resubscribeFileSubscriptions() {
|
|
1455
|
-
for (const [subscriptionId, subscription] of this.fileSubscriptions) {
|
|
1456
|
-
void this.sendCorrelatedSessionRequest({
|
|
1457
|
-
message: {
|
|
1458
|
-
type: "fs.file.subscribe.request",
|
|
1459
|
-
cwd: subscription.cwd,
|
|
1460
|
-
path: subscription.path,
|
|
1461
|
-
subscriptionId,
|
|
1462
|
-
},
|
|
1463
|
-
responseType: "fs.file.subscribe.response",
|
|
1464
|
-
})
|
|
1465
|
-
.then((payload) => subscription.onUpdate(payload.initial))
|
|
1466
|
-
.catch(() => undefined);
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
1553
|
async createAgent(options) {
|
|
1470
1554
|
const result = await this.creations.createAgent({
|
|
1471
1555
|
...options,
|
|
@@ -1476,8 +1560,6 @@ export class DaemonClient {
|
|
|
1476
1560
|
return result.agent;
|
|
1477
1561
|
}
|
|
1478
1562
|
async createLegacyAgent(options) {
|
|
1479
|
-
if (options.idempotencyKey !== undefined)
|
|
1480
|
-
this.requireAgentRequestReceipts();
|
|
1481
1563
|
const requestId = this.createRequestId(options.requestId);
|
|
1482
1564
|
const config = resolveAgentConfig(options);
|
|
1483
1565
|
const message = SessionInboundMessageSchema.parse({
|
|
@@ -1527,12 +1609,6 @@ export class DaemonClient {
|
|
|
1527
1609
|
}
|
|
1528
1610
|
return status.agent;
|
|
1529
1611
|
}
|
|
1530
|
-
requireAgentRequestReceipts() {
|
|
1531
|
-
// COMPAT(agentRequestReceipts): added in v0.7.3; remove gate after 2027-03-05.
|
|
1532
|
-
if (this.lastServerInfoMessage?.features?.agentRequestReceipts !== true) {
|
|
1533
|
-
throw new Error("Update the host to use retry-safe agent creation.");
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
1612
|
async deleteAgent(agentId) {
|
|
1537
1613
|
const requestId = this.createRequestId();
|
|
1538
1614
|
const message = SessionInboundMessageSchema.parse({
|
|
@@ -1832,6 +1908,17 @@ export class DaemonClient {
|
|
|
1832
1908
|
});
|
|
1833
1909
|
return { seq: payload.seq, epoch: payload.epoch };
|
|
1834
1910
|
}
|
|
1911
|
+
async searchAgentTimeline({ agentId, query, cursor, }) {
|
|
1912
|
+
const requestId = this.createRequestId();
|
|
1913
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
1914
|
+
requestId,
|
|
1915
|
+
message: { type: "agent.timeline.search.request", requestId, agentId, query, cursor },
|
|
1916
|
+
responseType: "agent.timeline.search.response",
|
|
1917
|
+
});
|
|
1918
|
+
if (payload.error)
|
|
1919
|
+
throw new Error(payload.error);
|
|
1920
|
+
return payload;
|
|
1921
|
+
}
|
|
1835
1922
|
async listAgentTimelinePrompts(agentId, options = {}) {
|
|
1836
1923
|
const requestId = this.createRequestId(options.requestId);
|
|
1837
1924
|
const message = SessionInboundMessageSchema.parse({
|
|
@@ -1877,6 +1964,10 @@ export class DaemonClient {
|
|
|
1877
1964
|
return payload;
|
|
1878
1965
|
}
|
|
1879
1966
|
async fetchProviderSubagentTimeline(parentAgentId, subagentId, options = {}) {
|
|
1967
|
+
// COMPAT(projectedSubagentTimeline): added after v0.8.0, remove after 2027-03-14.
|
|
1968
|
+
if (this.lastServerInfoMessage?.features?.projectedSubagentTimeline !== true) {
|
|
1969
|
+
throw new Error("Update the host to view subagent conversations.");
|
|
1970
|
+
}
|
|
1880
1971
|
const requestId = this.createRequestId(options.requestId);
|
|
1881
1972
|
const message = SessionInboundMessageSchema.parse({
|
|
1882
1973
|
type: "agent.provider_subagents.timeline.get.request",
|
|
@@ -1902,6 +1993,9 @@ export class DaemonClient {
|
|
|
1902
1993
|
}
|
|
1903
1994
|
return payload;
|
|
1904
1995
|
}
|
|
1996
|
+
observeTimeline(agentIds, options) {
|
|
1997
|
+
return this.observe("agent.timeline.set_subscription.response", { type: "agent.timeline.set_subscription.request", agentIds }, options);
|
|
1998
|
+
}
|
|
1905
1999
|
async askSideConversation(parentAgentId, threadId, question, options = {}) {
|
|
1906
2000
|
this.requireSideConversationSupport();
|
|
1907
2001
|
const requestId = this.createRequestId(options.requestId);
|
|
@@ -1971,77 +2065,8 @@ export class DaemonClient {
|
|
|
1971
2065
|
throw new Error(payload.error);
|
|
1972
2066
|
return payload;
|
|
1973
2067
|
}
|
|
1974
|
-
setAgentTimelineSubscription(agentIds) {
|
|
1975
|
-
return this.subscriptions.setViewed(agentIds);
|
|
1976
|
-
}
|
|
1977
2068
|
subscribeAgentTimeline(agentId, handler) {
|
|
1978
|
-
|
|
1979
|
-
if (message.payload.agentId === agentId)
|
|
1980
|
-
handler(message);
|
|
1981
|
-
});
|
|
1982
|
-
const replacement = this.on("agent.timeline.replacement", (message) => {
|
|
1983
|
-
if (message.payload.agentId === agentId)
|
|
1984
|
-
handler(message);
|
|
1985
|
-
});
|
|
1986
|
-
const release = this.subscriptions.observeTimeline(agentId);
|
|
1987
|
-
return Object.assign(() => {
|
|
1988
|
-
stream();
|
|
1989
|
-
replacement();
|
|
1990
|
-
release();
|
|
1991
|
-
}, { ready: release.ready });
|
|
1992
|
-
}
|
|
1993
|
-
updateEventSubscriptions() {
|
|
1994
|
-
const events = [
|
|
1995
|
-
"project.update",
|
|
1996
|
-
"providers_snapshot_update",
|
|
1997
|
-
"agent_attention_required",
|
|
1998
|
-
"agent_permission_request",
|
|
1999
|
-
"agent_permission_resolved",
|
|
2000
|
-
];
|
|
2001
|
-
if (this.eventListeners.size === 0 && !this.messageHandlers.has("providers_snapshot_update")) {
|
|
2002
|
-
this.providerSnapshotUpdates.clear();
|
|
2003
|
-
}
|
|
2004
|
-
this.subscriptions.setEvents(events.filter((type) => this.eventListeners.size > 0 || (this.messageHandlers.get(type)?.size ?? 0) > 0));
|
|
2005
|
-
}
|
|
2006
|
-
async sendEventSubscription(events) {
|
|
2007
|
-
if (this.connectionState.status !== "connected")
|
|
2008
|
-
return;
|
|
2009
|
-
// COMPAT(explicitEventSubscriptions): added in v0.8.0, remove legacy broadcast handling after 2027-03-08.
|
|
2010
|
-
if (!this.lastServerInfoMessage?.features?.explicitEventSubscriptions ||
|
|
2011
|
-
this.config.capabilities?.[CLIENT_CAPS.explicitEventSubscriptions] === false)
|
|
2012
|
-
return;
|
|
2013
|
-
await this.sendCorrelatedSessionRequest({
|
|
2014
|
-
message: { type: "session.events.set_subscription.request", events },
|
|
2015
|
-
responseType: "session.events.set_subscription.response",
|
|
2016
|
-
});
|
|
2017
|
-
}
|
|
2018
|
-
async sendTimelineSubscription(agentIds) {
|
|
2019
|
-
if (this.connectionState.status !== "connected")
|
|
2020
|
-
return;
|
|
2021
|
-
// COMPAT(selectiveAgentTimeline): added in v0.1.106. Old daemons keep their
|
|
2022
|
-
// legacy global stream and do not understand this RPC. Remove after
|
|
2023
|
-
// 2027-01-12 once the supported daemon floor is >= v0.1.106.
|
|
2024
|
-
if (!this.lastServerInfoMessage?.features?.selectiveAgentTimeline) {
|
|
2025
|
-
return;
|
|
2026
|
-
}
|
|
2027
|
-
const requestId = this.createRequestId();
|
|
2028
|
-
const normalizedAgentIds = [...new Set(agentIds)].sort();
|
|
2029
|
-
const message = SessionInboundMessageSchema.parse({
|
|
2030
|
-
type: "agent.timeline.set_subscription.request",
|
|
2031
|
-
agentIds: normalizedAgentIds,
|
|
2032
|
-
requestId,
|
|
2033
|
-
});
|
|
2034
|
-
await this.sendRequest({
|
|
2035
|
-
requestId,
|
|
2036
|
-
message,
|
|
2037
|
-
options: { skipQueue: true },
|
|
2038
|
-
select: (response) => {
|
|
2039
|
-
if (response.type !== "agent.timeline.set_subscription.response") {
|
|
2040
|
-
return null;
|
|
2041
|
-
}
|
|
2042
|
-
return response.payload.requestId === requestId ? response.payload : null;
|
|
2043
|
-
},
|
|
2044
|
-
});
|
|
2069
|
+
return subscribeTimeline(agentId, this.observeTimeline([agentId]), handler, (error) => this.logger.error({ err: error }, "Timeline observation failed"));
|
|
2045
2070
|
}
|
|
2046
2071
|
async buildAgentForkContext(agentId, options = {}) {
|
|
2047
2072
|
const resolvedRequestId = this.createRequestId(options.requestId);
|
|
@@ -2303,7 +2328,7 @@ export class DaemonClient {
|
|
|
2303
2328
|
}
|
|
2304
2329
|
return payload.notice ?? null;
|
|
2305
2330
|
}
|
|
2306
|
-
async restartServer(reason, requestId) {
|
|
2331
|
+
async restartServer(reason, requestId, options) {
|
|
2307
2332
|
const resolvedRequestId = this.createRequestId(requestId);
|
|
2308
2333
|
const message = SessionInboundMessageSchema.parse({
|
|
2309
2334
|
type: "restart_server_request",
|
|
@@ -2313,6 +2338,7 @@ export class DaemonClient {
|
|
|
2313
2338
|
return this.sendRequest({
|
|
2314
2339
|
requestId: resolvedRequestId,
|
|
2315
2340
|
message,
|
|
2341
|
+
timeout: options?.timeout,
|
|
2316
2342
|
options: { skipQueue: true },
|
|
2317
2343
|
select: (msg) => {
|
|
2318
2344
|
if (msg.type !== "status") {
|
|
@@ -2638,75 +2664,22 @@ export class DaemonClient {
|
|
|
2638
2664
|
: { mode: "base", baseRef: trimmedBaseRef };
|
|
2639
2665
|
}
|
|
2640
2666
|
async getCheckoutDiff(cwd, compare, requestId) {
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
files: payload.files,
|
|
2650
|
-
error: payload.error,
|
|
2651
|
-
diffTooLarge: payload.diffTooLarge,
|
|
2652
|
-
requestId: payload.requestId,
|
|
2653
|
-
};
|
|
2654
|
-
}
|
|
2655
|
-
finally {
|
|
2656
|
-
try {
|
|
2657
|
-
this.unsubscribeCheckoutDiff(oneShotSubscriptionId);
|
|
2658
|
-
}
|
|
2659
|
-
catch {
|
|
2660
|
-
// Ignore disconnect races during one-shot cleanup.
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
async subscribeCheckoutDiff(cwd, compare, options) {
|
|
2665
|
-
const subscriptionId = options?.subscriptionId ?? crypto.randomUUID();
|
|
2666
|
-
const normalizedCompare = this.normalizeCheckoutDiffCompare(compare);
|
|
2667
|
-
const previousSubscription = this.checkoutDiffSubscriptions.get(subscriptionId) ?? null;
|
|
2668
|
-
this.checkoutDiffSubscriptions.set(subscriptionId, {
|
|
2669
|
-
cwd,
|
|
2670
|
-
compare: normalizedCompare,
|
|
2667
|
+
return this.sendCorrelatedSessionRequest({
|
|
2668
|
+
message: {
|
|
2669
|
+
type: "checkout.diff.get.request",
|
|
2670
|
+
cwd,
|
|
2671
|
+
compare: this.normalizeCheckoutDiffCompare(compare),
|
|
2672
|
+
},
|
|
2673
|
+
responseType: "checkout.diff.get.response",
|
|
2674
|
+
requestId,
|
|
2671
2675
|
});
|
|
2672
|
-
|
|
2673
|
-
|
|
2676
|
+
}
|
|
2677
|
+
observeCheckoutDiff(cwd, compare, options) {
|
|
2678
|
+
return this.observe("subscribe_checkout_diff_response", {
|
|
2674
2679
|
type: "subscribe_checkout_diff_request",
|
|
2675
|
-
subscriptionId,
|
|
2676
2680
|
cwd,
|
|
2677
|
-
compare:
|
|
2678
|
-
|
|
2679
|
-
});
|
|
2680
|
-
try {
|
|
2681
|
-
return await this.sendCorrelatedRequest({
|
|
2682
|
-
requestId: resolvedRequestId,
|
|
2683
|
-
message,
|
|
2684
|
-
responseType: "subscribe_checkout_diff_response",
|
|
2685
|
-
options: { skipQueue: true },
|
|
2686
|
-
selectPayload: (payload) => {
|
|
2687
|
-
if (payload.subscriptionId !== subscriptionId) {
|
|
2688
|
-
return null;
|
|
2689
|
-
}
|
|
2690
|
-
return payload;
|
|
2691
|
-
},
|
|
2692
|
-
});
|
|
2693
|
-
}
|
|
2694
|
-
catch (error) {
|
|
2695
|
-
if (previousSubscription) {
|
|
2696
|
-
this.checkoutDiffSubscriptions.set(subscriptionId, previousSubscription);
|
|
2697
|
-
}
|
|
2698
|
-
else {
|
|
2699
|
-
this.checkoutDiffSubscriptions.delete(subscriptionId);
|
|
2700
|
-
}
|
|
2701
|
-
throw error;
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
unsubscribeCheckoutDiff(subscriptionId) {
|
|
2705
|
-
this.checkoutDiffSubscriptions.delete(subscriptionId);
|
|
2706
|
-
this.sendSessionMessage({
|
|
2707
|
-
type: "unsubscribe_checkout_diff_request",
|
|
2708
|
-
subscriptionId,
|
|
2709
|
-
});
|
|
2681
|
+
compare: this.normalizeCheckoutDiffCompare(compare),
|
|
2682
|
+
}, options);
|
|
2710
2683
|
}
|
|
2711
2684
|
async checkoutCommit(cwd, input, requestId) {
|
|
2712
2685
|
return this.sendCorrelatedSessionRequest({
|
|
@@ -3020,17 +2993,16 @@ export class DaemonClient {
|
|
|
3020
2993
|
};
|
|
3021
2994
|
}
|
|
3022
2995
|
async createLegacyWorkspace(input, requestId) {
|
|
3023
|
-
// COMPAT(workspaceRequestReceipts): added in v0.8.0; remove gate after 2027-03-07.
|
|
3024
|
-
if (input.idempotencyKey !== undefined &&
|
|
3025
|
-
!this.lastServerInfoMessage?.features?.workspaceRequestReceipts) {
|
|
3026
|
-
throw new Error("Update the host to use retry-safe workspace creation.");
|
|
3027
|
-
}
|
|
3028
2996
|
return this.sendCorrelatedSessionRequest({
|
|
3029
2997
|
requestId,
|
|
3030
2998
|
message: {
|
|
3031
2999
|
type: "workspace.create.request",
|
|
3032
3000
|
source: input.source,
|
|
3033
|
-
|
|
3001
|
+
// COMPAT(workspaceRequestReceipts): added in v0.8.0; remove after 2027-03-15 once the daemon floor supports workspace receipts.
|
|
3002
|
+
...(this.lastServerInfoMessage?.features?.workspaceRequestReceipts &&
|
|
3003
|
+
input.idempotencyKey !== undefined
|
|
3004
|
+
? { idempotencyKey: input.idempotencyKey }
|
|
3005
|
+
: {}),
|
|
3034
3006
|
...(input.title !== undefined ? { title: input.title } : {}),
|
|
3035
3007
|
...(input.firstAgentContext !== undefined
|
|
3036
3008
|
? { firstAgentContext: input.firstAgentContext }
|
|
@@ -3156,35 +3128,26 @@ export class DaemonClient {
|
|
|
3156
3128
|
this.activeBinaryFileTransfers.delete(resolvedRequestId);
|
|
3157
3129
|
}
|
|
3158
3130
|
}
|
|
3131
|
+
observeFile(input) {
|
|
3132
|
+
return this.observe("fs.file.subscribe.response", { type: "fs.file.subscribe.request", cwd: input.cwd, path: input.path }, { signal: input.signal });
|
|
3133
|
+
}
|
|
3159
3134
|
async subscribeFile(input, onUpdate) {
|
|
3160
|
-
const
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
return;
|
|
3177
|
-
void this.sendCorrelatedSessionRequest({
|
|
3178
|
-
message: { type: "fs.file.unsubscribe.request", subscriptionId },
|
|
3179
|
-
responseType: "fs.file.unsubscribe.response",
|
|
3180
|
-
}).catch(() => undefined);
|
|
3181
|
-
},
|
|
3182
|
-
};
|
|
3183
|
-
}
|
|
3184
|
-
catch (error) {
|
|
3185
|
-
this.fileSubscriptions.delete(subscriptionId);
|
|
3186
|
-
throw error;
|
|
3187
|
-
}
|
|
3135
|
+
const subscription = this.observeFile(input);
|
|
3136
|
+
let initial = true;
|
|
3137
|
+
subscription.subscribe({
|
|
3138
|
+
snapshot: (snapshot) => {
|
|
3139
|
+
// The initial version is returned below. Subsequent snapshots repair reconnects.
|
|
3140
|
+
if (!initial)
|
|
3141
|
+
onUpdate(snapshot.initial);
|
|
3142
|
+
initial = false;
|
|
3143
|
+
},
|
|
3144
|
+
update: (message) => {
|
|
3145
|
+
if (message.type === "fs.file.update")
|
|
3146
|
+
onUpdate(message.payload.version);
|
|
3147
|
+
},
|
|
3148
|
+
});
|
|
3149
|
+
const snapshot = await subscription.ready;
|
|
3150
|
+
return { initial: snapshot.initial, subscription, unsubscribe: subscription.release };
|
|
3188
3151
|
}
|
|
3189
3152
|
async writeFile(input) {
|
|
3190
3153
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
@@ -3223,6 +3186,7 @@ export class DaemonClient {
|
|
|
3223
3186
|
if (!bytes) {
|
|
3224
3187
|
throw new Error("File bytes are required.");
|
|
3225
3188
|
}
|
|
3189
|
+
const uploadTransport = this.transport;
|
|
3226
3190
|
const resolvedRequestId = this.createRequestId(input.requestId);
|
|
3227
3191
|
const modifiedAt = input.modifiedAt ?? new Date().toISOString();
|
|
3228
3192
|
const responsePromise = this.sendCorrelatedRequest({
|
|
@@ -3238,29 +3202,51 @@ export class DaemonClient {
|
|
|
3238
3202
|
responseType: "file.upload.response",
|
|
3239
3203
|
options: { skipQueue: true },
|
|
3240
3204
|
});
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3205
|
+
let settled = false;
|
|
3206
|
+
void responsePromise.then(() => {
|
|
3207
|
+
settled = true;
|
|
3208
|
+
return undefined;
|
|
3209
|
+
}, () => {
|
|
3210
|
+
settled = true;
|
|
3211
|
+
return undefined;
|
|
3212
|
+
});
|
|
3213
|
+
try {
|
|
3214
|
+
this.sendBinaryFrame(encodeFileTransferFrame({
|
|
3215
|
+
opcode: FileTransferOpcode.FileBegin,
|
|
3216
|
+
requestId: resolvedRequestId,
|
|
3217
|
+
metadata: {
|
|
3218
|
+
mime: input.mimeType,
|
|
3219
|
+
size: bytes.byteLength,
|
|
3220
|
+
encoding: "binary",
|
|
3221
|
+
modifiedAt,
|
|
3222
|
+
fileName: input.fileName,
|
|
3223
|
+
},
|
|
3224
|
+
}));
|
|
3225
|
+
const chunkSize = input.chunkSize ?? 128 * 1024;
|
|
3226
|
+
for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
|
|
3227
|
+
// Native WebSocket.send encodes binary synchronously. Let rendering and
|
|
3228
|
+
// incoming messages run between bounded pieces on every platform.
|
|
3229
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
3230
|
+
if (settled)
|
|
3231
|
+
return await responsePromise;
|
|
3232
|
+
if (this.transport !== uploadTransport || this.connectionState.status !== "connected") {
|
|
3233
|
+
throw new DaemonConnectionError("Connection changed during file upload");
|
|
3234
|
+
}
|
|
3235
|
+
this.sendBinaryFrame(encodeFileTransferFrame({
|
|
3236
|
+
opcode: FileTransferOpcode.FileChunk,
|
|
3237
|
+
requestId: resolvedRequestId,
|
|
3238
|
+
payload: bytes.subarray(offset, Math.min(offset + chunkSize, bytes.byteLength)),
|
|
3239
|
+
}));
|
|
3240
|
+
}
|
|
3254
3241
|
this.sendBinaryFrame(encodeFileTransferFrame({
|
|
3255
|
-
opcode: FileTransferOpcode.
|
|
3242
|
+
opcode: FileTransferOpcode.FileEnd,
|
|
3256
3243
|
requestId: resolvedRequestId,
|
|
3257
|
-
payload: bytes.subarray(offset, Math.min(offset + chunkSize, bytes.byteLength)),
|
|
3258
3244
|
}));
|
|
3259
3245
|
}
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
}
|
|
3246
|
+
catch (error) {
|
|
3247
|
+
this.rejectWaitersForRequestId(resolvedRequestId, error instanceof Error ? error : new Error(String(error)));
|
|
3248
|
+
throw error;
|
|
3249
|
+
}
|
|
3264
3250
|
return responsePromise;
|
|
3265
3251
|
}
|
|
3266
3252
|
async requestDownloadToken(cwd, path, requestId) {
|
|
@@ -3364,6 +3350,11 @@ export class DaemonClient {
|
|
|
3364
3350
|
});
|
|
3365
3351
|
}
|
|
3366
3352
|
async getDaemonStatus(options) {
|
|
3353
|
+
if (!this.lastServerInfoMessage)
|
|
3354
|
+
throw new DaemonConnectionError("Transport not connected");
|
|
3355
|
+
if (this.lastServerInfoMessage?.features?.daemonStatusRpc !== true) {
|
|
3356
|
+
throw new Error("Update the host to read daemon status.");
|
|
3357
|
+
}
|
|
3367
3358
|
return this.sendCorrelatedSessionRequest({
|
|
3368
3359
|
requestId: options?.requestId,
|
|
3369
3360
|
message: {
|
|
@@ -3605,18 +3596,21 @@ export class DaemonClient {
|
|
|
3605
3596
|
}
|
|
3606
3597
|
async installPluginSource(input) {
|
|
3607
3598
|
const requestId = this.createRequestId();
|
|
3608
|
-
|
|
3599
|
+
// COMPAT(pluginSourceInstallation): added in v0.8.0; remove after 2027-03-16 once daemon floor supports source identifiers.
|
|
3600
|
+
if (this.getLastServerInfoMessage()?.features?.pluginSourceInstallation !== true) {
|
|
3601
|
+
throw new Error("Update the host to install plugin sources.");
|
|
3602
|
+
}
|
|
3609
3603
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
3610
3604
|
requestId,
|
|
3611
3605
|
message: {
|
|
3612
3606
|
type: "plugin.source.install.request",
|
|
3613
3607
|
requestId,
|
|
3614
|
-
source:
|
|
3615
|
-
...(reference.pluginPath ? { pluginPath: reference.pluginPath } : {}),
|
|
3608
|
+
source: input.source,
|
|
3616
3609
|
...(input.id ? { id: input.id } : {}),
|
|
3617
3610
|
...(input.ref ? { ref: input.ref } : {}),
|
|
3618
3611
|
},
|
|
3619
3612
|
responseType: "plugin.source.install.response",
|
|
3613
|
+
timeout: 5 * 60 * 1000,
|
|
3620
3614
|
});
|
|
3621
3615
|
return payload.plugin;
|
|
3622
3616
|
}
|
|
@@ -3633,6 +3627,33 @@ export class DaemonClient {
|
|
|
3633
3627
|
});
|
|
3634
3628
|
return payload.plugins;
|
|
3635
3629
|
}
|
|
3630
|
+
requirePluginUpdates() {
|
|
3631
|
+
// COMPAT(pluginSourceUpdates): added in v0.8.0; remove after 2027-03-16 once daemon floor supports reviewed updates.
|
|
3632
|
+
if (this.getLastServerInfoMessage()?.features?.pluginSourceUpdates !== true)
|
|
3633
|
+
throw new Error("Update the host to review plugin updates.");
|
|
3634
|
+
}
|
|
3635
|
+
async previewPluginUpdates(input = {}) {
|
|
3636
|
+
this.requirePluginUpdates();
|
|
3637
|
+
const requestId = this.createRequestId();
|
|
3638
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3639
|
+
requestId,
|
|
3640
|
+
message: { type: "plugin.source.update.preview.request", requestId, ...input },
|
|
3641
|
+
responseType: "plugin.source.update.preview.response",
|
|
3642
|
+
timeout: 300000,
|
|
3643
|
+
});
|
|
3644
|
+
return payload.plugins;
|
|
3645
|
+
}
|
|
3646
|
+
async applyPluginUpdates(proposals) {
|
|
3647
|
+
this.requirePluginUpdates();
|
|
3648
|
+
const requestId = this.createRequestId();
|
|
3649
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3650
|
+
requestId,
|
|
3651
|
+
message: { type: "plugin.source.update.apply.request", requestId, proposals },
|
|
3652
|
+
responseType: "plugin.source.update.apply.response",
|
|
3653
|
+
timeout: 300000,
|
|
3654
|
+
});
|
|
3655
|
+
return payload.plugins;
|
|
3656
|
+
}
|
|
3636
3657
|
async updatePluginSources(pluginId) {
|
|
3637
3658
|
const requestId = this.createRequestId();
|
|
3638
3659
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
@@ -3840,30 +3861,9 @@ export class DaemonClient {
|
|
|
3840
3861
|
// ============================================================================
|
|
3841
3862
|
// Terminals
|
|
3842
3863
|
// ============================================================================
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
workspaceId: input.workspaceId,
|
|
3847
|
-
});
|
|
3848
|
-
if (!this.transport || this.connectionState.status !== "connected") {
|
|
3849
|
-
return;
|
|
3850
|
-
}
|
|
3851
|
-
this.sendSessionMessage({
|
|
3852
|
-
type: "subscribe_terminals_request",
|
|
3853
|
-
cwd: input.cwd,
|
|
3854
|
-
...(input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}),
|
|
3855
|
-
});
|
|
3856
|
-
}
|
|
3857
|
-
unsubscribeTerminals(input) {
|
|
3858
|
-
this.terminalDirectorySubscriptions.delete(terminalSubscriptionKey(input.cwd, input.workspaceId));
|
|
3859
|
-
if (!this.transport || this.connectionState.status !== "connected") {
|
|
3860
|
-
return;
|
|
3861
|
-
}
|
|
3862
|
-
this.sendSessionMessage({
|
|
3863
|
-
type: "unsubscribe_terminals_request",
|
|
3864
|
-
cwd: input.cwd,
|
|
3865
|
-
...(input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}),
|
|
3866
|
-
});
|
|
3864
|
+
observeTerminals(input) {
|
|
3865
|
+
const { signal, ...query } = input;
|
|
3866
|
+
return this.observe("terminals_changed", { type: "subscribe_terminals_request", ...query }, { signal });
|
|
3867
3867
|
}
|
|
3868
3868
|
async listTerminals(cwd, requestId, options) {
|
|
3869
3869
|
const resolvedRequestId = this.createRequestId(requestId);
|
|
@@ -3911,45 +3911,72 @@ export class DaemonClient {
|
|
|
3911
3911
|
responseType: "terminal.rename.response",
|
|
3912
3912
|
});
|
|
3913
3913
|
}
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3914
|
+
/**
|
|
3915
|
+
* Snapshot/restore and output arrive in receive. Subscribe to the returned handle
|
|
3916
|
+
* for terminal_stream_exit: payload.error means observation failure, not PTY exit.
|
|
3917
|
+
* Either outcome detaches this slot and releases the handle automatically.
|
|
3918
|
+
*/
|
|
3919
|
+
observeTerminal(terminalId, receive, options) {
|
|
3920
|
+
options?.signal?.throwIfAborted();
|
|
3921
|
+
const observation = this.observe("subscribe_terminal_response", {
|
|
3919
3922
|
type: "subscribe_terminal_request",
|
|
3920
3923
|
terminalId,
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
}
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
options
|
|
3924
|
+
...(options?.restore ? { restore: options.restore } : {}),
|
|
3925
|
+
}, options);
|
|
3926
|
+
let detach = () => { };
|
|
3927
|
+
let released = false;
|
|
3928
|
+
const release = () => {
|
|
3929
|
+
released = true;
|
|
3930
|
+
detach();
|
|
3931
|
+
options?.signal?.removeEventListener("abort", abort);
|
|
3932
|
+
return observation.release();
|
|
3933
|
+
};
|
|
3934
|
+
const abort = () => {
|
|
3935
|
+
void release().catch((error) => this.logger.error({ err: error }, "Terminal release failed"));
|
|
3936
|
+
};
|
|
3937
|
+
options?.signal?.addEventListener("abort", abort, { once: true });
|
|
3938
|
+
observation.subscribe({
|
|
3939
|
+
snapshot: (payload) => {
|
|
3940
|
+
detach();
|
|
3941
|
+
if (released || options?.signal?.aborted || payload.error !== null)
|
|
3942
|
+
return;
|
|
3943
|
+
const subscriptionId = payload.subscriptionId;
|
|
3944
|
+
detach = this.terminalStreams.attach(subscriptionId, terminalId, payload.slot, (event) => {
|
|
3945
|
+
if (released || observation.subscriptionId !== subscriptionId)
|
|
3946
|
+
return;
|
|
3947
|
+
try {
|
|
3948
|
+
receive(event);
|
|
3949
|
+
}
|
|
3950
|
+
catch (error) {
|
|
3951
|
+
this.logger.error({ err: error }, "Terminal observer failed");
|
|
3952
|
+
}
|
|
3953
|
+
});
|
|
3954
|
+
},
|
|
3955
|
+
update: (message) => {
|
|
3956
|
+
if (message.type === "terminal_stream_exit") {
|
|
3957
|
+
detach();
|
|
3958
|
+
queueMicrotask(abort);
|
|
3959
|
+
}
|
|
3960
|
+
},
|
|
3961
|
+
error: () => {
|
|
3962
|
+
detach();
|
|
3963
|
+
},
|
|
3929
3964
|
});
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3965
|
+
return {
|
|
3966
|
+
get subscriptionId() {
|
|
3967
|
+
return observation.subscriptionId;
|
|
3968
|
+
},
|
|
3969
|
+
ready: observation.ready,
|
|
3970
|
+
subscribe: observation.subscribe,
|
|
3971
|
+
release,
|
|
3972
|
+
};
|
|
3934
3973
|
}
|
|
3935
|
-
|
|
3936
|
-
this.
|
|
3937
|
-
|
|
3938
|
-
type: "unsubscribe_terminal_request",
|
|
3939
|
-
terminalId,
|
|
3940
|
-
});
|
|
3974
|
+
async subscribeTerminal(terminalId, options) {
|
|
3975
|
+
const subscription = this.observeTerminal(terminalId, () => { }, options);
|
|
3976
|
+
return { ...(await subscription.ready), subscription };
|
|
3941
3977
|
}
|
|
3942
3978
|
sendTerminalInput(terminalId, message) {
|
|
3943
|
-
|
|
3944
|
-
if (frame) {
|
|
3945
|
-
this.sendBinaryFrame(frame);
|
|
3946
|
-
return;
|
|
3947
|
-
}
|
|
3948
|
-
this.sendSessionMessage({
|
|
3949
|
-
type: "terminal_input",
|
|
3950
|
-
terminalId,
|
|
3951
|
-
message,
|
|
3952
|
-
});
|
|
3979
|
+
this.sendSessionMessage({ type: "terminal_input", terminalId, message });
|
|
3953
3980
|
}
|
|
3954
3981
|
async killTerminal(terminalId, requestId) {
|
|
3955
3982
|
const resolvedRequestId = this.createRequestId(requestId);
|
|
@@ -4182,7 +4209,8 @@ export class DaemonClient {
|
|
|
4182
4209
|
}
|
|
4183
4210
|
}
|
|
4184
4211
|
disposeTransport(code = 1001, reason = "Reconnecting") {
|
|
4185
|
-
this.
|
|
4212
|
+
this.owned.disconnected();
|
|
4213
|
+
this.providerSnapshotUpdates.clear();
|
|
4186
4214
|
this.stopLivenessHeartbeat();
|
|
4187
4215
|
this.cleanupTransport();
|
|
4188
4216
|
if (this.transport) {
|
|
@@ -4223,10 +4251,12 @@ export class DaemonClient {
|
|
|
4223
4251
|
if (typeof Blob !== "undefined" &&
|
|
4224
4252
|
rawData instanceof Blob &&
|
|
4225
4253
|
typeof rawData.arrayBuffer === "function") {
|
|
4254
|
+
const transport = this.transport;
|
|
4226
4255
|
void rawData
|
|
4227
4256
|
.arrayBuffer()
|
|
4228
4257
|
.then((buffer) => {
|
|
4229
|
-
this.
|
|
4258
|
+
if (this.transport === transport)
|
|
4259
|
+
this.handleTransportMessage(buffer);
|
|
4230
4260
|
return;
|
|
4231
4261
|
})
|
|
4232
4262
|
.catch(() => {
|
|
@@ -4455,12 +4485,13 @@ export class DaemonClient {
|
|
|
4455
4485
|
if (typeof reason === "string" && reason.trim().length > 0) {
|
|
4456
4486
|
this.lastErrorValue = reason.trim();
|
|
4457
4487
|
}
|
|
4458
|
-
this.
|
|
4488
|
+
this.owned.disconnected();
|
|
4489
|
+
this.providerSnapshotUpdates.clear();
|
|
4459
4490
|
// Clear all pending waiters and queued sends since the connection was lost
|
|
4460
4491
|
// and responses from the previous connection will never arrive.
|
|
4461
|
-
this.clearWaiters(new
|
|
4462
|
-
this.rejectPendingSendQueue(new
|
|
4463
|
-
this.rejectPingProbe(new
|
|
4492
|
+
this.clearWaiters(new DaemonConnectionError(reason ?? "Connection lost"));
|
|
4493
|
+
this.rejectPendingSendQueue(new DaemonConnectionError(reason ?? "Connection lost"));
|
|
4494
|
+
this.rejectPingProbe(new DaemonConnectionError(reason ?? "Connection lost"));
|
|
4464
4495
|
this.terminalStreams.clearSlots();
|
|
4465
4496
|
this.lastServerInfoMessage = null;
|
|
4466
4497
|
if (wasDisposed) {
|
|
@@ -4539,13 +4570,13 @@ export class DaemonClient {
|
|
|
4539
4570
|
});
|
|
4540
4571
|
}
|
|
4541
4572
|
handleSessionMessage(msg) {
|
|
4573
|
+
msg = this.owned.normalize(msg);
|
|
4542
4574
|
if (msg.type === "providers_snapshot_update" &&
|
|
4543
4575
|
this.config.providerSnapshots !== "wire" &&
|
|
4544
4576
|
msg.payload.snapshotHash &&
|
|
4545
4577
|
!msg.payload.compactSnapshot) {
|
|
4546
|
-
if (this.
|
|
4578
|
+
if (this.owned.owns(msg))
|
|
4547
4579
|
this.providerSnapshotUpdates.receive(msg);
|
|
4548
|
-
}
|
|
4549
4580
|
return;
|
|
4550
4581
|
}
|
|
4551
4582
|
this.deliverSessionMessage(msg);
|
|
@@ -4561,25 +4592,16 @@ export class DaemonClient {
|
|
|
4561
4592
|
this.reconnectAttempt = 0;
|
|
4562
4593
|
this.updateConnectionState({ status: "connected" }, { event: "HELLO_SERVER_INFO" });
|
|
4563
4594
|
this.startLivenessHeartbeat();
|
|
4564
|
-
this.
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
this.resubscribeFileSubscriptions();
|
|
4595
|
+
this.owned.restore(serverInfo, {
|
|
4596
|
+
...DEFAULT_CLIENT_CAPABILITIES,
|
|
4597
|
+
...this.config.capabilities,
|
|
4598
|
+
});
|
|
4569
4599
|
this.creations.reconnect();
|
|
4570
4600
|
this.flushPendingSendQueue();
|
|
4571
4601
|
this.resolveConnect();
|
|
4572
4602
|
}
|
|
4573
4603
|
}
|
|
4574
4604
|
}
|
|
4575
|
-
if (consumerMessage.type === "terminal_stream_exit") {
|
|
4576
|
-
this.terminalStreams.removeTerminal(consumerMessage.payload.terminalId);
|
|
4577
|
-
}
|
|
4578
|
-
if (consumerMessage.type === "fs.file.update") {
|
|
4579
|
-
this.fileSubscriptions
|
|
4580
|
-
.get(consumerMessage.payload.subscriptionId)
|
|
4581
|
-
?.onUpdate(consumerMessage.payload.version);
|
|
4582
|
-
}
|
|
4583
4605
|
if (this.rawMessageListeners.size > 0) {
|
|
4584
4606
|
for (const handler of this.rawMessageListeners) {
|
|
4585
4607
|
try {
|
|
@@ -4608,20 +4630,30 @@ export class DaemonClient {
|
|
|
4608
4630
|
}
|
|
4609
4631
|
}
|
|
4610
4632
|
if (consumerMessage.type === "workspace.create.update" ||
|
|
4611
|
-
consumerMessage.type === "agent.create.update")
|
|
4612
|
-
|
|
4633
|
+
consumerMessage.type === "agent.create.update") {
|
|
4634
|
+
if (!consumerMessage.payload.subscriptionId)
|
|
4635
|
+
this.creations.receive(consumerMessage.payload);
|
|
4636
|
+
}
|
|
4613
4637
|
this.resolveWaiters(consumerMessage);
|
|
4638
|
+
this.owned.receive(consumerMessage);
|
|
4614
4639
|
}
|
|
4615
4640
|
resolveWaiters(msg) {
|
|
4616
4641
|
for (const waiter of Array.from(this.waiters)) {
|
|
4617
|
-
|
|
4618
|
-
|
|
4642
|
+
try {
|
|
4643
|
+
const result = waiter.predicate(msg);
|
|
4644
|
+
if (result === null)
|
|
4645
|
+
continue;
|
|
4619
4646
|
this.waiters.delete(waiter);
|
|
4620
|
-
if (waiter.timeoutHandle)
|
|
4647
|
+
if (waiter.timeoutHandle)
|
|
4621
4648
|
clearTimeout(waiter.timeoutHandle);
|
|
4622
|
-
}
|
|
4623
4649
|
waiter.resolve(result);
|
|
4624
4650
|
}
|
|
4651
|
+
catch (error) {
|
|
4652
|
+
this.waiters.delete(waiter);
|
|
4653
|
+
if (waiter.timeoutHandle)
|
|
4654
|
+
clearTimeout(waiter.timeoutHandle);
|
|
4655
|
+
waiter.reject(error instanceof Error ? error : new Error(String(error)));
|
|
4656
|
+
}
|
|
4625
4657
|
}
|
|
4626
4658
|
}
|
|
4627
4659
|
rejectWaitersForRequestId(requestId, error) {
|
|
@@ -4704,7 +4736,7 @@ export class DaemonClient {
|
|
|
4704
4736
|
}
|
|
4705
4737
|
waitForWithCancel(predicate, timeout = 30000, options) {
|
|
4706
4738
|
// Capture stack trace at call site, not inside setTimeout
|
|
4707
|
-
const timeoutError = new
|
|
4739
|
+
const timeoutError = new DaemonConnectionError(`Timeout waiting for message (${timeout}ms)`, "DAEMON_REQUEST_TIMEOUT");
|
|
4708
4740
|
let waiter = null;
|
|
4709
4741
|
let settled = false;
|
|
4710
4742
|
let rejectFn = null;
|