@otto-code/client 0.7.5 → 0.8.0
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/compat/normalize-provider-models.js +1 -1
- 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 +281 -27
- package/dist/daemon-client.js +693 -49
- 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) {
|
|
@@ -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.
|
|
@@ -244,7 +293,7 @@ export class DaemonClient {
|
|
|
244
293
|
const runtimeMetricsIntervalMs = typeof config.runtimeMetricsIntervalMs === "number" && config.runtimeMetricsIntervalMs > 0
|
|
245
294
|
? config.runtimeMetricsIntervalMs
|
|
246
295
|
: 0;
|
|
247
|
-
// The metrics object is always constructed
|
|
296
|
+
// The metrics object is always constructed - it is a handful of Maps keyed
|
|
248
297
|
// by message type, and its per-message cost is dwarfed by the JSON.parse it
|
|
249
298
|
// is measuring. What `runtimeMetricsIntervalMs` gates is the *periodic log*,
|
|
250
299
|
// which is the part that is actually noisy. Keeping the counters on
|
|
@@ -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 } : {}),
|
|
@@ -1343,7 +1393,7 @@ export class DaemonClient {
|
|
|
1343
1393
|
* the whole archived set. Pass `dryRun: true` first to get the count the
|
|
1344
1394
|
* confirm dialog quotes, then the same call with `dryRun: false` to delete.
|
|
1345
1395
|
*
|
|
1346
|
-
* Removes Otto's records only
|
|
1396
|
+
* Removes Otto's records only - provider transcripts are left on disk. Gated
|
|
1347
1397
|
* by `server_info.features.historyDelete`; there is no fallback path, so check
|
|
1348
1398
|
* the flag before offering the action.
|
|
1349
1399
|
*/
|
|
@@ -1374,7 +1424,7 @@ export class DaemonClient {
|
|
|
1374
1424
|
* `dryRun: false` to delete.
|
|
1375
1425
|
*
|
|
1376
1426
|
* Cleared images do not come back: a message that referenced one renders its
|
|
1377
|
-
* alt text from then on. Scope is the whole host
|
|
1427
|
+
* alt text from then on. Scope is the whole host - filenames are a content
|
|
1378
1428
|
* hash, so per-chat or per-workspace scope does not exist. Gated by
|
|
1379
1429
|
* `server_info.features.attachmentStorage`.
|
|
1380
1430
|
*/
|
|
@@ -1440,7 +1490,7 @@ export class DaemonClient {
|
|
|
1440
1490
|
}
|
|
1441
1491
|
/**
|
|
1442
1492
|
* Stop a running background shell task (Claude Bash tool run_in_background).
|
|
1443
|
-
* Not an AI subagent
|
|
1493
|
+
* Not an AI subagent - the daemon resolves it to its owning provider task and
|
|
1444
1494
|
* calls the provider's stopTask, same mechanism as stopObservedSubagent.
|
|
1445
1495
|
*/
|
|
1446
1496
|
async stopBackgroundShellTask(parentAgentId, taskId) {
|
|
@@ -1473,7 +1523,7 @@ export class DaemonClient {
|
|
|
1473
1523
|
}
|
|
1474
1524
|
/**
|
|
1475
1525
|
* Start one or more suggested tasks (from `spawn_task` chips), applying the
|
|
1476
|
-
* same mode to each
|
|
1526
|
+
* same mode to each - a new worktree workspace per task, a new local session
|
|
1477
1527
|
* per task, or steering the parent's current session. Individual actions pass
|
|
1478
1528
|
* a single-element array; the "Start all" collective action passes the whole
|
|
1479
1529
|
* pending queue. Resolves to the count started; throws only if all failed.
|
|
@@ -1541,7 +1591,7 @@ export class DaemonClient {
|
|
|
1541
1591
|
}
|
|
1542
1592
|
/**
|
|
1543
1593
|
* Orchestration: delete one finished (or draft) run. Throws with the
|
|
1544
|
-
* daemon's reason when it refuses
|
|
1594
|
+
* daemon's reason when it refuses - an active run has to be canceled first.
|
|
1545
1595
|
*/
|
|
1546
1596
|
async deleteRun(runId) {
|
|
1547
1597
|
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
@@ -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({
|
|
@@ -1987,7 +2298,7 @@ export class DaemonClient {
|
|
|
1987
2298
|
}
|
|
1988
2299
|
/**
|
|
1989
2300
|
* Move one queued message to a new position. Resolves false when the entry
|
|
1990
|
-
* was already drained or was already there
|
|
2301
|
+
* was already drained or was already there - the authoritative order arrives
|
|
1991
2302
|
* on the agent snapshot either way. Requires
|
|
1992
2303
|
* `server_info.features.steerQueueReorder`.
|
|
1993
2304
|
*/
|
|
@@ -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();
|
|
@@ -2208,7 +2527,7 @@ export class DaemonClient {
|
|
|
2208
2527
|
/**
|
|
2209
2528
|
* Live-switch a running agent's personality (null clears it). The daemon
|
|
2210
2529
|
* re-resolves the roster id against the agent's cwd and applies the full
|
|
2211
|
-
* personality
|
|
2530
|
+
* personality - system prompt, identity, model/mode/effort - restarting the
|
|
2212
2531
|
* provider query so the prompt takes effect on the next turn. Gate on
|
|
2213
2532
|
* server_info.features.setAgentPersonality.
|
|
2214
2533
|
*/
|
|
@@ -2300,7 +2619,7 @@ export class DaemonClient {
|
|
|
2300
2619
|
return this.sendRequest({
|
|
2301
2620
|
requestId: resolvedRequestId,
|
|
2302
2621
|
message,
|
|
2303
|
-
timeout: 300000, // 5 minutes
|
|
2622
|
+
timeout: 300000, // 5 minutes - npm update can be slow on remote machines
|
|
2304
2623
|
options: { skipQueue: true },
|
|
2305
2624
|
select: (msg) => {
|
|
2306
2625
|
const parsed = DaemonUpdateResponseSchema.safeParse(msg);
|
|
@@ -2698,7 +3017,7 @@ export class DaemonClient {
|
|
|
2698
3017
|
});
|
|
2699
3018
|
}
|
|
2700
3019
|
// ── Git file investigation ────────────────────────────────────────────────
|
|
2701
|
-
// Local git only: no remote, no forge, and no per-provider variant
|
|
3020
|
+
// Local git only: no remote, no forge, and no per-provider variant - the same
|
|
2702
3021
|
// four calls answer for every agent provider. Gated by
|
|
2703
3022
|
// server_info.features.checkoutGitFileHistory.
|
|
2704
3023
|
async checkoutGitFileHistory(cwd, input, requestId) {
|
|
@@ -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,
|
|
@@ -3177,13 +3510,25 @@ export class DaemonClient {
|
|
|
3177
3510
|
content: file.content,
|
|
3178
3511
|
size: file.size,
|
|
3179
3512
|
modifiedAt: file.modifiedAt,
|
|
3180
|
-
// COMPAT(textEditor): added in v0.4.4
|
|
3513
|
+
// COMPAT(textEditor): added in v0.4.4 - the editor is gated on
|
|
3181
3514
|
// features.textEditor, so a gated daemon always sends both fields.
|
|
3182
3515
|
eol: file.eol ?? "lf",
|
|
3183
3516
|
hash: file.hash ?? null,
|
|
3184
3517
|
};
|
|
3185
3518
|
}
|
|
3186
|
-
/**
|
|
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
|
+
}
|
|
3531
|
+
/** Conditional save - see FileWriteRequestSchema for the no-clobber contract. */
|
|
3187
3532
|
async writeFile(options) {
|
|
3188
3533
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
3189
3534
|
requestId: options.requestId,
|
|
@@ -3201,7 +3546,74 @@ export class DaemonClient {
|
|
|
3201
3546
|
});
|
|
3202
3547
|
return payload.result;
|
|
3203
3548
|
}
|
|
3204
|
-
/**
|
|
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
|
+
}
|
|
3616
|
+
/** Create an empty file or a directory. Never overwrites - see FileCreateResultSchema. */
|
|
3205
3617
|
async createFileEntry(options) {
|
|
3206
3618
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
3207
3619
|
requestId: options.requestId,
|
|
@@ -3215,7 +3627,7 @@ export class DaemonClient {
|
|
|
3215
3627
|
});
|
|
3216
3628
|
return payload.result;
|
|
3217
3629
|
}
|
|
3218
|
-
/** Permanent delete
|
|
3630
|
+
/** Permanent delete - an unlink, not a move to any trash. */
|
|
3219
3631
|
async deleteFileEntry(options) {
|
|
3220
3632
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
3221
3633
|
requestId: options.requestId,
|
|
@@ -3337,7 +3749,7 @@ export class DaemonClient {
|
|
|
3337
3749
|
}
|
|
3338
3750
|
/**
|
|
3339
3751
|
* Mirror the editor's current buffer to the daemon so definitions resolve against
|
|
3340
|
-
* unsaved edits. Debounced by the caller
|
|
3752
|
+
* unsaved edits. Debounced by the caller - this is not a per-keystroke RPC.
|
|
3341
3753
|
*/
|
|
3342
3754
|
async syncCodeDocument(cwd, path, text, requestId) {
|
|
3343
3755
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
@@ -3401,7 +3813,7 @@ export class DaemonClient {
|
|
|
3401
3813
|
return { status: payload.status, locations: payload.locations, error: payload.error };
|
|
3402
3814
|
}
|
|
3403
3815
|
/**
|
|
3404
|
-
* A rename **dry run**
|
|
3816
|
+
* A rename **dry run** - every edit it would make, and nothing written. The client
|
|
3405
3817
|
* puts this in front of the user as a job to audit before applying.
|
|
3406
3818
|
*/
|
|
3407
3819
|
async previewCodeRename(input, requestId) {
|
|
@@ -3512,7 +3924,7 @@ export class DaemonClient {
|
|
|
3512
3924
|
*
|
|
3513
3925
|
* Never throws and never carries an error the caller has to render. A workspace with no
|
|
3514
3926
|
* solution, a host with no .NET SDK, and a host with the feature switched off all answer with an
|
|
3515
|
-
* empty list, so the caller has one silent case
|
|
3927
|
+
* empty list, so the caller has one silent case - "no switcher" - rather than four states.
|
|
3516
3928
|
*/
|
|
3517
3929
|
async listSolutions(cwd, requestId) {
|
|
3518
3930
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
@@ -3587,7 +3999,7 @@ export class DaemonClient {
|
|
|
3587
3999
|
}
|
|
3588
4000
|
return payload.symbols;
|
|
3589
4001
|
}
|
|
3590
|
-
/** Preview-first project replace
|
|
4002
|
+
/** Preview-first project replace - see FileReplaceRequestSchema. */
|
|
3591
4003
|
async replaceFiles(options) {
|
|
3592
4004
|
return this.sendCorrelatedSessionRequest({
|
|
3593
4005
|
requestId: options.requestId,
|
|
@@ -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}
|
|
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.
|
|
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) {
|
|
@@ -3774,7 +4171,7 @@ export class DaemonClient {
|
|
|
3774
4171
|
*
|
|
3775
4172
|
* Pass `workspaceId` whenever the write may be project-scoped: the daemon
|
|
3776
4173
|
* binds the entry to the repo root that workspace resolves to, and an entry
|
|
3777
|
-
* scoped to "project" with no root is filtered out of every brief
|
|
4174
|
+
* scoped to "project" with no root is filtered out of every brief - stored,
|
|
3778
4175
|
* listed, and never sent.
|
|
3779
4176
|
*/
|
|
3780
4177
|
async updatePersonalityMemory(input, requestId) {
|
|
@@ -3961,11 +4358,45 @@ export class DaemonClient {
|
|
|
3961
4358
|
responseType: "connectors.list_tools.response",
|
|
3962
4359
|
});
|
|
3963
4360
|
}
|
|
3964
|
-
|
|
4361
|
+
/**
|
|
4362
|
+
* Start a connector's OAuth login. Resolves with the URL to open, or with
|
|
4363
|
+
* status "authorized" when the daemon already held a usable token. The login
|
|
4364
|
+
* itself settles later on the `connectors.oauth.status` push, because the user
|
|
4365
|
+
* is in a browser by then.
|
|
4366
|
+
*/
|
|
4367
|
+
async connectorsOauthAuthorize(connectorId, scope, requestId) {
|
|
4368
|
+
return this.sendCorrelatedSessionRequest({
|
|
4369
|
+
requestId,
|
|
4370
|
+
message: {
|
|
4371
|
+
type: "connectors.oauth.authorize.request",
|
|
4372
|
+
connectorId,
|
|
4373
|
+
...(scope ? { scope } : {}),
|
|
4374
|
+
},
|
|
4375
|
+
responseType: "connectors.oauth.authorize.response",
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
/** Drop a connector's stored authorization. */
|
|
4379
|
+
async connectorsOauthDisconnect(connectorId, requestId) {
|
|
4380
|
+
return this.sendCorrelatedSessionRequest({
|
|
4381
|
+
requestId,
|
|
4382
|
+
message: {
|
|
4383
|
+
type: "connectors.oauth.disconnect.request",
|
|
4384
|
+
connectorId,
|
|
4385
|
+
},
|
|
4386
|
+
responseType: "connectors.oauth.disconnect.response",
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
/**
|
|
4390
|
+
* The brain's status. Pass `resources` only from a surface that renders the
|
|
4391
|
+
* live CPU/RAM/GPU/slot numbers: it costs an `nvidia-smi` spawn plus a /slots
|
|
4392
|
+
* round trip on the brain, and this call is also the liveness poll.
|
|
4393
|
+
*/
|
|
4394
|
+
async brainHostStatus(options, requestId) {
|
|
3965
4395
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
3966
4396
|
requestId,
|
|
3967
4397
|
message: {
|
|
3968
4398
|
type: "brain.host.status.request",
|
|
4399
|
+
resources: options?.resources ?? false,
|
|
3969
4400
|
},
|
|
3970
4401
|
responseType: "brain.host.status.response",
|
|
3971
4402
|
});
|
|
@@ -4191,6 +4622,123 @@ export class DaemonClient {
|
|
|
4191
4622
|
}
|
|
4192
4623
|
return payload.jobs;
|
|
4193
4624
|
}
|
|
4625
|
+
// --- Brain Console --------------------------------------------------------
|
|
4626
|
+
// These proxy the brain's own /__host/* management API and work against a
|
|
4627
|
+
// local or a remote brain identically. Gated by features.brainConsole on the
|
|
4628
|
+
// daemon, and by `capabilities` on brain.host.status for the brain itself.
|
|
4629
|
+
// Each throws the brain's own message on failure, because "could not delete
|
|
4630
|
+
// the model" with no reason is not a usable error.
|
|
4631
|
+
/**
|
|
4632
|
+
* The joined model inventory: scan row, GGUF metadata, saved profile,
|
|
4633
|
+
* calibration state, VRAM budget and benchmark score per model, plus disk
|
|
4634
|
+
* usage. One call feeds the whole Models tab.
|
|
4635
|
+
*/
|
|
4636
|
+
async brainModelsInventory(requestId) {
|
|
4637
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4638
|
+
requestId,
|
|
4639
|
+
message: { type: "brain.models.inventory.request" },
|
|
4640
|
+
responseType: "brain.models.inventory.response",
|
|
4641
|
+
});
|
|
4642
|
+
if (payload.error) {
|
|
4643
|
+
throw new Error(payload.error);
|
|
4644
|
+
}
|
|
4645
|
+
return { models: payload.models, disk: payload.disk };
|
|
4646
|
+
}
|
|
4647
|
+
/** One model's saved profile, the field descriptors, and its warnings. */
|
|
4648
|
+
async brainModelProfileGet(modelId, requestId) {
|
|
4649
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4650
|
+
requestId,
|
|
4651
|
+
message: { type: "brain.model.profile.get.request", modelId },
|
|
4652
|
+
responseType: "brain.model.profile.get.response",
|
|
4653
|
+
});
|
|
4654
|
+
if (payload.error) {
|
|
4655
|
+
throw new Error(payload.error);
|
|
4656
|
+
}
|
|
4657
|
+
return payload;
|
|
4658
|
+
}
|
|
4659
|
+
/**
|
|
4660
|
+
* Write the editable profile fields. The reply carries the recomputed budget,
|
|
4661
|
+
* so an edit costs one round trip rather than a write the UI has to follow
|
|
4662
|
+
* with a read.
|
|
4663
|
+
*/
|
|
4664
|
+
async brainModelProfileSet(modelId, patch, requestId) {
|
|
4665
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4666
|
+
requestId,
|
|
4667
|
+
message: { type: "brain.model.profile.set.request", modelId, patch },
|
|
4668
|
+
responseType: "brain.model.profile.set.response",
|
|
4669
|
+
});
|
|
4670
|
+
if (payload.error) {
|
|
4671
|
+
throw new Error(payload.error);
|
|
4672
|
+
}
|
|
4673
|
+
return payload;
|
|
4674
|
+
}
|
|
4675
|
+
/**
|
|
4676
|
+
* The VRAM budget for a hypothetical profile. `overrides` are string-encoded
|
|
4677
|
+
* field values, so the UI can preview the budget while a control is mid-drag
|
|
4678
|
+
* without persisting a value the user is scrubbing past.
|
|
4679
|
+
*/
|
|
4680
|
+
async brainModelBudget(modelId, overrides, requestId) {
|
|
4681
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4682
|
+
requestId,
|
|
4683
|
+
message: { type: "brain.model.budget.get.request", modelId, overrides: overrides ?? {} },
|
|
4684
|
+
responseType: "brain.model.budget.get.response",
|
|
4685
|
+
});
|
|
4686
|
+
if (payload.error) {
|
|
4687
|
+
throw new Error(payload.error);
|
|
4688
|
+
}
|
|
4689
|
+
return payload;
|
|
4690
|
+
}
|
|
4691
|
+
/**
|
|
4692
|
+
* Load a model into the running brain. This is not `brainHostStart`, which
|
|
4693
|
+
* restarts the daemon's child process and has no remote equivalent.
|
|
4694
|
+
*/
|
|
4695
|
+
async brainModelLoad(modelId, requestId) {
|
|
4696
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4697
|
+
requestId,
|
|
4698
|
+
message: { type: "brain.model.load.request", modelId },
|
|
4699
|
+
responseType: "brain.model.load.response",
|
|
4700
|
+
});
|
|
4701
|
+
if (payload.error) {
|
|
4702
|
+
throw new Error(payload.error);
|
|
4703
|
+
}
|
|
4704
|
+
return payload;
|
|
4705
|
+
}
|
|
4706
|
+
/** Unload the resident model, leaving the brain up and serving nothing. */
|
|
4707
|
+
async brainModelUnload(requestId) {
|
|
4708
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4709
|
+
requestId,
|
|
4710
|
+
message: { type: "brain.model.unload.request" },
|
|
4711
|
+
responseType: "brain.model.unload.response",
|
|
4712
|
+
});
|
|
4713
|
+
if (payload.error) {
|
|
4714
|
+
throw new Error(payload.error);
|
|
4715
|
+
}
|
|
4716
|
+
return payload.status;
|
|
4717
|
+
}
|
|
4718
|
+
/** Delete a model's files. The brain refuses while that model is loaded. */
|
|
4719
|
+
async brainModelDelete(modelId, requestId) {
|
|
4720
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4721
|
+
requestId,
|
|
4722
|
+
message: { type: "brain.model.delete.request", modelId },
|
|
4723
|
+
responseType: "brain.model.delete.response",
|
|
4724
|
+
});
|
|
4725
|
+
if (payload.error) {
|
|
4726
|
+
throw new Error(payload.error);
|
|
4727
|
+
}
|
|
4728
|
+
return payload;
|
|
4729
|
+
}
|
|
4730
|
+
/** Tail the brain's llama-server log. */
|
|
4731
|
+
async brainLogsTail(limit, requestId) {
|
|
4732
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
4733
|
+
requestId,
|
|
4734
|
+
message: { type: "brain.logs.tail.request", limit: limit ?? null },
|
|
4735
|
+
responseType: "brain.logs.tail.response",
|
|
4736
|
+
});
|
|
4737
|
+
if (payload.error) {
|
|
4738
|
+
throw new Error(payload.error);
|
|
4739
|
+
}
|
|
4740
|
+
return payload;
|
|
4741
|
+
}
|
|
4194
4742
|
async getSpeechSettingsOptions(requestId) {
|
|
4195
4743
|
return this.sendNamespacedCorrelatedSessionRequest({
|
|
4196
4744
|
requestId,
|
|
@@ -4336,7 +4884,7 @@ export class DaemonClient {
|
|
|
4336
4884
|
},
|
|
4337
4885
|
});
|
|
4338
4886
|
}
|
|
4339
|
-
/** Daemon-wide "fun stats"
|
|
4887
|
+
/** Daemon-wide "fun stats" - see docs/data-model.md ActivityStatsStore. */
|
|
4340
4888
|
async getActivityStats(options) {
|
|
4341
4889
|
return this.sendNamespacedCorrelatedSessionRequest({
|
|
4342
4890
|
requestId: options?.requestId,
|
|
@@ -4354,7 +4902,7 @@ export class DaemonClient {
|
|
|
4354
4902
|
},
|
|
4355
4903
|
});
|
|
4356
4904
|
}
|
|
4357
|
-
/** Itemized usage ledger
|
|
4905
|
+
/** Itemized usage ledger - the scrollable rows behind the stats tiles (usage-ledger). */
|
|
4358
4906
|
async getUsageLog(options) {
|
|
4359
4907
|
return this.sendNamespacedCorrelatedSessionRequest({
|
|
4360
4908
|
requestId: options?.requestId,
|
|
@@ -5078,7 +5626,7 @@ export class DaemonClient {
|
|
|
5078
5626
|
/**
|
|
5079
5627
|
* Session totals for inbound daemon traffic, including the main-thread time
|
|
5080
5628
|
* spent handling it. Null when runtime metrics are disabled for this client.
|
|
5081
|
-
* Read by the app's resource monitor
|
|
5629
|
+
* Read by the app's resource monitor - the wire is a first-class suspect when
|
|
5082
5630
|
* the UI thread degrades, so it has to be measurable rather than inferred.
|
|
5083
5631
|
*/
|
|
5084
5632
|
getTrafficTotals() {
|
|
@@ -5109,6 +5657,10 @@ export class DaemonClient {
|
|
|
5109
5657
|
[CLIENT_CAPS.customModeIcons]: true,
|
|
5110
5658
|
[CLIENT_CAPS.reasoningMergeEnum]: true,
|
|
5111
5659
|
[CLIENT_CAPS.terminalReflowableSnapshot]: true,
|
|
5660
|
+
[CLIENT_CAPS.providerSubagents]: true,
|
|
5661
|
+
// The daemon gates project.updated.notification on this (session.ts),
|
|
5662
|
+
// so dropping it silently kills cross-session project renames.
|
|
5663
|
+
[CLIENT_CAPS.projectUpdates]: true,
|
|
5112
5664
|
...this.config.capabilities,
|
|
5113
5665
|
},
|
|
5114
5666
|
...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}),
|
|
@@ -5198,13 +5750,18 @@ export class DaemonClient {
|
|
|
5198
5750
|
}
|
|
5199
5751
|
const parsed = validateWSOutboundMessage(parsedJson);
|
|
5200
5752
|
if (!parsed.success) {
|
|
5201
|
-
const
|
|
5753
|
+
const responseIdentity = extractCorrelatedResponseIdentity(parsedJson);
|
|
5754
|
+
const envelopeType = parsedJson != null &&
|
|
5202
5755
|
typeof parsedJson === "object" &&
|
|
5203
5756
|
"type" in parsedJson &&
|
|
5204
5757
|
typeof parsedJson.type === "string"
|
|
5205
5758
|
? parsedJson.type
|
|
5206
5759
|
: "unknown";
|
|
5760
|
+
const msgType = responseIdentity?.responseType ?? envelopeType;
|
|
5207
5761
|
this.logger.warn({ msgType, error: parsed.error.message }, "Message validation failed");
|
|
5762
|
+
if (responseIdentity) {
|
|
5763
|
+
this.rejectWaitersForRequestId(responseIdentity.requestId, new DaemonProtocolError(responseIdentity));
|
|
5764
|
+
}
|
|
5208
5765
|
return;
|
|
5209
5766
|
}
|
|
5210
5767
|
this.consecutiveLivenessFailures = 0;
|
|
@@ -5485,6 +6042,18 @@ export class DaemonClient {
|
|
|
5485
6042
|
}
|
|
5486
6043
|
}
|
|
5487
6044
|
}
|
|
6045
|
+
rejectWaitersForRequestId(requestId, error) {
|
|
6046
|
+
for (const waiter of Array.from(this.waiters)) {
|
|
6047
|
+
if (waiter.requestId !== requestId) {
|
|
6048
|
+
continue;
|
|
6049
|
+
}
|
|
6050
|
+
this.waiters.delete(waiter);
|
|
6051
|
+
if (waiter.timeoutHandle) {
|
|
6052
|
+
clearTimeout(waiter.timeoutHandle);
|
|
6053
|
+
}
|
|
6054
|
+
waiter.reject(error);
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
5488
6057
|
clearWaiters(error) {
|
|
5489
6058
|
for (const waiter of Array.from(this.waiters)) {
|
|
5490
6059
|
if (waiter.timeoutHandle) {
|
|
@@ -5549,7 +6118,7 @@ export class DaemonClient {
|
|
|
5549
6118
|
return null;
|
|
5550
6119
|
}
|
|
5551
6120
|
}
|
|
5552
|
-
waitForWithCancel(predicate, timeout = 30000,
|
|
6121
|
+
waitForWithCancel(predicate, timeout = 30000, options) {
|
|
5553
6122
|
// Capture stack trace at call site, not inside setTimeout
|
|
5554
6123
|
const timeoutError = new Error(`Timeout waiting for message (${timeout}ms)`);
|
|
5555
6124
|
let waiter = null;
|
|
@@ -5582,6 +6151,7 @@ export class DaemonClient {
|
|
|
5582
6151
|
resolve: wrappedResolve,
|
|
5583
6152
|
reject: wrappedReject,
|
|
5584
6153
|
timeoutHandle,
|
|
6154
|
+
requestId: options?.requestId,
|
|
5585
6155
|
};
|
|
5586
6156
|
this.waiters.add(waiter);
|
|
5587
6157
|
});
|
|
@@ -5608,6 +6178,80 @@ export class DaemonClient {
|
|
|
5608
6178
|
};
|
|
5609
6179
|
return { promise, cancel };
|
|
5610
6180
|
}
|
|
6181
|
+
async cloneGithubProject(input, requestId) {
|
|
6182
|
+
const message = {
|
|
6183
|
+
type: "project.github.clone.request",
|
|
6184
|
+
repo: input.repo,
|
|
6185
|
+
targetDirectory: input.targetDirectory,
|
|
6186
|
+
...(input.cloneProtocol ? { cloneProtocol: input.cloneProtocol } : {}),
|
|
6187
|
+
};
|
|
6188
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
6189
|
+
requestId,
|
|
6190
|
+
message,
|
|
6191
|
+
timeout: PROJECT_GITHUB_CLONE_TIMEOUT_MS,
|
|
6192
|
+
});
|
|
6193
|
+
}
|
|
6194
|
+
/**
|
|
6195
|
+
* `includeDiscovered` also returns the Scripts the workspace's own project
|
|
6196
|
+
* files declare (package.json scripts today), each tagged with its `source`.
|
|
6197
|
+
* Gate it on `server_info.features.workspaceScriptDiscovery` - an older
|
|
6198
|
+
* daemon ignores the flag and answers with the otto.json list only.
|
|
6199
|
+
*/
|
|
6200
|
+
async listWorkspaceScripts(workspaceId, options) {
|
|
6201
|
+
return this.sendCorrelatedSessionRequest({
|
|
6202
|
+
requestId: options?.requestId,
|
|
6203
|
+
message: {
|
|
6204
|
+
type: "workspace.script.list.request",
|
|
6205
|
+
workspaceId,
|
|
6206
|
+
includeDiscovered: options?.includeDiscovered ?? false,
|
|
6207
|
+
},
|
|
6208
|
+
responseType: "workspace.script.list.response",
|
|
6209
|
+
});
|
|
6210
|
+
}
|
|
6211
|
+
async startWorkspaceScriptWithStatus(workspaceId, scriptName, requestId) {
|
|
6212
|
+
return this.sendCorrelatedSessionRequest({
|
|
6213
|
+
requestId,
|
|
6214
|
+
message: { type: "workspace.script.start.request", workspaceId, scriptName },
|
|
6215
|
+
responseType: "workspace.script.start.response",
|
|
6216
|
+
});
|
|
6217
|
+
}
|
|
6218
|
+
async stopWorkspaceScript(workspaceId, scriptName, requestId) {
|
|
6219
|
+
return this.sendCorrelatedSessionRequest({
|
|
6220
|
+
requestId,
|
|
6221
|
+
message: { type: "workspace.script.stop.request", workspaceId, scriptName },
|
|
6222
|
+
responseType: "workspace.script.stop.response",
|
|
6223
|
+
});
|
|
6224
|
+
}
|
|
6225
|
+
async connectHub(hubUrl, token, requestId) {
|
|
6226
|
+
this.requireHubRelationshipSupport();
|
|
6227
|
+
return this.sendCorrelatedSessionRequest({
|
|
6228
|
+
requestId,
|
|
6229
|
+
message: { type: "hub.management.daemon.connect.request", hubUrl, token },
|
|
6230
|
+
responseType: "hub.management.daemon.connect.response",
|
|
6231
|
+
});
|
|
6232
|
+
}
|
|
6233
|
+
async getHubStatus(requestId) {
|
|
6234
|
+
this.requireHubRelationshipSupport();
|
|
6235
|
+
return this.sendCorrelatedSessionRequest({
|
|
6236
|
+
requestId,
|
|
6237
|
+
message: { type: "hub.management.daemon.get_status.request" },
|
|
6238
|
+
responseType: "hub.management.daemon.get_status.response",
|
|
6239
|
+
});
|
|
6240
|
+
}
|
|
6241
|
+
async disconnectHub(force = false, requestId) {
|
|
6242
|
+
this.requireHubRelationshipSupport();
|
|
6243
|
+
return this.sendCorrelatedSessionRequest({
|
|
6244
|
+
requestId,
|
|
6245
|
+
message: { type: "hub.management.daemon.disconnect.request", force },
|
|
6246
|
+
responseType: "hub.management.daemon.disconnect.response",
|
|
6247
|
+
});
|
|
6248
|
+
}
|
|
6249
|
+
requireHubRelationshipSupport() {
|
|
6250
|
+
// COMPAT(hubRelationship): added in v0.2.5, drop the gate when floor >= v0.2.5.
|
|
6251
|
+
if (this.lastServerInfoMessage?.features?.hubRelationship !== true) {
|
|
6252
|
+
throw new Error("Update the host to use Hub relationship management.");
|
|
6253
|
+
}
|
|
6254
|
+
}
|
|
5611
6255
|
}
|
|
5612
6256
|
function resolveAgentConfig(options) {
|
|
5613
6257
|
const { config, provider, cwd, env: _env, workspaceId: _workspaceId, initialPrompt: _initialPrompt, images: _images, git: _git, worktreeName: _worktreeName, requestId: _requestId, labels: _labels, ...overrides } = options;
|