@otto-code/client 0.7.5 → 0.7.6

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.
@@ -1,5 +1,5 @@
1
1
  import { createClientChannel, } from "@otto-code/relay/e2ee";
2
- import { extractRelayMessageData, normalizeTransportPayload, } from "./daemon-client-transport-utils.js";
2
+ import { extractRelayMessage, normalizeTransportPayload } from "./daemon-client-transport-utils.js";
3
3
  export function createRelayE2eeTransportFactory(args) {
4
4
  return ({ url, headers }) => {
5
5
  const base = args.baseFactory({ url, headers });
@@ -38,7 +38,7 @@ export function createEncryptedTransport(base, daemonPublicKeyB64, logger) {
38
38
  if (closed) {
39
39
  return;
40
40
  }
41
- emitHandlers(messageHandlers, data);
41
+ emitHandlers(messageHandlers, data, data instanceof ArrayBuffer);
42
42
  };
43
43
  const relayTransport = {
44
44
  send: (data) => {
@@ -81,8 +81,8 @@ export function createEncryptedTransport(base, daemonPublicKeyB64, logger) {
81
81
  base.onOpen(() => {
82
82
  void startHandshake();
83
83
  });
84
- base.onMessage((event) => {
85
- relayTransport.onmessage?.(extractRelayMessageData(event));
84
+ base.onMessage((data, isBinary) => {
85
+ relayTransport.onmessage?.(extractRelayMessage(data, isBinary));
86
86
  });
87
87
  base.onClose((event) => {
88
88
  const record = event;
@@ -1,7 +1,7 @@
1
1
  export interface DaemonTransport {
2
2
  send: (data: string | Uint8Array | ArrayBuffer) => void;
3
3
  close: (code?: number, reason?: string) => void;
4
- onMessage: (handler: (data: unknown) => void) => () => void;
4
+ onMessage: (handler: (data: unknown, isBinary: boolean) => void) => () => void;
5
5
  onOpen: (handler: () => void) => () => void;
6
6
  onClose: (handler: (event?: unknown) => void) => () => void;
7
7
  onError: (handler: (event?: unknown) => void) => () => void;
@@ -1,6 +1,10 @@
1
1
  export declare function copyArrayBufferViewToBuffer(data: ArrayBufferView): ArrayBuffer;
2
2
  export declare function normalizeTransportPayload(data: string | Uint8Array | ArrayBuffer): string | ArrayBuffer;
3
- export declare function extractRelayMessageData(event: unknown): string | ArrayBuffer;
3
+ export interface RelayTransportMessage {
4
+ data: string | ArrayBuffer;
5
+ isBinary: boolean;
6
+ }
7
+ export declare function extractRelayMessage(event: unknown, nodeIsBinary?: boolean): RelayTransportMessage;
4
8
  export declare function describeTransportClose(event?: unknown): string;
5
9
  export declare function describeTransportError(event?: unknown): string;
6
10
  export declare function safeRandomId(): string;
@@ -10,18 +10,20 @@ export function normalizeTransportPayload(data) {
10
10
  }
11
11
  return copyArrayBufferViewToBuffer(data);
12
12
  }
13
- export function extractRelayMessageData(event) {
13
+ export function extractRelayMessage(event, nodeIsBinary) {
14
14
  const raw = event && typeof event === "object" && "data" in event
15
15
  ? event.data
16
16
  : event;
17
- if (typeof raw === "string")
18
- return raw;
17
+ const isBinary = nodeIsBinary ?? typeof raw !== "string";
18
+ if (!isBinary) {
19
+ return { data: decodeMessageData(raw) ?? String(raw ?? ""), isBinary: false };
20
+ }
19
21
  if (raw instanceof ArrayBuffer)
20
- return raw;
22
+ return { data: raw, isBinary: true };
21
23
  if (ArrayBuffer.isView(raw)) {
22
- return copyArrayBufferViewToBuffer(raw);
24
+ return { data: copyArrayBufferViewToBuffer(raw), isBinary: true };
23
25
  }
24
- return String(raw ?? "");
26
+ return { data: String(raw ?? ""), isBinary: true };
25
27
  }
26
28
  export function describeTransportClose(event) {
27
29
  if (!event) {
@@ -1,5 +1,5 @@
1
1
  export type { DaemonTransport, DaemonTransportFactory, TransportLogger, WebSocketFactory, WebSocketLike, } from "./daemon-client-transport-types.js";
2
- export { decodeMessageData, describeTransportClose, describeTransportError, encodeUtf8String, extractRelayMessageData, normalizeTransportPayload, safeRandomId, } from "./daemon-client-transport-utils.js";
2
+ export { decodeMessageData, describeTransportClose, describeTransportError, encodeUtf8String, extractRelayMessage, normalizeTransportPayload, safeRandomId, } from "./daemon-client-transport-utils.js";
3
3
  export { createEncryptedTransport, createRelayE2eeTransportFactory, } from "./daemon-client-relay-e2ee-transport.js";
4
4
  export { bindWsHandler, createWebSocketTransportFactory, defaultWebSocketFactory, } from "./daemon-client-websocket-transport.js";
5
5
  //# sourceMappingURL=daemon-client-transport.d.ts.map
@@ -1,4 +1,4 @@
1
- export { decodeMessageData, describeTransportClose, describeTransportError, encodeUtf8String, extractRelayMessageData, normalizeTransportPayload, safeRandomId, } from "./daemon-client-transport-utils.js";
1
+ export { decodeMessageData, describeTransportClose, describeTransportError, encodeUtf8String, extractRelayMessage, normalizeTransportPayload, safeRandomId, } from "./daemon-client-transport-utils.js";
2
2
  export { createEncryptedTransport, createRelayE2eeTransportFactory, } from "./daemon-client-relay-e2ee-transport.js";
3
3
  export { bindWsHandler, createWebSocketTransportFactory, defaultWebSocketFactory, } from "./daemon-client-websocket-transport.js";
4
4
  //# sourceMappingURL=daemon-client-transport.js.map
@@ -1,3 +1,4 @@
1
+ import { extractRelayMessage } from "./daemon-client-transport-utils.js";
1
2
  export function defaultWebSocketFactory(url, options) {
2
3
  const globalWs = globalThis.WebSocket;
3
4
  if (!globalWs) {
@@ -40,10 +41,17 @@ export function createWebSocketTransportFactory(factory) {
40
41
  onOpen: (handler) => bindWsHandler(ws, "open", handler),
41
42
  onClose: (handler) => bindWsHandler(ws, "close", handler),
42
43
  onError: (handler) => bindWsHandler(ws, "error", handler),
43
- onMessage: (handler) => bindWsHandler(ws, "message", handler),
44
+ onMessage: (handler) => bindWsMessageHandler(ws, handler),
44
45
  };
45
46
  };
46
47
  }
48
+ function bindWsMessageHandler(ws, handler) {
49
+ const listener = (...args) => {
50
+ const message = extractRelayMessage(args[0], typeof args[1] === "boolean" ? args[1] : undefined);
51
+ handler(message.data, message.isBinary);
52
+ };
53
+ return bindWsHandler(ws, "message", listener);
54
+ }
47
55
  function bindTemporaryEarlyCloseErrorHandler(ws) {
48
56
  const noop = () => { };
49
57
  if (typeof ws.addEventListener === "function") {
@@ -1,7 +1,9 @@
1
+ import type { AgentAttentionNotificationPayload } from "@otto-code/protocol/agent-attention-notification";
1
2
  import type { z } from "zod";
3
+ import type { ProjectGithubCloneProtocol } from "@otto-code/protocol/messages";
2
4
  import { type ClientCapability } from "@otto-code/protocol/client-capabilities";
3
5
  import { AgentRefreshedStatusPayloadSchema, CheckoutRenameBranchResponseSchema, RenameTerminalResponseSchema, RestartRequestedStatusPayloadSchema, ShutdownRequestedStatusPayloadSchema, DaemonUpdateResponseSchema, type ServerInfoStatusPayload } from "@otto-code/protocol/messages";
4
- import type { AgentStreamEventPayload, AgentSnapshotPayload, ProjectPlacementPayload, AgentPermissionResolvedMessage, CreateAgentRequestMessage, CreateOttoWorktreeRequest, CodeListFilesResponse, CodeSymbolLocation, CodeDefinitionLocation, CodeDefinitionStatus, CodeRenameApplyStatus, CodeRenameFileOutcome, CodeRenameUndoFile, CodeRenameUndoStatus, CodeHoverRange, CodeRenameEdit, CodeRenameFilePlan, LspLanguageState, LspRunningServer, CodeSolutionGetTreeResponse, CodeSolutionLoadProjectResponse, SolutionFormat, SolutionRef, SolutionTreeFolder, SolutionTreeProject, SolutionProjectNode, SolutionProjectStatus, SolutionPackageReference, FileCreateResult, FileDeleteResult, FileDownloadTokenResponse, FileEntryKind, FileEol, FileRenameResult, FileReplaceFileResult, FileReplaceRequest, FileReplaceResponse, FileSearchResultPayload, FileSearchSummary, FileUploadResponse, FileExplorerResponse, FileWatchEventPayload, FileWriteResult, FileRefineResult, FileRefineDocument, FileRefineReference, FetchAgentTimelineResponseMessage, AgentForkContextResponseMessage, GitSetupOptions, CheckoutStatusResponse, CheckoutCommitResponse, CheckoutGitCommitResponse, CheckoutGitCommitAgentResponse, CheckoutGitRollbackResponse, CheckoutGitFileHistoryResponse, CheckoutGitFileCommitDiffResponse, CheckoutGitFileBlameResponse, CheckoutGitFileOriginResponse, CheckoutGitGetOperationLogResponse, CheckoutMergeResponse, CheckoutMergeFromBaseResponse, CheckoutPullResponse, CheckoutPushResponse, CheckoutRefreshResponse, CheckoutPrCreateResponse, CheckoutPrMergeResponse, CheckoutPrMergeMethod, CheckoutGithubSetAutoMergeResponse, CheckoutGithubGetCheckDetailsResponse, PreviewListConfigResponse, PreviewStartResponse, PreviewBindTabResponse, PreviewStopResponse, CheckoutPrStatusResponse, PullRequestTimelineResponse, CheckoutSwitchBranchResponse, StashSaveResponse, StashPopResponse, StashListResponse, ValidateBranchResponse, BranchSuggestionsResponse, GitHubSearchResponse, GitHubSearchRequest, GitHostingProviderId, HostingSearchRequest, HostingSearchResponse, HostingAuthStatusResponse, DirectorySuggestionsResponse, OttoWorktreeListResponse, OttoWorktreeArchiveResponse, ProjectIconResponse, ContextCategory, ContextPromptPreviewGetResponseMessage, ContextReportGetResponseMessage, ContextEdgeConvertResponseMessage, ContextFindingsFixResponseMessage, PersonalityMemoryListResponseMessage, PersonalityMemoryUpdateResponseMessage, PersonalityMemoryTransferResponseMessage, PersonalityMemoryStatsResponseMessage, ProjectAddResponse, ProjectScaffoldGit, ProjectScaffoldProgress, ProjectScaffoldResponse, HostingListRepositoriesResponse, HostingListOwnersResponse, OpenProjectResponseMessage, ArchiveWorkspaceResponseMessage, WorkspaceArchivePreflightResponse, WorktreeBaseRefSetResponse, WorktreeReattachListResponse, WorktreeReattachResponse, WorktreeReattachTarget, WorkspaceSetupStatusResponseMessage, ListCommandsResponse, ListProviderFeaturesResponseMessage, ListProviderModelsResponseMessage, ListProviderModesResponseMessage, ListAvailableProvidersResponse, GetProvidersSnapshotResponseMessage, RefreshProvidersSnapshotResponseMessage, ProviderDiagnosticResponseMessage, ProviderUsageListResponseMessage, StatsActivityGetResponseMessage, StatsActivityResetResponseMessage, UsageLogGetResponseMessage, AgentContextGetUsageResponseMessage, DaemonGetStatusResponse, DaemonGetPairingOfferResponse, DiagnosticsResponse, AgentRewindResponseMessage, ListTerminalsResponse, CreateTerminalResponse, SubscribeTerminalResponse, SubscribeTerminalRequest, CloseItemsResponse, AttachmentsImagesClearResponse, AttachmentsImagesStatsResponse, HistoryAgentsClearArchivedResponse, KillTerminalResponse, CaptureTerminalResponse, TerminalInput, SessionInboundMessage, SessionOutboundMessage, SendAgentMessageRequest, AgentPromptDelivery, TasksSuggestedStartMode, OttoConfigRaw, OttoConfigRevision, WorkspaceCreateRequest } from "@otto-code/protocol/messages";
6
+ import type { AgentStreamEventPayload, AgentSnapshotPayload, ProjectPlacementPayload, AgentPermissionResolvedMessage, CreateAgentRequestMessage, CreateOttoWorktreeRequest, CodeListFilesResponse, CodeSymbolLocation, CodeDefinitionLocation, CodeDefinitionStatus, CodeRenameApplyStatus, CodeRenameFileOutcome, CodeRenameUndoFile, CodeRenameUndoStatus, CodeHoverRange, CodeRenameEdit, CodeRenameFilePlan, LspLanguageState, LspRunningServer, CodeSolutionGetTreeResponse, CodeSolutionLoadProjectResponse, SolutionFormat, SolutionRef, SolutionTreeFolder, SolutionTreeProject, SolutionProjectNode, SolutionProjectStatus, SolutionPackageReference, FileCreateResult, FileDeleteResult, FileDownloadTokenResponse, FileEntryKind, FileEol, FileRenameResult, FileReplaceFileResult, FileReplaceRequest, FileReplaceResponse, FileSearchResultPayload, FileSearchSummary, FileUploadResponse, FileExplorerResponse, FileWatchEventPayload, FileWriteResult, FileRefineResult, FileRefineDocument, FileRefineReference, FetchAgentTimelineResponseMessage, AgentForkContextResponseMessage, GitSetupOptions, CheckoutStatusResponse, CheckoutCommitResponse, CheckoutGitCommitResponse, CheckoutGitCommitAgentResponse, CheckoutGitRollbackResponse, CheckoutGitFileHistoryResponse, CheckoutGitFileCommitDiffResponse, CheckoutGitFileBlameResponse, CheckoutGitFileOriginResponse, CheckoutGitGetOperationLogResponse, CheckoutMergeResponse, CheckoutMergeFromBaseResponse, CheckoutPullResponse, CheckoutPushResponse, CheckoutRefreshResponse, CheckoutPrCreateResponse, CheckoutPrMergeResponse, CheckoutPrMergeMethod, CheckoutGithubSetAutoMergeResponse, CheckoutGithubGetCheckDetailsResponse, PreviewListConfigResponse, PreviewStartResponse, PreviewBindTabResponse, PreviewStopResponse, CheckoutPrStatusResponse, PullRequestTimelineResponse, CheckoutSwitchBranchResponse, StashSaveResponse, StashPopResponse, StashListResponse, ValidateBranchResponse, BranchSuggestionsResponse, FileVersion, CheckoutCommit, ParsedDiffFile, WorkspaceRecoveryState, CheckoutForgeGetCheckDetailsResponse, CheckoutForgeSetAutoMergeResponse, ProjectCreateDirectoryResponse, FsFileWriteResult, FsFileWriteBinaryResult, GitHubSearchResponse, GitHubSearchRequest, ForgeSearchResponse, ForgeSearchRequest, GitHostingProviderId, HostingSearchRequest, HostingSearchResponse, HostingAuthStatusResponse, DirectorySuggestionsResponse, OttoWorktreeListResponse, OttoWorktreeArchiveResponse, ProjectIconResponse, ContextCategory, ContextPromptPreviewGetResponseMessage, ContextReportGetResponseMessage, ContextEdgeConvertResponseMessage, ContextFindingsFixResponseMessage, PersonalityMemoryListResponseMessage, PersonalityMemoryUpdateResponseMessage, PersonalityMemoryTransferResponseMessage, PersonalityMemoryStatsResponseMessage, ProjectAddResponse, ProjectScaffoldGit, ProjectScaffoldProgress, ProjectScaffoldResponse, HostingListRepositoriesResponse, HostingListOwnersResponse, OpenProjectResponseMessage, ArchiveWorkspaceResponseMessage, WorkspaceArchivePreflightResponse, WorktreeBaseRefSetResponse, WorktreeReattachListResponse, WorktreeReattachResponse, WorktreeReattachTarget, WorkspaceSetupStatusResponseMessage, ListCommandsResponse, ListProviderFeaturesResponseMessage, ListProviderModelsResponseMessage, ListProviderModesResponseMessage, ListAvailableProvidersResponse, GetProvidersSnapshotResponseMessage, RefreshProvidersSnapshotResponseMessage, ProviderDiagnosticResponseMessage, ProviderUsageListResponseMessage, StatsActivityGetResponseMessage, StatsActivityResetResponseMessage, UsageLogGetResponseMessage, AgentContextGetUsageResponseMessage, DaemonGetStatusResponse, DaemonGetPairingOfferResponse, DiagnosticsResponse, AgentRewindResponseMessage, ListTerminalsResponse, CreateTerminalResponse, SubscribeTerminalResponse, SubscribeTerminalRequest, CloseItemsResponse, AttachmentsImagesClearResponse, AttachmentsImagesStatsResponse, HistoryAgentsClearArchivedResponse, KillTerminalResponse, CaptureTerminalResponse, TerminalInput, SessionInboundMessage, SessionOutboundMessage, SendAgentMessageRequest, AgentPromptDelivery, TasksSuggestedStartMode, OttoConfigRaw, OttoConfigRevision, WorkspaceCreateRequest } from "@otto-code/protocol/messages";
5
7
  import type { AgentPermissionRequest, AgentPermissionResponse, AgentPersistenceHandle, AgentProviderNotice, AgentProvider, AgentSessionConfig } from "@otto-code/protocol/agent-types";
6
8
  import type { OrchestrationGraph, PromptTemplate, Run } from "@otto-code/protocol/orchestration";
7
9
  import type { BrainCatalogModel, BrainEvals, BrainHfSearchResult, BrainHostStatus, BrainInstalledModel, BrainJob, BrainNetworkInfo, BrainRemoteConfig, BrainRepoQuant, BrainRuntime, ConnectorsListToolsResponse, CueMoment, MutableDaemonConfig, MutableDaemonConfigPatch, ProjectLink, SpeechSettingsOptions, SpeechTtsPreviewResult, SpeechTtsSpeakResult, SpeechTtsSpeakCancelResult, VisualizerVoiceCuesResult, AgentPersonalitiesGenerateProfileResult } from "@otto-code/protocol/messages";
@@ -156,6 +158,12 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides {
156
158
  personality?: CreateAgentRequestMessage["personality"];
157
159
  env?: CreateAgentRequestMessage["env"];
158
160
  workspaceId?: string;
161
+ /**
162
+ * Caller agent making this request. The daemon resolves the caller's own
163
+ * workspace and parentage from it, so a managed CLI run nests under its
164
+ * parent exactly as agent-scoped MCP creation does.
165
+ */
166
+ callerAgentId?: string;
159
167
  initialPrompt?: string;
160
168
  clientMessageId?: string;
161
169
  outputSchema?: Record<string, unknown>;
@@ -168,7 +176,7 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides {
168
176
  requestId?: string;
169
177
  labels?: Record<string, string>;
170
178
  }
171
- export interface CreateOttoWorktreeInput extends Pick<CreateOttoWorktreeRequest, "cwd" | "projectId" | "worktreeSlug" | "firstAgentContext" | "refName" | "action" | "githubPrNumber"> {
179
+ export interface CreateOttoWorktreeInput extends Pick<CreateOttoWorktreeRequest, "cwd" | "projectId" | "worktreeSlug" | "firstAgentContext" | "refName" | "action" | "githubPrNumber" | "checkoutSource"> {
172
180
  }
173
181
  type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
174
182
  type SubscribeCheckoutDiffPayload = Extract<SessionOutboundMessage, {
@@ -207,6 +215,23 @@ type StashListPayload = StashListResponse["payload"];
207
215
  type ValidateBranchPayload = ValidateBranchResponse["payload"];
208
216
  type BranchSuggestionsPayload = BranchSuggestionsResponse["payload"];
209
217
  type GitHubSearchPayload = GitHubSearchResponse["payload"];
218
+ type CheckoutForgeGetCheckDetailsPayload = CheckoutForgeGetCheckDetailsResponse["payload"];
219
+ type CheckoutForgeSetAutoMergePayload = CheckoutForgeSetAutoMergeResponse["payload"];
220
+ export type ProjectCreateDirectoryPayload = ProjectCreateDirectoryResponse["payload"];
221
+ export type ProjectListPayload = Extract<SessionOutboundMessage, {
222
+ type: "project.list.response";
223
+ }>["payload"];
224
+ export type WorkspaceGithubSearchRepositoriesPayload = Extract<SessionOutboundMessage, {
225
+ type: "workspace.github.search_repositories.response";
226
+ }>["payload"];
227
+ export interface AgentAttentionRequiredNotification {
228
+ agentId: string;
229
+ reason: "finished" | "error" | "permission";
230
+ timestamp: string;
231
+ shouldNotify: boolean;
232
+ notification?: AgentAttentionNotificationPayload;
233
+ }
234
+ type ForgeSearchPayload = ForgeSearchResponse["payload"];
210
235
  export type HostingSearchPayload = HostingSearchResponse["payload"];
211
236
  export type HostingAuthStatusPayload = HostingAuthStatusResponse["payload"];
212
237
  type DirectorySuggestionsPayload = DirectorySuggestionsResponse["payload"];
@@ -234,6 +259,8 @@ export interface FileReadResult {
234
259
  * daemons older than v0.4.4. Absent means "not reported", not "LF".
235
260
  */
236
261
  eol?: FileEol;
262
+ /** Opaque version tag for optimistic-concurrency writes; same caveat as eol. */
263
+ revision?: string;
237
264
  }
238
265
  export interface TextFileReadResult {
239
266
  path: string;
@@ -254,6 +281,23 @@ export interface FileWriteOptions {
254
281
  eol?: FileEol;
255
282
  requestId?: string;
256
283
  }
284
+ /**
285
+ * Bytes to a workspace path. Gated on `features.binaryFileWrite`. Unlike
286
+ * {@link FileWriteOptions} this carries no precondition: `overwrite` is the
287
+ * whole policy — see `FsFileWriteBinaryRequestSchema` for why a generated
288
+ * artifact has nothing to reconcile against.
289
+ */
290
+ export interface FileWriteBinaryOptions {
291
+ cwd: string;
292
+ /** Workspace-relative, like every other file RPC. */
293
+ path: string;
294
+ /** Sent as file-transfer frames, not inside the request. */
295
+ bytes: Uint8Array | ArrayBuffer;
296
+ overwrite?: boolean;
297
+ requestId?: string;
298
+ /** Frame size, for tests that want to observe chunking. Defaults to 1 MB. */
299
+ chunkSize?: number;
300
+ }
257
301
  /**
258
302
  * The general file-mutation surface — what exists in a directory, rather than
259
303
  * what is inside a file. Gated on `features.fileMutations`; there is no
@@ -541,7 +585,21 @@ export interface FetchAgentTimelineOptions {
541
585
  requestId?: string;
542
586
  timeout?: number;
543
587
  }
588
+ export type ProviderSubagentListPayload = Extract<SessionOutboundMessage, {
589
+ type: "agent.provider_subagents.list.response";
590
+ }>["payload"];
591
+ export type ProviderSubagentTimelinePayload = Extract<SessionOutboundMessage, {
592
+ type: "agent.provider_subagents.timeline.get.response";
593
+ }>["payload"];
594
+ export interface FetchProviderSubagentTimelineOptions {
595
+ direction?: ProviderSubagentTimelinePayload["direction"];
596
+ cursor?: FetchAgentTimelineCursor;
597
+ limit?: number;
598
+ requestId?: string;
599
+ timeout?: number;
600
+ }
544
601
  export interface AgentForkContextOptions {
602
+ boundaryCursor?: FetchAgentTimelineCursor;
545
603
  boundaryMessageId?: string;
546
604
  requestId?: string;
547
605
  }
@@ -779,6 +837,9 @@ export interface WaitForFinishResult {
779
837
  error: string | null;
780
838
  lastMessage: string | null;
781
839
  }
840
+ type ProjectGithubClonePayload = Extract<SessionOutboundMessage, {
841
+ type: "project.github.clone.response";
842
+ }>["payload"];
782
843
  export declare class DaemonClient {
783
844
  private config;
784
845
  private transport;
@@ -801,6 +862,7 @@ export declare class DaemonClient {
801
862
  private connectionState;
802
863
  private checkoutDiffSubscriptions;
803
864
  private terminalDirectorySubscriptions;
865
+ private fileSubscriptions;
804
866
  private readonly terminalStreams;
805
867
  private readonly scaffoldProgressListeners;
806
868
  private pendingBinaryFileReads;
@@ -1091,6 +1153,55 @@ export declare class DaemonClient {
1091
1153
  resumeAgent(handle: AgentPersistenceHandle, overrides?: Partial<AgentSessionConfig>): Promise<AgentSnapshotPayload>;
1092
1154
  importAgent(input: ImportAgentInput): Promise<AgentSnapshotPayload>;
1093
1155
  refreshAgent(agentId: string, requestId?: string): Promise<AgentRefreshedStatusPayload>;
1156
+ listProviderSubagents(parentAgentId: string, options?: {
1157
+ requestId?: string;
1158
+ timeout?: number;
1159
+ }): Promise<ProviderSubagentListPayload>;
1160
+ fetchProviderSubagentTimeline(parentAgentId: string, subagentId: string, options?: FetchProviderSubagentTimelineOptions): Promise<ProviderSubagentTimelinePayload>;
1161
+ checkoutForgeGetCheckDetails(input: {
1162
+ cwd: string;
1163
+ repoOwner?: string;
1164
+ repoName?: string;
1165
+ checkRunId?: number;
1166
+ workflowRunId?: number;
1167
+ changeRequestNumber?: number;
1168
+ }, requestId?: string): Promise<CheckoutForgeGetCheckDetailsPayload>;
1169
+ checkoutForgeSetAutoMerge(cwd: string, input: {
1170
+ enabled: true;
1171
+ method: CheckoutPrMergeMethod;
1172
+ } | {
1173
+ enabled: false;
1174
+ }, requestId?: string): Promise<CheckoutForgeSetAutoMergePayload>;
1175
+ createProjectDirectory(input: {
1176
+ parentPath: string;
1177
+ name: string;
1178
+ }, requestId?: string): Promise<ProjectCreateDirectoryPayload>;
1179
+ getCommitFileDiff(cwd: string, sha: string, path: string, requestId?: string): Promise<{
1180
+ file: ParsedDiffFile | null;
1181
+ }>;
1182
+ inspectWorkspaceRecovery(workspaceId: string, requestId?: string): Promise<WorkspaceRecoveryState>;
1183
+ listCheckoutCommits(cwd: string, requestId?: string): Promise<{
1184
+ baseRef: string | null;
1185
+ commits: CheckoutCommit[];
1186
+ }>;
1187
+ listProjects(requestId?: string): Promise<ProjectListPayload>;
1188
+ onAgentAttentionRequired(handler: (notification: AgentAttentionRequiredNotification) => void): () => void;
1189
+ restoreWorkspace(workspaceId: string, requestId?: string): Promise<void>;
1190
+ searchGithubRepositories(input: {
1191
+ query: string;
1192
+ limit?: number;
1193
+ }, requestId?: string): Promise<WorkspaceGithubSearchRepositoriesPayload>;
1194
+ setAgentTimelineSubscription(agentIds: string[]): Promise<void>;
1195
+ setWorkspacePinned(workspaceId: string, pinned: boolean, requestId?: string): Promise<{
1196
+ pinnedAt: string | null;
1197
+ }>;
1198
+ subscribeFile(input: {
1199
+ cwd: string;
1200
+ path: string;
1201
+ }, onUpdate: (version: FileVersion) => void): Promise<{
1202
+ initial: FileVersion;
1203
+ unsubscribe: () => void;
1204
+ }>;
1094
1205
  fetchAgentTimeline(agentId: string, options?: FetchAgentTimelineOptions): Promise<FetchAgentTimelinePayload>;
1095
1206
  buildAgentForkContext(agentId: string, options?: AgentForkContextOptions): Promise<AgentForkContextPayload>;
1096
1207
  sendAgentMessage(agentId: string, text: string, options?: SendMessageOptions): Promise<SendAgentMessageResult>;
@@ -1118,7 +1229,7 @@ export declare class DaemonClient {
1118
1229
  cancelled?: boolean;
1119
1230
  }>;
1120
1231
  setAgentMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null>;
1121
- setAgentModel(agentId: string, modelId: string | null): Promise<void>;
1232
+ setAgentModel(agentId: string, modelId: string | null): Promise<AgentProviderNotice | null>;
1122
1233
  setAgentFeature(agentId: string, featureId: string, value: unknown): Promise<void>;
1123
1234
  setAgentThinkingOption(agentId: string, thinkingOptionId: string | null): Promise<AgentProviderNotice | null>;
1124
1235
  /**
@@ -1276,6 +1387,12 @@ export declare class DaemonClient {
1276
1387
  query?: string;
1277
1388
  limit?: number;
1278
1389
  }, requestId?: string): Promise<BranchSuggestionsPayload>;
1390
+ searchForge(options: {
1391
+ cwd: string;
1392
+ query: string;
1393
+ limit?: number;
1394
+ kinds?: ForgeSearchRequest["kinds"];
1395
+ }, requestId?: string): Promise<ForgeSearchPayload>;
1279
1396
  searchGitHub(options: {
1280
1397
  cwd: string;
1281
1398
  query: string;
@@ -1307,8 +1424,36 @@ export declare class DaemonClient {
1307
1424
  * save-precondition baseline (modifiedAt, eol, hash) alongside the content.
1308
1425
  */
1309
1426
  readTextFile(cwd: string, path: string, requestId?: string): Promise<TextFileReadResult>;
1427
+ /**
1428
+ * The fs.file.write RPC: optimistic-concurrency writes keyed by an opaque
1429
+ * revision. The hash-keyed `writeFile` below is kept for callers
1430
+ * that have not moved over.
1431
+ */
1432
+ writeFsFile(input: {
1433
+ cwd: string;
1434
+ path: string;
1435
+ content: string;
1436
+ expectedModifiedAt: string;
1437
+ expectedRevision?: string;
1438
+ }): Promise<FsFileWriteResult>;
1310
1439
  /** Conditional save — see FileWriteRequestSchema for the no-clobber contract. */
1311
1440
  writeFile(options: FileWriteOptions): Promise<FileWriteResult>;
1441
+ /**
1442
+ * Write bytes to a workspace file — the path for generated artifacts the
1443
+ * text write cannot carry (it refuses binary targets outright). Gated on
1444
+ * `features.binaryFileWrite`; there is no client-side substitute, because
1445
+ * the client never touches a workspace file on any platform.
1446
+ *
1447
+ * Shaped like {@link uploadFile}: the JSON request says where the bytes go
1448
+ * and how many to expect, then the bytes follow as file-transfer frames
1449
+ * correlated on the same `requestId`. The daemon answers at FileEnd.
1450
+ */
1451
+ writeBinaryFile(options: FileWriteBinaryOptions): Promise<FsFileWriteBinaryResult>;
1452
+ /**
1453
+ * FileBegin, chunks, FileEnd. Synchronous through `sendBinaryFrame`, so the
1454
+ * frames leave in order and behind the JSON request that announced them.
1455
+ */
1456
+ private sendFileTransfer;
1312
1457
  /** Create an empty file or a directory. Never overwrites — see FileCreateResultSchema. */
1313
1458
  createFileEntry(options: FileCreateOptions): Promise<FileCreateResult>;
1314
1459
  /** Permanent delete — an unlink, not a move to any trash. */
@@ -1775,8 +1920,67 @@ export declare class DaemonClient {
1775
1920
  private recordLivenessFailure;
1776
1921
  private handleSessionMessage;
1777
1922
  private resolveWaiters;
1923
+ private rejectWaitersForRequestId;
1778
1924
  private clearWaiters;
1779
1925
  private toEvent;
1780
1926
  private waitForWithCancel;
1927
+ cloneGithubProject(input: {
1928
+ repo: string;
1929
+ targetDirectory: string;
1930
+ cloneProtocol?: ProjectGithubCloneProtocol;
1931
+ }, requestId?: string): Promise<ProjectGithubClonePayload>;
1932
+ /**
1933
+ * `includeDiscovered` also returns the Scripts the workspace's own project
1934
+ * files declare (package.json scripts today), each tagged with its `source`.
1935
+ * Gate it on `server_info.features.workspaceScriptDiscovery` — an older
1936
+ * daemon ignores the flag and answers with the otto.json list only.
1937
+ */
1938
+ listWorkspaceScripts(workspaceId: string, options?: {
1939
+ includeDiscovered?: boolean;
1940
+ requestId?: string;
1941
+ }): Promise<Extract<SessionOutboundMessage, {
1942
+ type: "workspace.script.list.response";
1943
+ }>["payload"]>;
1944
+ startWorkspaceScriptWithStatus(workspaceId: string, scriptName: string, requestId?: string): Promise<Extract<SessionOutboundMessage, {
1945
+ type: "workspace.script.start.response";
1946
+ }>["payload"]>;
1947
+ stopWorkspaceScript(workspaceId: string, scriptName: string, requestId?: string): Promise<Extract<SessionOutboundMessage, {
1948
+ type: "workspace.script.stop.response";
1949
+ }>["payload"]>;
1950
+ connectHub(hubUrl: string, token: string, requestId?: string): Promise<{
1951
+ requestId: string;
1952
+ status: {
1953
+ state: "connecting" | "connected" | "not_connected" | "reconnecting" | "disconnecting" | "revoked";
1954
+ daemonId: string | null;
1955
+ hubOrigin: string | null;
1956
+ scopes: string[];
1957
+ connectedAt: string | null;
1958
+ lastError: string | null;
1959
+ };
1960
+ }>;
1961
+ getHubStatus(requestId?: string): Promise<{
1962
+ requestId: string;
1963
+ status: {
1964
+ state: "connecting" | "connected" | "not_connected" | "reconnecting" | "disconnecting" | "revoked";
1965
+ daemonId: string | null;
1966
+ hubOrigin: string | null;
1967
+ scopes: string[];
1968
+ connectedAt: string | null;
1969
+ lastError: string | null;
1970
+ };
1971
+ }>;
1972
+ disconnectHub(force?: boolean, requestId?: string): Promise<{
1973
+ requestId: string;
1974
+ status: {
1975
+ state: "connecting" | "connected" | "not_connected" | "reconnecting" | "disconnecting" | "revoked";
1976
+ daemonId: string | null;
1977
+ hubOrigin: string | null;
1978
+ scopes: string[];
1979
+ connectedAt: string | null;
1980
+ lastError: string | null;
1981
+ };
1982
+ warning?: string | undefined;
1983
+ }>;
1984
+ private requireHubRelationshipSupport;
1781
1985
  }
1782
1986
  //# sourceMappingURL=daemon-client.d.ts.map
@@ -57,6 +57,16 @@ class DaemonRpcError extends Error {
57
57
  this.code = params.code;
58
58
  }
59
59
  }
60
+ class DaemonProtocolError extends Error {
61
+ constructor(identity) {
62
+ const responseLabel = identity.responseType ?? "unknown response";
63
+ super(`Response validation failed for ${responseLabel}`);
64
+ this.code = "invalid_response";
65
+ this.name = "DaemonProtocolError";
66
+ this.requestId = identity.requestId;
67
+ this.responseType = identity.responseType;
68
+ }
69
+ }
60
70
  class PingTimeoutError extends Error {
61
71
  constructor(timeoutMs) {
62
72
  super(`Ping timed out (${timeoutMs}ms)`);
@@ -64,6 +74,40 @@ class PingTimeoutError extends Error {
64
74
  this.name = "PingTimeoutError";
65
75
  }
66
76
  }
77
+ /**
78
+ * Pull the request correlation out of a frame that failed schema validation.
79
+ * Reads only the envelope and `payload.requestId`, which is exactly the part a
80
+ * malformed response still gets right, so the waiting caller can be failed with
81
+ * a real reason rather than left to time out.
82
+ */
83
+ function extractCorrelatedResponseIdentity(input) {
84
+ if (!input || typeof input !== "object") {
85
+ return null;
86
+ }
87
+ const envelope = input;
88
+ if (envelope.type !== "session" || !envelope.message || typeof envelope.message !== "object") {
89
+ return null;
90
+ }
91
+ const message = envelope.message;
92
+ if (typeof message.type !== "string" ||
93
+ !(message.type === "rpc_error" ||
94
+ message.type.endsWith("_response") ||
95
+ message.type.endsWith(".response") ||
96
+ message.type.endsWith("/response"))) {
97
+ return null;
98
+ }
99
+ if (!message.payload || typeof message.payload !== "object") {
100
+ return null;
101
+ }
102
+ const payload = message.payload;
103
+ if (typeof payload.requestId !== "string") {
104
+ return null;
105
+ }
106
+ return {
107
+ requestId: payload.requestId,
108
+ responseType: message.type,
109
+ };
110
+ }
67
111
  function toTimeoutError(error, label, timeoutMs) {
68
112
  if (error instanceof PingTimeoutError) {
69
113
  return new Error(`${label} timed out (${timeoutMs}ms)`);
@@ -120,6 +164,7 @@ function legacyExplorerFileToBytes(file) {
120
164
  kind: file.kind,
121
165
  modifiedAt: file.modifiedAt,
122
166
  eol: file.eol,
167
+ revision: file.revision,
123
168
  };
124
169
  }
125
170
  function binaryFileKind(mime, encoding) {
@@ -180,6 +225,9 @@ function unwrapBrainJob(payload) {
180
225
  }
181
226
  return payload.job;
182
227
  }
228
+ // A repo clone can take minutes on a large history, so it gets its own budget
229
+ // rather than the default request timeout.
230
+ const PROJECT_GITHUB_CLONE_TIMEOUT_MS = 5 * 60 * 1000;
183
231
  export class DaemonClient {
184
232
  constructor(config) {
185
233
  this.config = config;
@@ -203,6 +251,7 @@ export class DaemonClient {
203
251
  this.connectionState = { status: "idle" };
204
252
  this.checkoutDiffSubscriptions = new Map();
205
253
  this.terminalDirectorySubscriptions = new Map();
254
+ this.fileSubscriptions = new Map();
206
255
  this.terminalStreams = new TerminalStreamRouter();
207
256
  // requestId -> progress listener for an in-flight project.scaffold.request.
208
257
  // Entries are always removed in scaffoldProject's finally block.
@@ -670,7 +719,7 @@ export class DaemonClient {
670
719
  return null;
671
720
  }
672
721
  return { kind: "ok", value };
673
- }, timeout, params.options);
722
+ }, timeout, { ...params.options, requestId: params.requestId });
674
723
  try {
675
724
  await this.sendSessionMessageOrThrow(params.message);
676
725
  }
@@ -1276,6 +1325,7 @@ export class DaemonClient {
1276
1325
  ...(options.personality ? { personality: options.personality } : {}),
1277
1326
  ...(options.env ? { env: options.env } : {}),
1278
1327
  ...(options.workspaceId !== undefined ? { workspaceId: options.workspaceId } : {}),
1328
+ ...(options.callerAgentId !== undefined ? { callerAgentId: options.callerAgentId } : {}),
1279
1329
  ...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}),
1280
1330
  ...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}),
1281
1331
  ...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
@@ -1853,6 +1903,266 @@ export class DaemonClient {
1853
1903
  },
1854
1904
  });
1855
1905
  }
1906
+ async listProviderSubagents(parentAgentId, options = {}) {
1907
+ const requestId = this.createRequestId(options.requestId);
1908
+ const message = SessionInboundMessageSchema.parse({
1909
+ type: "agent.provider_subagents.list.request",
1910
+ parentAgentId,
1911
+ requestId,
1912
+ });
1913
+ const payload = await this.sendRequest({
1914
+ requestId,
1915
+ message,
1916
+ timeout: options.timeout,
1917
+ options: { skipQueue: true },
1918
+ select: (response) => response.type === "agent.provider_subagents.list.response" &&
1919
+ response.payload.requestId === requestId
1920
+ ? response.payload
1921
+ : null,
1922
+ });
1923
+ if (payload.error) {
1924
+ throw new Error(payload.error);
1925
+ }
1926
+ return payload;
1927
+ }
1928
+ async fetchProviderSubagentTimeline(parentAgentId, subagentId, options = {}) {
1929
+ const requestId = this.createRequestId(options.requestId);
1930
+ const message = SessionInboundMessageSchema.parse({
1931
+ type: "agent.provider_subagents.timeline.get.request",
1932
+ parentAgentId,
1933
+ subagentId,
1934
+ requestId,
1935
+ ...(options.direction ? { direction: options.direction } : {}),
1936
+ ...(options.cursor ? { cursor: options.cursor } : {}),
1937
+ ...(typeof options.limit === "number" ? { limit: options.limit } : {}),
1938
+ });
1939
+ const payload = await this.sendRequest({
1940
+ requestId,
1941
+ message,
1942
+ timeout: options.timeout,
1943
+ options: { skipQueue: true },
1944
+ select: (response) => response.type === "agent.provider_subagents.timeline.get.response" &&
1945
+ response.payload.requestId === requestId
1946
+ ? response.payload
1947
+ : null,
1948
+ });
1949
+ if (payload.error) {
1950
+ throw new Error(payload.error);
1951
+ }
1952
+ return payload;
1953
+ }
1954
+ async checkoutForgeGetCheckDetails(input, requestId) {
1955
+ return this.sendNamespacedCorrelatedSessionRequest({
1956
+ requestId,
1957
+ message: {
1958
+ type: "checkout.forge.get_check_details.request",
1959
+ cwd: input.cwd,
1960
+ repoOwner: input.repoOwner,
1961
+ repoName: input.repoName,
1962
+ checkRunId: input.checkRunId,
1963
+ workflowRunId: input.workflowRunId,
1964
+ changeRequestNumber: input.changeRequestNumber,
1965
+ },
1966
+ timeout: 60000,
1967
+ });
1968
+ }
1969
+ async checkoutForgeSetAutoMerge(cwd, input, requestId) {
1970
+ return this.sendNamespacedCorrelatedSessionRequest({
1971
+ requestId,
1972
+ message: {
1973
+ type: "checkout.forge.set_auto_merge.request",
1974
+ cwd,
1975
+ enabled: input.enabled,
1976
+ ...(input.enabled ? { mergeMethod: input.method } : {}),
1977
+ },
1978
+ timeout: 60000,
1979
+ });
1980
+ }
1981
+ async createProjectDirectory(input, requestId) {
1982
+ return this.sendNamespacedCorrelatedSessionRequest({
1983
+ requestId,
1984
+ message: {
1985
+ type: "project.create_directory.request",
1986
+ parentPath: input.parentPath,
1987
+ name: input.name,
1988
+ },
1989
+ });
1990
+ }
1991
+ async getCommitFileDiff(cwd, sha, path, requestId) {
1992
+ const payload = await this.sendNamespacedCorrelatedSessionRequest({
1993
+ requestId,
1994
+ message: {
1995
+ type: "checkout.commits.file_diff.request",
1996
+ cwd,
1997
+ sha,
1998
+ path,
1999
+ },
2000
+ timeout: 60000,
2001
+ });
2002
+ if (payload.error) {
2003
+ throw new Error(payload.error.message);
2004
+ }
2005
+ return { file: payload.file };
2006
+ }
2007
+ async inspectWorkspaceRecovery(workspaceId, requestId) {
2008
+ const payload = await this.sendNamespacedCorrelatedSessionRequest({
2009
+ requestId,
2010
+ message: {
2011
+ type: "workspace.recovery.inspect.request",
2012
+ workspaceId,
2013
+ },
2014
+ });
2015
+ return payload.state;
2016
+ }
2017
+ async listCheckoutCommits(cwd, requestId) {
2018
+ const payload = await this.sendNamespacedCorrelatedSessionRequest({
2019
+ requestId,
2020
+ message: {
2021
+ type: "checkout.commits.list.request",
2022
+ cwd,
2023
+ },
2024
+ timeout: 60000,
2025
+ });
2026
+ if (payload.error) {
2027
+ throw new Error(payload.error.message);
2028
+ }
2029
+ return { baseRef: payload.baseRef, commits: payload.commits };
2030
+ }
2031
+ async listProjects(requestId) {
2032
+ const resolvedRequestId = this.createRequestId(requestId);
2033
+ const message = SessionInboundMessageSchema.parse({
2034
+ type: "project.list.request",
2035
+ requestId: resolvedRequestId,
2036
+ });
2037
+ return this.sendRequest({
2038
+ requestId: resolvedRequestId,
2039
+ message,
2040
+ options: { skipQueue: true },
2041
+ select: (msg) => {
2042
+ if (msg.type !== "project.list.response")
2043
+ return null;
2044
+ if (msg.payload.requestId !== resolvedRequestId)
2045
+ return null;
2046
+ return msg.payload;
2047
+ },
2048
+ });
2049
+ }
2050
+ onAgentAttentionRequired(handler) {
2051
+ const unsubscribeLegacy = this.on("agent_stream", (message) => {
2052
+ if (message.payload.event.type !== "attention_required") {
2053
+ return;
2054
+ }
2055
+ const event = message.payload.event;
2056
+ handler({
2057
+ agentId: message.payload.agentId,
2058
+ reason: event.reason,
2059
+ timestamp: event.timestamp,
2060
+ shouldNotify: event.shouldNotify,
2061
+ ...(event.notification ? { notification: event.notification } : {}),
2062
+ });
2063
+ });
2064
+ const unsubscribeDedicated = this.on("agent_attention_required", (message) => {
2065
+ handler(message.payload);
2066
+ });
2067
+ return () => {
2068
+ unsubscribeLegacy();
2069
+ unsubscribeDedicated();
2070
+ };
2071
+ }
2072
+ async restoreWorkspace(workspaceId, requestId) {
2073
+ const payload = await this.sendNamespacedCorrelatedSessionRequest({
2074
+ requestId,
2075
+ message: {
2076
+ type: "workspace.recovery.restore.request",
2077
+ workspaceId,
2078
+ },
2079
+ timeout: 150000,
2080
+ });
2081
+ if (!payload.accepted) {
2082
+ throw new Error(payload.error ?? "Workspace recovery was rejected by the host");
2083
+ }
2084
+ }
2085
+ async searchGithubRepositories(input, requestId) {
2086
+ return this.sendNamespacedCorrelatedSessionRequest({
2087
+ requestId,
2088
+ message: {
2089
+ type: "workspace.github.search_repositories.request",
2090
+ query: input.query,
2091
+ limit: input.limit,
2092
+ },
2093
+ });
2094
+ }
2095
+ async setAgentTimelineSubscription(agentIds) {
2096
+ // COMPAT(selectiveAgentTimeline): added in v0.1.106. Old daemons keep their
2097
+ // legacy global stream and do not understand this RPC. Remove after
2098
+ // 2027-01-12 once the supported daemon floor is >= v0.1.106.
2099
+ if (!this.lastServerInfoMessage?.features?.selectiveAgentTimeline) {
2100
+ return;
2101
+ }
2102
+ const requestId = this.createRequestId();
2103
+ const normalizedAgentIds = [...new Set(agentIds)].sort();
2104
+ const message = SessionInboundMessageSchema.parse({
2105
+ type: "agent.timeline.set_subscription.request",
2106
+ agentIds: normalizedAgentIds,
2107
+ requestId,
2108
+ });
2109
+ await this.sendRequest({
2110
+ requestId,
2111
+ message,
2112
+ options: { skipQueue: true },
2113
+ select: (response) => {
2114
+ if (response.type !== "agent.timeline.set_subscription.response") {
2115
+ return null;
2116
+ }
2117
+ return response.payload.requestId === requestId ? response.payload : null;
2118
+ },
2119
+ });
2120
+ }
2121
+ async setWorkspacePinned(workspaceId, pinned, requestId) {
2122
+ const payload = await this.sendCorrelatedSessionRequest({
2123
+ requestId,
2124
+ message: {
2125
+ type: "workspace.pin.set.request",
2126
+ workspaceId,
2127
+ pinned,
2128
+ },
2129
+ responseType: "workspace.pin.set.response",
2130
+ });
2131
+ if (!payload.accepted) {
2132
+ throw new Error(payload.error ?? "setWorkspacePinned rejected");
2133
+ }
2134
+ return { pinnedAt: payload.pinnedAt };
2135
+ }
2136
+ async subscribeFile(input, onUpdate) {
2137
+ const subscriptionId = this.createRequestId();
2138
+ this.fileSubscriptions.set(subscriptionId, { ...input, onUpdate });
2139
+ try {
2140
+ const payload = await this.sendCorrelatedSessionRequest({
2141
+ message: {
2142
+ type: "fs.file.subscribe.request",
2143
+ cwd: input.cwd,
2144
+ path: input.path,
2145
+ subscriptionId,
2146
+ },
2147
+ responseType: "fs.file.subscribe.response",
2148
+ });
2149
+ return {
2150
+ initial: payload.initial,
2151
+ unsubscribe: () => {
2152
+ if (!this.fileSubscriptions.delete(subscriptionId))
2153
+ return;
2154
+ void this.sendCorrelatedSessionRequest({
2155
+ message: { type: "fs.file.unsubscribe.request", subscriptionId },
2156
+ responseType: "fs.file.unsubscribe.response",
2157
+ }).catch(() => undefined);
2158
+ },
2159
+ };
2160
+ }
2161
+ catch (error) {
2162
+ this.fileSubscriptions.delete(subscriptionId);
2163
+ throw error;
2164
+ }
2165
+ }
1856
2166
  async fetchAgentTimeline(agentId, options = {}) {
1857
2167
  const resolvedRequestId = this.createRequestId(options.requestId);
1858
2168
  const message = SessionInboundMessageSchema.parse({
@@ -1890,6 +2200,7 @@ export class DaemonClient {
1890
2200
  type: "agent.fork_context.request",
1891
2201
  agentId,
1892
2202
  requestId: resolvedRequestId,
2203
+ ...(options.boundaryCursor ? { boundaryCursor: options.boundaryCursor } : {}),
1893
2204
  ...(options.boundaryMessageId ? { boundaryMessageId: options.boundaryMessageId } : {}),
1894
2205
  });
1895
2206
  const payload = await this.sendRequest({
@@ -2095,6 +2406,13 @@ export class DaemonClient {
2095
2406
  return msg.payload;
2096
2407
  },
2097
2408
  });
2409
+ // A refused cancellation comes back as an error payload, not a rejected
2410
+ // frame. Dropping it reported Stop as successful while the provider was
2411
+ // still running and still spending tokens — the exact outcome the daemon
2412
+ // refuses the cancel to avoid.
2413
+ if (payload.error) {
2414
+ throw new Error(payload.error);
2415
+ }
2098
2416
  // Absent ⇒ old daemon that doesn't report whether a run was interrupted.
2099
2417
  return payload.cancelled !== undefined ? { cancelled: payload.cancelled } : {};
2100
2418
  }
@@ -2150,6 +2468,7 @@ export class DaemonClient {
2150
2468
  if (!payload.accepted) {
2151
2469
  throw new Error(payload.error ?? "setAgentModel rejected");
2152
2470
  }
2471
+ return payload.notice ?? null;
2153
2472
  }
2154
2473
  async setAgentFeature(agentId, featureId, value) {
2155
2474
  const requestId = this.createRequestId();
@@ -3049,6 +3368,20 @@ export class DaemonClient {
3049
3368
  responseType: "branch_suggestions_response",
3050
3369
  });
3051
3370
  }
3371
+ async searchForge(options, requestId) {
3372
+ return this.sendCorrelatedSessionRequest({
3373
+ requestId,
3374
+ message: {
3375
+ type: "forge.search.request",
3376
+ cwd: options.cwd,
3377
+ query: options.query,
3378
+ limit: options.limit,
3379
+ kinds: options.kinds,
3380
+ },
3381
+ responseType: "forge.search.response",
3382
+ timeout: 15000,
3383
+ });
3384
+ }
3052
3385
  async searchGitHub(options, requestId) {
3053
3386
  return this.sendCorrelatedSessionRequest({
3054
3387
  requestId,
@@ -3183,6 +3516,18 @@ export class DaemonClient {
3183
3516
  hash: file.hash ?? null,
3184
3517
  };
3185
3518
  }
3519
+ /**
3520
+ * The fs.file.write RPC: optimistic-concurrency writes keyed by an opaque
3521
+ * revision. The hash-keyed `writeFile` below is kept for callers
3522
+ * that have not moved over.
3523
+ */
3524
+ async writeFsFile(input) {
3525
+ const payload = await this.sendCorrelatedSessionRequest({
3526
+ message: { type: "fs.file.write.request", ...input },
3527
+ responseType: "fs.file.write.response",
3528
+ });
3529
+ return payload.result;
3530
+ }
3186
3531
  /** Conditional save — see FileWriteRequestSchema for the no-clobber contract. */
3187
3532
  async writeFile(options) {
3188
3533
  const payload = await this.sendCorrelatedSessionRequest({
@@ -3201,6 +3546,73 @@ export class DaemonClient {
3201
3546
  });
3202
3547
  return payload.result;
3203
3548
  }
3549
+ /**
3550
+ * Write bytes to a workspace file — the path for generated artifacts the
3551
+ * text write cannot carry (it refuses binary targets outright). Gated on
3552
+ * `features.binaryFileWrite`; there is no client-side substitute, because
3553
+ * the client never touches a workspace file on any platform.
3554
+ *
3555
+ * Shaped like {@link uploadFile}: the JSON request says where the bytes go
3556
+ * and how many to expect, then the bytes follow as file-transfer frames
3557
+ * correlated on the same `requestId`. The daemon answers at FileEnd.
3558
+ */
3559
+ async writeBinaryFile(options) {
3560
+ const bytes = asUint8Array(options.bytes);
3561
+ if (!bytes) {
3562
+ throw new Error("File bytes are required.");
3563
+ }
3564
+ const resolvedRequestId = this.createRequestId(options.requestId);
3565
+ const responsePromise = this.sendCorrelatedSessionRequest({
3566
+ requestId: resolvedRequestId,
3567
+ message: {
3568
+ type: "fs.file.write_binary.request",
3569
+ cwd: options.cwd,
3570
+ path: options.path,
3571
+ size: bytes.byteLength,
3572
+ overwrite: options.overwrite,
3573
+ },
3574
+ responseType: "fs.file.write_binary.response",
3575
+ });
3576
+ this.sendFileTransfer({
3577
+ requestId: resolvedRequestId,
3578
+ bytes,
3579
+ // Nothing downstream reads the mime for a workspace write — the path
3580
+ // decides what the file is — but the frame metadata requires one.
3581
+ mime: "application/octet-stream",
3582
+ chunkSize: options.chunkSize,
3583
+ });
3584
+ const payload = await responsePromise;
3585
+ return payload.result;
3586
+ }
3587
+ /**
3588
+ * FileBegin, chunks, FileEnd. Synchronous through `sendBinaryFrame`, so the
3589
+ * frames leave in order and behind the JSON request that announced them.
3590
+ */
3591
+ sendFileTransfer(input) {
3592
+ this.sendBinaryFrame(encodeFileTransferFrame({
3593
+ opcode: FileTransferOpcode.FileBegin,
3594
+ requestId: input.requestId,
3595
+ metadata: {
3596
+ mime: input.mime,
3597
+ size: input.bytes.byteLength,
3598
+ encoding: "binary",
3599
+ modifiedAt: input.modifiedAt ?? new Date().toISOString(),
3600
+ ...(input.fileName ? { fileName: input.fileName } : {}),
3601
+ },
3602
+ }));
3603
+ const chunkSize = input.chunkSize ?? 1024 * 1024;
3604
+ for (let offset = 0; offset < input.bytes.byteLength; offset += chunkSize) {
3605
+ this.sendBinaryFrame(encodeFileTransferFrame({
3606
+ opcode: FileTransferOpcode.FileChunk,
3607
+ requestId: input.requestId,
3608
+ payload: input.bytes.subarray(offset, Math.min(offset + chunkSize, input.bytes.byteLength)),
3609
+ }));
3610
+ }
3611
+ this.sendBinaryFrame(encodeFileTransferFrame({
3612
+ opcode: FileTransferOpcode.FileEnd,
3613
+ requestId: input.requestId,
3614
+ }));
3615
+ }
3204
3616
  /** Create an empty file or a directory. Never overwrites — see FileCreateResultSchema. */
3205
3617
  async createFileEntry(options) {
3206
3618
  const payload = await this.sendCorrelatedSessionRequest({
@@ -3606,7 +4018,7 @@ export class DaemonClient {
3606
4018
  * torn down when the last disposer runs; events fan out to every caller.
3607
4019
  */
3608
4020
  watchFile(cwd, path, onEvent) {
3609
- const key = `${cwd}${path}`;
4021
+ const key = `${cwd}${path}`;
3610
4022
  const offMessage = this.on("file.watch.event", (message) => {
3611
4023
  if (message.type !== "file.watch.event") {
3612
4024
  return;
@@ -3665,29 +4077,14 @@ export class DaemonClient {
3665
4077
  responseType: "file.upload.response",
3666
4078
  options: { skipQueue: true },
3667
4079
  });
3668
- this.sendBinaryFrame(encodeFileTransferFrame({
3669
- opcode: FileTransferOpcode.FileBegin,
3670
- requestId: resolvedRequestId,
3671
- metadata: {
3672
- mime: input.mimeType,
3673
- size: bytes.byteLength,
3674
- encoding: "binary",
3675
- modifiedAt,
3676
- fileName: input.fileName,
3677
- },
3678
- }));
3679
- const chunkSize = input.chunkSize ?? 1024 * 1024;
3680
- for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
3681
- this.sendBinaryFrame(encodeFileTransferFrame({
3682
- opcode: FileTransferOpcode.FileChunk,
3683
- requestId: resolvedRequestId,
3684
- payload: bytes.subarray(offset, Math.min(offset + chunkSize, bytes.byteLength)),
3685
- }));
3686
- }
3687
- this.sendBinaryFrame(encodeFileTransferFrame({
3688
- opcode: FileTransferOpcode.FileEnd,
4080
+ this.sendFileTransfer({
3689
4081
  requestId: resolvedRequestId,
3690
- }));
4082
+ bytes,
4083
+ mime: input.mimeType,
4084
+ fileName: input.fileName,
4085
+ modifiedAt,
4086
+ ...(input.chunkSize === undefined ? {} : { chunkSize: input.chunkSize }),
4087
+ });
3691
4088
  return responsePromise;
3692
4089
  }
3693
4090
  async requestDownloadToken(cwd, path, requestId) {
@@ -5109,6 +5506,10 @@ export class DaemonClient {
5109
5506
  [CLIENT_CAPS.customModeIcons]: true,
5110
5507
  [CLIENT_CAPS.reasoningMergeEnum]: true,
5111
5508
  [CLIENT_CAPS.terminalReflowableSnapshot]: true,
5509
+ [CLIENT_CAPS.providerSubagents]: true,
5510
+ // The daemon gates project.updated.notification on this (session.ts),
5511
+ // so dropping it silently kills cross-session project renames.
5512
+ [CLIENT_CAPS.projectUpdates]: true,
5112
5513
  ...this.config.capabilities,
5113
5514
  },
5114
5515
  ...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}),
@@ -5198,13 +5599,18 @@ export class DaemonClient {
5198
5599
  }
5199
5600
  const parsed = validateWSOutboundMessage(parsedJson);
5200
5601
  if (!parsed.success) {
5201
- const msgType = parsedJson != null &&
5602
+ const responseIdentity = extractCorrelatedResponseIdentity(parsedJson);
5603
+ const envelopeType = parsedJson != null &&
5202
5604
  typeof parsedJson === "object" &&
5203
5605
  "type" in parsedJson &&
5204
5606
  typeof parsedJson.type === "string"
5205
5607
  ? parsedJson.type
5206
5608
  : "unknown";
5609
+ const msgType = responseIdentity?.responseType ?? envelopeType;
5207
5610
  this.logger.warn({ msgType, error: parsed.error.message }, "Message validation failed");
5611
+ if (responseIdentity) {
5612
+ this.rejectWaitersForRequestId(responseIdentity.requestId, new DaemonProtocolError(responseIdentity));
5613
+ }
5208
5614
  return;
5209
5615
  }
5210
5616
  this.consecutiveLivenessFailures = 0;
@@ -5485,6 +5891,18 @@ export class DaemonClient {
5485
5891
  }
5486
5892
  }
5487
5893
  }
5894
+ rejectWaitersForRequestId(requestId, error) {
5895
+ for (const waiter of Array.from(this.waiters)) {
5896
+ if (waiter.requestId !== requestId) {
5897
+ continue;
5898
+ }
5899
+ this.waiters.delete(waiter);
5900
+ if (waiter.timeoutHandle) {
5901
+ clearTimeout(waiter.timeoutHandle);
5902
+ }
5903
+ waiter.reject(error);
5904
+ }
5905
+ }
5488
5906
  clearWaiters(error) {
5489
5907
  for (const waiter of Array.from(this.waiters)) {
5490
5908
  if (waiter.timeoutHandle) {
@@ -5549,7 +5967,7 @@ export class DaemonClient {
5549
5967
  return null;
5550
5968
  }
5551
5969
  }
5552
- waitForWithCancel(predicate, timeout = 30000, _options) {
5970
+ waitForWithCancel(predicate, timeout = 30000, options) {
5553
5971
  // Capture stack trace at call site, not inside setTimeout
5554
5972
  const timeoutError = new Error(`Timeout waiting for message (${timeout}ms)`);
5555
5973
  let waiter = null;
@@ -5582,6 +6000,7 @@ export class DaemonClient {
5582
6000
  resolve: wrappedResolve,
5583
6001
  reject: wrappedReject,
5584
6002
  timeoutHandle,
6003
+ requestId: options?.requestId,
5585
6004
  };
5586
6005
  this.waiters.add(waiter);
5587
6006
  });
@@ -5608,6 +6027,80 @@ export class DaemonClient {
5608
6027
  };
5609
6028
  return { promise, cancel };
5610
6029
  }
6030
+ async cloneGithubProject(input, requestId) {
6031
+ const message = {
6032
+ type: "project.github.clone.request",
6033
+ repo: input.repo,
6034
+ targetDirectory: input.targetDirectory,
6035
+ ...(input.cloneProtocol ? { cloneProtocol: input.cloneProtocol } : {}),
6036
+ };
6037
+ return this.sendNamespacedCorrelatedSessionRequest({
6038
+ requestId,
6039
+ message,
6040
+ timeout: PROJECT_GITHUB_CLONE_TIMEOUT_MS,
6041
+ });
6042
+ }
6043
+ /**
6044
+ * `includeDiscovered` also returns the Scripts the workspace's own project
6045
+ * files declare (package.json scripts today), each tagged with its `source`.
6046
+ * Gate it on `server_info.features.workspaceScriptDiscovery` — an older
6047
+ * daemon ignores the flag and answers with the otto.json list only.
6048
+ */
6049
+ async listWorkspaceScripts(workspaceId, options) {
6050
+ return this.sendCorrelatedSessionRequest({
6051
+ requestId: options?.requestId,
6052
+ message: {
6053
+ type: "workspace.script.list.request",
6054
+ workspaceId,
6055
+ includeDiscovered: options?.includeDiscovered ?? false,
6056
+ },
6057
+ responseType: "workspace.script.list.response",
6058
+ });
6059
+ }
6060
+ async startWorkspaceScriptWithStatus(workspaceId, scriptName, requestId) {
6061
+ return this.sendCorrelatedSessionRequest({
6062
+ requestId,
6063
+ message: { type: "workspace.script.start.request", workspaceId, scriptName },
6064
+ responseType: "workspace.script.start.response",
6065
+ });
6066
+ }
6067
+ async stopWorkspaceScript(workspaceId, scriptName, requestId) {
6068
+ return this.sendCorrelatedSessionRequest({
6069
+ requestId,
6070
+ message: { type: "workspace.script.stop.request", workspaceId, scriptName },
6071
+ responseType: "workspace.script.stop.response",
6072
+ });
6073
+ }
6074
+ async connectHub(hubUrl, token, requestId) {
6075
+ this.requireHubRelationshipSupport();
6076
+ return this.sendCorrelatedSessionRequest({
6077
+ requestId,
6078
+ message: { type: "hub.management.daemon.connect.request", hubUrl, token },
6079
+ responseType: "hub.management.daemon.connect.response",
6080
+ });
6081
+ }
6082
+ async getHubStatus(requestId) {
6083
+ this.requireHubRelationshipSupport();
6084
+ return this.sendCorrelatedSessionRequest({
6085
+ requestId,
6086
+ message: { type: "hub.management.daemon.get_status.request" },
6087
+ responseType: "hub.management.daemon.get_status.response",
6088
+ });
6089
+ }
6090
+ async disconnectHub(force = false, requestId) {
6091
+ this.requireHubRelationshipSupport();
6092
+ return this.sendCorrelatedSessionRequest({
6093
+ requestId,
6094
+ message: { type: "hub.management.daemon.disconnect.request", force },
6095
+ responseType: "hub.management.daemon.disconnect.response",
6096
+ });
6097
+ }
6098
+ requireHubRelationshipSupport() {
6099
+ // COMPAT(hubRelationship): added in v0.2.5, drop the gate when floor >= v0.2.5.
6100
+ if (this.lastServerInfoMessage?.features?.hubRelationship !== true) {
6101
+ throw new Error("Update the host to use Hub relationship management.");
6102
+ }
6103
+ }
5611
6104
  }
5612
6105
  function resolveAgentConfig(options) {
5613
6106
  const { config, provider, cwd, env: _env, workspaceId: _workspaceId, initialPrompt: _initialPrompt, images: _images, git: _git, worktreeName: _worktreeName, requestId: _requestId, labels: _labels, ...overrides } = options;
package/dist/index.d.ts CHANGED
@@ -118,13 +118,13 @@ export interface OttoAgentCreateOptions extends OttoAgentConfigOverrides {
118
118
  provider?: CreateAgentRequestMessage["config"]["provider"];
119
119
  cwd?: string;
120
120
  workspaceId?: string;
121
+ callerAgentId?: string;
121
122
  initialPrompt?: string;
122
123
  clientMessageId?: string;
123
124
  outputSchema?: Record<string, unknown>;
124
125
  images?: CreateAgentRequestMessage["images"];
125
126
  attachments?: CreateAgentRequestMessage["attachments"];
126
127
  git?: CreateAgentRequestMessage["git"];
127
- worktreeName?: string;
128
128
  requestId?: string;
129
129
  labels?: Record<string, string>;
130
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-code/client",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "Otto client SDK package",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "files": [
@@ -36,8 +36,8 @@
36
36
  "test": "vitest run"
37
37
  },
38
38
  "dependencies": {
39
- "@otto-code/protocol": "0.7.5",
40
- "@otto-code/relay": "0.7.5",
39
+ "@otto-code/protocol": "0.7.6",
40
+ "@otto-code/relay": "0.7.6",
41
41
  "zod": "^4.4.3"
42
42
  },
43
43
  "devDependencies": {