@otto-code/client 0.8.12 → 0.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -7
- package/dist/compat/normalize-provider-models.js +4 -1
- package/dist/daemon-client-runtime-metrics.d.ts +29 -0
- package/dist/daemon-client-runtime-metrics.js +31 -0
- package/dist/daemon-client-websocket-transport.d.ts +4 -0
- package/dist/daemon-client-websocket-transport.js +14 -2
- package/dist/daemon-client.d.ts +79 -125
- package/dist/daemon-client.js +256 -208
- package/dist/index.d.ts +73 -55
- package/dist/index.js +208 -48
- package/dist/terminal-stream-router.js +1 -0
- package/package.json +7 -3
package/dist/daemon-client.js
CHANGED
|
@@ -117,6 +117,7 @@ function toTimeoutError(error, label, timeoutMs) {
|
|
|
117
117
|
const DEFAULT_RECONNECT_BASE_DELAY_MS = 1500;
|
|
118
118
|
const DEFAULT_RECONNECT_MAX_DELAY_MS = 30000;
|
|
119
119
|
const DEFAULT_SESSION_RPC_TIMEOUT_MS = 60000;
|
|
120
|
+
const PUSH_TOKEN_REVOCATION_TIMEOUT_MS = 2000;
|
|
120
121
|
const DEFAULT_CONNECT_TIMEOUT_MS = 15000;
|
|
121
122
|
const DEFAULT_LIVENESS_TIMEOUT_MS = 5000;
|
|
122
123
|
const LIVENESS_HEARTBEAT_INTERVAL_MS = 10000;
|
|
@@ -185,6 +186,30 @@ function concatByteChunks(chunks, size) {
|
|
|
185
186
|
}
|
|
186
187
|
return bytes;
|
|
187
188
|
}
|
|
189
|
+
function extractDispatchAgentId(message) {
|
|
190
|
+
if (message.type === "agent_stream") {
|
|
191
|
+
return message.payload.agentId;
|
|
192
|
+
}
|
|
193
|
+
if (message.type === "agent_update") {
|
|
194
|
+
return message.payload.kind === "upsert" ? message.payload.agent.id : message.payload.agentId;
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
function getTransportFrameSize(frame) {
|
|
199
|
+
if (typeof frame === "string") {
|
|
200
|
+
return frame.length;
|
|
201
|
+
}
|
|
202
|
+
return frame.byteLength;
|
|
203
|
+
}
|
|
204
|
+
function describeInboundTransportFrame(frame, rawBytes) {
|
|
205
|
+
if (typeof frame === "string") {
|
|
206
|
+
return { kind: "text", size: String(frame.length) };
|
|
207
|
+
}
|
|
208
|
+
if (rawBytes) {
|
|
209
|
+
return { kind: "binary", size: String(rawBytes.byteLength) };
|
|
210
|
+
}
|
|
211
|
+
return { kind: "unknown", size: "0" };
|
|
212
|
+
}
|
|
188
213
|
function hashForLog(value) {
|
|
189
214
|
let hash = 0;
|
|
190
215
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -595,6 +620,45 @@ export class DaemonClient {
|
|
|
595
620
|
// ============================================================================
|
|
596
621
|
// Core Send Helpers
|
|
597
622
|
// ============================================================================
|
|
623
|
+
beginTraceSection(name, args) {
|
|
624
|
+
const trace = this.config.trace;
|
|
625
|
+
if (!trace?.isEnabled()) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
trace.beginSection(name, args);
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
endTraceSection(isOpen) {
|
|
632
|
+
if (isOpen) {
|
|
633
|
+
this.config.trace?.endSection();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
traceInstant(name, args) {
|
|
637
|
+
const isOpen = this.beginTraceSection(name, args);
|
|
638
|
+
this.endTraceSection(isOpen);
|
|
639
|
+
}
|
|
640
|
+
sendJsonMessage(envelopeType, messageType, message) {
|
|
641
|
+
this.traceInstant("otto.ws.message.outbound", {
|
|
642
|
+
envelopeType,
|
|
643
|
+
messageType,
|
|
644
|
+
});
|
|
645
|
+
this.sendTransportFrame(JSON.stringify(message));
|
|
646
|
+
}
|
|
647
|
+
sendTransportFrame(frame) {
|
|
648
|
+
if (!this.transport) {
|
|
649
|
+
throw new Error("Transport not connected");
|
|
650
|
+
}
|
|
651
|
+
const isOpen = this.beginTraceSection("otto.ws.frame.outbound", {
|
|
652
|
+
kind: typeof frame === "string" ? "text" : "binary",
|
|
653
|
+
size: String(getTransportFrameSize(frame)),
|
|
654
|
+
});
|
|
655
|
+
try {
|
|
656
|
+
this.transport.send(frame);
|
|
657
|
+
}
|
|
658
|
+
finally {
|
|
659
|
+
this.endTraceSection(isOpen);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
598
662
|
/**
|
|
599
663
|
* Send a session message. For fire-and-forget messages (heartbeats, etc.),
|
|
600
664
|
* failures are suppressed if `suppressSendErrors` is configured.
|
|
@@ -609,7 +673,7 @@ export class DaemonClient {
|
|
|
609
673
|
}
|
|
610
674
|
const payload = SessionInboundMessageSchema.parse(message);
|
|
611
675
|
try {
|
|
612
|
-
this.
|
|
676
|
+
this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
|
|
613
677
|
}
|
|
614
678
|
catch (error) {
|
|
615
679
|
if (this.config.suppressSendErrors) {
|
|
@@ -626,7 +690,11 @@ export class DaemonClient {
|
|
|
626
690
|
throw new Error(`Transport not connected (status: ${this.connectionState.status})`);
|
|
627
691
|
}
|
|
628
692
|
try {
|
|
629
|
-
this.
|
|
693
|
+
this.traceInstant("otto.ws.message.outbound", {
|
|
694
|
+
envelopeType: "binary",
|
|
695
|
+
messageType: "binary",
|
|
696
|
+
});
|
|
697
|
+
this.sendTransportFrame(frame);
|
|
630
698
|
}
|
|
631
699
|
catch (error) {
|
|
632
700
|
if (this.config.suppressSendErrors) {
|
|
@@ -646,7 +714,7 @@ export class DaemonClient {
|
|
|
646
714
|
// If connected, send immediately
|
|
647
715
|
if (this.transport && status === "connected") {
|
|
648
716
|
const payload = SessionInboundMessageSchema.parse(message);
|
|
649
|
-
this.
|
|
717
|
+
this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
|
|
650
718
|
return Promise.resolve();
|
|
651
719
|
}
|
|
652
720
|
// If connecting, queue the message to be sent once connected
|
|
@@ -677,7 +745,7 @@ export class DaemonClient {
|
|
|
677
745
|
try {
|
|
678
746
|
if (this.transport && this.connectionState.status === "connected") {
|
|
679
747
|
const payload = SessionInboundMessageSchema.parse(pending.message);
|
|
680
|
-
this.
|
|
748
|
+
this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
|
|
681
749
|
pending.resolve();
|
|
682
750
|
}
|
|
683
751
|
else {
|
|
@@ -785,7 +853,7 @@ export class DaemonClient {
|
|
|
785
853
|
}
|
|
786
854
|
const payload = SessionInboundMessageSchema.parse(message);
|
|
787
855
|
try {
|
|
788
|
-
this.
|
|
856
|
+
this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
|
|
789
857
|
}
|
|
790
858
|
catch (error) {
|
|
791
859
|
throw error instanceof Error ? error : new Error(String(error));
|
|
@@ -855,6 +923,15 @@ export class DaemonClient {
|
|
|
855
923
|
token,
|
|
856
924
|
});
|
|
857
925
|
}
|
|
926
|
+
async unregisterPushToken(token) {
|
|
927
|
+
const requestId = this.createRequestId();
|
|
928
|
+
await this.sendCorrelatedSessionRequest({
|
|
929
|
+
requestId,
|
|
930
|
+
message: { type: "push.unregister.request", token, requestId },
|
|
931
|
+
responseType: "push.unregister.response",
|
|
932
|
+
timeout: PUSH_TOKEN_REVOCATION_TIMEOUT_MS,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
858
935
|
async ping(params) {
|
|
859
936
|
const requestId = params?.requestId ?? `ping-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
860
937
|
const clientSentAt = Date.now();
|
|
@@ -934,7 +1011,7 @@ export class DaemonClient {
|
|
|
934
1011
|
};
|
|
935
1012
|
this.pingProbe = probe;
|
|
936
1013
|
try {
|
|
937
|
-
this.
|
|
1014
|
+
this.sendJsonMessage("ping", "ping", { type: "ping" });
|
|
938
1015
|
}
|
|
939
1016
|
catch (error) {
|
|
940
1017
|
this.clearPingProbe();
|
|
@@ -1007,6 +1084,7 @@ export class DaemonClient {
|
|
|
1007
1084
|
type: "fetch_agent_history_request",
|
|
1008
1085
|
requestId: resolvedRequestId,
|
|
1009
1086
|
...(options?.filter ? { filter: options.filter } : {}),
|
|
1087
|
+
...(options?.search ? { search: options.search } : {}),
|
|
1010
1088
|
...(options?.sort ? { sort: options.sort } : {}),
|
|
1011
1089
|
...(options?.page ? { page: options.page } : {}),
|
|
1012
1090
|
});
|
|
@@ -1778,6 +1856,14 @@ export class DaemonClient {
|
|
|
1778
1856
|
}
|
|
1779
1857
|
return { target: payload.target };
|
|
1780
1858
|
}
|
|
1859
|
+
async setProjectIcon(projectId, source, requestId) {
|
|
1860
|
+
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
1861
|
+
requestId,
|
|
1862
|
+
message: { type: "project.icon.set.request", projectId, source },
|
|
1863
|
+
});
|
|
1864
|
+
if (!payload.accepted)
|
|
1865
|
+
throw new Error(payload.error ?? "setProjectIcon rejected");
|
|
1866
|
+
}
|
|
1781
1867
|
async removeProject(projectId, requestId) {
|
|
1782
1868
|
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
1783
1869
|
requestId,
|
|
@@ -2262,6 +2348,7 @@ export class DaemonClient {
|
|
|
2262
2348
|
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
2263
2349
|
...(typeof options.limit === "number" ? { limit: options.limit } : {}),
|
|
2264
2350
|
...(options.projection ? { projection: options.projection } : {}),
|
|
2351
|
+
...(options.mergeWindow === true ? { mergeWindow: true } : {}),
|
|
2265
2352
|
});
|
|
2266
2353
|
const payload = await this.sendRequest({
|
|
2267
2354
|
requestId: resolvedRequestId,
|
|
@@ -2283,6 +2370,28 @@ export class DaemonClient {
|
|
|
2283
2370
|
}
|
|
2284
2371
|
return payload;
|
|
2285
2372
|
}
|
|
2373
|
+
async listAgentTimelinePrompts(agentId, options = {}) {
|
|
2374
|
+
const requestId = this.createRequestId(options.requestId);
|
|
2375
|
+
const message = SessionInboundMessageSchema.parse({
|
|
2376
|
+
type: "agent.timeline.list_prompts.request",
|
|
2377
|
+
agentId,
|
|
2378
|
+
requestId,
|
|
2379
|
+
});
|
|
2380
|
+
const payload = await this.sendRequest({
|
|
2381
|
+
requestId,
|
|
2382
|
+
message,
|
|
2383
|
+
timeout: options.timeout,
|
|
2384
|
+
options: { skipQueue: true },
|
|
2385
|
+
select: (response) => response.type === "agent.timeline.list_prompts.response" &&
|
|
2386
|
+
response.payload.requestId === requestId
|
|
2387
|
+
? response.payload
|
|
2388
|
+
: null,
|
|
2389
|
+
});
|
|
2390
|
+
if (payload.error) {
|
|
2391
|
+
throw new Error(payload.error);
|
|
2392
|
+
}
|
|
2393
|
+
return payload;
|
|
2394
|
+
}
|
|
2286
2395
|
async buildAgentForkContext(agentId, options = {}) {
|
|
2287
2396
|
const resolvedRequestId = this.createRequestId(options.requestId);
|
|
2288
2397
|
const message = SessionInboundMessageSchema.parse({
|
|
@@ -2647,6 +2756,40 @@ export class DaemonClient {
|
|
|
2647
2756
|
}
|
|
2648
2757
|
return payload.notice ?? null;
|
|
2649
2758
|
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Applies a whole agent-config bundle in one request. Use this instead of
|
|
2761
|
+
* chaining the single-field setters when the values belong together so client
|
|
2762
|
+
* interruption and other mutations cannot interleave between steps. A
|
|
2763
|
+
* provider rejection can still leave earlier steps applied.
|
|
2764
|
+
* Gated on `server_info.features.agentConfigApply`.
|
|
2765
|
+
*/
|
|
2766
|
+
async applyAgentConfig(agentId, config) {
|
|
2767
|
+
const requestId = this.createRequestId();
|
|
2768
|
+
const message = SessionInboundMessageSchema.parse({
|
|
2769
|
+
type: "agent.config.apply.request",
|
|
2770
|
+
agentId,
|
|
2771
|
+
config,
|
|
2772
|
+
requestId,
|
|
2773
|
+
});
|
|
2774
|
+
const payload = await this.sendRequest({
|
|
2775
|
+
requestId,
|
|
2776
|
+
message,
|
|
2777
|
+
options: { skipQueue: true },
|
|
2778
|
+
select: (msg) => {
|
|
2779
|
+
if (msg.type !== "agent.config.apply.response") {
|
|
2780
|
+
return null;
|
|
2781
|
+
}
|
|
2782
|
+
if (msg.payload.requestId !== requestId) {
|
|
2783
|
+
return null;
|
|
2784
|
+
}
|
|
2785
|
+
return msg.payload;
|
|
2786
|
+
},
|
|
2787
|
+
});
|
|
2788
|
+
if (!payload.accepted) {
|
|
2789
|
+
throw new Error(payload.error ?? "applyAgentConfig rejected");
|
|
2790
|
+
}
|
|
2791
|
+
return payload.notice ?? null;
|
|
2792
|
+
}
|
|
2650
2793
|
async restartServer(reason, requestId) {
|
|
2651
2794
|
const resolvedRequestId = this.createRequestId(requestId);
|
|
2652
2795
|
const message = SessionInboundMessageSchema.parse({
|
|
@@ -3712,48 +3855,6 @@ export class DaemonClient {
|
|
|
3712
3855
|
requestId: input.requestId,
|
|
3713
3856
|
}));
|
|
3714
3857
|
}
|
|
3715
|
-
/** Create an empty file or a directory. Never overwrites - see FileCreateResultSchema. */
|
|
3716
|
-
async createFileEntry(options) {
|
|
3717
|
-
const payload = await this.sendCorrelatedSessionRequest({
|
|
3718
|
-
requestId: options.requestId,
|
|
3719
|
-
message: {
|
|
3720
|
-
type: "file.create.request",
|
|
3721
|
-
cwd: options.cwd,
|
|
3722
|
-
path: options.path,
|
|
3723
|
-
kind: options.kind,
|
|
3724
|
-
},
|
|
3725
|
-
responseType: "file.create.response",
|
|
3726
|
-
});
|
|
3727
|
-
return payload.result;
|
|
3728
|
-
}
|
|
3729
|
-
/** Permanent delete - an unlink, not a move to any trash. */
|
|
3730
|
-
async deleteFileEntry(options) {
|
|
3731
|
-
const payload = await this.sendCorrelatedSessionRequest({
|
|
3732
|
-
requestId: options.requestId,
|
|
3733
|
-
message: {
|
|
3734
|
-
type: "file.delete.request",
|
|
3735
|
-
cwd: options.cwd,
|
|
3736
|
-
path: options.path,
|
|
3737
|
-
recursive: options.recursive,
|
|
3738
|
-
},
|
|
3739
|
-
responseType: "file.delete.response",
|
|
3740
|
-
});
|
|
3741
|
-
return payload.result;
|
|
3742
|
-
}
|
|
3743
|
-
/** Rename, which is also move. Never clobbers an occupied destination. */
|
|
3744
|
-
async renameFileEntry(options) {
|
|
3745
|
-
const payload = await this.sendCorrelatedSessionRequest({
|
|
3746
|
-
requestId: options.requestId,
|
|
3747
|
-
message: {
|
|
3748
|
-
type: "file.rename.request",
|
|
3749
|
-
cwd: options.cwd,
|
|
3750
|
-
path: options.path,
|
|
3751
|
-
newPath: options.newPath,
|
|
3752
|
-
},
|
|
3753
|
-
responseType: "file.rename.response",
|
|
3754
|
-
});
|
|
3755
|
-
return payload.result;
|
|
3756
|
-
}
|
|
3757
3858
|
async refineFile(options) {
|
|
3758
3859
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
3759
3860
|
requestId: options.requestId,
|
|
@@ -4446,6 +4547,7 @@ export class DaemonClient {
|
|
|
4446
4547
|
message: {
|
|
4447
4548
|
type: "get_providers_snapshot_request",
|
|
4448
4549
|
cwd: options?.cwd,
|
|
4550
|
+
ifNoneMatch: options?.ifNoneMatch,
|
|
4449
4551
|
},
|
|
4450
4552
|
responseType: "get_providers_snapshot_response",
|
|
4451
4553
|
});
|
|
@@ -4978,6 +5080,31 @@ export class DaemonClient {
|
|
|
4978
5080
|
});
|
|
4979
5081
|
return unwrapBrainJob(payload);
|
|
4980
5082
|
}
|
|
5083
|
+
async createFileEntry(input) {
|
|
5084
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
5085
|
+
message: { type: "fs.entry.create.request", ...input },
|
|
5086
|
+
});
|
|
5087
|
+
}
|
|
5088
|
+
async renameFileEntry(input) {
|
|
5089
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
5090
|
+
message: { type: "fs.entry.rename.request", ...input },
|
|
5091
|
+
});
|
|
5092
|
+
}
|
|
5093
|
+
async duplicateFileEntry(input) {
|
|
5094
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
5095
|
+
message: { type: "fs.entry.duplicate.request", ...input },
|
|
5096
|
+
});
|
|
5097
|
+
}
|
|
5098
|
+
async deleteFileEntry(input) {
|
|
5099
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
5100
|
+
message: { type: "fs.entry.delete.request", ...input },
|
|
5101
|
+
});
|
|
5102
|
+
}
|
|
5103
|
+
async checkoutDiscardChanges(cwd, input) {
|
|
5104
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
5105
|
+
message: { type: "checkout.discard_changes.request", cwd, paths: input.paths },
|
|
5106
|
+
});
|
|
5107
|
+
}
|
|
4981
5108
|
async brainJobsList(requestId) {
|
|
4982
5109
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
4983
5110
|
requestId,
|
|
@@ -5093,6 +5220,15 @@ export class DaemonClient {
|
|
|
5093
5220
|
}
|
|
5094
5221
|
return payload.status;
|
|
5095
5222
|
}
|
|
5223
|
+
async getProjectIcon(projectId, requestId) {
|
|
5224
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
5225
|
+
requestId,
|
|
5226
|
+
message: { type: "project.icon.get.request", projectId },
|
|
5227
|
+
});
|
|
5228
|
+
}
|
|
5229
|
+
// ============================================================================
|
|
5230
|
+
// Provider Models / Commands
|
|
5231
|
+
// ============================================================================
|
|
5096
5232
|
/** Delete a model's files. The brain refuses while that model is loaded. */
|
|
5097
5233
|
async brainModelDelete(modelId, requestId) {
|
|
5098
5234
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
@@ -5689,86 +5825,6 @@ export class DaemonClient {
|
|
|
5689
5825
|
responseType: "terminal.compatibility.diagnostic.response",
|
|
5690
5826
|
});
|
|
5691
5827
|
}
|
|
5692
|
-
async createChatRoom(options) {
|
|
5693
|
-
return this.sendCorrelatedSessionRequest({
|
|
5694
|
-
requestId: options.requestId,
|
|
5695
|
-
message: {
|
|
5696
|
-
type: "chat/create",
|
|
5697
|
-
name: options.name,
|
|
5698
|
-
...(options.purpose ? { purpose: options.purpose } : {}),
|
|
5699
|
-
},
|
|
5700
|
-
responseType: "chat/create/response",
|
|
5701
|
-
});
|
|
5702
|
-
}
|
|
5703
|
-
async listChatRooms(requestId) {
|
|
5704
|
-
return this.sendCorrelatedSessionRequest({
|
|
5705
|
-
requestId,
|
|
5706
|
-
message: {
|
|
5707
|
-
type: "chat/list",
|
|
5708
|
-
},
|
|
5709
|
-
responseType: "chat/list/response",
|
|
5710
|
-
});
|
|
5711
|
-
}
|
|
5712
|
-
async inspectChatRoom(options) {
|
|
5713
|
-
return this.sendCorrelatedSessionRequest({
|
|
5714
|
-
requestId: options.requestId,
|
|
5715
|
-
message: {
|
|
5716
|
-
type: "chat/inspect",
|
|
5717
|
-
room: options.room,
|
|
5718
|
-
},
|
|
5719
|
-
responseType: "chat/inspect/response",
|
|
5720
|
-
});
|
|
5721
|
-
}
|
|
5722
|
-
async deleteChatRoom(options) {
|
|
5723
|
-
return this.sendCorrelatedSessionRequest({
|
|
5724
|
-
requestId: options.requestId,
|
|
5725
|
-
message: {
|
|
5726
|
-
type: "chat/delete",
|
|
5727
|
-
room: options.room,
|
|
5728
|
-
},
|
|
5729
|
-
responseType: "chat/delete/response",
|
|
5730
|
-
});
|
|
5731
|
-
}
|
|
5732
|
-
async postChatMessage(options) {
|
|
5733
|
-
return this.sendCorrelatedSessionRequest({
|
|
5734
|
-
requestId: options.requestId,
|
|
5735
|
-
message: {
|
|
5736
|
-
type: "chat/post",
|
|
5737
|
-
room: options.room,
|
|
5738
|
-
body: options.body,
|
|
5739
|
-
...(options.authorAgentId ? { authorAgentId: options.authorAgentId } : {}),
|
|
5740
|
-
...(options.replyToMessageId ? { replyToMessageId: options.replyToMessageId } : {}),
|
|
5741
|
-
},
|
|
5742
|
-
responseType: "chat/post/response",
|
|
5743
|
-
});
|
|
5744
|
-
}
|
|
5745
|
-
async readChatMessages(options) {
|
|
5746
|
-
return this.sendCorrelatedSessionRequest({
|
|
5747
|
-
requestId: options.requestId,
|
|
5748
|
-
message: {
|
|
5749
|
-
type: "chat/read",
|
|
5750
|
-
room: options.room,
|
|
5751
|
-
...(typeof options.limit === "number" ? { limit: options.limit } : {}),
|
|
5752
|
-
...(options.since ? { since: options.since } : {}),
|
|
5753
|
-
...(options.authorAgentId ? { authorAgentId: options.authorAgentId } : {}),
|
|
5754
|
-
},
|
|
5755
|
-
responseType: "chat/read/response",
|
|
5756
|
-
timeout: options.timeout,
|
|
5757
|
-
});
|
|
5758
|
-
}
|
|
5759
|
-
async waitForChatMessages(options) {
|
|
5760
|
-
return this.sendCorrelatedSessionRequest({
|
|
5761
|
-
requestId: options.requestId,
|
|
5762
|
-
message: {
|
|
5763
|
-
type: "chat/wait",
|
|
5764
|
-
room: options.room,
|
|
5765
|
-
...(options.afterMessageId ? { afterMessageId: options.afterMessageId } : {}),
|
|
5766
|
-
...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
|
|
5767
|
-
},
|
|
5768
|
-
responseType: "chat/wait/response",
|
|
5769
|
-
timeout: (options.timeoutMs ?? 0) + 10000,
|
|
5770
|
-
});
|
|
5771
|
-
}
|
|
5772
5828
|
async scheduleCreate(options) {
|
|
5773
5829
|
return this.sendCorrelatedSessionRequest({
|
|
5774
5830
|
requestId: options.requestId,
|
|
@@ -5968,76 +6024,6 @@ export class DaemonClient {
|
|
|
5968
6024
|
responseType: "artifact.get-content.response",
|
|
5969
6025
|
});
|
|
5970
6026
|
}
|
|
5971
|
-
async loopRun(options) {
|
|
5972
|
-
return this.sendCorrelatedSessionRequest({
|
|
5973
|
-
requestId: options.requestId,
|
|
5974
|
-
message: {
|
|
5975
|
-
type: "loop/run",
|
|
5976
|
-
prompt: options.prompt,
|
|
5977
|
-
cwd: options.cwd,
|
|
5978
|
-
...(options.provider ? { provider: options.provider } : {}),
|
|
5979
|
-
...(options.model ? { model: options.model } : {}),
|
|
5980
|
-
...(options.modeId ? { modeId: options.modeId } : {}),
|
|
5981
|
-
...(options.verifierProvider ? { verifierProvider: options.verifierProvider } : {}),
|
|
5982
|
-
...(options.verifierModel ? { verifierModel: options.verifierModel } : {}),
|
|
5983
|
-
...(options.verifierModeId ? { verifierModeId: options.verifierModeId } : {}),
|
|
5984
|
-
...(options.verifyPrompt ? { verifyPrompt: options.verifyPrompt } : {}),
|
|
5985
|
-
...(options.verifyChecks && options.verifyChecks.length > 0
|
|
5986
|
-
? { verifyChecks: options.verifyChecks }
|
|
5987
|
-
: {}),
|
|
5988
|
-
...(options.name ? { name: options.name } : {}),
|
|
5989
|
-
...(typeof options.sleepMs === "number" ? { sleepMs: options.sleepMs } : {}),
|
|
5990
|
-
...(typeof options.maxIterations === "number"
|
|
5991
|
-
? { maxIterations: options.maxIterations }
|
|
5992
|
-
: {}),
|
|
5993
|
-
...(typeof options.maxTimeMs === "number" ? { maxTimeMs: options.maxTimeMs } : {}),
|
|
5994
|
-
},
|
|
5995
|
-
responseType: "loop/run/response",
|
|
5996
|
-
});
|
|
5997
|
-
}
|
|
5998
|
-
async loopList(requestId) {
|
|
5999
|
-
return this.sendCorrelatedSessionRequest({
|
|
6000
|
-
requestId,
|
|
6001
|
-
message: {
|
|
6002
|
-
type: "loop/list",
|
|
6003
|
-
},
|
|
6004
|
-
responseType: "loop/list/response",
|
|
6005
|
-
});
|
|
6006
|
-
}
|
|
6007
|
-
async loopInspect(options) {
|
|
6008
|
-
const normalized = typeof options === "string" ? { id: options } : options;
|
|
6009
|
-
return this.sendCorrelatedSessionRequest({
|
|
6010
|
-
requestId: normalized.requestId,
|
|
6011
|
-
message: {
|
|
6012
|
-
type: "loop/inspect",
|
|
6013
|
-
id: normalized.id,
|
|
6014
|
-
},
|
|
6015
|
-
responseType: "loop/inspect/response",
|
|
6016
|
-
});
|
|
6017
|
-
}
|
|
6018
|
-
async loopLogs(options, afterSeq) {
|
|
6019
|
-
const normalized = typeof options === "string" ? { id: options, afterSeq } : options;
|
|
6020
|
-
return this.sendCorrelatedSessionRequest({
|
|
6021
|
-
requestId: normalized.requestId,
|
|
6022
|
-
message: {
|
|
6023
|
-
type: "loop/logs",
|
|
6024
|
-
id: normalized.id,
|
|
6025
|
-
...(typeof normalized.afterSeq === "number" ? { afterSeq: normalized.afterSeq } : {}),
|
|
6026
|
-
},
|
|
6027
|
-
responseType: "loop/logs/response",
|
|
6028
|
-
});
|
|
6029
|
-
}
|
|
6030
|
-
async loopStop(options) {
|
|
6031
|
-
const normalized = typeof options === "string" ? { id: options } : options;
|
|
6032
|
-
return this.sendCorrelatedSessionRequest({
|
|
6033
|
-
requestId: normalized.requestId,
|
|
6034
|
-
message: {
|
|
6035
|
-
type: "loop/stop",
|
|
6036
|
-
id: normalized.id,
|
|
6037
|
-
},
|
|
6038
|
-
responseType: "loop/stop/response",
|
|
6039
|
-
});
|
|
6040
|
-
}
|
|
6041
6027
|
onTerminalStreamEvent(handler) {
|
|
6042
6028
|
return this.terminalStreams.onEvent(handler);
|
|
6043
6029
|
}
|
|
@@ -6078,6 +6064,14 @@ export class DaemonClient {
|
|
|
6078
6064
|
getTrafficHotspots(limit) {
|
|
6079
6065
|
return this.runtimeMetrics?.getTrafficHotspots(limit) ?? [];
|
|
6080
6066
|
}
|
|
6067
|
+
/**
|
|
6068
|
+
* Bounded, timestamped dispatch phases for matching an inbound daemon
|
|
6069
|
+
* message to a browser Long Animation Frame. This is diagnostic evidence,
|
|
6070
|
+
* not a protocol surface.
|
|
6071
|
+
*/
|
|
6072
|
+
getInboundDispatchTimings(sinceMs) {
|
|
6073
|
+
return this.runtimeMetrics?.getInboundDispatchTimings(sinceMs) ?? [];
|
|
6074
|
+
}
|
|
6081
6075
|
resolveTransportUrlForAttempt() {
|
|
6082
6076
|
return this.config.url;
|
|
6083
6077
|
}
|
|
@@ -6091,7 +6085,7 @@ export class DaemonClient {
|
|
|
6091
6085
|
return;
|
|
6092
6086
|
}
|
|
6093
6087
|
try {
|
|
6094
|
-
this.
|
|
6088
|
+
this.sendJsonMessage("hello", "hello", {
|
|
6095
6089
|
type: "hello",
|
|
6096
6090
|
clientId: this.config.clientId,
|
|
6097
6091
|
clientType: this.config.clientType ?? "cli",
|
|
@@ -6105,10 +6099,11 @@ export class DaemonClient {
|
|
|
6105
6099
|
// so dropping it silently kills cross-session project renames.
|
|
6106
6100
|
[CLIENT_CAPS.projectUpdates]: true,
|
|
6107
6101
|
[CLIENT_CAPS.communicationsPresenceUpdates]: true,
|
|
6102
|
+
[CLIENT_CAPS.compactProviderSnapshots]: true,
|
|
6108
6103
|
...this.config.capabilities,
|
|
6109
6104
|
},
|
|
6110
6105
|
...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}),
|
|
6111
|
-
})
|
|
6106
|
+
});
|
|
6112
6107
|
}
|
|
6113
6108
|
catch (error) {
|
|
6114
6109
|
const message = error instanceof Error ? error.message : "Failed to send hello message";
|
|
@@ -6173,25 +6168,38 @@ export class DaemonClient {
|
|
|
6173
6168
|
return;
|
|
6174
6169
|
}
|
|
6175
6170
|
const rawBytes = asUint8Array(rawData);
|
|
6176
|
-
|
|
6177
|
-
|
|
6171
|
+
const isOpen = this.beginTraceSection("otto.ws.frame.inbound", describeInboundTransportFrame(rawData, rawBytes));
|
|
6172
|
+
try {
|
|
6173
|
+
if (rawBytes && this.tryHandleBinaryFrame(rawBytes)) {
|
|
6174
|
+
return;
|
|
6175
|
+
}
|
|
6176
|
+
const payload = decodeMessageData(rawData);
|
|
6177
|
+
if (!payload) {
|
|
6178
|
+
return;
|
|
6179
|
+
}
|
|
6180
|
+
this.handleJsonPayload(payload, rawBytes?.byteLength);
|
|
6178
6181
|
}
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
return;
|
|
6182
|
+
finally {
|
|
6183
|
+
this.endTraceSection(isOpen);
|
|
6182
6184
|
}
|
|
6183
|
-
this.handleJsonPayload(payload, rawBytes?.byteLength);
|
|
6184
6185
|
}
|
|
6185
6186
|
handleJsonPayload(payload, rawBytesLength) {
|
|
6186
6187
|
const bytes = rawBytesLength ?? payload.length;
|
|
6188
|
+
const dispatchAt = Date.now();
|
|
6187
6189
|
const startMs = perfNow();
|
|
6188
6190
|
let parsedJson;
|
|
6191
|
+
const parseTraceOpen = this.beginTraceSection("otto.ws.json.parse", {
|
|
6192
|
+
size: String(bytes),
|
|
6193
|
+
});
|
|
6189
6194
|
try {
|
|
6190
6195
|
parsedJson = JSON.parse(payload);
|
|
6191
6196
|
}
|
|
6192
6197
|
catch {
|
|
6193
6198
|
return;
|
|
6194
6199
|
}
|
|
6200
|
+
finally {
|
|
6201
|
+
this.endTraceSection(parseTraceOpen);
|
|
6202
|
+
}
|
|
6195
6203
|
const parsed = validateWSOutboundMessage(parsedJson);
|
|
6196
6204
|
if (!parsed.success) {
|
|
6197
6205
|
const responseIdentity = extractCorrelatedResponseIdentity(parsedJson);
|
|
@@ -6210,13 +6218,33 @@ export class DaemonClient {
|
|
|
6210
6218
|
}
|
|
6211
6219
|
this.consecutiveLivenessFailures = 0;
|
|
6212
6220
|
if (parsed.data.type === "pong") {
|
|
6221
|
+
this.traceInstant("otto.ws.message.inbound", {
|
|
6222
|
+
envelopeType: "pong",
|
|
6223
|
+
messageType: "pong",
|
|
6224
|
+
});
|
|
6213
6225
|
this.resolvePingProbe();
|
|
6214
6226
|
this.runtimeMetrics?.recordMessage("pong", bytes, perfNow() - startMs);
|
|
6215
6227
|
return;
|
|
6216
6228
|
}
|
|
6217
|
-
this.
|
|
6229
|
+
this.traceInstant("otto.ws.message.inbound", {
|
|
6230
|
+
envelopeType: "session",
|
|
6231
|
+
messageType: parsed.data.message.type,
|
|
6232
|
+
});
|
|
6233
|
+
const phases = this.handleSessionMessage(parsed.data.message);
|
|
6218
6234
|
const msgType = parsed.data.message.type;
|
|
6219
|
-
|
|
6235
|
+
const totalMs = perfNow() - startMs;
|
|
6236
|
+
this.runtimeMetrics?.recordMessage(msgType, bytes, totalMs);
|
|
6237
|
+
this.runtimeMetrics?.recordInboundDispatch({
|
|
6238
|
+
at: dispatchAt,
|
|
6239
|
+
type: msgType,
|
|
6240
|
+
agentId: extractDispatchAgentId(parsed.data.message),
|
|
6241
|
+
bytes,
|
|
6242
|
+
decodeAndValidateMs: phases.startedAtMs - startMs,
|
|
6243
|
+
internalDispatchMs: phases.internalDispatchMs,
|
|
6244
|
+
rawListenersMs: phases.rawListenersMs,
|
|
6245
|
+
typedHandlersMs: phases.typedHandlersMs,
|
|
6246
|
+
totalMs,
|
|
6247
|
+
});
|
|
6220
6248
|
if (parsed.data.message.type === "agent_stream") {
|
|
6221
6249
|
this.runtimeMetrics?.recordAgentStream(parsed.data.message.payload);
|
|
6222
6250
|
}
|
|
@@ -6224,6 +6252,11 @@ export class DaemonClient {
|
|
|
6224
6252
|
tryHandleBinaryFrame(rawBytes) {
|
|
6225
6253
|
const fileFrame = decodeFileTransferFrame(rawBytes);
|
|
6226
6254
|
if (fileFrame) {
|
|
6255
|
+
this.traceInstant("otto.ws.message.inbound", {
|
|
6256
|
+
envelopeType: "binary",
|
|
6257
|
+
messageType: "file",
|
|
6258
|
+
opcode: String(fileFrame.opcode),
|
|
6259
|
+
});
|
|
6227
6260
|
this.consecutiveLivenessFailures = 0;
|
|
6228
6261
|
this.handleFileTransferFrame(fileFrame);
|
|
6229
6262
|
this.runtimeMetrics?.recordBinaryFrame("other", rawBytes.byteLength, 0);
|
|
@@ -6233,6 +6266,11 @@ export class DaemonClient {
|
|
|
6233
6266
|
if (!frame) {
|
|
6234
6267
|
return false;
|
|
6235
6268
|
}
|
|
6269
|
+
this.traceInstant("otto.ws.message.inbound", {
|
|
6270
|
+
envelopeType: "binary",
|
|
6271
|
+
messageType: "terminal",
|
|
6272
|
+
opcode: String(frame.opcode),
|
|
6273
|
+
});
|
|
6236
6274
|
this.consecutiveLivenessFailures = 0;
|
|
6237
6275
|
const binaryStartMs = perfNow();
|
|
6238
6276
|
this.terminalStreams.handleFrame(frame);
|
|
@@ -6418,6 +6456,7 @@ export class DaemonClient {
|
|
|
6418
6456
|
});
|
|
6419
6457
|
}
|
|
6420
6458
|
handleSessionMessage(msg) {
|
|
6459
|
+
const startedAtMs = perfNow();
|
|
6421
6460
|
const consumerMessage = normalizeProviderSnapshotUpdateMessage(msg);
|
|
6422
6461
|
if (consumerMessage.type === "status") {
|
|
6423
6462
|
const serverInfo = parseServerInfoStatusPayload(consumerMessage.payload);
|
|
@@ -6445,6 +6484,7 @@ export class DaemonClient {
|
|
|
6445
6484
|
if (consumerMessage.type === "project.scaffold.progress") {
|
|
6446
6485
|
this.scaffoldProgressListeners.get(consumerMessage.payload.requestId)?.(consumerMessage.payload);
|
|
6447
6486
|
}
|
|
6487
|
+
const rawListenersStartedAtMs = perfNow();
|
|
6448
6488
|
if (this.rawMessageListeners.size > 0) {
|
|
6449
6489
|
for (const handler of this.rawMessageListeners) {
|
|
6450
6490
|
try {
|
|
@@ -6455,6 +6495,7 @@ export class DaemonClient {
|
|
|
6455
6495
|
}
|
|
6456
6496
|
}
|
|
6457
6497
|
}
|
|
6498
|
+
const typedHandlersStartedAtMs = perfNow();
|
|
6458
6499
|
const handlers = this.messageHandlers.get(consumerMessage.type);
|
|
6459
6500
|
if (handlers) {
|
|
6460
6501
|
for (const handler of handlers) {
|
|
@@ -6473,6 +6514,13 @@ export class DaemonClient {
|
|
|
6473
6514
|
}
|
|
6474
6515
|
}
|
|
6475
6516
|
this.resolveWaiters(consumerMessage);
|
|
6517
|
+
const finishedAtMs = perfNow();
|
|
6518
|
+
return {
|
|
6519
|
+
startedAtMs,
|
|
6520
|
+
internalDispatchMs: rawListenersStartedAtMs - startedAtMs,
|
|
6521
|
+
rawListenersMs: typedHandlersStartedAtMs - rawListenersStartedAtMs,
|
|
6522
|
+
typedHandlersMs: finishedAtMs - typedHandlersStartedAtMs,
|
|
6523
|
+
};
|
|
6476
6524
|
}
|
|
6477
6525
|
resolveWaiters(msg) {
|
|
6478
6526
|
for (const waiter of Array.from(this.waiters)) {
|