@otto-code/client 0.8.10 → 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
  });
@@ -1757,6 +1826,35 @@ export class DaemonClient {
1757
1826
  }
1758
1827
  return { customName: payload.customName };
1759
1828
  }
1829
+ /**
1830
+ * Sets (or with a null target, clears) which tracking board a project shows
1831
+ * on the Kanban screen. The daemon normalizes what it stores - a pasted board
1832
+ * URL comes back as the parsed id - so the caller should render the returned
1833
+ * target rather than its own draft.
1834
+ */
1835
+ async setKanbanProjectTarget(input, requestId) {
1836
+ const payload = await this.sendCorrelatedSessionRequest({
1837
+ requestId,
1838
+ message: {
1839
+ type: "kanban.project.target.set.request",
1840
+ projectId: input.projectId,
1841
+ target: input.target,
1842
+ },
1843
+ responseType: "kanban.project.target.set.response",
1844
+ });
1845
+ if (!payload.accepted) {
1846
+ throw new Error(payload.error ?? "setKanbanProjectTarget rejected");
1847
+ }
1848
+ return { target: payload.target };
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
+ }
1760
1858
  async removeProject(projectId, requestId) {
1761
1859
  const payload = await this.sendNamespacedCorrelatedSessionRequest({
1762
1860
  requestId,
@@ -2004,6 +2102,58 @@ export class DaemonClient {
2004
2102
  },
2005
2103
  });
2006
2104
  }
