@otto-code/client 0.6.6 → 0.7.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/daemon-client-runtime-metrics.d.ts +31 -0
- package/dist/daemon-client-runtime-metrics.js +57 -0
- package/dist/daemon-client.d.ts +388 -7
- package/dist/daemon-client.js +783 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -3
- package/package.json +3 -3
package/dist/daemon-client.js
CHANGED
|
@@ -5,7 +5,7 @@ import { isRelayClientWebSocketUrl } from "@otto-code/protocol/daemon-endpoints"
|
|
|
5
5
|
import { terminalSubscriptionKey } from "@otto-code/protocol/terminal-subscription-key";
|
|
6
6
|
import { asUint8Array, decodeFileTransferFrame, encodeFileTransferFrame, decodeTerminalStreamFrame, FileTransferOpcode, TerminalStreamOpcode, } from "@otto-code/protocol/binary-frames/index";
|
|
7
7
|
import { createRelayE2eeTransportFactory, createWebSocketTransportFactory, decodeMessageData, defaultWebSocketFactory, describeTransportClose, describeTransportError, } from "./daemon-client-transport.js";
|
|
8
|
-
import { DaemonClientRuntimeMetrics } from "./daemon-client-runtime-metrics.js";
|
|
8
|
+
import { DaemonClientRuntimeMetrics, } from "./daemon-client-runtime-metrics.js";
|
|
9
9
|
import { normalizeListProviderModelsPayload, normalizeProviderSnapshotUpdateMessage, normalizeProvidersSnapshotPayload, } from "./compat/normalize-provider-models.js";
|
|
10
10
|
import { TerminalStreamRouter } from "./terminal-stream-router.js";
|
|
11
11
|
const consoleLogger = {
|
|
@@ -193,6 +193,9 @@ export class DaemonClient {
|
|
|
193
193
|
this.checkoutDiffSubscriptions = new Map();
|
|
194
194
|
this.terminalDirectorySubscriptions = new Map();
|
|
195
195
|
this.terminalStreams = new TerminalStreamRouter();
|
|
196
|
+
// requestId -> progress listener for an in-flight project.scaffold.request.
|
|
197
|
+
// Entries are always removed in scaffoldProject's finally block.
|
|
198
|
+
this.scaffoldProgressListeners = new Map();
|
|
196
199
|
this.pendingBinaryFileReads = new Map();
|
|
197
200
|
this.activeBinaryFileTransfers = new Map();
|
|
198
201
|
this.completedBinaryFileReads = new Map();
|
|
@@ -230,15 +233,23 @@ export class DaemonClient {
|
|
|
230
233
|
const runtimeMetricsIntervalMs = typeof config.runtimeMetricsIntervalMs === "number" && config.runtimeMetricsIntervalMs > 0
|
|
231
234
|
? config.runtimeMetricsIntervalMs
|
|
232
235
|
: 0;
|
|
236
|
+
// The metrics object is always constructed — it is a handful of Maps keyed
|
|
237
|
+
// by message type, and its per-message cost is dwarfed by the JSON.parse it
|
|
238
|
+
// is measuring. What `runtimeMetricsIntervalMs` gates is the *periodic log*,
|
|
239
|
+
// which is the part that is actually noisy. Keeping the counters on
|
|
240
|
+
// unconditionally is what lets the app read cumulative traffic (see
|
|
241
|
+
// getTrafficTotals) without every embedder having to opt in; before this
|
|
242
|
+
// split, nothing in the app package passed the interval, so client-side wire
|
|
243
|
+
// accounting existed but never ran.
|
|
244
|
+
const runtimeMetricsWindowMs = typeof config.runtimeMetricsWindowMs === "number" && config.runtimeMetricsWindowMs > 0
|
|
245
|
+
? Math.max(config.runtimeMetricsWindowMs, runtimeMetricsIntervalMs)
|
|
246
|
+
: undefined;
|
|
247
|
+
this.runtimeMetrics = new DaemonClientRuntimeMetrics(this.logger, {
|
|
248
|
+
connectionPath: this.logConnectionPath,
|
|
249
|
+
serverId: this.logServerId,
|
|
250
|
+
getConnectionStatus: () => this.connectionState.status,
|
|
251
|
+
}, runtimeMetricsWindowMs ? { windowMs: runtimeMetricsWindowMs } : undefined);
|
|
233
252
|
if (runtimeMetricsIntervalMs > 0) {
|
|
234
|
-
const runtimeMetricsWindowMs = typeof config.runtimeMetricsWindowMs === "number" && config.runtimeMetricsWindowMs > 0
|
|
235
|
-
? Math.max(config.runtimeMetricsWindowMs, runtimeMetricsIntervalMs)
|
|
236
|
-
: undefined;
|
|
237
|
-
this.runtimeMetrics = new DaemonClientRuntimeMetrics(this.logger, {
|
|
238
|
-
connectionPath: this.logConnectionPath,
|
|
239
|
-
serverId: this.logServerId,
|
|
240
|
-
getConnectionStatus: () => this.connectionState.status,
|
|
241
|
-
}, runtimeMetricsWindowMs ? { windowMs: runtimeMetricsWindowMs } : undefined);
|
|
242
253
|
this.runtimeMetricsInterval = setInterval(() => {
|
|
243
254
|
this.runtimeMetrics?.flush();
|
|
244
255
|
}, runtimeMetricsIntervalMs);
|
|
@@ -445,9 +456,11 @@ export class DaemonClient {
|
|
|
445
456
|
if (this.runtimeMetricsInterval) {
|
|
446
457
|
clearInterval(this.runtimeMetricsInterval);
|
|
447
458
|
this.runtimeMetricsInterval = null;
|
|
459
|
+
// Only the interval-logging clients emit the closing window; a
|
|
460
|
+
// counters-only client has nothing to log.
|
|
448
461
|
this.runtimeMetrics?.flush({ final: true });
|
|
449
|
-
this.runtimeMetrics = null;
|
|
450
462
|
}
|
|
463
|
+
this.runtimeMetrics = null;
|
|
451
464
|
this.updateConnectionState({ status: "disposed" }, { event: "DISPOSE", reason: "Client closed", reasonCode: "disposed" });
|
|
452
465
|
}
|
|
453
466
|
ensureConnected() {
|
|
@@ -1022,6 +1035,54 @@ export class DaemonClient {
|
|
|
1022
1035
|
responseType: "project.add.response",
|
|
1023
1036
|
});
|
|
1024
1037
|
}
|
|
1038
|
+
// Creates a project directory from scratch and registers it. Requires
|
|
1039
|
+
// server_info features.projectScaffold. `onProgress` is optional: the
|
|
1040
|
+
// resolved payload carries the authoritative step list either way.
|
|
1041
|
+
async scaffoldProject(options, requestId) {
|
|
1042
|
+
// Resolved here rather than inside sendCorrelatedSessionRequest so the
|
|
1043
|
+
// progress listener is registered under the same id before the send.
|
|
1044
|
+
const resolvedRequestId = this.createRequestId(requestId);
|
|
1045
|
+
if (options.onProgress) {
|
|
1046
|
+
this.scaffoldProgressListeners.set(resolvedRequestId, options.onProgress);
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
return await this.sendCorrelatedSessionRequest({
|
|
1050
|
+
requestId: resolvedRequestId,
|
|
1051
|
+
message: {
|
|
1052
|
+
type: "project.scaffold.request",
|
|
1053
|
+
parentDirectory: options.parentDirectory,
|
|
1054
|
+
folderName: options.folderName,
|
|
1055
|
+
git: options.git,
|
|
1056
|
+
},
|
|
1057
|
+
responseType: "project.scaffold.response",
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
finally {
|
|
1061
|
+
this.scaffoldProgressListeners.delete(resolvedRequestId);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async listHostingRepositories(options, requestId) {
|
|
1065
|
+
return this.sendCorrelatedSessionRequest({
|
|
1066
|
+
requestId,
|
|
1067
|
+
message: {
|
|
1068
|
+
type: "hosting.list_repositories.request",
|
|
1069
|
+
provider: options.provider,
|
|
1070
|
+
query: options.query,
|
|
1071
|
+
limit: options.limit,
|
|
1072
|
+
},
|
|
1073
|
+
responseType: "hosting.list_repositories.response",
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
async listHostingOwners(options, requestId) {
|
|
1077
|
+
return this.sendCorrelatedSessionRequest({
|
|
1078
|
+
requestId,
|
|
1079
|
+
message: {
|
|
1080
|
+
type: "hosting.list_owners.request",
|
|
1081
|
+
provider: options.provider,
|
|
1082
|
+
},
|
|
1083
|
+
responseType: "hosting.list_owners.response",
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1025
1086
|
async startWorkspaceScript(workspaceId, scriptName, requestId) {
|
|
1026
1087
|
return this.sendCorrelatedSessionRequest({
|
|
1027
1088
|
requestId,
|
|
@@ -1033,16 +1094,69 @@ export class DaemonClient {
|
|
|
1033
1094
|
responseType: "start_workspace_script_response",
|
|
1034
1095
|
});
|
|
1035
1096
|
}
|
|
1036
|
-
async archiveWorkspace(workspaceId,
|
|
1097
|
+
async archiveWorkspace(workspaceId, options) {
|
|
1037
1098
|
return this.sendCorrelatedSessionRequest({
|
|
1038
|
-
requestId,
|
|
1099
|
+
requestId: options?.requestId,
|
|
1039
1100
|
message: {
|
|
1040
1101
|
type: "archive_workspace_request",
|
|
1041
1102
|
workspaceId,
|
|
1103
|
+
...(options?.branchDisposition ? { branchDisposition: options.branchDisposition } : {}),
|
|
1042
1104
|
},
|
|
1043
1105
|
responseType: "archive_workspace_response",
|
|
1044
1106
|
});
|
|
1045
1107
|
}
|
|
1108
|
+
// Read-only pre-archive inspection of a worktree's leftover branch (merge
|
|
1109
|
+
// state, deletability). Gated by server_info.features.worktreeArchiveBranchCleanup.
|
|
1110
|
+
async workspaceArchivePreflight(workspaceId, requestId) {
|
|
1111
|
+
return this.sendCorrelatedSessionRequest({
|
|
1112
|
+
requestId,
|
|
1113
|
+
message: {
|
|
1114
|
+
type: "workspace.archive.preflight.request",
|
|
1115
|
+
workspaceId,
|
|
1116
|
+
},
|
|
1117
|
+
responseType: "workspace.archive.preflight.response",
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
// Repoint a worktree-backed workspace's base branch (what Changes diffs against,
|
|
1121
|
+
// and what merge-into-base / PR creation target). Pass null to reset to the
|
|
1122
|
+
// repository default. Gated by server_info.features.worktreeDiffBase.
|
|
1123
|
+
async setWorktreeBaseRef(workspaceId, baseRef, requestId) {
|
|
1124
|
+
return this.sendCorrelatedSessionRequest({
|
|
1125
|
+
requestId,
|
|
1126
|
+
message: {
|
|
1127
|
+
type: "worktree.baseRef.set.request",
|
|
1128
|
+
workspaceId,
|
|
1129
|
+
baseRef,
|
|
1130
|
+
},
|
|
1131
|
+
responseType: "worktree.baseRef.set.response",
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
// List re-attachable Otto worktrees for a project (or the repo containing cwd):
|
|
1135
|
+
// archived worktree workspaces with a kept branch, plus orphaned on-disk
|
|
1136
|
+
// worktrees. Gated by server_info.features.worktreeReattach.
|
|
1137
|
+
async listReattachableWorktrees(scope, requestId) {
|
|
1138
|
+
return this.sendCorrelatedSessionRequest({
|
|
1139
|
+
requestId,
|
|
1140
|
+
message: {
|
|
1141
|
+
type: "worktree.reattach.list.request",
|
|
1142
|
+
...(scope.projectId ? { projectId: scope.projectId } : {}),
|
|
1143
|
+
...(scope.cwd ? { cwd: scope.cwd } : {}),
|
|
1144
|
+
},
|
|
1145
|
+
responseType: "worktree.reattach.list.response",
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
// Re-attach a "left" worktree as a live workspace: revive an archived workspace
|
|
1149
|
+
// record in place, or bind a fresh workspace to an orphaned on-disk worktree.
|
|
1150
|
+
async reattachWorktree(target, requestId) {
|
|
1151
|
+
return this.sendCorrelatedSessionRequest({
|
|
1152
|
+
requestId,
|
|
1153
|
+
message: {
|
|
1154
|
+
type: "worktree.reattach.request",
|
|
1155
|
+
target,
|
|
1156
|
+
},
|
|
1157
|
+
responseType: "worktree.reattach.response",
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1046
1160
|
async fetchWorkspaceSetupStatus(workspaceId, requestId) {
|
|
1047
1161
|
return this.sendCorrelatedSessionRequest({
|
|
1048
1162
|
requestId,
|
|
@@ -1207,6 +1321,26 @@ export class DaemonClient {
|
|
|
1207
1321
|
},
|
|
1208
1322
|
});
|
|
1209
1323
|
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Bulk-delete archived chat records on this host. Server-side by necessity:
|
|
1326
|
+
* the client's history list is cursor-paginated across hosts and never holds
|
|
1327
|
+
* the whole archived set. Pass `dryRun: true` first to get the count the
|
|
1328
|
+
* confirm dialog quotes, then the same call with `dryRun: false` to delete.
|
|
1329
|
+
*
|
|
1330
|
+
* Removes Otto's records only — provider transcripts are left on disk. Gated
|
|
1331
|
+
* by `server_info.features.historyDelete`; there is no fallback path, so check
|
|
1332
|
+
* the flag before offering the action.
|
|
1333
|
+
*/
|
|
1334
|
+
async clearArchivedAgents(options) {
|
|
1335
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
1336
|
+
requestId: options.requestId,
|
|
1337
|
+
message: {
|
|
1338
|
+
type: "history.agents.clear_archived.request",
|
|
1339
|
+
dryRun: options.dryRun,
|
|
1340
|
+
olderThanDays: options.olderThanDays ?? 0,
|
|
1341
|
+
},
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1210
1344
|
async archiveAgent(agentId) {
|
|
1211
1345
|
const requestId = this.createRequestId();
|
|
1212
1346
|
const message = SessionInboundMessageSchema.parse({
|
|
@@ -1358,6 +1492,114 @@ export class DaemonClient {
|
|
|
1358
1492
|
});
|
|
1359
1493
|
return payload.runIds;
|
|
1360
1494
|
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Orchestration: delete one finished (or draft) run. Throws with the
|
|
1497
|
+
* daemon's reason when it refuses — an active run has to be canceled first.
|
|
1498
|
+
*/
|
|
1499
|
+
async deleteRun(runId) {
|
|
1500
|
+
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
1501
|
+
message: { type: "runs.delete.request", runId },
|
|
1502
|
+
});
|
|
1503
|
+
if (!payload.runId) {
|
|
1504
|
+
throw new Error(payload.error ?? "Failed to delete the orchestration");
|
|
1505
|
+
}
|
|
1506
|
+
return payload.runId;
|
|
1507
|
+
}
|
|
1508
|
+
/** Orchestration: list the host's reusable graph templates. */
|
|
1509
|
+
async listOrchestrationGraphs() {
|
|
1510
|
+
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
1511
|
+
message: { type: "runs.graphs.list.request" },
|
|
1512
|
+
});
|
|
1513
|
+
return payload.graphs;
|
|
1514
|
+
}
|
|
1515
|
+
/** Orchestration: upsert a graph template. Returns the persisted graph. */
|
|
1516
|
+
async saveOrchestrationGraph(graph) {
|
|
1517
|
+
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
1518
|
+
message: { type: "runs.graphs.save.request", graph },
|
|
1519
|
+
});
|
|
1520
|
+
if (payload.error !== undefined || payload.graph === undefined) {
|
|
1521
|
+
throw new Error(payload.error ?? "saveOrchestrationGraph rejected");
|
|
1522
|
+
}
|
|
1523
|
+
return payload.graph;
|
|
1524
|
+
}
|
|
1525
|
+
/** Orchestration: delete a graph template (built-in starters refuse). */
|
|
1526
|
+
async deleteOrchestrationGraph(graphId) {
|
|
1527
|
+
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
1528
|
+
message: { type: "runs.graphs.delete.request", graphId },
|
|
1529
|
+
});
|
|
1530
|
+
if (payload.error !== undefined) {
|
|
1531
|
+
throw new Error(payload.error);
|
|
1532
|
+
}
|
|
1533
|
+
return payload.deleted;
|
|
1534
|
+
}
|
|
1535
|
+
/** Orchestration: list the host's reusable prompt templates and snippets. */
|
|
1536
|
+
async listPromptTemplates() {
|
|
1537
|
+
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
1538
|
+
message: { type: "runs.templates.list.request" },
|
|
1539
|
+
});
|
|
1540
|
+
return payload.templates;
|
|
1541
|
+
}
|
|
1542
|
+
/** Orchestration: upsert a prompt template. Returns the persisted template. */
|
|
1543
|
+
async savePromptTemplate(template) {
|
|
1544
|
+
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
1545
|
+
message: { type: "runs.templates.save.request", template },
|
|
1546
|
+
});
|
|
1547
|
+
if (payload.error !== undefined || payload.template === undefined) {
|
|
1548
|
+
throw new Error(payload.error ?? "savePromptTemplate rejected");
|
|
1549
|
+
}
|
|
1550
|
+
return payload.template;
|
|
1551
|
+
}
|
|
1552
|
+
/** Orchestration: delete a prompt template (built-in starters refuse). */
|
|
1553
|
+
async deletePromptTemplate(templateId) {
|
|
1554
|
+
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
1555
|
+
message: { type: "runs.templates.delete.request", templateId },
|
|
1556
|
+
});
|
|
1557
|
+
if (payload.error !== undefined) {
|
|
1558
|
+
throw new Error(payload.error);
|
|
1559
|
+
}
|
|
1560
|
+
return payload.deleted;
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Orchestration: start (or draft) a user-initiated orchestration. Returns the
|
|
1564
|
+
* run id (graph flavor) and the orchestrator chat's agent id to navigate to.
|
|
1565
|
+
*/
|
|
1566
|
+
async startOrchestration(input) {
|
|
1567
|
+
const payload = await this.sendNamespacedCorrelatedSessionRequest({
|
|
1568
|
+
message: {
|
|
1569
|
+
type: "runs.start.request",
|
|
1570
|
+
flavor: input.flavor,
|
|
1571
|
+
cwd: input.cwd,
|
|
1572
|
+
...(input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}),
|
|
1573
|
+
...(input.title !== undefined ? { title: input.title } : {}),
|
|
1574
|
+
...(input.description !== undefined ? { description: input.description } : {}),
|
|
1575
|
+
...(input.orchestratorPersonalityId !== undefined
|
|
1576
|
+
? { orchestratorPersonalityId: input.orchestratorPersonalityId }
|
|
1577
|
+
: {}),
|
|
1578
|
+
...(input.orchestratorProvider !== undefined
|
|
1579
|
+
? { orchestratorProvider: input.orchestratorProvider }
|
|
1580
|
+
: {}),
|
|
1581
|
+
...(input.orchestratorModel !== undefined
|
|
1582
|
+
? { orchestratorModel: input.orchestratorModel }
|
|
1583
|
+
: {}),
|
|
1584
|
+
...(input.orchestratorThinkingOptionId !== undefined
|
|
1585
|
+
? { orchestratorThinkingOptionId: input.orchestratorThinkingOptionId }
|
|
1586
|
+
: {}),
|
|
1587
|
+
...(input.prompt !== undefined ? { prompt: input.prompt } : {}),
|
|
1588
|
+
...(input.graphId !== undefined ? { graphId: input.graphId } : {}),
|
|
1589
|
+
...(input.graphInputs !== undefined ? { graphInputs: input.graphInputs } : {}),
|
|
1590
|
+
...(input.draft !== undefined ? { draft: input.draft } : {}),
|
|
1591
|
+
...(input.runId !== undefined ? { runId: input.runId } : {}),
|
|
1592
|
+
},
|
|
1593
|
+
});
|
|
1594
|
+
if (payload.error !== undefined) {
|
|
1595
|
+
throw new Error(payload.error);
|
|
1596
|
+
}
|
|
1597
|
+
return {
|
|
1598
|
+
...(payload.runId !== undefined ? { runId: payload.runId } : {}),
|
|
1599
|
+
...(payload.agentId !== undefined ? { agentId: payload.agentId } : {}),
|
|
1600
|
+
...(payload.workspaceId !== undefined ? { workspaceId: payload.workspaceId } : {}),
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1361
1603
|
async updateAgent(agentId, updates) {
|
|
1362
1604
|
const requestId = this.createRequestId();
|
|
1363
1605
|
const message = SessionInboundMessageSchema.parse({
|
|
@@ -1616,6 +1858,7 @@ export class DaemonClient {
|
|
|
1616
1858
|
...(messageId ? { messageId } : {}),
|
|
1617
1859
|
...(options?.images ? { images: options.images } : {}),
|
|
1618
1860
|
...(options?.attachments ? { attachments: options.attachments } : {}),
|
|
1861
|
+
...(options?.delivery ? { delivery: options.delivery } : {}),
|
|
1619
1862
|
});
|
|
1620
1863
|
const payload = await this.sendRequest({
|
|
1621
1864
|
requestId,
|
|
@@ -1634,10 +1877,107 @@ export class DaemonClient {
|
|
|
1634
1877
|
if (!payload.accepted) {
|
|
1635
1878
|
throw new Error(payload.error ?? "sendAgentMessage rejected");
|
|
1636
1879
|
}
|
|
1880
|
+
return {
|
|
1881
|
+
queued: payload.queued ?? false,
|
|
1882
|
+
queuedMessageId: payload.queuedMessageId ?? null,
|
|
1883
|
+
};
|
|
1637
1884
|
}
|
|
1638
1885
|
async sendMessage(agentId, text, options) {
|
|
1639
1886
|
await this.sendAgentMessage(agentId, text, options);
|
|
1640
1887
|
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Pull one message back out of an agent's queue. Returns its text so the
|
|
1890
|
+
* caller can put it back in the composer, or null when the turn already
|
|
1891
|
+
* drained it. Requires `server_info.features.steerQueue`.
|
|
1892
|
+
*/
|
|
1893
|
+
async removeQueuedAgentMessage(agentId, messageId) {
|
|
1894
|
+
const requestId = this.createRequestId();
|
|
1895
|
+
const message = SessionInboundMessageSchema.parse({
|
|
1896
|
+
type: "agent.queue.remove.request",
|
|
1897
|
+
requestId,
|
|
1898
|
+
agentId,
|
|
1899
|
+
messageId,
|
|
1900
|
+
});
|
|
1901
|
+
const payload = await this.sendRequest({
|
|
1902
|
+
requestId,
|
|
1903
|
+
message,
|
|
1904
|
+
options: { skipQueue: true },
|
|
1905
|
+
select: (msg) => {
|
|
1906
|
+
if (msg.type !== "agent.queue.remove.response") {
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
if (msg.payload.requestId !== requestId) {
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
return msg.payload;
|
|
1913
|
+
},
|
|
1914
|
+
});
|
|
1915
|
+
if (payload.error) {
|
|
1916
|
+
throw new Error(payload.error);
|
|
1917
|
+
}
|
|
1918
|
+
return payload.removed;
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Move one queued message to a new position. Resolves false when the entry
|
|
1922
|
+
* was already drained or was already there — the authoritative order arrives
|
|
1923
|
+
* on the agent snapshot either way. Requires
|
|
1924
|
+
* `server_info.features.steerQueueReorder`.
|
|
1925
|
+
*/
|
|
1926
|
+
async reorderQueuedAgentMessage(agentId, messageId, toIndex) {
|
|
1927
|
+
const requestId = this.createRequestId();
|
|
1928
|
+
const message = SessionInboundMessageSchema.parse({
|
|
1929
|
+
type: "agent.queue.reorder.request",
|
|
1930
|
+
requestId,
|
|
1931
|
+
agentId,
|
|
1932
|
+
messageId,
|
|
1933
|
+
toIndex,
|
|
1934
|
+
});
|
|
1935
|
+
const payload = await this.sendRequest({
|
|
1936
|
+
requestId,
|
|
1937
|
+
message,
|
|
1938
|
+
options: { skipQueue: true },
|
|
1939
|
+
select: (msg) => {
|
|
1940
|
+
if (msg.type !== "agent.queue.reorder.response") {
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
if (msg.payload.requestId !== requestId) {
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
return msg.payload;
|
|
1947
|
+
},
|
|
1948
|
+
});
|
|
1949
|
+
if (payload.error) {
|
|
1950
|
+
throw new Error(payload.error);
|
|
1951
|
+
}
|
|
1952
|
+
return payload.moved;
|
|
1953
|
+
}
|
|
1954
|
+
/** Drop every message queued behind an agent's current turn. */
|
|
1955
|
+
async clearAgentQueue(agentId) {
|
|
1956
|
+
const requestId = this.createRequestId();
|
|
1957
|
+
const message = SessionInboundMessageSchema.parse({
|
|
1958
|
+
type: "agent.queue.clear.request",
|
|
1959
|
+
requestId,
|
|
1960
|
+
agentId,
|
|
1961
|
+
});
|
|
1962
|
+
const payload = await this.sendRequest({
|
|
1963
|
+
requestId,
|
|
1964
|
+
message,
|
|
1965
|
+
options: { skipQueue: true },
|
|
1966
|
+
select: (msg) => {
|
|
1967
|
+
if (msg.type !== "agent.queue.clear.response") {
|
|
1968
|
+
return null;
|
|
1969
|
+
}
|
|
1970
|
+
if (msg.payload.requestId !== requestId) {
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
return msg.payload;
|
|
1974
|
+
},
|
|
1975
|
+
});
|
|
1976
|
+
if (payload.error) {
|
|
1977
|
+
throw new Error(payload.error);
|
|
1978
|
+
}
|
|
1979
|
+
return payload.clearedCount;
|
|
1980
|
+
}
|
|
1641
1981
|
async rewindAgent(agentId, messageId, mode) {
|
|
1642
1982
|
const requestId = this.createRequestId();
|
|
1643
1983
|
const message = SessionInboundMessageSchema.parse({
|
|
@@ -2793,6 +3133,62 @@ export class DaemonClient {
|
|
|
2793
3133
|
});
|
|
2794
3134
|
return payload.result;
|
|
2795
3135
|
}
|
|
3136
|
+
/** Create an empty file or a directory. Never overwrites — see FileCreateResultSchema. */
|
|
3137
|
+
async createFileEntry(options) {
|
|
3138
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3139
|
+
requestId: options.requestId,
|
|
3140
|
+
message: {
|
|
3141
|
+
type: "file.create.request",
|
|
3142
|
+
cwd: options.cwd,
|
|
3143
|
+
path: options.path,
|
|
3144
|
+
kind: options.kind,
|
|
3145
|
+
},
|
|
3146
|
+
responseType: "file.create.response",
|
|
3147
|
+
});
|
|
3148
|
+
return payload.result;
|
|
3149
|
+
}
|
|
3150
|
+
/** Permanent delete — an unlink, not a move to any trash. */
|
|
3151
|
+
async deleteFileEntry(options) {
|
|
3152
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3153
|
+
requestId: options.requestId,
|
|
3154
|
+
message: {
|
|
3155
|
+
type: "file.delete.request",
|
|
3156
|
+
cwd: options.cwd,
|
|
3157
|
+
path: options.path,
|
|
3158
|
+
recursive: options.recursive,
|
|
3159
|
+
},
|
|
3160
|
+
responseType: "file.delete.response",
|
|
3161
|
+
});
|
|
3162
|
+
return payload.result;
|
|
3163
|
+
}
|
|
3164
|
+
/** Rename, which is also move. Never clobbers an occupied destination. */
|
|
3165
|
+
async renameFileEntry(options) {
|
|
3166
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3167
|
+
requestId: options.requestId,
|
|
3168
|
+
message: {
|
|
3169
|
+
type: "file.rename.request",
|
|
3170
|
+
cwd: options.cwd,
|
|
3171
|
+
path: options.path,
|
|
3172
|
+
newPath: options.newPath,
|
|
3173
|
+
},
|
|
3174
|
+
responseType: "file.rename.response",
|
|
3175
|
+
});
|
|
3176
|
+
return payload.result;
|
|
3177
|
+
}
|
|
3178
|
+
async refineFile(options) {
|
|
3179
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3180
|
+
requestId: options.requestId,
|
|
3181
|
+
message: {
|
|
3182
|
+
type: "file.refine.request",
|
|
3183
|
+
cwd: options.cwd,
|
|
3184
|
+
documents: options.documents,
|
|
3185
|
+
references: options.references,
|
|
3186
|
+
instruction: options.instruction,
|
|
3187
|
+
},
|
|
3188
|
+
responseType: "file.refine.response",
|
|
3189
|
+
});
|
|
3190
|
+
return payload.result;
|
|
3191
|
+
}
|
|
2796
3192
|
/**
|
|
2797
3193
|
* Project-wide search. Per-file results stream through onFileResult (the
|
|
2798
3194
|
* daemon emits them in order, before the summary response resolves); the
|
|
@@ -2848,6 +3244,265 @@ export class DaemonClient {
|
|
|
2848
3244
|
}
|
|
2849
3245
|
return payload.locations;
|
|
2850
3246
|
}
|
|
3247
|
+
/**
|
|
3248
|
+
* Language-server-backed go-to-definition. Unlike `findCodeSymbols` this resolves the
|
|
3249
|
+
* reference *at a position*, so multiple results mean real overloads or
|
|
3250
|
+
* implementations rather than "two files happen to use this name".
|
|
3251
|
+
*
|
|
3252
|
+
* Line and column are 1-based. Returns the whole payload, not just the locations,
|
|
3253
|
+
* because `indexing` and `unavailable` are answers the caller must show differently
|
|
3254
|
+
* from an empty result.
|
|
3255
|
+
*/
|
|
3256
|
+
async findCodeDefinition(input, requestId) {
|
|
3257
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3258
|
+
requestId,
|
|
3259
|
+
message: {
|
|
3260
|
+
type: "code.definition.request",
|
|
3261
|
+
cwd: input.cwd,
|
|
3262
|
+
path: input.path,
|
|
3263
|
+
line: input.line,
|
|
3264
|
+
column: input.column,
|
|
3265
|
+
},
|
|
3266
|
+
responseType: "code.definition.response",
|
|
3267
|
+
});
|
|
3268
|
+
return { status: payload.status, locations: payload.locations, error: payload.error };
|
|
3269
|
+
}
|
|
3270
|
+
/**
|
|
3271
|
+
* Mirror the editor's current buffer to the daemon so definitions resolve against
|
|
3272
|
+
* unsaved edits. Debounced by the caller — this is not a per-keystroke RPC.
|
|
3273
|
+
*/
|
|
3274
|
+
async syncCodeDocument(cwd, path, text, requestId) {
|
|
3275
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3276
|
+
requestId,
|
|
3277
|
+
message: { type: "code.document.sync.request", cwd, path, text },
|
|
3278
|
+
responseType: "code.document.sync.response",
|
|
3279
|
+
});
|
|
3280
|
+
if (payload.error) {
|
|
3281
|
+
throw new Error(payload.error);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
/** Release the daemon-side mirror when a file tab closes. */
|
|
3285
|
+
async closeCodeDocument(cwd, path, requestId) {
|
|
3286
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3287
|
+
requestId,
|
|
3288
|
+
message: { type: "code.document.close.request", cwd, path },
|
|
3289
|
+
responseType: "code.document.close.response",
|
|
3290
|
+
});
|
|
3291
|
+
if (payload.error) {
|
|
3292
|
+
throw new Error(payload.error);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
/**
|
|
3296
|
+
* The language server's own explanation of the symbol at a position. Returns the
|
|
3297
|
+
* whole payload: `indexing` and `unavailable` read differently to a user than "the
|
|
3298
|
+
* server had nothing to say", which is `ok` with a null `markdown`.
|
|
3299
|
+
*/
|
|
3300
|
+
async getCodeHover(input, requestId) {
|
|
3301
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3302
|
+
requestId,
|
|
3303
|
+
message: {
|
|
3304
|
+
type: "code.hover.request",
|
|
3305
|
+
cwd: input.cwd,
|
|
3306
|
+
path: input.path,
|
|
3307
|
+
line: input.line,
|
|
3308
|
+
column: input.column,
|
|
3309
|
+
},
|
|
3310
|
+
responseType: "code.hover.response",
|
|
3311
|
+
});
|
|
3312
|
+
return {
|
|
3313
|
+
status: payload.status,
|
|
3314
|
+
markdown: payload.markdown,
|
|
3315
|
+
range: payload.range,
|
|
3316
|
+
serverId: payload.serverId,
|
|
3317
|
+
error: payload.error,
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
/** Every reference to the symbol at a position, for the references results tab. */
|
|
3321
|
+
async findCodeReferences(input, requestId) {
|
|
3322
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3323
|
+
requestId,
|
|
3324
|
+
message: {
|
|
3325
|
+
type: "code.references.request",
|
|
3326
|
+
cwd: input.cwd,
|
|
3327
|
+
path: input.path,
|
|
3328
|
+
line: input.line,
|
|
3329
|
+
column: input.column,
|
|
3330
|
+
},
|
|
3331
|
+
responseType: "code.references.response",
|
|
3332
|
+
});
|
|
3333
|
+
return { status: payload.status, locations: payload.locations, error: payload.error };
|
|
3334
|
+
}
|
|
3335
|
+
/**
|
|
3336
|
+
* A rename **dry run** — every edit it would make, and nothing written. The client
|
|
3337
|
+
* puts this in front of the user as a job to audit before applying.
|
|
3338
|
+
*/
|
|
3339
|
+
async previewCodeRename(input, requestId) {
|
|
3340
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3341
|
+
requestId,
|
|
3342
|
+
message: {
|
|
3343
|
+
type: "code.rename.preview.request",
|
|
3344
|
+
cwd: input.cwd,
|
|
3345
|
+
path: input.path,
|
|
3346
|
+
line: input.line,
|
|
3347
|
+
column: input.column,
|
|
3348
|
+
newName: input.newName,
|
|
3349
|
+
},
|
|
3350
|
+
responseType: "code.rename.preview.response",
|
|
3351
|
+
});
|
|
3352
|
+
return {
|
|
3353
|
+
status: payload.status,
|
|
3354
|
+
files: payload.files,
|
|
3355
|
+
fileCount: payload.fileCount,
|
|
3356
|
+
editCount: payload.editCount,
|
|
3357
|
+
planId: payload.planId,
|
|
3358
|
+
error: payload.error,
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
3362
|
+
* Execute a rename the user audited. Sends the and NOT the edits: the daemon
|
|
3363
|
+
* recomputes the plan and refuses unless the identity still matches, which is what keeps
|
|
3364
|
+
* this from being an arbitrary-write RPC and what makes "what you approved is what
|
|
3365
|
+
* happens" enforceable rather than merely intended.
|
|
3366
|
+
*/
|
|
3367
|
+
async applyCodeRename(input, requestId) {
|
|
3368
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3369
|
+
requestId,
|
|
3370
|
+
message: {
|
|
3371
|
+
type: "code.rename.apply.request",
|
|
3372
|
+
cwd: input.cwd,
|
|
3373
|
+
path: input.path,
|
|
3374
|
+
line: input.line,
|
|
3375
|
+
column: input.column,
|
|
3376
|
+
newName: input.newName,
|
|
3377
|
+
planId: input.planId,
|
|
3378
|
+
},
|
|
3379
|
+
responseType: "code.rename.apply.response",
|
|
3380
|
+
});
|
|
3381
|
+
return {
|
|
3382
|
+
status: payload.status,
|
|
3383
|
+
runId: payload.runId,
|
|
3384
|
+
files: payload.files,
|
|
3385
|
+
appliedFiles: payload.appliedFiles,
|
|
3386
|
+
appliedEdits: payload.appliedEdits,
|
|
3387
|
+
skippedEdits: payload.skippedEdits,
|
|
3388
|
+
complete: payload.complete,
|
|
3389
|
+
error: payload.error,
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Take a rename run back. Sends only the run id: the daemon holds the before-images, and
|
|
3394
|
+
* restores a file only if it still holds exactly what the run wrote.
|
|
3395
|
+
*/
|
|
3396
|
+
async undoCodeRename(cwd, runId, requestId) {
|
|
3397
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3398
|
+
requestId,
|
|
3399
|
+
message: { type: "code.rename.undo.request", cwd, runId },
|
|
3400
|
+
responseType: "code.rename.undo.response",
|
|
3401
|
+
});
|
|
3402
|
+
return {
|
|
3403
|
+
status: payload.status,
|
|
3404
|
+
files: payload.files,
|
|
3405
|
+
restoredFiles: payload.restoredFiles,
|
|
3406
|
+
complete: payload.complete,
|
|
3407
|
+
error: payload.error,
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Live language-server state for the Daemon → Code screen: what this host can
|
|
3412
|
+
* supply, and what is running now. `cwd` scopes availability, since a server can
|
|
3413
|
+
* be present in one workspace's `node_modules` and absent in another's.
|
|
3414
|
+
*/
|
|
3415
|
+
async listLspServers(cwd, requestId) {
|
|
3416
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3417
|
+
requestId,
|
|
3418
|
+
message: { type: "lsp.servers.list.request", cwd },
|
|
3419
|
+
responseType: "lsp.servers.list.response",
|
|
3420
|
+
});
|
|
3421
|
+
if (payload.error) {
|
|
3422
|
+
throw new Error(payload.error);
|
|
3423
|
+
}
|
|
3424
|
+
return { languages: payload.languages, running: payload.running };
|
|
3425
|
+
}
|
|
3426
|
+
/** Stop one running language server. */
|
|
3427
|
+
async stopLspServer(rootPath, serverId, requestId) {
|
|
3428
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3429
|
+
requestId,
|
|
3430
|
+
message: { type: "lsp.server.stop.request", rootPath, serverId },
|
|
3431
|
+
responseType: "lsp.server.stop.response",
|
|
3432
|
+
});
|
|
3433
|
+
if (payload.error) {
|
|
3434
|
+
throw new Error(payload.error);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
/**
|
|
3438
|
+
* Solutions in a workspace, which is what decides whether the Files tab shows a view switcher
|
|
3439
|
+
* at all.
|
|
3440
|
+
*
|
|
3441
|
+
* Never throws and never carries an error the caller has to render. A workspace with no
|
|
3442
|
+
* solution, a host with no .NET SDK, and a host with the feature switched off all answer with an
|
|
3443
|
+
* empty list, so the caller has one silent case — "no switcher" — rather than four states.
|
|
3444
|
+
*/
|
|
3445
|
+
async listSolutions(cwd, requestId) {
|
|
3446
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3447
|
+
requestId,
|
|
3448
|
+
message: { type: "code.solution.list.request", cwd },
|
|
3449
|
+
responseType: "code.solution.list.response",
|
|
3450
|
+
});
|
|
3451
|
+
return payload.solutions;
|
|
3452
|
+
}
|
|
3453
|
+
/** One solution's organisation: folders, the projects inside them, configurations. */
|
|
3454
|
+
async getSolutionTree(input, requestId) {
|
|
3455
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3456
|
+
requestId,
|
|
3457
|
+
message: {
|
|
3458
|
+
type: "code.solution.get_tree.request",
|
|
3459
|
+
cwd: input.cwd,
|
|
3460
|
+
solutionPath: input.solutionPath,
|
|
3461
|
+
},
|
|
3462
|
+
responseType: "code.solution.get_tree.response",
|
|
3463
|
+
});
|
|
3464
|
+
if (payload.error) {
|
|
3465
|
+
throw new Error(payload.error);
|
|
3466
|
+
}
|
|
3467
|
+
return {
|
|
3468
|
+
solutionPath: payload.solutionPath,
|
|
3469
|
+
name: payload.name,
|
|
3470
|
+
format: payload.format,
|
|
3471
|
+
folders: payload.folders,
|
|
3472
|
+
projects: payload.projects,
|
|
3473
|
+
buildTypes: payload.buildTypes,
|
|
3474
|
+
platforms: payload.platforms,
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
/**
|
|
3478
|
+
* One project's evaluated file membership, fetched on expand.
|
|
3479
|
+
*
|
|
3480
|
+
* A `failed` status is a normal answer, not an exception: the daemon carries MSBuild's own
|
|
3481
|
+
* message for a project it refused, and one bad project must not blank the tree.
|
|
3482
|
+
*/
|
|
3483
|
+
async loadSolutionProject(input, requestId) {
|
|
3484
|
+
const payload = await this.sendCorrelatedSessionRequest({
|
|
3485
|
+
requestId,
|
|
3486
|
+
message: {
|
|
3487
|
+
type: "code.solution.load_project.request",
|
|
3488
|
+
cwd: input.cwd,
|
|
3489
|
+
solutionPath: input.solutionPath,
|
|
3490
|
+
projectPath: input.projectPath,
|
|
3491
|
+
},
|
|
3492
|
+
responseType: "code.solution.load_project.response",
|
|
3493
|
+
});
|
|
3494
|
+
return {
|
|
3495
|
+
projectPath: payload.projectPath,
|
|
3496
|
+
status: payload.status,
|
|
3497
|
+
nodes: payload.nodes,
|
|
3498
|
+
projectReferences: payload.projectReferences,
|
|
3499
|
+
packageReferences: payload.packageReferences,
|
|
3500
|
+
targetFrameworks: payload.targetFrameworks,
|
|
3501
|
+
outputType: payload.outputType,
|
|
3502
|
+
isSdkStyle: payload.isSdkStyle,
|
|
3503
|
+
error: payload.error,
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
2851
3506
|
/** Definition symbols for a single file (document outline). */
|
|
2852
3507
|
async getCodeOutline(cwd, path, requestId) {
|
|
2853
3508
|
const payload = await this.sendCorrelatedSessionRequest({
|
|
@@ -3000,10 +3655,76 @@ export class DaemonClient {
|
|
|
3000
3655
|
workspaceId: input.workspaceId,
|
|
3001
3656
|
...(input.provider ? { provider: input.provider } : {}),
|
|
3002
3657
|
...(typeof input.windowTokens === "number" ? { windowTokens: input.windowTokens } : {}),
|
|
3658
|
+
...(input.personalityId ? { personalityId: input.personalityId } : {}),
|
|
3003
3659
|
},
|
|
3004
3660
|
responseType: "context.report.get.response",
|
|
3005
3661
|
});
|
|
3006
3662
|
}
|
|
3663
|
+
// ============================================================================
|
|
3664
|
+
// Personality memory
|
|
3665
|
+
// ============================================================================
|
|
3666
|
+
/**
|
|
3667
|
+
* A personality's accrued lessons plus the EXACT brief the daemon would inject
|
|
3668
|
+
* for `projectRoot`. The brief is returned rather than rebuilt client-side
|
|
3669
|
+
* because memory is only trustworthy if what you are shown is what is sent.
|
|
3670
|
+
*/
|
|
3671
|
+
async listPersonalityMemory(input, requestId) {
|
|
3672
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
3673
|
+
requestId,
|
|
3674
|
+
message: {
|
|
3675
|
+
type: "personality.memory.list.request",
|
|
3676
|
+
personalityId: input.personalityId,
|
|
3677
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
3678
|
+
...(input.projectRoot ? { projectRoot: input.projectRoot } : {}),
|
|
3679
|
+
},
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
/**
|
|
3683
|
+
* Add (no `entryId`), edit, or forget (`drop`) one lesson.
|
|
3684
|
+
*
|
|
3685
|
+
* Pass `workspaceId` whenever the write may be project-scoped: the daemon
|
|
3686
|
+
* binds the entry to the repo root that workspace resolves to, and an entry
|
|
3687
|
+
* scoped to "project" with no root is filtered out of every brief — stored,
|
|
3688
|
+
* listed, and never sent.
|
|
3689
|
+
*/
|
|
3690
|
+
async updatePersonalityMemory(input, requestId) {
|
|
3691
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
3692
|
+
requestId,
|
|
3693
|
+
message: {
|
|
3694
|
+
type: "personality.memory.update.request",
|
|
3695
|
+
personalityId: input.personalityId,
|
|
3696
|
+
...(input.entryId ? { entryId: input.entryId } : {}),
|
|
3697
|
+
...(input.text !== undefined ? { text: input.text } : {}),
|
|
3698
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
3699
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
3700
|
+
...(input.projectRoot ? { projectRoot: input.projectRoot } : {}),
|
|
3701
|
+
...(input.drop ? { drop: true } : {}),
|
|
3702
|
+
},
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
/**
|
|
3706
|
+
* Resolve a deleted personality's lessons: move them to another personality or
|
|
3707
|
+
* discard them. Called BEFORE the roster write, so a failure leaves both the
|
|
3708
|
+
* personality and its memory intact.
|
|
3709
|
+
*/
|
|
3710
|
+
async transferPersonalityMemory(input, requestId) {
|
|
3711
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
3712
|
+
requestId,
|
|
3713
|
+
message: {
|
|
3714
|
+
type: "personality.memory.transfer.request",
|
|
3715
|
+
fromPersonalityId: input.fromPersonalityId,
|
|
3716
|
+
...(input.toPersonalityId ? { toPersonalityId: input.toPersonalityId } : {}),
|
|
3717
|
+
mode: input.mode,
|
|
3718
|
+
},
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
/** Per-personality lesson counts, for the accrual indicator and the selector. */
|
|
3722
|
+
async getPersonalityMemoryStats(requestId) {
|
|
3723
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
3724
|
+
requestId,
|
|
3725
|
+
message: { type: "personality.memory.stats.request" },
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3007
3728
|
/** Rewrites one reference between "always loaded" and "link only". */
|
|
3008
3729
|
async requestContextEdgeConvert(input, requestId) {
|
|
3009
3730
|
return this.sendCorrelatedSessionRequest({
|
|
@@ -3019,6 +3740,17 @@ export class DaemonClient {
|
|
|
3019
3740
|
responseType: "context.edge.convert.response",
|
|
3020
3741
|
});
|
|
3021
3742
|
}
|
|
3743
|
+
/** Deletes every mechanically-fixable finding's range in one pass. */
|
|
3744
|
+
async requestContextFindingsFix(input, requestId) {
|
|
3745
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
3746
|
+
requestId,
|
|
3747
|
+
message: {
|
|
3748
|
+
type: "context.findings.fix.request",
|
|
3749
|
+
workspaceId: input.workspaceId,
|
|
3750
|
+
findings: input.findings,
|
|
3751
|
+
},
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3022
3754
|
// ============================================================================
|
|
3023
3755
|
// Provider Models / Commands
|
|
3024
3756
|
// ============================================================================
|
|
@@ -3147,6 +3879,27 @@ export class DaemonClient {
|
|
|
3147
3879
|
},
|
|
3148
3880
|
});
|
|
3149
3881
|
}
|
|
3882
|
+
// Stream a full message aloud on demand (per-message playback button). Audio
|
|
3883
|
+
// arrives as `audio_output` chunks the session already plays; this promise
|
|
3884
|
+
// resolves when playback finishes, is canceled, or errors. `voice` is the
|
|
3885
|
+
// speaking agent's personality voice (resolved on the client).
|
|
3886
|
+
async speakMessage(params, requestId) {
|
|
3887
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
3888
|
+
requestId,
|
|
3889
|
+
message: {
|
|
3890
|
+
type: "speech.tts.speak.request",
|
|
3891
|
+
text: params.text,
|
|
3892
|
+
...(params.voice ? { voice: params.voice } : {}),
|
|
3893
|
+
},
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3896
|
+
// Stop the in-flight message playback started by speakMessage.
|
|
3897
|
+
async cancelSpeakMessage(requestId) {
|
|
3898
|
+
return this.sendNamespacedCorrelatedSessionRequest({
|
|
3899
|
+
requestId,
|
|
3900
|
+
message: { type: "speech.tts.speak.cancel.request" },
|
|
3901
|
+
});
|
|
3902
|
+
}
|
|
3150
3903
|
// Author short spoken cue lines (join / thinking / done) for a persona,
|
|
3151
3904
|
// described inline (name + prompt) so it works for an unsaved editor draft.
|
|
3152
3905
|
// Routed through the Writer chain; the caller stores the result on the
|
|
@@ -3970,6 +4723,18 @@ export class DaemonClient {
|
|
|
3970
4723
|
getLastServerInfoMessage() {
|
|
3971
4724
|
return this.lastServerInfoMessage;
|
|
3972
4725
|
}
|
|
4726
|
+
/**
|
|
4727
|
+
* Session totals for inbound daemon traffic, including the main-thread time
|
|
4728
|
+
* spent handling it. Null when runtime metrics are disabled for this client.
|
|
4729
|
+
* Read by the app's resource monitor — the wire is a first-class suspect when
|
|
4730
|
+
* the UI thread degrades, so it has to be measurable rather than inferred.
|
|
4731
|
+
*/
|
|
4732
|
+
getTrafficTotals() {
|
|
4733
|
+
return this.runtimeMetrics?.getTrafficTotals() ?? null;
|
|
4734
|
+
}
|
|
4735
|
+
getTrafficHotspots(limit) {
|
|
4736
|
+
return this.runtimeMetrics?.getTrafficHotspots(limit) ?? [];
|
|
4737
|
+
}
|
|
3973
4738
|
resolveTransportUrlForAttempt() {
|
|
3974
4739
|
return this.config.url;
|
|
3975
4740
|
}
|
|
@@ -4321,6 +5086,12 @@ export class DaemonClient {
|
|
|
4321
5086
|
if (consumerMessage.type === "terminal_stream_exit") {
|
|
4322
5087
|
this.terminalStreams.removeTerminal(consumerMessage.payload.terminalId);
|
|
4323
5088
|
}
|
|
5089
|
+
// Scaffold progress is advisory and scoped to one in-flight request, so it
|
|
5090
|
+
// is delivered to that request's own listener rather than the global
|
|
5091
|
+
// DaemonEvent stream every consumer would then have to ignore.
|
|
5092
|
+
if (consumerMessage.type === "project.scaffold.progress") {
|
|
5093
|
+
this.scaffoldProgressListeners.get(consumerMessage.payload.requestId)?.(consumerMessage.payload);
|
|
5094
|
+
}
|
|
4324
5095
|
if (this.rawMessageListeners.size > 0) {
|
|
4325
5096
|
for (const handler of this.rawMessageListeners) {
|
|
4326
5097
|
try {
|