@otto-code/client 0.7.4 → 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.
- package/dist/daemon-client-relay-e2ee-transport.js +4 -4
- package/dist/daemon-client-transport-types.d.ts +1 -1
- package/dist/daemon-client-transport-utils.d.ts +5 -1
- package/dist/daemon-client-transport-utils.js +8 -6
- package/dist/daemon-client-transport.d.ts +1 -1
- package/dist/daemon-client-transport.js +1 -1
- package/dist/daemon-client-websocket-transport.js +9 -1
- package/dist/daemon-client.d.ts +238 -4
- package/dist/daemon-client.js +792 -26
- package/dist/index.d.ts +1 -1
- package/package.json +3 -3
package/dist/daemon-client.js
CHANGED
|
@@ -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) {
|
|
@@ -169,6 +214,20 @@ function toReasonCode(reason) {
|
|
|
169
214
|
}
|
|
170
215
|
return "unknown";
|
|
171
216
|
}
|
|
217
|
+
// A job-starting brain RPC returns { job, error }; surface the error as a throw
|
|
218
|
+
// so callers get a plain Promise<BrainJob>.
|
|
219
|
+
function unwrapBrainJob(payload) {
|
|
220
|
+
if (payload.error) {
|
|
221
|
+
throw new Error(payload.error);
|
|
222
|
+
}
|
|
223
|
+
if (!payload.job) {
|
|
224
|
+
throw new Error("The brain did not start the operation.");
|
|
225
|
+
}
|
|
226
|
+
return payload.job;
|
|
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;
|
|
172
231
|
export class DaemonClient {
|
|
173
232
|
constructor(config) {
|
|
174
233
|
this.config = config;
|
|
@@ -192,6 +251,7 @@ export class DaemonClient {
|
|
|
192
251
|
this.connectionState = { status: "idle" };
|
|
193
252
|
this.checkoutDiffSubscriptions = new Map();
|
|
194
253
|
this.terminalDirectorySubscriptions = new Map();
|
|
254
|
+
this.fileSubscriptions = new Map();
|
|
195
255
|
this.terminalStreams = new TerminalStreamRouter();
|
|
196
256
|
// requestId -> progress listener for an in-flight project.scaffold.request.
|
|
197
257
|
// Entries are always removed in scaffoldProject's finally block.
|
|
@@ -659,7 +719,7 @@ export class DaemonClient {
|
|
|
659
719
|
return null;
|
|
660
720
|
}
|
|
661
721
|
return { kind: "ok", value };
|
|
662
|
-
}, timeout, params.options);
|
|
722
|
+
}, timeout, { ...params.options, requestId: params.requestId });
|
|
663
723
|
try {
|
|
664
724
|
await this.sendSessionMessageOrThrow(params.message);
|
|
665
725
|
}
|
|
@@ -1265,6 +1325,7 @@ export class DaemonClient {
|
|
|
1265
1325
|
...(options.personality ? { personality: options.personality } : {}),
|
|
1266
1326
|
...(options.env ? { env: options.env } : {}),
|
|
1267
1327
|
...(options.workspaceId !== undefined ? { workspaceId: options.workspaceId } : {}),
|
|
1328
|
+
...(options.callerAgentId !== undefined ? { callerAgentId: options.callerAgentId } : {}),
|
|
1268
1329
|
...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}),
|
|
1269
1330
|
...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}),
|
|
1270
1331
|
...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
|
|
@@ -1842,6 +1903,266 @@ export class DaemonClient {
|
|
|
1842
1903
|
},
|
|
1843
1904
|
});
|
|
1844
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
|
+
}
|
|
1845
2166
|
async fetchAgentTimeline(agentId, options = {}) {
|
|
1846
2167
|
const resolvedRequestId = this.createRequestId(options.requestId);
|
|
1847
2168
|
const message = SessionInboundMessageSchema.parse({
|
|
@@ -1879,6 +2200,7 @@ export class DaemonClient {
|
|
|
1879
2200
|
type: "agent.fork_context.request",
|
|
1880
2201
|
agentId,
|
|
1881
2202
|
requestId: resolvedRequestId,
|
|
2203
|
+
...(options.boundaryCursor ? { boundaryCursor: options.boundaryCursor } : {}),
|
|
1882
2204
|
...(options.boundaryMessageId ? { boundaryMessageId: options.boundaryMessageId } : {}),
|
|
1883
2205
|
});
|
|
1884
2206
|
const payload = await this.sendRequest({
|
|
@@ -2084,6 +2406,13 @@ export class DaemonClient {
|
|
|
2084
2406
|
return msg.payload;
|
|
2085
2407
|
},
|
|
2086
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
|
+
}
|
|
2087
2416
|
// Absent ⇒ old daemon that doesn't report whether a run was interrupted.
|
|
2088
2417
|
return payload.cancelled !== undefined ? { cancelled: payload.cancelled } : {};
|
|
2089
2418
|
}
|
|
@@ -2139,6 +2468,7 @@ export class DaemonClient {
|
|
|
2139
2468
|
if (!payload.accepted) {
|
|
2140
2469
|
throw new Error(payload.error ?? "setAgentModel rejected");
|
|
2141
2470
|
}
|
|
2471
|
+
return payload.notice ?? null;
|
|
2142
2472
|
}
|
|
2143
2473
|
async setAgentFeature(agentId, featureId, value) {
|
|
2144
2474
|
const requestId = this.createRequestId();
|
|
@@ -3038,6 +3368,20 @@ export class DaemonClient {
|
|
|
3038
3368
|
responseType: "branch_suggestions_response",
|
|
3039
3369
|
});
|
|
3040
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
|
+
}
|
|
3041
3385
|
async searchGitHub(options, requestId) {
|
|
3042
3386
|
return this.sendCorrelatedSessionRequest({
|
|
3043
3387
|
requestId,
|
|
@@ -3172,6 +3516,18 @@ export class DaemonClient {
|
|
|
3172
3516
|
hash: file.hash ?? null,
|
|
3173
3517
|
};
|
|
3174
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
|
+
}
|
|
3175
3531
|
/** Conditional save — see FileWriteRequestSchema for the no-clobber contract. */
|
|
3176
3532
|
async writeFile(options) {
|
|
3177
3533
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
@@ -3190,6 +3546,73 @@ export class DaemonClient {
|
|
|
3190
3546
|
});
|
|
3191
3547
|
return payload.result;
|
|
3192
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
|
+
}
|
|
3193
3616
|
/** Create an empty file or a directory. Never overwrites — see FileCreateResultSchema. */
|
|
3194
3617
|
async createFileEntry(options) {
|
|
3195
3618
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
@@ -3595,7 +4018,7 @@ export class DaemonClient {
|
|
|
3595
4018
|
* torn down when the last disposer runs; events fan out to every caller.
|
|
3596
4019
|
*/
|
|
3597
4020
|
watchFile(cwd, path, onEvent) {
|
|
3598
|
-
const key = `${cwd}
|
|
4021
|
+
const key = `${cwd}${path}`;
|
|
3599
4022
|
const offMessage = this.on("file.watch.event", (message) => {
|
|
3600
4023
|
if (message.type !== "file.watch.event") {
|
|
3601
4024
|
return;
|
|
@@ -3654,29 +4077,14 @@ export class DaemonClient {
|
|
|
3654
4077
|
responseType: "file.upload.response",
|
|
3655
4078
|
options: { skipQueue: true },
|
|
3656
4079
|
});
|
|
3657
|
-
this.
|
|
3658
|
-
opcode: FileTransferOpcode.FileBegin,
|
|
4080
|
+
this.sendFileTransfer({
|
|
3659
4081
|
requestId: resolvedRequestId,
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
},
|
|
3667
|
-
}));
|
|
3668
|
-
const chunkSize = input.chunkSize ?? 1024 * 1024;
|
|
3669
|
-
for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
|
|
3670
|
-
this.sendBinaryFrame(encodeFileTransferFrame({
|
|
3671
|
-
opcode: FileTransferOpcode.FileChunk,
|
|
3672
|
-
requestId: resolvedRequestId,
|
|
3673
|
-
payload: bytes.subarray(offset, Math.min(offset + chunkSize, bytes.byteLength)),
|
|
3674
|
-
}));
|
|
3675
|
-
}
|
|
3676
|
-
this.sendBinaryFrame(encodeFileTransferFrame({
|
|
3677
|
-
opcode: FileTransferOpcode.FileEnd,
|
|
3678
|
-
requestId: resolvedRequestId,
|
|
3679
|
-
}));
|
|
4082
|
+
bytes,
|
|
4083
|
+
mime: input.mimeType,
|
|
4084
|
+
fileName: input.fileName,
|
|
4085
|
+
modifiedAt,
|
|
4086
|
+
...(input.chunkSize === undefined ? {} : { chunkSize: input.chunkSize }),
|
|
4087
|
+
});
|
|
3680
4088
|
return responsePromise;
|
|
3681
4089
|
}
|
|
3682
4090
|
async requestDownloadToken(cwd, path, requestId) {
|
|
@@ -3937,6 +4345,249 @@ export class DaemonClient {
|
|
|
3937
4345
|
responseType: "set_daemon_config_response",
|
|
3938
4346
|
});
|
|
3939
4347
|
}
|
|
4348
|
+
// Enumerate a connector's tools live (connect + listTools), each flagged with
|
|
4349
|
+
// its configured disabled state. Registry edits (add/enable/disable a
|
|
4350
|
+
// connector or a tool) go through patchDaemonConfig's `connectors` instead.
|
|
4351
|
+
async connectorsListTools(connectorId, requestId) {
|
|
4352
|
+
return this.sendCorrelatedSessionRequest({
|
|
4353
|
+
requestId,
|
|
4354
|
+
message: {
|
|
4355
|
+
type: "connectors.list_tools.request",
|
|
4356
|
+
connectorId,
|
|
4357
|
+
},
|
|
4358
|
+
responseType: "connectors.list_tools.response",
|
|
4359
|
+
});
|
|
4360
|
+
}
|
|
4361
|
+
async brainHostStatus(requestId) {
|
|
4362
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4363
|
+
requestId,
|
|
4364
|
+
message: {
|
|
4365
|
+
type: "brain.host.status.request",
|
|
4366
|
+
},
|
|
4367
|
+
responseType: "brain.host.status.response",
|
|
4368
|
+
});
|
|
4369
|
+
return payload.status;
|
|
4370
|
+
}
|
|
4371
|
+
async brainHostStart(model, requestId) {
|
|
4372
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4373
|
+
requestId,
|
|
4374
|
+
message: {
|
|
4375
|
+
type: "brain.host.start.request",
|
|
4376
|
+
model: model ?? null,
|
|
4377
|
+
},
|
|
4378
|
+
responseType: "brain.host.start.response",
|
|
4379
|
+
});
|
|
4380
|
+
return payload.status;
|
|
4381
|
+
}
|
|
4382
|
+
async brainHostStop(requestId) {
|
|
4383
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4384
|
+
requestId,
|
|
4385
|
+
message: {
|
|
4386
|
+
type: "brain.host.stop.request",
|
|
4387
|
+
},
|
|
4388
|
+
responseType: "brain.host.stop.response",
|
|
4389
|
+
});
|
|
4390
|
+
return payload.status;
|
|
4391
|
+
}
|
|
4392
|
+
async brainHostRestart(model, requestId) {
|
|
4393
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4394
|
+
requestId,
|
|
4395
|
+
message: {
|
|
4396
|
+
type: "brain.host.restart.request",
|
|
4397
|
+
model: model ?? null,
|
|
4398
|
+
},
|
|
4399
|
+
responseType: "brain.host.restart.response",
|
|
4400
|
+
});
|
|
4401
|
+
return payload.status;
|
|
4402
|
+
}
|
|
4403
|
+
async brainEvalsGet(requestId) {
|
|
4404
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4405
|
+
requestId,
|
|
4406
|
+
message: {
|
|
4407
|
+
type: "brain.evals.get.request",
|
|
4408
|
+
},
|
|
4409
|
+
responseType: "brain.evals.get.response",
|
|
4410
|
+
});
|
|
4411
|
+
return payload.evals;
|
|
4412
|
+
}
|
|
4413
|
+
async brainRemoteConfigGet(requestId) {
|
|
4414
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4415
|
+
requestId,
|
|
4416
|
+
message: {
|
|
4417
|
+
type: "brain.remote.config.get.request",
|
|
4418
|
+
},
|
|
4419
|
+
responseType: "brain.remote.config.get.response",
|
|
4420
|
+
});
|
|
4421
|
+
if (payload.error) {
|
|
4422
|
+
throw new Error(payload.error);
|
|
4423
|
+
}
|
|
4424
|
+
return payload.config;
|
|
4425
|
+
}
|
|
4426
|
+
async brainRemoteConfigPatch(patch, requestId) {
|
|
4427
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4428
|
+
requestId,
|
|
4429
|
+
message: {
|
|
4430
|
+
type: "brain.remote.config.patch.request",
|
|
4431
|
+
patch,
|
|
4432
|
+
},
|
|
4433
|
+
responseType: "brain.remote.config.patch.response",
|
|
4434
|
+
});
|
|
4435
|
+
if (payload.error) {
|
|
4436
|
+
throw new Error(payload.error);
|
|
4437
|
+
}
|
|
4438
|
+
return payload.config;
|
|
4439
|
+
}
|
|
4440
|
+
async brainModelsList(requestId) {
|
|
4441
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4442
|
+
requestId,
|
|
4443
|
+
message: {
|
|
4444
|
+
type: "brain.models.list.request",
|
|
4445
|
+
},
|
|
4446
|
+
responseType: "brain.models.list.response",
|
|
4447
|
+
});
|
|
4448
|
+
if (payload.error) {
|
|
4449
|
+
throw new Error(payload.error);
|
|
4450
|
+
}
|
|
4451
|
+
return payload.models;
|
|
4452
|
+
}
|
|
4453
|
+
async brainNetworkDiscover(requestId) {
|
|
4454
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4455
|
+
requestId,
|
|
4456
|
+
message: {
|
|
4457
|
+
type: "brain.network.discover.request",
|
|
4458
|
+
},
|
|
4459
|
+
responseType: "brain.network.discover.response",
|
|
4460
|
+
});
|
|
4461
|
+
if (payload.error) {
|
|
4462
|
+
throw new Error(payload.error);
|
|
4463
|
+
}
|
|
4464
|
+
return payload.info;
|
|
4465
|
+
}
|
|
4466
|
+
async brainModelsScan(requestId) {
|
|
4467
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4468
|
+
requestId,
|
|
4469
|
+
message: { type: "brain.models.scan.request" },
|
|
4470
|
+
responseType: "brain.models.scan.response",
|
|
4471
|
+
});
|
|
4472
|
+
if (payload.error) {
|
|
4473
|
+
throw new Error(payload.error);
|
|
4474
|
+
}
|
|
4475
|
+
return payload.models;
|
|
4476
|
+
}
|
|
4477
|
+
async brainCatalogList(requestId) {
|
|
4478
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4479
|
+
requestId,
|
|
4480
|
+
message: { type: "brain.catalog.list.request" },
|
|
4481
|
+
responseType: "brain.catalog.list.response",
|
|
4482
|
+
});
|
|
4483
|
+
if (payload.error) {
|
|
4484
|
+
throw new Error(payload.error);
|
|
4485
|
+
}
|
|
4486
|
+
return payload.models;
|
|
4487
|
+
}
|
|
4488
|
+
async brainRuntimeList(requestId) {
|
|
4489
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4490
|
+
requestId,
|
|
4491
|
+
message: { type: "brain.runtime.list.request" },
|
|
4492
|
+
responseType: "brain.runtime.list.response",
|
|
4493
|
+
});
|
|
4494
|
+
if (payload.error) {
|
|
4495
|
+
throw new Error(payload.error);
|
|
4496
|
+
}
|
|
4497
|
+
return payload.runtimes;
|
|
4498
|
+
}
|
|
4499
|
+
async brainModelsPull(model, requestId) {
|
|
4500
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4501
|
+
requestId,
|
|
4502
|
+
message: { type: "brain.models.pull.request", model },
|
|
4503
|
+
responseType: "brain.models.pull.response",
|
|
4504
|
+
});
|
|
4505
|
+
return unwrapBrainJob(payload);
|
|
4506
|
+
}
|
|
4507
|
+
async brainHfSearch(query, limit, requestId) {
|
|
4508
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4509
|
+
requestId,
|
|
4510
|
+
message: { type: "brain.hf.search.request", query, limit: limit ?? null },
|
|
4511
|
+
responseType: "brain.hf.search.response",
|
|
4512
|
+
});
|
|
4513
|
+
if (payload.error) {
|
|
4514
|
+
throw new Error(payload.error);
|
|
4515
|
+
}
|
|
4516
|
+
return payload.results;
|
|
4517
|
+
}
|
|
4518
|
+
async brainHfQuants(repo, requestId) {
|
|
4519
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4520
|
+
requestId,
|
|
4521
|
+
message: { type: "brain.hf.quants.request", repo },
|
|
4522
|
+
responseType: "brain.hf.quants.response",
|
|
4523
|
+
});
|
|
4524
|
+
if (payload.error) {
|
|
4525
|
+
throw new Error(payload.error);
|
|
4526
|
+
}
|
|
4527
|
+
return payload.quants;
|
|
4528
|
+
}
|
|
4529
|
+
async brainModelsAdd(repo, quant, requestId) {
|
|
4530
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4531
|
+
requestId,
|
|
4532
|
+
message: { type: "brain.models.add.request", repo, quant },
|
|
4533
|
+
responseType: "brain.models.add.response",
|
|
4534
|
+
});
|
|
4535
|
+
return unwrapBrainJob(payload);
|
|
4536
|
+
}
|
|
4537
|
+
async brainRuntimeInstall(build, requestId) {
|
|
4538
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4539
|
+
requestId,
|
|
4540
|
+
message: { type: "brain.runtime.install.request", build: build ?? null },
|
|
4541
|
+
responseType: "brain.runtime.install.response",
|
|
4542
|
+
});
|
|
4543
|
+
return unwrapBrainJob(payload);
|
|
4544
|
+
}
|
|
4545
|
+
async brainCalibrate(model, requestId) {
|
|
4546
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4547
|
+
requestId,
|
|
4548
|
+
message: { type: "brain.calibrate.request", model },
|
|
4549
|
+
responseType: "brain.calibrate.response",
|
|
4550
|
+
});
|
|
4551
|
+
return unwrapBrainJob(payload);
|
|
4552
|
+
}
|
|
4553
|
+
async brainSweep(model, requestId) {
|
|
4554
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4555
|
+
requestId,
|
|
4556
|
+
message: { type: "brain.sweep.request", model },
|
|
4557
|
+
responseType: "brain.sweep.response",
|
|
4558
|
+
});
|
|
4559
|
+
return unwrapBrainJob(payload);
|
|
4560
|
+
}
|
|
4561
|
+
async brainBench(model, requestId) {
|
|
4562
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4563
|
+
requestId,
|
|
4564
|
+
message: { type: "brain.bench.request", model: model ?? null },
|
|
4565
|
+
responseType: "brain.bench.response",
|
|
4566
|
+
});
|
|
4567
|
+
return unwrapBrainJob(payload);
|
|
4568
|
+
}
|
|
4569
|
+
async brainJobsList(requestId) {
|
|
4570
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4571
|
+
requestId,
|
|
4572
|
+
message: { type: "brain.jobs.list.request" },
|
|
4573
|
+
responseType: "brain.jobs.list.response",
|
|
4574
|
+
});
|
|
4575
|
+
if (payload.error) {
|
|
4576
|
+
throw new Error(payload.error);
|
|
4577
|
+
}
|
|
4578
|
+
return payload.jobs;
|
|
4579
|
+
}
|
|
4580
|
+
async brainJobsCancel(jobId, requestId) {
|
|
4581
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4582
|
+
requestId,
|
|
4583
|
+
message: { type: "brain.jobs.cancel.request", jobId },
|
|
4584
|
+
responseType: "brain.jobs.cancel.response",
|
|
4585
|
+
});
|
|
4586
|
+
if (payload.error) {
|
|
4587
|
+
throw new Error(payload.error);
|
|
4588
|
+
}
|
|
4589
|
+
return payload.jobs;
|
|
4590
|
+
}
|
|
3940
4591
|
async getSpeechSettingsOptions(requestId) {
|
|
3941
4592
|
return this.sendNamespacedCorrelatedSessionRequest({
|
|
3942
4593
|
requestId,
|
|
@@ -4007,6 +4658,25 @@ export class DaemonClient {
|
|
|
4007
4658
|
},
|
|
4008
4659
|
});
|
|
4009
4660
|
}
|
|
4661
|
+
// Author a personality profile (the prose personality prompt) from a draft's
|
|
4662
|
+
// name, roles, and spinner colors. Routed through the Writer chain; the caller
|
|
4663
|
+
// drops the result into the editor's prompt field.
|
|
4664
|
+
async generatePersonalityProfile(params, requestId) {
|
|
4665
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
4666
|
+
requestId,
|
|
4667
|
+
message: {
|
|
4668
|
+
type: "agentPersonalities.generate_profile.request",
|
|
4669
|
+
name: params.name,
|
|
4670
|
+
...(params.roles && params.roles.length > 0 ? { roles: params.roles } : {}),
|
|
4671
|
+
...(params.glowA ? { glowA: params.glowA } : {}),
|
|
4672
|
+
...(params.glowB ? { glowB: params.glowB } : {}),
|
|
4673
|
+
...(params.cwd ? { cwd: params.cwd } : {}),
|
|
4674
|
+
},
|
|
4675
|
+
// Same reason as the voice cues: the daemon spawns a structured-generation
|
|
4676
|
+
// agent, and provider SDK cold starts blow past the 60s default.
|
|
4677
|
+
timeout: 180000,
|
|
4678
|
+
});
|
|
4679
|
+
}
|
|
4010
4680
|
sendBrowserAutomationExecuteResponse(response) {
|
|
4011
4681
|
this.sendSessionMessageStrict(response);
|
|
4012
4682
|
}
|
|
@@ -4836,6 +5506,10 @@ export class DaemonClient {
|
|
|
4836
5506
|
[CLIENT_CAPS.customModeIcons]: true,
|
|
4837
5507
|
[CLIENT_CAPS.reasoningMergeEnum]: true,
|
|
4838
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,
|
|
4839
5513
|
...this.config.capabilities,
|
|
4840
5514
|
},
|
|
4841
5515
|
...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}),
|
|
@@ -4925,13 +5599,18 @@ export class DaemonClient {
|
|
|
4925
5599
|
}
|
|
4926
5600
|
const parsed = validateWSOutboundMessage(parsedJson);
|
|
4927
5601
|
if (!parsed.success) {
|
|
4928
|
-
const
|
|
5602
|
+
const responseIdentity = extractCorrelatedResponseIdentity(parsedJson);
|
|
5603
|
+
const envelopeType = parsedJson != null &&
|
|
4929
5604
|
typeof parsedJson === "object" &&
|
|
4930
5605
|
"type" in parsedJson &&
|
|
4931
5606
|
typeof parsedJson.type === "string"
|
|
4932
5607
|
? parsedJson.type
|
|
4933
5608
|
: "unknown";
|
|
5609
|
+
const msgType = responseIdentity?.responseType ?? envelopeType;
|
|
4934
5610
|
this.logger.warn({ msgType, error: parsed.error.message }, "Message validation failed");
|
|
5611
|
+
if (responseIdentity) {
|
|
5612
|
+
this.rejectWaitersForRequestId(responseIdentity.requestId, new DaemonProtocolError(responseIdentity));
|
|
5613
|
+
}
|
|
4935
5614
|
return;
|
|
4936
5615
|
}
|
|
4937
5616
|
this.consecutiveLivenessFailures = 0;
|
|
@@ -5212,6 +5891,18 @@ export class DaemonClient {
|
|
|
5212
5891
|
}
|
|
5213
5892
|
}
|
|
5214
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
|
+
}
|
|
5215
5906
|
clearWaiters(error) {
|
|
5216
5907
|
for (const waiter of Array.from(this.waiters)) {
|
|
5217
5908
|
if (waiter.timeoutHandle) {
|
|
@@ -5276,7 +5967,7 @@ export class DaemonClient {
|
|
|
5276
5967
|
return null;
|
|
5277
5968
|
}
|
|
5278
5969
|
}
|
|
5279
|
-
waitForWithCancel(predicate, timeout = 30000,
|
|
5970
|
+
waitForWithCancel(predicate, timeout = 30000, options) {
|
|
5280
5971
|
// Capture stack trace at call site, not inside setTimeout
|
|
5281
5972
|
const timeoutError = new Error(`Timeout waiting for message (${timeout}ms)`);
|
|
5282
5973
|
let waiter = null;
|
|
@@ -5309,6 +6000,7 @@ export class DaemonClient {
|
|
|
5309
6000
|
resolve: wrappedResolve,
|
|
5310
6001
|
reject: wrappedReject,
|
|
5311
6002
|
timeoutHandle,
|
|
6003
|
+
requestId: options?.requestId,
|
|
5312
6004
|
};
|
|
5313
6005
|
this.waiters.add(waiter);
|
|
5314
6006
|
});
|
|
@@ -5335,6 +6027,80 @@ export class DaemonClient {
|
|
|
5335
6027
|
};
|
|
5336
6028
|
return { promise, cancel };
|
|
5337
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
|
+
}
|
|
5338
6104
|
}
|
|
5339
6105
|
function resolveAgentConfig(options) {
|
|
5340
6106
|
const { config, provider, cwd, env: _env, workspaceId: _workspaceId, initialPrompt: _initialPrompt, images: _images, git: _git, worktreeName: _worktreeName, requestId: _requestId, labels: _labels, ...overrides } = options;
|