2105
+ /**
2106
+ * The provider-agnostic Kanban board surface. Each call names its provider
2107
+ * ("memory", "github", ...) - the daemon dispatches to the registered
2108
+ * KanbanProvider implementation and the wire never carries provider-native
2109
+ * identifiers beyond the opaque board/card/column ids.
2110
+ *
2111
+ * A project-scoped request is authoritative: the daemon resolves the
2112
+ * project's configured board target and overrides providerId from it. The
2113
+ * wire still carries providerId so older clients keep working; pass an inert
2114
+ * value (e.g. "github") when a project is supplied.
2115
+ */
2116
+ async kanbanListBoards(input, requestId) {
2117
+ return this.sendNamespacedCorrelatedSessionRequest({
2118
+ requestId,
2119
+ message: {
2120
+ type: "kanban.boards.list.request",
2121
+ providerId: input.providerId,
2122
+ ...(input.projectId ? { projectId: input.projectId } : {}),
2123
+ ...(input.projectKey ? { projectKey: input.projectKey } : {}),
2124
+ requestId: "",
2125
+ },
2126
+ timeout: 60000,
2127
+ });
2128
+ }
2129
+ async kanbanGetBoard(providerId, boardId, requestId) {
2130
+ return this.sendNamespacedCorrelatedSessionRequest({
2131
+ requestId,
2132
+ message: { type: "kanban.board.get.request", providerId, boardId, requestId: "" },
2133
+ timeout: 60000,
2134
+ });
2135
+ }
2136
+ async kanbanMoveCard(input, requestId) {
2137
+ return this.sendNamespacedCorrelatedSessionRequest({
2138
+ requestId,
2139
+ message: { ...input, type: "kanban.card.move.request", requestId: "" },
2140
+ timeout: 60000,
2141
+ });
2142
+ }
2143
+ async kanbanCreateCard(input, requestId) {
2144
+ return this.sendNamespacedCorrelatedSessionRequest({
2145
+ requestId,
2146
+ message: { ...input, type: "kanban.card.create.request", requestId: "" },
2147
+ timeout: 60000,
2148
+ });
2149
+ }
2150
+ async kanbanLinkTask(input, requestId) {
2151
+ return this.sendNamespacedCorrelatedSessionRequest({
2152
+ requestId,
2153
+ message: { ...input, type: "kanban.task.link.request", requestId: "" },
2154
+ timeout: 60000,
2155
+ });
2156
+ }
2007
2157
  async getCommitFileDiff(cwd, sha, path, requestId) {
2008
2158
  const payload = await this.sendNamespacedCorrelatedSessionRequest({
2009
2159
  requestId,
@@ -2189,6 +2339,7 @@ export class DaemonClient {
2189
2339
  ...(options.cursor ? { cursor: options.cursor } : {}),
2190
2340
  ...(typeof options.limit === "number" ? { limit: options.limit } : {}),
2191
2341
  ...(options.projection ? { projection: options.projection } : {}),
2342
+ ...(options.mergeWindow === true ? { mergeWindow: true } : {}),
2192
2343
  });
2193
2344
  const payload = await this.sendRequest({
2194
2345
  requestId: resolvedRequestId,
@@ -2210,6 +2361,28 @@ export class DaemonClient {
2210
2361
  }
2211
2362
  return payload;
2212
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
+ }
2213
2386
  async buildAgentForkContext(agentId, options = {}) {
2214
2387
  const resolvedRequestId = this.createRequestId(options.requestId);
2215
2388
  const message = SessionInboundMessageSchema.parse({
@@ -2574,6 +2747,40 @@ export class DaemonClient {
2574
2747
  }
2575
2748
  return payload.notice ?? null;
2576
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
+ }
2577
2784
  async restartServer(reason, requestId) {
2578
2785
  const resolvedRequestId = this.createRequestId(requestId);
2579
2786
  const message = SessionInboundMessageSchema.parse({
@@ -3140,6 +3347,16 @@ export class DaemonClient {
3140
3347
  responseType: "checkout.refresh.response",
3141
3348
  });
3142
3349
  }
3350
+ async checkoutGitFetch(cwd, requestId) {
3351
+ return this.sendCorrelatedSessionRequest({
3352
+ requestId,
3353
+ message: {
3354
+ type: "checkout.git.fetch.request",
3355
+ cwd,
3356
+ },
3357
+ responseType: "checkout.git.fetch.response",
3358
+ });
3359
+ }
3143
3360
  async checkoutPrCreate(cwd, input, requestId) {
3144
3361
  return this.sendCorrelatedSessionRequest({
3145
3362
  requestId,
@@ -3629,48 +3846,6 @@ export class DaemonClient {
3629
3846
  requestId: input.requestId,
3630
3847
  }));
3631
3848
  }
3632
- /** Create an empty file or a directory. Never overwrites - see FileCreateResultSchema. */
3633
- async createFileEntry(options) {
3634
- const payload = await this.sendCorrelatedSessionRequest({
3635
- requestId: options.requestId,
3636
- message: {
3637
- type: "file.create.request",
3638
- cwd: options.cwd,
3639
- path: options.path,
3640
- kind: options.kind,
3641
- },
3642
- responseType: "file.create.response",
3643
- });
3644
- return payload.result;
3645
- }
3646
- /** Permanent delete - an unlink, not a move to any trash. */
3647
- async deleteFileEntry(options) {
3648
- const payload = await this.sendCorrelatedSessionRequest({
3649
- requestId: options.requestId,
3650
- message: {
3651
- type: "file.delete.request",
3652
- cwd: options.cwd,
3653
- path: options.path,
3654
- recursive: options.recursive,
3655
- },
3656
- responseType: "file.delete.response",
3657
- });
3658
- return payload.result;
3659
- }
3660
- /** Rename, which is also move. Never clobbers an occupied destination. */
3661
- async renameFileEntry(options) {
3662
- const payload = await this.sendCorrelatedSessionRequest({
3663
- requestId: options.requestId,
3664
- message: {
3665
- type: "file.rename.request",
3666
- cwd: options.cwd,
3667
- path: options.path,
3668
- newPath: options.newPath,
3669
- },
3670
- responseType: "file.rename.response",
3671
- });
3672
- return payload.result;
3673
- }
3674
3849
  async refineFile(options) {
3675
3850
  const payload = await this.sendCorrelatedSessionRequest({
3676
3851
  requestId: options.requestId,
@@ -4363,6 +4538,7 @@ export class DaemonClient {
4363
4538
  message: {
4364
4539
  type: "get_providers_snapshot_request",
4365
4540
  cwd: options?.cwd,
4541
+ ifNoneMatch: options?.ifNoneMatch,
4366
4542
  },
4367
4543
  responseType: "get_providers_snapshot_response",
4368
4544
  });
@@ -4456,6 +4632,208 @@ export class DaemonClient {
4456
4632
  responseType: "connectors.oauth.disconnect.response",
4457
4633
  });
4458
4634
  }
4635
+ /**
4636
+ * Read the daemon-owned, provider-neutral communications inbox projection.
4637
+ * Requires server_info.features.communications; callers own that one gate.
4638
+ */
4639
+ async communicationsGetOverview(requestId) {
4640
+ const payload = await this.sendCorrelatedSessionRequest({
4641
+ requestId,
4642
+ message: { type: "communications.get_overview.request" },
4643
+ responseType: "communications.get_overview.response",
4644
+ });
4645
+ return payload.overview;
4646
+ }
4647
+ async communicationsInboxGetHome(providerId, requestId) {
4648
+ const payload = await this.sendCorrelatedSessionRequest({
4649
+ requestId,
4650
+ message: { type: "communications.inbox.get_home.request", providerId },
4651
+ responseType: "communications.inbox.get_home.response",
4652
+ });
4653
+ return payload.home;
4654
+ }
4655
+ async communicationsInboxAcknowledgeNotifications(input, requestId) {
4656
+ const payload = await this.sendCorrelatedSessionRequest({
4657
+ requestId,
4658
+ message: { type: "communications.inbox.notifications.acknowledge.request", ...input },
4659
+ responseType: "communications.inbox.notifications.acknowledge.response",
4660
+ });
4661
+ return payload.home;
4662
+ }
4663
+ async communicationsInboxSearch(input, requestId) {
4664
+ const payload = await this.sendCorrelatedSessionRequest({
4665
+ requestId,
4666
+ message: { type: "communications.inbox.search.request", ...input },
4667
+ responseType: "communications.inbox.search.response",
4668
+ });
4669
+ return payload.results;
4670
+ }
4671
+ async communicationsInboxSetFavorite(input, requestId) {
4672
+ const payload = await this.sendCorrelatedSessionRequest({
4673
+ requestId,
4674
+ message: { type: "communications.inbox.set_favorite.request", ...input },
4675
+ responseType: "communications.inbox.set_favorite.response",
4676
+ });
4677
+ return payload.home;
4678
+ }
4679
+ async communicationsInboxGetPresence(providerId, requestId) {
4680
+ const payload = await this.sendCorrelatedSessionRequest({
4681
+ requestId,
4682
+ message: { type: "communications.inbox.get_presence.request", providerId },
4683
+ responseType: "communications.inbox.get_presence.response",
4684
+ });
4685
+ return payload.presence;
4686
+ }
4687
+ async communicationsInboxSetPresence(input, requestId) {
4688
+ const payload = await this.sendCorrelatedSessionRequest({
4689
+ requestId,
4690
+ message: { type: "communications.inbox.set_presence.request", ...input },
4691
+ responseType: "communications.inbox.set_presence.response",
4692
+ });
4693
+ return payload.presence;
4694
+ }
4695
+ async communicationsInboxSetEnabled(input, requestId) {
4696
+ const payload = await this.sendCorrelatedSessionRequest({
4697
+ requestId,
4698
+ message: { type: "communications.inbox.set_enabled.request", ...input },
4699
+ responseType: "communications.inbox.set_enabled.response",
4700
+ });
4701
+ return payload.presence;
4702
+ }
4703
+ async communicationsInboxGetMessages(input, requestId) {
4704
+ const payload = await this.sendCorrelatedSessionRequest({
4705
+ requestId,
4706
+ message: { type: "communications.inbox.get_messages.request", ...input },
4707
+ responseType: "communications.inbox.get_messages.response",
4708
+ });
4709
+ return payload.messages;
4710
+ }
4711
+ async communicationsInboxSendMessage(input, requestId) {
4712
+ const payload = await this.sendCorrelatedSessionRequest({
4713
+ requestId,
4714
+ message: { type: "communications.inbox.send_message.request", ...input },
4715
+ responseType: "communications.inbox.send_message.response",
4716
+ });
4717
+ return payload.message;
4718
+ }
4719
+ /** Requires server_info.features.communicationsRooms. */
4720
+ async communicationsRoomGet(input, requestId) {
4721
+ const payload = await this.sendCorrelatedSessionRequest({
4722
+ requestId,
4723
+ message: { type: "communications.room.get.request", ...input },
4724
+ responseType: "communications.room.get.response",
4725
+ });
4726
+ return payload.room;
4727
+ }
4728
+ async communicationsRoomThreadGet(input, requestId) {
4729
+ const payload = await this.sendCorrelatedSessionRequest({
4730
+ requestId,
4731
+ message: { type: "communications.room.thread.get.request", ...input },
4732
+ responseType: "communications.room.thread.get.response",
4733
+ });
4734
+ return payload.messages;
4735
+ }
4736
+ async communicationsRoomMessageSend(input, requestId) {
4737
+ const payload = await this.sendCorrelatedSessionRequest({
4738
+ requestId,
4739
+ message: { type: "communications.room.message.send.request", ...input },
4740
+ responseType: "communications.room.message.send.response",
4741
+ });
4742
+ return payload.message;
4743
+ }
4744
+ async communicationsRoomReactionSet(input, requestId) {
4745
+ const payload = await this.sendCorrelatedSessionRequest({
4746
+ requestId,
4747
+ message: { type: "communications.room.reaction.set.request", ...input },
4748
+ responseType: "communications.room.reaction.set.response",
4749
+ });
4750
+ return payload.message;
4751
+ }
4752
+ /**
4753
+ * Daemon-owned meeting transcript library. Requires
4754
+ * `server_info.features.meetingTranscripts`; callers own that capability gate.
4755
+ */
4756
+ async meetingsTranscriptsList(requestId) {
4757
+ const payload = await this.sendCorrelatedSessionRequest({
4758
+ requestId,
4759
+ message: { type: "meetings.transcripts.list.request" },
4760
+ responseType: "meetings.transcripts.list.response",
4761
+ });
4762
+ return payload.records;
4763
+ }
4764
+ async meetingsTranscriptsCreate(input, requestId) {
4765
+ const payload = await this.sendCorrelatedSessionRequest({
4766
+ requestId,
4767
+ message: { type: "meetings.transcripts.create.request", ...input },
4768
+ responseType: "meetings.transcripts.create.response",
4769
+ });
4770
+ return payload.record;
4771
+ }
4772
+ async meetingsTranscriptsUpdate(input, requestId) {
4773
+ const payload = await this.sendCorrelatedSessionRequest({
4774
+ requestId,
4775
+ message: { type: "meetings.transcripts.update.request", ...input },
4776
+ responseType: "meetings.transcripts.update.response",
4777
+ });
4778
+ return payload.record;
4779
+ }
4780
+ async meetingsTranscriptsDelete(id, requestId) {
4781
+ const payload = await this.sendCorrelatedSessionRequest({
4782
+ requestId,
4783
+ message: { type: "meetings.transcripts.delete.request", id },
4784
+ responseType: "meetings.transcripts.delete.response",
4785
+ });
4786
+ return payload.deleted;
4787
+ }
4788
+ /**
4789
+ * Read daemon-owned connection metadata for reusable integration settings.
4790
+ * Requires server_info.features.integrationAuthorization; callers own that
4791
+ * one capability gate.
4792
+ */
4793
+ async integrationsAuthorizationGetOverview(requestId) {
4794
+ const payload = await this.sendCorrelatedSessionRequest({
4795
+ requestId,
4796
+ message: { type: "integrations.authorization.get_overview.request" },
4797
+ responseType: "integrations.authorization.get_overview.response",
4798
+ });
4799
+ return payload.overview;
4800
+ }
4801
+ /**
4802
+ * List the daemon's nonsecret authorization choices for an integration.
4803
+ * Requires server_info.features.integrationAuthorization; callers own that
4804
+ * one capability gate.
4805
+ */
4806
+ async integrationsAuthorizationGetMethods(integrationId, requestId) {
4807
+ const payload = await this.sendCorrelatedSessionRequest({
4808
+ requestId,
4809
+ message: {
4810
+ type: "integrations.authorization.get_methods.request",
4811
+ ...(integrationId ? { integrationId } : {}),
4812
+ },
4813
+ responseType: "integrations.authorization.get_methods.response",
4814
+ });
4815
+ return payload.methods;
4816
+ }
4817
+ /**
4818
+ * Starts a daemon-owned browser sign-in through a registered integration
4819
+ * driver. Requires server_info.features.integrationAuthorizationBrowserFlow;
4820
+ * callers own that one capability gate.
4821
+ */
4822
+ async integrationsAuthorizationStartBrowser(input, requestId) {
4823
+ return this.sendCorrelatedSessionRequest({
4824
+ requestId,
4825
+ message: { type: "integrations.authorization.start_browser.request", ...input },
4826
+ responseType: "integrations.authorization.start_browser.response",
4827
+ });
4828
+ }
4829
+ /** Starts the daemon-owned Zoom Team Chat browser sign-in. */
4830
+ async integrationsZoomStartAuthorization(requestId) {
4831
+ return this.sendCorrelatedSessionRequest({
4832
+ requestId,
4833
+ message: { type: "integrations.zoom.start_authorization.request" },
4834
+ responseType: "integrations.zoom.start_authorization.response",
4835
+ });
4836
+ }
4459
4837
  /**
4460
4838
  * The brain's status. Pass `resources` only from a surface that renders the
4461
4839
  * live CPU/RAM/GPU numbers: it costs an `nvidia-smi` spawn on the brain, and
@@ -4600,7 +4978,7 @@ export class DaemonClient {
4600
4978
  }
4601
4979
  return payload.runtimes;
4602
4980
  }
4603
- async brainModelsPull(model, componentsOrRequestId, quantOrRequestId) {
4981
+ async brainModelsPull(model, componentsOrRequestId, quantOrRequestId, expectedBytes) {
4604
4982
  const components = Array.isArray(componentsOrRequestId) ? componentsOrRequestId : undefined;
4605
4983
  const quant = Array.isArray(componentsOrRequestId) ? quantOrRequestId : undefined;
4606
4984
  const correlationId = typeof componentsOrRequestId === "string" ? componentsOrRequestId : undefined;
@@ -4611,6 +4989,7 @@ export class DaemonClient {
4611
4989
  model,
4612
4990
  ...(components ? { components } : {}),
4613
4991
  ...(quant ? { quant } : {}),
4992
+ ...(expectedBytes !== undefined ? { expectedBytes } : {}),
4614
4993
  },
4615
4994
  responseType: "brain.models.pull.response",
4616
4995
  });
@@ -4638,10 +5017,16 @@ export class DaemonClient {
4638
5017
  }
4639
5018
  return payload.quants;
4640
5019
  }
4641
- async brainModelsAdd(repo, quant, components, requestId) {
5020
+ async brainModelsAdd(repo, quant, components, requestId, expectedBytes) {
4642
5021
  const payload = await this.sendCorrelatedSessionRequest({
4643
5022
  requestId,
4644
- message: { type: "brain.models.add.request", repo, quant, components },
5023
+ message: {
5024
+ type: "brain.models.add.request",
5025
+ repo,
5026
+ quant,
5027
+ components,
5028
+ ...(expectedBytes !== undefined ? { expectedBytes } : {}),
5029
+ },
4645
5030
  responseType: "brain.models.add.response",
4646
5031
  });
4647
5032
  return unwrapBrainJob(payload);
@@ -4686,6 +5071,31 @@ export class DaemonClient {
4686
5071
  });
4687
5072
  return unwrapBrainJob(payload);
4688
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
+ }
4689
5099
  async brainJobsList(requestId) {
4690
5100
  const payload = await this.sendCorrelatedSessionRequest({
4691
5101
  requestId,
@@ -4801,6 +5211,15 @@ export class DaemonClient {
4801
5211
  }
4802
5212
  return payload.status;
4803
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
+ // ============================================================================
4804
5223
  /** Delete a model's files. The brain refuses while that model is loaded. */
4805
5224
  async brainModelDelete(modelId, requestId) {
4806
5225
  const payload = await this.sendCorrelatedSessionRequest({
@@ -4862,6 +5281,21 @@ export class DaemonClient {
4862
5281
  }
4863
5282
  return payload;
4864
5283
  }
5284
+ /**
5285
+ * Turn the live Brain log feed on or off for this socket.
5286
+ *
5287
+ * Only meaningful against a daemon advertising `features.brainLogWatch`; older
5288
+ * daemons push every line regardless, and the request would go unrouted.
5289
+ * Watching is per socket, so this does not affect the same account's other
5290
+ * connected clients.
5291
+ */
5292
+ async brainLogsWatch(watching, requestId) {
5293
+ return this.sendCorrelatedSessionRequest({
5294
+ requestId,
5295
+ message: { type: "brain.logs.watch.request", watching },
5296
+ responseType: "brain.logs.watch.response",
5297
+ });
5298
+ }
4865
5299
  async getSpeechSettingsOptions(requestId) {
4866
5300
  return this.sendNamespacedCorrelatedSessionRequest({
4867
5301
  requestId,
@@ -5382,86 +5816,6 @@ export class DaemonClient {
5382
5816
  responseType: "terminal.compatibility.diagnostic.response",
5383
5817
  });
5384
5818
  }
5385
- async createChatRoom(options) {
5386
- return this.sendCorrelatedSessionRequest({
5387
- requestId: options.requestId,
5388
- message: {
5389
- type: "chat/create",
5390
- name: options.name,
5391
- ...(options.purpose ? { purpose: options.purpose } : {}),
5392
- },
5393
- responseType: "chat/create/response",
5394
- });
5395
- }
5396
- async listChatRooms(requestId) {
5397
- return this.sendCorrelatedSessionRequest({
5398
- requestId,
5399
- message: {
5400
- type: "chat/list",
5401
- },
5402
- responseType: "chat/list/response",
5403
- });
5404
- }
5405
- async inspectChatRoom(options) {
5406
- return this.sendCorrelatedSessionRequest({
5407
- requestId: options.requestId,
5408
- message: {
5409
- type: "chat/inspect",
5410
- room: options.room,
5411
- },
5412
- responseType: "chat/inspect/response",
5413
- });
5414
- }
5415
- async deleteChatRoom(options) {
5416
- return this.sendCorrelatedSessionRequest({
5417
- requestId: options.requestId,
5418
- message: {
5419
- type: "chat/delete",
5420
- room: options.room,
5421
- },
5422
- responseType: "chat/delete/response",
5423
- });
5424
- }
5425
- async postChatMessage(options) {
5426
- return this.sendCorrelatedSessionRequest({
5427
- requestId: options.requestId,
5428
- message: {
5429
- type: "chat/post",
5430
- room: options.room,
5431
- body: options.body,
5432
- ...(options.authorAgentId ? { authorAgentId: options.authorAgentId } : {}),
5433
- ...(options.replyToMessageId ? { replyToMessageId: options.replyToMessageId } : {}),
5434
- },
5435
- responseType: "chat/post/response",
5436
- });
5437
- }
5438
- async readChatMessages(options) {
5439
- return this.sendCorrelatedSessionRequest({
5440
- requestId: options.requestId,
5441
- message: {
5442
- type: "chat/read",
5443
- room: options.room,
5444
- ...(typeof options.limit === "number" ? { limit: options.limit } : {}),
5445
- ...(options.since ? { since: options.since } : {}),
5446
- ...(options.authorAgentId ? { authorAgentId: options.authorAgentId } : {}),
5447
- },
5448
- responseType: "chat/read/response",
5449
- timeout: options.timeout,
5450
- });
5451
- }
5452
- async waitForChatMessages(options) {
5453
- return this.sendCorrelatedSessionRequest({
5454
- requestId: options.requestId,
5455
- message: {
5456
- type: "chat/wait",
5457
- room: options.room,
5458
- ...(options.afterMessageId ? { afterMessageId: options.afterMessageId } : {}),
5459
- ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
5460
- },
5461
- responseType: "chat/wait/response",
5462
- timeout: (options.timeoutMs ?? 0) + 10000,
5463
- });
5464
- }
5465
5819
  async scheduleCreate(options) {
5466
5820
  return this.sendCorrelatedSessionRequest({
5467
5821
  requestId: options.requestId,
@@ -5661,76 +6015,6 @@ export class DaemonClient {
5661
6015
  responseType: "artifact.get-content.response",
5662
6016
  });
5663
6017
  }
5664
- async loopRun(options) {
5665
- return this.sendCorrelatedSessionRequest({
5666
- requestId: options.requestId,
5667
- message: {
5668
- type: "loop/run",
5669
- prompt: options.prompt,
5670
- cwd: options.cwd,
5671
- ...(options.provider ? { provider: options.provider } : {}),
5672
- ...(options.model ? { model: options.model } : {}),
5673
- ...(options.modeId ? { modeId: options.modeId } : {}),
5674
- ...(options.verifierProvider ? { verifierProvider: options.verifierProvider } : {}),
5675
- ...(options.verifierModel ? { verifierModel: options.verifierModel } : {}),
5676
- ...(options.verifierModeId ? { verifierModeId: options.verifierModeId } : {}),
5677
- ...(options.verifyPrompt ? { verifyPrompt: options.verifyPrompt } : {}),
5678
- ...(options.verifyChecks && options.verifyChecks.length > 0
5679
- ? { verifyChecks: options.verifyChecks }
5680
- : {}),
5681
- ...(options.name ? { name: options.name } : {}),
5682
- ...(typeof options.sleepMs === "number" ? { sleepMs: options.sleepMs } : {}),
5683
- ...(typeof options.maxIterations === "number"
5684
- ? { maxIterations: options.maxIterations }
5685
- : {}),
5686
- ...(typeof options.maxTimeMs === "number" ? { maxTimeMs: options.maxTimeMs } : {}),
5687
- },
5688
- responseType: "loop/run/response",
5689
- });
5690
- }
5691
- async loopList(requestId) {
5692
- return this.sendCorrelatedSessionRequest({
5693
- requestId,
5694
- message: {
5695
- type: "loop/list",
5696
- },
5697
- responseType: "loop/list/response",
5698
- });
5699
- }
5700
- async loopInspect(options) {
5701
- const normalized = typeof options === "string" ? { id: options } : options;
5702
- return this.sendCorrelatedSessionRequest({
5703
- requestId: normalized.requestId,
5704
- message: {
5705
- type: "loop/inspect",
5706
- id: normalized.id,
5707
- },
5708
- responseType: "loop/inspect/response",
5709
- });
5710
- }
5711
- async loopLogs(options, afterSeq) {
5712
- const normalized = typeof options === "string" ? { id: options, afterSeq } : options;
5713
- return this.sendCorrelatedSessionRequest({
5714
- requestId: normalized.requestId,
5715
- message: {
5716
- type: "loop/logs",
5717
- id: normalized.id,
5718
- ...(typeof normalized.afterSeq === "number" ? { afterSeq: normalized.afterSeq } : {}),
5719
- },
5720
- responseType: "loop/logs/response",
5721
- });
5722
- }
5723
- async loopStop(options) {
5724
- const normalized = typeof options === "string" ? { id: options } : options;
5725
- return this.sendCorrelatedSessionRequest({
5726
- requestId: normalized.requestId,
5727
- message: {
5728
- type: "loop/stop",
5729
- id: normalized.id,
5730
- },
5731
- responseType: "loop/stop/response",
5732
- });
5733
- }
5734
6018
  onTerminalStreamEvent(handler) {
5735
6019
  return this.terminalStreams.onEvent(handler);
5736
6020
  }
@@ -5784,7 +6068,7 @@ export class DaemonClient {
5784
6068
  return;
5785
6069
  }
5786
6070
  try {
5787
- this.transport.send(JSON.stringify({
6071
+ this.sendJsonMessage("hello", "hello", {
5788
6072
  type: "hello",
5789
6073
  clientId: this.config.clientId,
5790
6074
  clientType: this.config.clientType ?? "cli",
@@ -5797,10 +6081,12 @@ export class DaemonClient {
5797
6081
  // The daemon gates project.updated.notification on this (session.ts),
5798
6082
  // so dropping it silently kills cross-session project renames.
5799
6083
  [CLIENT_CAPS.projectUpdates]: true,
6084
+ [CLIENT_CAPS.communicationsPresenceUpdates]: true,
6085
+ [CLIENT_CAPS.compactProviderSnapshots]: true,
5800
6086
  ...this.config.capabilities,
5801
6087
  },
5802
6088
  ...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}),
5803
- }));
6089
+ });
5804
6090
  }
5805
6091
  catch (error) {
5806
6092
  const message = error instanceof Error ? error.message : "Failed to send hello message";
@@ -5865,25 +6151,37 @@ export class DaemonClient {
5865
6151
  return;
5866
6152
  }
5867
6153
  const rawBytes = asUint8Array(rawData);
5868
- if (rawBytes && this.tryHandleBinaryFrame(rawBytes)) {
5869
- 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);
5870
6164
  }
5871
- const payload = decodeMessageData(rawData);
5872
- if (!payload) {
5873
- return;
6165
+ finally {
6166
+ this.endTraceSection(isOpen);
5874
6167
  }
5875
- this.handleJsonPayload(payload, rawBytes?.byteLength);
5876
6168
  }
5877
6169
  handleJsonPayload(payload, rawBytesLength) {
5878
6170
  const bytes = rawBytesLength ?? payload.length;
5879
6171
  const startMs = perfNow();
5880
6172
  let parsedJson;
6173
+ const parseTraceOpen = this.beginTraceSection("otto.ws.json.parse", {
6174
+ size: String(bytes),
6175
+ });
5881
6176
  try {
5882
6177
  parsedJson = JSON.parse(payload);
5883
6178
  }
5884
6179
  catch {
5885
6180
  return;
5886
6181
  }
6182
+ finally {
6183
+ this.endTraceSection(parseTraceOpen);
6184
+ }
5887
6185
  const parsed = validateWSOutboundMessage(parsedJson);
5888
6186
  if (!parsed.success) {
5889
6187
  const responseIdentity = extractCorrelatedResponseIdentity(parsedJson);
@@ -5902,10 +6200,18 @@ export class DaemonClient {
5902
6200
  }
5903
6201
  this.consecutiveLivenessFailures = 0;
5904
6202
  if (parsed.data.type === "pong") {
6203
+ this.traceInstant("otto.ws.message.inbound", {
6204
+ envelopeType: "pong",
6205
+ messageType: "pong",
6206
+ });
5905
6207
  this.resolvePingProbe();
5906
6208
  this.runtimeMetrics?.recordMessage("pong", bytes, perfNow() - startMs);
5907
6209
  return;
5908
6210
  }
6211
+ this.traceInstant("otto.ws.message.inbound", {
6212
+ envelopeType: "session",
6213
+ messageType: parsed.data.message.type,
6214
+ });
5909
6215
  this.handleSessionMessage(parsed.data.message);
5910
6216
  const msgType = parsed.data.message.type;
5911
6217
  this.runtimeMetrics?.recordMessage(msgType, bytes, perfNow() - startMs);
@@ -5916,6 +6222,11 @@ export class DaemonClient {
5916
6222
  tryHandleBinaryFrame(rawBytes) {
5917
6223
  const fileFrame = decodeFileTransferFrame(rawBytes);
5918
6224
  if (fileFrame) {
6225
+ this.traceInstant("otto.ws.message.inbound", {
6226
+ envelopeType: "binary",
6227
+ messageType: "file",
6228
+ opcode: String(fileFrame.opcode),
6229
+ });
5919
6230
  this.consecutiveLivenessFailures = 0;
5920
6231
  this.handleFileTransferFrame(fileFrame);
5921
6232
  this.runtimeMetrics?.recordBinaryFrame("other", rawBytes.byteLength, 0);
@@ -5925,6 +6236,11 @@ export class DaemonClient {
5925
6236
  if (!frame) {
5926
6237
  return false;
5927
6238
  }
6239
+ this.traceInstant("otto.ws.message.inbound", {
6240
+ envelopeType: "binary",
6241
+ messageType: "terminal",
6242
+ opcode: String(frame.opcode),
6243
+ });
5928
6244
  this.consecutiveLivenessFailures = 0;
5929
6245
  const binaryStartMs = perfNow();
5930
6246
  this.terminalStreams.handleFrame(frame);