@otto-code/client 0.8.12 → 0.8.13

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.
@@ -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,21 @@ function concatByteChunks(chunks, size) {
185
186
  }
186
187
  return bytes;
187
188
  }
189
+ function getTransportFrameSize(frame) {
190
+ if (typeof frame === "string") {
191
+ return frame.length;
192
+ }
193
+ return frame.byteLength;
194
+ }
195
+ function describeInboundTransportFrame(frame, rawBytes) {
196
+ if (typeof frame === "string") {
197
+ return { kind: "text", size: String(frame.length) };
198
+ }
199
+ if (rawBytes) {
200
+ return { kind: "binary", size: String(rawBytes.byteLength) };
201
+ }
202
+ return { kind: "unknown", size: "0" };
203
+ }
188
204
  function hashForLog(value) {
189
205
  let hash = 0;
190
206
  for (let index = 0; index < value.length; index += 1) {
@@ -595,6 +611,45 @@ export class DaemonClient {
595
611
  // ============================================================================
596
612
  // Core Send Helpers
597
613
  // ============================================================================
614
+ beginTraceSection(name, args) {
615
+ const trace = this.config.trace;
616
+ if (!trace?.isEnabled()) {
617
+ return false;
618
+ }
619
+ trace.beginSection(name, args);
620
+ return true;
621
+ }
622
+ endTraceSection(isOpen) {
623
+ if (isOpen) {
624
+ this.config.trace?.endSection();
625
+ }
626
+ }
627
+ traceInstant(name, args) {
628
+ const isOpen = this.beginTraceSection(name, args);
629
+ this.endTraceSection(isOpen);
630
+ }
631
+ sendJsonMessage(envelopeType, messageType, message) {
632
+ this.traceInstant("otto.ws.message.outbound", {
633
+ envelopeType,
634
+ messageType,
635
+ });
636
+ this.sendTransportFrame(JSON.stringify(message));
637
+ }
638
+ sendTransportFrame(frame) {
639
+ if (!this.transport) {
640
+ throw new Error("Transport not connected");
641
+ }
642
+ const isOpen = this.beginTraceSection("otto.ws.frame.outbound", {
643
+ kind: typeof frame === "string" ? "text" : "binary",
644
+ size: String(getTransportFrameSize(frame)),
645
+ });
646
+ try {
647
+ this.transport.send(frame);
648
+ }
649
+ finally {
650
+ this.endTraceSection(isOpen);
651
+ }
652
+ }
598
653
  /**
599
654
  * Send a session message. For fire-and-forget messages (heartbeats, etc.),
600
655
  * failures are suppressed if `suppressSendErrors` is configured.
@@ -609,7 +664,7 @@ export class DaemonClient {
609
664
  }
610
665
  const payload = SessionInboundMessageSchema.parse(message);
611
666
  try {
612
- this.transport.send(JSON.stringify({ type: "session", message: payload }));
667
+ this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
613
668
  }
614
669
  catch (error) {
615
670
  if (this.config.suppressSendErrors) {
@@ -626,7 +681,11 @@ export class DaemonClient {
626
681
  throw new Error(`Transport not connected (status: ${this.connectionState.status})`);
627
682
  }
628
683
  try {
629
- this.transport.send(frame);
684
+ this.traceInstant("otto.ws.message.outbound", {
685
+ envelopeType: "binary",
686
+ messageType: "binary",
687
+ });
688
+ this.sendTransportFrame(frame);
630
689
  }
631
690
  catch (error) {
632
691
  if (this.config.suppressSendErrors) {
@@ -646,7 +705,7 @@ export class DaemonClient {
646
705
  // If connected, send immediately
647
706
  if (this.transport && status === "connected") {
648
707
  const payload = SessionInboundMessageSchema.parse(message);
649
- this.transport.send(JSON.stringify({ type: "session", message: payload }));
708
+ this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
650
709
  return Promise.resolve();
651
710
  }
652
711
  // If connecting, queue the message to be sent once connected
@@ -677,7 +736,7 @@ export class DaemonClient {
677
736
  try {
678
737
  if (this.transport && this.connectionState.status === "connected") {
679
738
  const payload = SessionInboundMessageSchema.parse(pending.message);
680
- this.transport.send(JSON.stringify({ type: "session", message: payload }));
739
+ this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
681
740
  pending.resolve();
682
741
  }
683
742
  else {
@@ -785,7 +844,7 @@ export class DaemonClient {
785
844
  }
786
845
  const payload = SessionInboundMessageSchema.parse(message);
787
846
  try {
788
- this.transport.send(JSON.stringify({ type: "session", message: payload }));
847
+ this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
789
848
  }
790
849
  catch (error) {
791
850
  throw error instanceof Error ? error : new Error(String(error));
@@ -855,6 +914,15 @@ export class DaemonClient {
855
914
  token,
856
915
  });
857
916
  }
917
+ async unregisterPushToken(token) {
918
+ const requestId = this.createRequestId();
919
+ await this.sendCorrelatedSessionRequest({
920
+ requestId,
921
+ message: { type: "push.unregister.request", token, requestId },
922
+ responseType: "push.unregister.response",
923
+ timeout: PUSH_TOKEN_REVOCATION_TIMEOUT_MS,
924
+ });
925
+ }
858
926
  async ping(params) {
859
927
  const requestId = params?.requestId ?? `ping-${Date.now()}-${Math.random().toString(36).slice(2)}`;
860
928
  const clientSentAt = Date.now();
@@ -934,7 +1002,7 @@ export class DaemonClient {
934
1002
  };
935
1003
  this.pingProbe = probe;
936
1004
  try {
937
- this.transport.send(JSON.stringify({ type: "ping" }));
1005
+ this.sendJsonMessage("ping", "ping", { type: "ping" });
938
1006
  }
939
1007
  catch (error) {
940
1008
  this.clearPingProbe();
@@ -1007,6 +1075,7 @@ export class DaemonClient {
1007
1075
  type: "fetch_agent_history_request",
1008
1076
  requestId: resolvedRequestId,
1009
1077
  ...(options?.filter ? { filter: options.filter } : {}),
1078
+ ...(options?.search ? { search: options.search } : {}),
1010
1079
  ...(options?.sort ? { sort: options.sort } : {}),
1011
1080
  ...(options?.page ? { page: options.page } : {}),
1012
1081
  });
@@ -1778,6 +1847,14 @@ export class DaemonClient {
1778
1847
  }
1779
1848
  return { target: payload.target };
1780
1849
  }
1850
+ async setProjectIcon(projectId, source, requestId) {
1851
+ const payload = await this.sendNamespacedCorrelatedSessionRequest({
1852
+ requestId,
1853
+ message: { type: "project.icon.set.request", projectId, source },
1854
+ });
1855
+ if (!payload.accepted)
1856
+ throw new Error(payload.error ?? "setProjectIcon rejected");
1857
+ }
1781
1858
  async removeProject(projectId, requestId) {
1782
1859
  const payload = await this.sendNamespacedCorrelatedSessionRequest({
1783
1860
  requestId,
@@ -2262,6 +2339,7 @@ export class DaemonClient {
2262
2339
  ...(options.cursor ? { cursor: options.cursor } : {}),
2263
2340
  ...(typeof options.limit === "number" ? { limit: options.limit } : {}),
2264
2341
  ...(options.projection ? { projection: options.projection } : {}),
2342
+ ...(options.mergeWindow === true ? { mergeWindow: true } : {}),
2265
2343
  });
2266
2344
  const payload = await this.sendRequest({
2267
2345
  requestId: resolvedRequestId,
@@ -2283,6 +2361,28 @@ export class DaemonClient {
2283
2361
  }
2284
2362
  return payload;
2285
2363
  }
2364
+ async listAgentTimelinePrompts(agentId, options = {}) {
2365
+ const requestId = this.createRequestId(options.requestId);
2366
+ const message = SessionInboundMessageSchema.parse({
2367
+ type: "agent.timeline.list_prompts.request",
2368
+ agentId,
2369
+ requestId,
2370
+ });
2371
+ const payload = await this.sendRequest({
2372
+ requestId,
2373
+ message,
2374
+ timeout: options.timeout,
2375
+ options: { skipQueue: true },
2376
+ select: (response) => response.type === "agent.timeline.list_prompts.response" &&
2377
+ response.payload.requestId === requestId
2378
+ ? response.payload
2379
+ : null,
2380
+ });
2381
+ if (payload.error) {
2382
+ throw new Error(payload.error);
2383
+ }
2384
+ return payload;
2385
+ }
2286
2386
  async buildAgentForkContext(agentId, options = {}) {
2287
2387
  const resolvedRequestId = this.createRequestId(options.requestId);
2288
2388
  const message = SessionInboundMessageSchema.parse({
@@ -2647,6 +2747,40 @@ export class DaemonClient {
2647
2747
  }
2648
2748
  return payload.notice ?? null;
2649
2749
  }
2750
+ /**
2751
+ * Applies a whole agent-config bundle in one request. Use this instead of
2752
+ * chaining the single-field setters when the values belong together so client
2753
+ * interruption and other mutations cannot interleave between steps. A
2754
+ * provider rejection can still leave earlier steps applied.
2755
+ * Gated on `server_info.features.agentConfigApply`.
2756
+ */
2757
+ async applyAgentConfig(agentId, config) {
2758
+ const requestId = this.createRequestId();
2759
+ const message = SessionInboundMessageSchema.parse({
2760
+ type: "agent.config.apply.request",
2761
+ agentId,
2762
+ config,
2763
+ requestId,
2764
+ });
2765
+ const payload = await this.sendRequest({
2766
+ requestId,
2767
+ message,
2768
+ options: { skipQueue: true },
2769
+ select: (msg) => {
2770
+ if (msg.type !== "agent.config.apply.response") {
2771
+ return null;
2772
+ }
2773
+ if (msg.payload.requestId !== requestId) {
2774
+ return null;
2775
+ }
2776
+ return msg.payload;
2777
+ },
2778
+ });
2779
+ if (!payload.accepted) {
2780
+ throw new Error(payload.error ?? "applyAgentConfig rejected");
2781
+ }
2782
+ return payload.notice ?? null;
2783
+ }
2650
2784
  async restartServer(reason, requestId) {
2651
2785
  const resolvedRequestId = this.createRequestId(requestId);
2652
2786
  const message = SessionInboundMessageSchema.parse({
@@ -3712,48 +3846,6 @@ export class DaemonClient {
3712
3846
  requestId: input.requestId,
3713
3847
  }));
3714
3848
  }
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
3849
  async refineFile(options) {
3758
3850
  const payload = await this.sendCorrelatedSessionRequest({
3759
3851
  requestId: options.requestId,
@@ -4446,6 +4538,7 @@ export class DaemonClient {
4446
4538
  message: {
4447
4539
  type: "get_providers_snapshot_request",
4448
4540
  cwd: options?.cwd,
4541
+ ifNoneMatch: options?.ifNoneMatch,
4449
4542
  },
4450
4543
  responseType: "get_providers_snapshot_response",
4451
4544
  });
@@ -4978,6 +5071,31 @@ export class DaemonClient {
4978
5071
  });
4979
5072
  return unwrapBrainJob(payload);
4980
5073
  }
5074
+ async createFileEntry(input) {
5075
+ return this.sendNamespacedCorrelatedSessionRequest({
5076
+ message: { type: "fs.entry.create.request", ...input },
5077
+ });
5078
+ }
5079
+ async renameFileEntry(input) {
5080
+ return this.sendNamespacedCorrelatedSessionRequest({
5081
+ message: { type: "fs.entry.rename.request", ...input },
5082
+ });
5083
+ }
5084
+ async duplicateFileEntry(input) {
5085
+ return this.sendNamespacedCorrelatedSessionRequest({
5086
+ message: { type: "fs.entry.duplicate.request", ...input },
5087
+ });
5088
+ }
5089
+ async deleteFileEntry(input) {
5090
+ return this.sendNamespacedCorrelatedSessionRequest({
5091
+ message: { type: "fs.entry.delete.request", ...input },
5092
+ });
5093
+ }
5094
+ async checkoutDiscardChanges(cwd, input) {
5095
+ return this.sendNamespacedCorrelatedSessionRequest({
5096
+ message: { type: "checkout.discard_changes.request", cwd, paths: input.paths },
5097
+ });
5098
+ }
4981
5099
  async brainJobsList(requestId) {
4982
5100
  const payload = await this.sendCorrelatedSessionRequest({
4983
5101
  requestId,
@@ -5093,6 +5211,15 @@ export class DaemonClient {
5093
5211
  }
5094
5212
  return payload.status;
5095
5213
  }
5214
+ async getProjectIcon(projectId, requestId) {
5215
+ return this.sendNamespacedCorrelatedSessionRequest({
5216
+ requestId,
5217
+ message: { type: "project.icon.get.request", projectId },
5218
+ });
5219
+ }
5220
+ // ============================================================================
5221
+ // Provider Models / Commands
5222
+ // ============================================================================
5096
5223
  /** Delete a model's files. The brain refuses while that model is loaded. */
5097
5224
  async brainModelDelete(modelId, requestId) {
5098
5225
  const payload = await this.sendCorrelatedSessionRequest({
@@ -5689,86 +5816,6 @@ export class DaemonClient {
5689
5816
  responseType: "terminal.compatibility.diagnostic.response",
5690
5817
  });
5691
5818
  }
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
5819
  async scheduleCreate(options) {
5773
5820
  return this.sendCorrelatedSessionRequest({
5774
5821
  requestId: options.requestId,
@@ -5968,76 +6015,6 @@ export class DaemonClient {
5968
6015
  responseType: "artifact.get-content.response",
5969
6016
  });
5970
6017
  }
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
6018
  onTerminalStreamEvent(handler) {
6042
6019
  return this.terminalStreams.onEvent(handler);
6043
6020
  }
@@ -6091,7 +6068,7 @@ export class DaemonClient {
6091
6068
  return;
6092
6069
  }
6093
6070
  try {
6094
- this.transport.send(JSON.stringify({
6071
+ this.sendJsonMessage("hello", "hello", {
6095
6072
  type: "hello",
6096
6073
  clientId: this.config.clientId,
6097
6074
  clientType: this.config.clientType ?? "cli",
@@ -6105,10 +6082,11 @@ export class DaemonClient {
6105
6082
  // so dropping it silently kills cross-session project renames.
6106
6083
  [CLIENT_CAPS.projectUpdates]: true,
6107
6084
  [CLIENT_CAPS.communicationsPresenceUpdates]: true,
6085
+ [CLIENT_CAPS.compactProviderSnapshots]: true,
6108
6086
  ...this.config.capabilities,
6109
6087
  },
6110
6088
  ...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}),
6111
- }));
6089
+ });
6112
6090
  }
6113
6091
  catch (error) {
6114
6092
  const message = error instanceof Error ? error.message : "Failed to send hello message";
@@ -6173,25 +6151,37 @@ export class DaemonClient {
6173
6151
  return;
6174
6152
  }
6175
6153
  const rawBytes = asUint8Array(rawData);
6176
- if (rawBytes && this.tryHandleBinaryFrame(rawBytes)) {
6177
- return;
6154
+ const isOpen = this.beginTraceSection("otto.ws.frame.inbound", describeInboundTransportFrame(rawData, rawBytes));
6155
+ try {
6156
+ if (rawBytes && this.tryHandleBinaryFrame(rawBytes)) {
6157
+ return;
6158
+ }
6159
+ const payload = decodeMessageData(rawData);
6160
+ if (!payload) {
6161
+ return;
6162
+ }
6163
+ this.handleJsonPayload(payload, rawBytes?.byteLength);
6178
6164
  }
6179
- const payload = decodeMessageData(rawData);
6180
- if (!payload) {
6181
- return;
6165
+ finally {
6166
+ this.endTraceSection(isOpen);
6182
6167
  }
6183
- this.handleJsonPayload(payload, rawBytes?.byteLength);
6184
6168
  }
6185
6169
  handleJsonPayload(payload, rawBytesLength) {
6186
6170
  const bytes = rawBytesLength ?? payload.length;
6187
6171
  const startMs = perfNow();
6188
6172
  let parsedJson;
6173
+ const parseTraceOpen = this.beginTraceSection("otto.ws.json.parse", {
6174
+ size: String(bytes),
6175
+ });
6189
6176
  try {
6190
6177
  parsedJson = JSON.parse(payload);
6191
6178
  }
6192
6179
  catch {
6193
6180
  return;
6194
6181
  }
6182
+ finally {
6183
+ this.endTraceSection(parseTraceOpen);
6184
+ }
6195
6185
  const parsed = validateWSOutboundMessage(parsedJson);
6196
6186
  if (!parsed.success) {
6197
6187
  const responseIdentity = extractCorrelatedResponseIdentity(parsedJson);
@@ -6210,10 +6200,18 @@ export class DaemonClient {
6210
6200
  }
6211
6201
  this.consecutiveLivenessFailures = 0;
6212
6202
  if (parsed.data.type === "pong") {
6203
+ this.traceInstant("otto.ws.message.inbound", {
6204
+ envelopeType: "pong",
6205
+ messageType: "pong",
6206
+ });
6213
6207
  this.resolvePingProbe();
6214
6208
  this.runtimeMetrics?.recordMessage("pong", bytes, perfNow() - startMs);
6215
6209
  return;
6216
6210
  }
6211
+ this.traceInstant("otto.ws.message.inbound", {
6212
+ envelopeType: "session",
6213
+ messageType: parsed.data.message.type,
6214
+ });
6217
6215
  this.handleSessionMessage(parsed.data.message);
6218
6216
  const msgType = parsed.data.message.type;
6219
6217
  this.runtimeMetrics?.recordMessage(msgType, bytes, perfNow() - startMs);
@@ -6224,6 +6222,11 @@ export class DaemonClient {
6224
6222
  tryHandleBinaryFrame(rawBytes) {
6225
6223
  const fileFrame = decodeFileTransferFrame(rawBytes);
6226
6224
  if (fileFrame) {
6225
+ this.traceInstant("otto.ws.message.inbound", {
6226
+ envelopeType: "binary",
6227
+ messageType: "file",
6228
+ opcode: String(fileFrame.opcode),
6229
+ });
6227
6230
  this.consecutiveLivenessFailures = 0;
6228
6231
  this.handleFileTransferFrame(fileFrame);
6229
6232
  this.runtimeMetrics?.recordBinaryFrame("other", rawBytes.byteLength, 0);
@@ -6233,6 +6236,11 @@ export class DaemonClient {
6233
6236
  if (!frame) {
6234
6237
  return false;
6235
6238
  }
6239
+ this.traceInstant("otto.ws.message.inbound", {
6240
+ envelopeType: "binary",
6241
+ messageType: "terminal",
6242
+ opcode: String(frame.opcode),
6243
+ });
6236
6244
  this.consecutiveLivenessFailures = 0;
6237
6245
  const binaryStartMs = perfNow();
6238
6246
  this.terminalStreams.handleFrame(frame);