@otto-code/client 0.8.13 → 0.8.15

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.
@@ -32,6 +32,28 @@ export interface DaemonClientTrafficHotspot {
32
32
  maxMs: number;
33
33
  bytes: number;
34
34
  }
35
+ /**
36
+ * One inbound session-message dispatch, retained only long enough to align it
37
+ * with a browser Long Animation Frame in the app's performance capture.
38
+ *
39
+ * `at` is an epoch timestamp so callers can compare it directly to the LoAF
40
+ * API. The phase timings are synchronous main-thread work and sum to
41
+ * approximately `totalMs`; small gaps are metric bookkeeping.
42
+ */
43
+ export interface DaemonClientInboundDispatchTiming {
44
+ at: number;
45
+ type: string;
46
+ /** Set for agent-scoped messages, so a capture can tell one hot agent from a spread. */
47
+ agentId?: string;
48
+ bytes: number;
49
+ decodeAndValidateMs: number;
50
+ internalDispatchMs: number;
51
+ rawListenersMs: number;
52
+ typedHandlersMs: number;
53
+ totalMs: number;
54
+ }
55
+ /** Bounded independently from rolling log buckets: this is capture evidence, not telemetry. */
56
+ export declare const INBOUND_DISPATCH_TIMING_CAPACITY = 500;
35
57
  export declare class DaemonClientRuntimeMetrics {
36
58
  private readonly logger;
37
59
  private readonly context;
@@ -49,11 +71,18 @@ export declare class DaemonClientRuntimeMetrics {
49
71
  private totalHandlerMs;
50
72
  private totalBinaryFrames;
51
73
  private readonly cumulativeByType;
74
+ private readonly inboundDispatchTimings;
52
75
  constructor(logger: RuntimeMetricsLogger, context: RuntimeMetricsContext, options?: RuntimeMetricsOptions);
53
76
  recordMessage(type: string, bytes: number, handlerMs: number): void;
54
77
  getTrafficTotals(): DaemonClientTrafficTotals;
55
78
  /** Inbound message types ranked by the main-thread time they have cost. */
56
79
  getTrafficHotspots(limit?: number): DaemonClientTrafficHotspot[];
80
+ recordInboundDispatch(timing: DaemonClientInboundDispatchTiming): void;
81
+ /**
82
+ * Recent inbound dispatches for a performance capture. Copies keep capture
83
+ * consumers from mutating a live client's bounded ring.
84
+ */
85
+ getInboundDispatchTimings(sinceMs?: number): DaemonClientInboundDispatchTiming[];
57
86
  private recordCumulative;
58
87
  recordAgentStream(payload: Extract<SessionOutboundMessage, {
59
88
  type: "agent_stream";
@@ -1,4 +1,6 @@
1
1
  const DEFAULT_ROLLING_WINDOW_MS = 60000;
2
+ /** Bounded independently from rolling log buckets: this is capture evidence, not telemetry. */
3
+ export const INBOUND_DISPATCH_TIMING_CAPACITY = 500;
2
4
  export class DaemonClientRuntimeMetrics {
3
5
  constructor(logger, context, options) {
4
6
  this.logger = logger;
@@ -17,6 +19,7 @@ export class DaemonClientRuntimeMetrics {
17
19
  this.totalHandlerMs = 0;
18
20
  this.totalBinaryFrames = 0;
19
21
  this.cumulativeByType = new Map();
22
+ this.inboundDispatchTimings = [];
20
23
  this.windowMs =
21
24
  typeof options?.windowMs === "number" && options.windowMs > 0
22
25
  ? options.windowMs
@@ -54,6 +57,21 @@ export class DaemonClientRuntimeMetrics {
54
57
  rows.sort((left, right) => right.totalMs - left.totalMs);
55
58
  return rows.slice(0, limit);
56
59
  }
60
+ recordInboundDispatch(timing) {
61
+ this.inboundDispatchTimings.push(cloneInboundDispatchTiming(timing));
62
+ if (this.inboundDispatchTimings.length > INBOUND_DISPATCH_TIMING_CAPACITY) {
63
+ this.inboundDispatchTimings.splice(0, this.inboundDispatchTimings.length - INBOUND_DISPATCH_TIMING_CAPACITY);
64
+ }
65
+ }
66
+ /**
67
+ * Recent inbound dispatches for a performance capture. Copies keep capture
68
+ * consumers from mutating a live client's bounded ring.
69
+ */
70
+ getInboundDispatchTimings(sinceMs) {
71
+ return this.inboundDispatchTimings
72
+ .filter((timing) => sinceMs === undefined || timing.at >= sinceMs)
73
+ .map(cloneInboundDispatchTiming);
74
+ }
57
75
  // Shared by JSON messages and binary frames; the per-sink totals
58
76
  // (`totalMessages` / `totalBinaryFrames`) are bumped by the callers so a
59
77
  // binary frame is never counted as both.
@@ -192,6 +210,19 @@ function cloneHandlerTimingMap(map) {
192
210
  { count: value.count, totalMs: value.totalMs, maxMs: value.maxMs },
193
211
  ]));
194
212
  }
213
+ function cloneInboundDispatchTiming(timing) {
214
+ return {
215
+ at: timing.at,
216
+ type: timing.type,
217
+ agentId: timing.agentId,
218
+ bytes: timing.bytes,
219
+ decodeAndValidateMs: timing.decodeAndValidateMs,
220
+ internalDispatchMs: timing.internalDispatchMs,
221
+ rawListenersMs: timing.rawListenersMs,
222
+ typedHandlersMs: timing.typedHandlersMs,
223
+ totalMs: timing.totalMs,
224
+ };
225
+ }
195
226
  function mergeCountMap(target, source) {
196
227
  for (const [key, value] of source) {
197
228
  incrementCount(target, key, value);
@@ -9,9 +9,10 @@ import type { OrchestrationGraph, PromptTemplate, Run } from "@otto-code/protoco
9
9
  import type { BrainCatalogModel, BrainDiskUsage, BrainEvals, BrainHfSearchResult, BrainHostStatus, BrainInstalledModel, BrainInventoryModel, BrainJob, BrainLogsTailResponse, BrainLogsWatchResponse, BrainModelBudgetGetResponse, BrainModelDeleteResponse, BrainModelLoadResponse, BrainModelProfileGetResponse, BrainModelProfileSetResponse, BrainModelRenameResetResponse, BrainModelRenameResponse, BrainNetworkInfo, BrainRemoteConfig, BrainRepoQuant, BrainRuntime, ConnectorsListToolsResponse, ConnectorsOauthAuthorizeResponse, ConnectorsOauthDisconnectResponse, CommunicationsGetOverviewResponse, CommunicationsInboxGetHomeResponse, CommunicationsInboxNotificationsAcknowledgeResponse, CommunicationsInboxSearchResponse, CommunicationsInboxSetFavoriteResponse, CommunicationsInboxGetPresenceResponse, CommunicationsInboxGetMessagesResponse, CommunicationsInboxSetPresenceResponse, CommunicationsInboxSetEnabledResponse, CommunicationsInboxSendMessageResponse, CommunicationsRoomGetResponse, CommunicationsRoomThreadGetResponse, CommunicationsRoomMessageSendResponse, CommunicationsRoomReactionSetResponse, IntegrationsAuthorizationGetOverviewResponse, IntegrationsAuthorizationGetMethodsResponse, IntegrationsAuthorizationStartBrowserResponse, IntegrationsZoomStartAuthorizationResponse, CueMoment, MutableDaemonConfig, MutableDaemonConfigPatch, ProjectLink, SpeechSettingsOptions, SpeechTtsPreviewResult, SpeechTtsSpeakResult, SpeechTtsSpeakCancelResult, VisualizerVoiceCuesResult, AgentPersonalitiesGenerateProfileResult } from "@otto-code/protocol/messages";
10
10
  import type { AgentConfigApply } from "@otto-code/protocol/messages";
11
11
  import { type DaemonTransportFactory, type WebSocketFactory } from "./daemon-client-transport.js";
12
- import { type DaemonClientTrafficHotspot, type DaemonClientTrafficTotals } from "./daemon-client-runtime-metrics.js";
12
+ import { type DaemonClientInboundDispatchTiming, type DaemonClientTrafficHotspot, type DaemonClientTrafficTotals } from "./daemon-client-runtime-metrics.js";
13
13
  import { type TerminalStreamEvent } from "./terminal-stream-router.js";
14
14
  import type { BrowserAutomationExecuteRequest, BrowserAutomationExecuteResponse } from "@otto-code/protocol/browser-automation/rpc-schemas";
15
+ export type { DaemonClientInboundDispatchTiming } from "./daemon-client-runtime-metrics.js";
15
16
  export interface Logger {
16
17
  debug(obj: object, msg?: string): void;
17
18
  info(obj: object, msg?: string): void;
@@ -2157,6 +2158,12 @@ export declare class DaemonClient {
2157
2158
  */
2158
2159
  getTrafficTotals(): DaemonClientTrafficTotals | null;
2159
2160
  getTrafficHotspots(limit?: number): DaemonClientTrafficHotspot[];
2161
+ /**
2162
+ * Bounded, timestamped dispatch phases for matching an inbound daemon
2163
+ * message to a browser Long Animation Frame. This is diagnostic evidence,
2164
+ * not a protocol surface.
2165
+ */
2166
+ getInboundDispatchTimings(sinceMs?: number): DaemonClientInboundDispatchTiming[];
2160
2167
  private resolveTransportUrlForAttempt;
2161
2168
  private sendHelloMessage;
2162
2169
  private disposeTransport;
@@ -186,6 +186,15 @@ function concatByteChunks(chunks, size) {
186
186
  }
187
187
  return bytes;
188
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
+ }
189
198
  function getTransportFrameSize(frame) {
190
199
  if (typeof frame === "string") {
191
200
  return frame.length;
@@ -6055,6 +6064,14 @@ export class DaemonClient {
6055
6064
  getTrafficHotspots(limit) {
6056
6065
  return this.runtimeMetrics?.getTrafficHotspots(limit) ?? [];
6057
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
+ }
6058
6075
  resolveTransportUrlForAttempt() {
6059
6076
  return this.config.url;
6060
6077
  }
@@ -6168,6 +6185,7 @@ export class DaemonClient {
6168
6185
  }
6169
6186
  handleJsonPayload(payload, rawBytesLength) {
6170
6187
  const bytes = rawBytesLength ?? payload.length;
6188
+ const dispatchAt = Date.now();
6171
6189
  const startMs = perfNow();
6172
6190
  let parsedJson;
6173
6191
  const parseTraceOpen = this.beginTraceSection("otto.ws.json.parse", {
@@ -6212,9 +6230,21 @@ export class DaemonClient {
6212
6230
  envelopeType: "session",
6213
6231
  messageType: parsed.data.message.type,
6214
6232
  });
6215
- this.handleSessionMessage(parsed.data.message);
6233
+ const phases = this.handleSessionMessage(parsed.data.message);
6216
6234
  const msgType = parsed.data.message.type;
6217
- this.runtimeMetrics?.recordMessage(msgType, bytes, perfNow() - startMs);
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
+ });
6218
6248
  if (parsed.data.message.type === "agent_stream") {
6219
6249
  this.runtimeMetrics?.recordAgentStream(parsed.data.message.payload);
6220
6250
  }
@@ -6426,6 +6456,7 @@ export class DaemonClient {
6426
6456
  });
6427
6457
  }
6428
6458
  handleSessionMessage(msg) {
6459
+ const startedAtMs = perfNow();
6429
6460
  const consumerMessage = normalizeProviderSnapshotUpdateMessage(msg);
6430
6461
  if (consumerMessage.type === "status") {
6431
6462
  const serverInfo = parseServerInfoStatusPayload(consumerMessage.payload);
@@ -6453,6 +6484,7 @@ export class DaemonClient {
6453
6484
  if (consumerMessage.type === "project.scaffold.progress") {
6454
6485
  this.scaffoldProgressListeners.get(consumerMessage.payload.requestId)?.(consumerMessage.payload);
6455
6486
  }
6487
+ const rawListenersStartedAtMs = perfNow();
6456
6488
  if (this.rawMessageListeners.size > 0) {
6457
6489
  for (const handler of this.rawMessageListeners) {
6458
6490
  try {
@@ -6463,6 +6495,7 @@ export class DaemonClient {
6463
6495
  }
6464
6496
  }
6465
6497
  }
6498
+ const typedHandlersStartedAtMs = perfNow();
6466
6499
  const handlers = this.messageHandlers.get(consumerMessage.type);
6467
6500
  if (handlers) {
6468
6501
  for (const handler of handlers) {
@@ -6481,6 +6514,13 @@ export class DaemonClient {
6481
6514
  }
6482
6515
  }
6483
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
+ };
6484
6524
  }
6485
6525
  resolveWaiters(msg) {
6486
6526
  for (const waiter of Array.from(this.waiters)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-code/client",
3
- "version": "0.8.13",
3
+ "version": "0.8.15",
4
4
  "description": "Otto client SDK package",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "files": [
@@ -40,8 +40,8 @@
40
40
  "test": "vitest run"
41
41
  },
42
42
  "dependencies": {
43
- "@otto-code/protocol": "0.8.13",
44
- "@otto-code/relay": "0.8.13",
43
+ "@otto-code/protocol": "0.8.15",
44
+ "@otto-code/relay": "0.8.15",
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "devDependencies": {