@hydra-acp/cli 0.1.48 → 0.1.49
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/cli.js +2075 -552
- package/dist/index.d.ts +45 -13
- package/dist/index.js +749 -56
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1757,7 +1757,10 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1757
1757
|
// every entry would be re-appended to history.jsonl, doubling the log
|
|
1758
1758
|
// each time the session was woken up.
|
|
1759
1759
|
drainBuffered(method) {
|
|
1760
|
+
const buf = this.bufferedNotifications.get(method);
|
|
1761
|
+
const count = buf?.length ?? 0;
|
|
1760
1762
|
this.bufferedNotifications.delete(method);
|
|
1763
|
+
return count;
|
|
1761
1764
|
}
|
|
1762
1765
|
onClose(handler) {
|
|
1763
1766
|
this.closeHandlers.push(handler);
|
|
@@ -2453,6 +2456,8 @@ var Session = class {
|
|
|
2453
2456
|
listSessions;
|
|
2454
2457
|
logger;
|
|
2455
2458
|
transformChain;
|
|
2459
|
+
extensionCommands;
|
|
2460
|
+
extensionCommandsUnsub;
|
|
2456
2461
|
// Outstanding "processing" claims: token → claim waiting for respondsTo discharge.
|
|
2457
2462
|
pendingClaims = /* @__PURE__ */ new Map();
|
|
2458
2463
|
agentChangeHandlers = [];
|
|
@@ -2548,6 +2553,14 @@ var Session = class {
|
|
|
2548
2553
|
this.listSessions = init.listSessions;
|
|
2549
2554
|
this.logger = init.logger;
|
|
2550
2555
|
this.transformChain = init.transformChain ?? [];
|
|
2556
|
+
this.extensionCommands = init.extensionCommands;
|
|
2557
|
+
if (this.extensionCommands) {
|
|
2558
|
+
this.extensionCommandsUnsub = this.extensionCommands.onChange(() => {
|
|
2559
|
+
if (!this.closed) {
|
|
2560
|
+
this.broadcastMergedCommands();
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2551
2564
|
if (init.firstPromptSeeded) {
|
|
2552
2565
|
this.firstPromptSeeded = true;
|
|
2553
2566
|
}
|
|
@@ -2562,18 +2575,11 @@ var Session = class {
|
|
|
2562
2575
|
this.notifyChain("session.opened", {});
|
|
2563
2576
|
}
|
|
2564
2577
|
broadcastMergedCommands() {
|
|
2565
|
-
const merged = [
|
|
2566
|
-
...hydraCommandsAsAdvertised(),
|
|
2567
|
-
{ name: "model <model-id>", description: "Switch model; omit arg to list available models" },
|
|
2568
|
-
{ name: "sessions", description: "List all sessions" },
|
|
2569
|
-
{ name: "help", description: "Show available commands" },
|
|
2570
|
-
...this.agentAdvertisedCommands
|
|
2571
|
-
];
|
|
2572
2578
|
this.recordAndBroadcast("session/update", {
|
|
2573
2579
|
sessionId: this.upstreamSessionId,
|
|
2574
2580
|
update: {
|
|
2575
2581
|
sessionUpdate: "available_commands_update",
|
|
2576
|
-
availableCommands:
|
|
2582
|
+
availableCommands: this.mergedAvailableCommands()
|
|
2577
2583
|
}
|
|
2578
2584
|
});
|
|
2579
2585
|
}
|
|
@@ -3736,6 +3742,9 @@ var Session = class {
|
|
|
3736
3742
|
if (!trimmed || trimmed === this.currentModel) {
|
|
3737
3743
|
return true;
|
|
3738
3744
|
}
|
|
3745
|
+
this.logger?.info(
|
|
3746
|
+
`live current_model_update: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
3747
|
+
);
|
|
3739
3748
|
this.currentModel = trimmed;
|
|
3740
3749
|
for (const handler of this.modelHandlers) {
|
|
3741
3750
|
try {
|
|
@@ -3781,6 +3790,9 @@ var Session = class {
|
|
|
3781
3790
|
if (typeof cv === "string") {
|
|
3782
3791
|
const trimmed = cv.trim();
|
|
3783
3792
|
if (trimmed && trimmed !== this.currentModel) {
|
|
3793
|
+
this.logger?.info(
|
|
3794
|
+
`live config_option_update(model): sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
3795
|
+
);
|
|
3784
3796
|
this.currentModel = trimmed;
|
|
3785
3797
|
for (const handler of this.modelHandlers) {
|
|
3786
3798
|
try {
|
|
@@ -3949,6 +3961,9 @@ var Session = class {
|
|
|
3949
3961
|
this.broadcastAvailableModes();
|
|
3950
3962
|
}
|
|
3951
3963
|
setAgentAdvertisedModels(models) {
|
|
3964
|
+
this.logger?.info(
|
|
3965
|
+
`setAgentAdvertisedModels: sessionId=${this.sessionId} currentModel=${JSON.stringify(this.currentModel)} newList=[${models.map((m) => m.modelId).join(",")}]`
|
|
3966
|
+
);
|
|
3952
3967
|
if (sameAdvertisedModels(this.agentAdvertisedModels, models)) {
|
|
3953
3968
|
this.broadcastAvailableModels();
|
|
3954
3969
|
return;
|
|
@@ -3980,6 +3995,38 @@ var Session = class {
|
|
|
3980
3995
|
onModeChange(handler) {
|
|
3981
3996
|
this.modeHandlers.push(handler);
|
|
3982
3997
|
}
|
|
3998
|
+
// Apply a model change initiated by a client request (session/set_model)
|
|
3999
|
+
// when the agent doesn't emit a current_model_update notification, or
|
|
4000
|
+
// emits a non-spec shape (e.g. config_option_update). Fires modelHandlers
|
|
4001
|
+
// (persistence) and broadcasts a synthetic current_model_update so all
|
|
4002
|
+
// attached clients — including the originator — repaint immediately.
|
|
4003
|
+
applyModelChange(modelId) {
|
|
4004
|
+
const trimmed = modelId.trim();
|
|
4005
|
+
if (!trimmed || trimmed === this.currentModel) {
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
this.logger?.info(
|
|
4009
|
+
`applyModelChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4010
|
+
);
|
|
4011
|
+
this.currentModel = trimmed;
|
|
4012
|
+
for (const handler of this.modelHandlers) {
|
|
4013
|
+
try {
|
|
4014
|
+
handler(trimmed);
|
|
4015
|
+
} catch {
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
const update = {
|
|
4019
|
+
sessionUpdate: "current_model_update",
|
|
4020
|
+
currentModel: trimmed
|
|
4021
|
+
};
|
|
4022
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
4023
|
+
update.availableModels = [...this.agentAdvertisedModels];
|
|
4024
|
+
}
|
|
4025
|
+
this.recordAndBroadcast("session/update", {
|
|
4026
|
+
sessionId: this.upstreamSessionId,
|
|
4027
|
+
update
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
3983
4030
|
// Apply a mode change initiated by a client request (session/set_mode)
|
|
3984
4031
|
// when the agent doesn't emit a current_mode_update notification on its
|
|
3985
4032
|
// own. Fires modeHandlers so the persistence hook and any other listeners
|
|
@@ -4003,11 +4050,31 @@ var Session = class {
|
|
|
4003
4050
|
onUsageChange(handler) {
|
|
4004
4051
|
this.usageHandlers.push(handler);
|
|
4005
4052
|
}
|
|
4006
|
-
// Returns a freshly merged command list (hydra ∪ agent) for
|
|
4007
|
-
// that need a snapshot — notably acp-ws.ts's buildResponseMeta
|
|
4008
|
-
// assembling the attach response.
|
|
4053
|
+
// Returns a freshly merged command list (hydra ∪ extension ∪ agent) for
|
|
4054
|
+
// callers that need a snapshot — notably acp-ws.ts's buildResponseMeta
|
|
4055
|
+
// when assembling the attach response. Order: built-in hydra verbs,
|
|
4056
|
+
// top-level daemon verbs (/model, /sessions, /help), extension-registered
|
|
4057
|
+
// entries, then whatever the agent advertised.
|
|
4009
4058
|
mergedAvailableCommands() {
|
|
4010
|
-
|
|
4059
|
+
const out = [
|
|
4060
|
+
...hydraCommandsAsAdvertised(),
|
|
4061
|
+
{ name: "model <model-id>", description: "Switch model; omit arg to list available models" },
|
|
4062
|
+
{ name: "sessions", description: "List all sessions" },
|
|
4063
|
+
{ name: "help", description: "Show available commands" }
|
|
4064
|
+
];
|
|
4065
|
+
if (this.extensionCommands) {
|
|
4066
|
+
for (const { name, command } of this.extensionCommands.list()) {
|
|
4067
|
+
const head = `hydra ${name} ${command.verb}`;
|
|
4068
|
+
const display = command.argsHint ? `${head} ${command.argsHint}` : head;
|
|
4069
|
+
const entry = { name: display };
|
|
4070
|
+
if (command.description) {
|
|
4071
|
+
entry.description = command.description;
|
|
4072
|
+
}
|
|
4073
|
+
out.push(entry);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
out.push(...this.agentAdvertisedCommands);
|
|
4077
|
+
return out;
|
|
4011
4078
|
}
|
|
4012
4079
|
// The agent's own advertised commands (not merged with hydra verbs).
|
|
4013
4080
|
// Used by SessionManager to persist into meta.json so cold resurrect
|
|
@@ -4059,39 +4126,118 @@ var Session = class {
|
|
|
4059
4126
|
// caller's promise resolves like a normal turn. To add a verb: append
|
|
4060
4127
|
// an entry to HYDRA_COMMANDS (drives validation + client advertising)
|
|
4061
4128
|
// and a dispatch case in the switch below.
|
|
4129
|
+
//
|
|
4130
|
+
// Extensions/transformers can also bind verbs via the
|
|
4131
|
+
// ExtensionCommandRegistry: "/hydra <process-name> <verb> [args]" routes
|
|
4132
|
+
// to that process's WS connection. Built-in hydra verbs win on name
|
|
4133
|
+
// collision so an extension can never shadow them.
|
|
4062
4134
|
async handleSlashCommand(text) {
|
|
4063
4135
|
const rest = text.slice("/hydra".length).trim();
|
|
4064
4136
|
const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
4065
|
-
const
|
|
4066
|
-
const
|
|
4067
|
-
if (
|
|
4137
|
+
const first = match?.[1] ?? "";
|
|
4138
|
+
const remainder = (match?.[2] ?? "").trim();
|
|
4139
|
+
if (first === "") {
|
|
4068
4140
|
return { stopReason: "end_turn" };
|
|
4069
4141
|
}
|
|
4070
|
-
if (
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4142
|
+
if (HYDRA_COMMANDS.some((c) => c.verb === first)) {
|
|
4143
|
+
switch (first) {
|
|
4144
|
+
case "title":
|
|
4145
|
+
return this.runTitleCommand(remainder);
|
|
4146
|
+
case "agent":
|
|
4147
|
+
return this.runAgentCommand(remainder);
|
|
4148
|
+
case "kill":
|
|
4149
|
+
return this.runKillCommand();
|
|
4150
|
+
case "restart":
|
|
4151
|
+
return this.runRestartCommand();
|
|
4152
|
+
default: {
|
|
4153
|
+
const err2 = new Error(
|
|
4154
|
+
`no dispatcher for /hydra verb ${first}`
|
|
4155
|
+
);
|
|
4156
|
+
err2.code = JsonRpcErrorCodes.InternalError;
|
|
4157
|
+
throw err2;
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4077
4160
|
}
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
`no dispatcher for /hydra verb ${verb}`
|
|
4090
|
-
);
|
|
4091
|
-
err.code = JsonRpcErrorCodes.InternalError;
|
|
4092
|
-
throw err;
|
|
4161
|
+
if (this.extensionCommands?.has(first)) {
|
|
4162
|
+
return this.runExtensionCommand(first, remainder);
|
|
4163
|
+
}
|
|
4164
|
+
const known = HYDRA_COMMANDS.map((c) => c.verb);
|
|
4165
|
+
if (this.extensionCommands) {
|
|
4166
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4167
|
+
for (const { name } of this.extensionCommands.list()) {
|
|
4168
|
+
if (!seen.has(name)) {
|
|
4169
|
+
known.push(name);
|
|
4170
|
+
seen.add(name);
|
|
4171
|
+
}
|
|
4093
4172
|
}
|
|
4094
4173
|
}
|
|
4174
|
+
const err = new Error(
|
|
4175
|
+
`unknown /hydra verb: ${first} (known: ${known.join(", ")})`
|
|
4176
|
+
);
|
|
4177
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
4178
|
+
throw err;
|
|
4179
|
+
}
|
|
4180
|
+
// "/hydra <name> <verb> [args]" — name matches a registered extension
|
|
4181
|
+
// or transformer. We split the remainder into verb + args, validate the
|
|
4182
|
+
// verb against what the process advertised, and forward as a
|
|
4183
|
+
// hydra-acp/extension_command request on the process's WS connection.
|
|
4184
|
+
// The reply's text (if any) is broadcast as a synthetic
|
|
4185
|
+
// agent_message_chunk so it appears in the conversation alongside the
|
|
4186
|
+
// user's invocation.
|
|
4187
|
+
runExtensionCommand(name, remainder) {
|
|
4188
|
+
return this.enqueuePrompt(async () => {
|
|
4189
|
+
const entry = this.extensionCommands?.get(name);
|
|
4190
|
+
if (!entry) {
|
|
4191
|
+
return this.emitExtensionReply(
|
|
4192
|
+
`extension "${name}" is no longer connected`
|
|
4193
|
+
);
|
|
4194
|
+
}
|
|
4195
|
+
const m = remainder.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
4196
|
+
const verb = m?.[1] ?? "";
|
|
4197
|
+
const args = (m?.[2] ?? "").trim();
|
|
4198
|
+
if (verb === "") {
|
|
4199
|
+
const verbs = entry.commands.map((c) => c.verb).join(", ");
|
|
4200
|
+
return this.emitExtensionReply(
|
|
4201
|
+
`/hydra ${name} requires a verb (known: ${verbs || "(none)"})`
|
|
4202
|
+
);
|
|
4203
|
+
}
|
|
4204
|
+
if (!entry.commands.some((c) => c.verb === verb)) {
|
|
4205
|
+
const verbs = entry.commands.map((c) => c.verb).join(", ");
|
|
4206
|
+
return this.emitExtensionReply(
|
|
4207
|
+
`unknown verb "${verb}" for ${name} (known: ${verbs || "(none)"})`
|
|
4208
|
+
);
|
|
4209
|
+
}
|
|
4210
|
+
let reply;
|
|
4211
|
+
try {
|
|
4212
|
+
reply = await entry.connection.request("hydra-acp/extension_command", {
|
|
4213
|
+
sessionId: this.sessionId,
|
|
4214
|
+
verb,
|
|
4215
|
+
args
|
|
4216
|
+
});
|
|
4217
|
+
} catch (err) {
|
|
4218
|
+
return this.emitExtensionReply(
|
|
4219
|
+
`${name} ${verb}: ${err.message}`
|
|
4220
|
+
);
|
|
4221
|
+
}
|
|
4222
|
+
const text = reply && typeof reply === "object" && typeof reply.text === "string" ? reply.text : "";
|
|
4223
|
+
if (text.length > 0) {
|
|
4224
|
+
return this.emitExtensionReply(text);
|
|
4225
|
+
}
|
|
4226
|
+
return { stopReason: "end_turn" };
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
4229
|
+
emitExtensionReply(text) {
|
|
4230
|
+
this.recordAndBroadcast("session/update", {
|
|
4231
|
+
sessionId: this.upstreamSessionId,
|
|
4232
|
+
update: {
|
|
4233
|
+
sessionUpdate: "agent_message_chunk",
|
|
4234
|
+
content: { type: "text", text: `
|
|
4235
|
+
${text}
|
|
4236
|
+
` },
|
|
4237
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
4238
|
+
}
|
|
4239
|
+
});
|
|
4240
|
+
return { stopReason: "end_turn" };
|
|
4095
4241
|
}
|
|
4096
4242
|
async handleSessionsCommand() {
|
|
4097
4243
|
let text;
|
|
@@ -4154,11 +4300,15 @@ ${text}
|
|
|
4154
4300
|
if (models.length === 0) {
|
|
4155
4301
|
body = current ? `Current model: ${current}` : "_(no models advertised yet)_";
|
|
4156
4302
|
} else {
|
|
4303
|
+
const inList = current ? models.some((m) => m.modelId === current) : true;
|
|
4157
4304
|
const lines = models.map((m) => {
|
|
4158
4305
|
const marker = m.modelId === current ? " \u25C0" : "";
|
|
4159
4306
|
const desc = m.name && m.name !== m.modelId ? ` ${m.name}` : "";
|
|
4160
4307
|
return `${m.modelId}${marker}${desc}`;
|
|
4161
4308
|
});
|
|
4309
|
+
if (!inList && current) {
|
|
4310
|
+
lines.unshift(`${current} \u25C0`);
|
|
4311
|
+
}
|
|
4162
4312
|
body = lines.join("\n");
|
|
4163
4313
|
}
|
|
4164
4314
|
this.recordAndBroadcast("session/update", {
|
|
@@ -4586,6 +4736,10 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
4586
4736
|
}
|
|
4587
4737
|
this.closed = true;
|
|
4588
4738
|
this.cancelIdleTimer();
|
|
4739
|
+
if (this.extensionCommandsUnsub) {
|
|
4740
|
+
this.extensionCommandsUnsub();
|
|
4741
|
+
this.extensionCommandsUnsub = void 0;
|
|
4742
|
+
}
|
|
4589
4743
|
if (this.currentEntry?.kind === "user") {
|
|
4590
4744
|
this.broadcastTurnComplete(
|
|
4591
4745
|
this.currentEntry.clientId,
|
|
@@ -5711,6 +5865,7 @@ var SessionManager = class {
|
|
|
5711
5865
|
this.defaultTransformers = options.defaultTransformers ?? [];
|
|
5712
5866
|
this.logger = options.logger;
|
|
5713
5867
|
this.npmRegistry = options.npmRegistry;
|
|
5868
|
+
this.extensionCommands = options.extensionCommands;
|
|
5714
5869
|
}
|
|
5715
5870
|
registry;
|
|
5716
5871
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -5729,6 +5884,7 @@ var SessionManager = class {
|
|
|
5729
5884
|
metaWriteQueues = /* @__PURE__ */ new Map();
|
|
5730
5885
|
logger;
|
|
5731
5886
|
npmRegistry;
|
|
5887
|
+
extensionCommands;
|
|
5732
5888
|
async create(params) {
|
|
5733
5889
|
const fresh = await this.bootstrapAgent({
|
|
5734
5890
|
agentId: params.agentId,
|
|
@@ -5782,7 +5938,8 @@ var SessionManager = class {
|
|
|
5782
5938
|
agentModes: fresh.initialModes,
|
|
5783
5939
|
agentModels: fresh.initialModels,
|
|
5784
5940
|
transformChain: params.transformChain,
|
|
5785
|
-
parentSessionId: params.parentSessionId
|
|
5941
|
+
parentSessionId: params.parentSessionId,
|
|
5942
|
+
extensionCommands: this.extensionCommands
|
|
5786
5943
|
});
|
|
5787
5944
|
await this.attachManagerHooks(session);
|
|
5788
5945
|
return session;
|
|
@@ -5853,12 +6010,14 @@ var SessionManager = class {
|
|
|
5853
6010
|
}
|
|
5854
6011
|
let loadResult;
|
|
5855
6012
|
try {
|
|
6013
|
+
const loadMeta = buildSessionLoadMeta(params.agentId, params.currentModel);
|
|
5856
6014
|
loadResult = await agent.connection.request(
|
|
5857
6015
|
"session/load",
|
|
5858
6016
|
{
|
|
5859
6017
|
sessionId: params.upstreamSessionId,
|
|
5860
6018
|
cwd: params.cwd,
|
|
5861
|
-
mcpServers: []
|
|
6019
|
+
mcpServers: [],
|
|
6020
|
+
...loadMeta && { _meta: loadMeta }
|
|
5862
6021
|
}
|
|
5863
6022
|
);
|
|
5864
6023
|
} catch (err) {
|
|
@@ -5874,7 +6033,10 @@ var SessionManager = class {
|
|
|
5874
6033
|
() => void 0
|
|
5875
6034
|
);
|
|
5876
6035
|
} else {
|
|
5877
|
-
agent.connection.drainBuffered("session/update");
|
|
6036
|
+
const drain1Count = agent.connection.drainBuffered("session/update");
|
|
6037
|
+
this.logger?.info(
|
|
6038
|
+
`resurrect: drain1 dropped ${drain1Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
|
|
6039
|
+
);
|
|
5878
6040
|
}
|
|
5879
6041
|
const agentReportedMode = extractInitialCurrentMode(loadResult ?? {});
|
|
5880
6042
|
const advertisedModes = params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {}));
|
|
@@ -5892,6 +6054,30 @@ var SessionManager = class {
|
|
|
5892
6054
|
this.logger?.info(
|
|
5893
6055
|
`resurrect: effectiveMode=${JSON.stringify(effectiveMode)} for sessionId=${params.hydraSessionId}`
|
|
5894
6056
|
);
|
|
6057
|
+
const agentReportedModel = extractInitialModel(loadResult ?? {});
|
|
6058
|
+
const advertisedModels = nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})) ?? params.agentModels;
|
|
6059
|
+
this.logger?.info(
|
|
6060
|
+
`resurrect: sessionId=${params.hydraSessionId} persistedModel=${JSON.stringify(params.currentModel)} agentReportedModel=${JSON.stringify(agentReportedModel)} advertisedModels=${JSON.stringify(advertisedModels?.map((m) => m.modelId))}`
|
|
6061
|
+
);
|
|
6062
|
+
if (params.pendingHistorySync !== true) {
|
|
6063
|
+
const drain2Count = agent.connection.drainBuffered("session/update");
|
|
6064
|
+
this.logger?.info(
|
|
6065
|
+
`resurrect: drain2 (post-mode-restore) dropped ${drain2Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
|
|
6066
|
+
);
|
|
6067
|
+
}
|
|
6068
|
+
const effectiveModel = await restoreCurrentModel({
|
|
6069
|
+
agent,
|
|
6070
|
+
upstreamSessionId: params.upstreamSessionId,
|
|
6071
|
+
persistedModel: params.currentModel,
|
|
6072
|
+
agentReportedModel,
|
|
6073
|
+
logger: this.logger
|
|
6074
|
+
});
|
|
6075
|
+
if (params.pendingHistorySync !== true) {
|
|
6076
|
+
const drain3Count = agent.connection.drainBuffered("session/update");
|
|
6077
|
+
this.logger?.info(
|
|
6078
|
+
`resurrect: drain3 (post-model-restore) dropped ${drain3Count} buffered session/update(s) for sessionId=${params.hydraSessionId}`
|
|
6079
|
+
);
|
|
6080
|
+
}
|
|
5895
6081
|
const session = new Session({
|
|
5896
6082
|
sessionId: params.hydraSessionId,
|
|
5897
6083
|
cwd: params.cwd,
|
|
@@ -5908,11 +6094,7 @@ var SessionManager = class {
|
|
|
5908
6094
|
listSessions: () => this.list(),
|
|
5909
6095
|
historyStore: this.histories,
|
|
5910
6096
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
5911
|
-
|
|
5912
|
-
// we never captured one (e.g. old opencode sessions on disk before
|
|
5913
|
-
// this fix), fall back to the model the agent ships in its
|
|
5914
|
-
// session/load response body.
|
|
5915
|
-
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
6097
|
+
currentModel: effectiveModel,
|
|
5916
6098
|
currentMode: effectiveMode,
|
|
5917
6099
|
currentUsage: params.currentUsage,
|
|
5918
6100
|
agentCommands: params.agentCommands,
|
|
@@ -5921,13 +6103,14 @@ var SessionManager = class {
|
|
|
5921
6103
|
// snapshot — the proxy's available models can change between daemon
|
|
5922
6104
|
// restarts (quota resets, rollouts), so meta.json is intentionally
|
|
5923
6105
|
// treated as a cold fallback here, not the authoritative source.
|
|
5924
|
-
agentModels:
|
|
6106
|
+
agentModels: advertisedModels,
|
|
5925
6107
|
// Only gate the first-prompt title heuristic when we actually have
|
|
5926
6108
|
// a title to preserve. A title-less session (lost to a write race
|
|
5927
6109
|
// or never seeded) should re-derive from the next prompt rather
|
|
5928
6110
|
// than stay stuck.
|
|
5929
6111
|
firstPromptSeeded: !!params.title,
|
|
5930
|
-
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
6112
|
+
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
6113
|
+
extensionCommands: this.extensionCommands
|
|
5931
6114
|
});
|
|
5932
6115
|
await this.attachManagerHooks(session);
|
|
5933
6116
|
return session;
|
|
@@ -5946,7 +6129,11 @@ var SessionManager = class {
|
|
|
5946
6129
|
cwd,
|
|
5947
6130
|
agentArgs: params.agentArgs,
|
|
5948
6131
|
mcpServers: [],
|
|
5949
|
-
onInstallProgress: params.onInstallProgress
|
|
6132
|
+
onInstallProgress: params.onInstallProgress,
|
|
6133
|
+
// Pass the persisted model so bootstrapAgent calls session/set_model
|
|
6134
|
+
// during session/new — the only context where the agent reliably
|
|
6135
|
+
// honours the switch.
|
|
6136
|
+
model: params.currentModel
|
|
5950
6137
|
});
|
|
5951
6138
|
const advertisedModes = params.agentModes ?? fresh.initialModes;
|
|
5952
6139
|
const effectiveMode = await restoreCurrentMode({
|
|
@@ -5957,6 +6144,15 @@ var SessionManager = class {
|
|
|
5957
6144
|
advertisedModes,
|
|
5958
6145
|
logger: this.logger
|
|
5959
6146
|
});
|
|
6147
|
+
const advertisedModels = params.agentModels ?? fresh.initialModels;
|
|
6148
|
+
const effectiveModel = await restoreCurrentModel({
|
|
6149
|
+
agent: fresh.agent,
|
|
6150
|
+
upstreamSessionId: fresh.upstreamSessionId,
|
|
6151
|
+
persistedModel: params.currentModel,
|
|
6152
|
+
agentReportedModel: fresh.initialModel,
|
|
6153
|
+
logger: this.logger
|
|
6154
|
+
});
|
|
6155
|
+
fresh.agent.connection.drainBuffered("session/update");
|
|
5960
6156
|
const session = new Session({
|
|
5961
6157
|
sessionId: params.hydraSessionId,
|
|
5962
6158
|
cwd,
|
|
@@ -5973,16 +6169,15 @@ var SessionManager = class {
|
|
|
5973
6169
|
listSessions: () => this.list(),
|
|
5974
6170
|
historyStore: this.histories,
|
|
5975
6171
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
5976
|
-
|
|
5977
|
-
// fall back to whatever the agent ships in its session/new response.
|
|
5978
|
-
currentModel: params.currentModel ?? fresh.initialModel,
|
|
6172
|
+
currentModel: effectiveModel,
|
|
5979
6173
|
currentMode: effectiveMode,
|
|
5980
6174
|
currentUsage: params.currentUsage,
|
|
5981
6175
|
agentCommands: params.agentCommands,
|
|
5982
6176
|
agentModes: advertisedModes,
|
|
5983
|
-
agentModels:
|
|
6177
|
+
agentModels: advertisedModels,
|
|
5984
6178
|
firstPromptSeeded: !!params.title,
|
|
5985
|
-
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
6179
|
+
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
6180
|
+
extensionCommands: this.extensionCommands
|
|
5986
6181
|
});
|
|
5987
6182
|
await this.attachManagerHooks(session);
|
|
5988
6183
|
void session.seedFromImport().catch(() => void 0);
|
|
@@ -6834,6 +7029,13 @@ function usageSnapshotToPersisted(usage) {
|
|
|
6834
7029
|
function persistedUsageToSnapshot(usage) {
|
|
6835
7030
|
return usage ? { ...usage } : void 0;
|
|
6836
7031
|
}
|
|
7032
|
+
function buildSessionLoadMeta(agentId, model) {
|
|
7033
|
+
if (!model)
|
|
7034
|
+
return void 0;
|
|
7035
|
+
if (agentId === "claude-acp")
|
|
7036
|
+
return { claudeCode: { options: { model } } };
|
|
7037
|
+
return void 0;
|
|
7038
|
+
}
|
|
6837
7039
|
function extractInitialModel(result) {
|
|
6838
7040
|
const direct = asString(result.currentModelId) ?? asString(result.currentModel) ?? asString(result.modelId) ?? asString(result.model);
|
|
6839
7041
|
if (direct) {
|
|
@@ -7013,6 +7215,33 @@ async function restoreCurrentMode(opts) {
|
|
|
7013
7215
|
return agentReportedMode;
|
|
7014
7216
|
}
|
|
7015
7217
|
}
|
|
7218
|
+
async function restoreCurrentModel(opts) {
|
|
7219
|
+
const { agent, upstreamSessionId, persistedModel, agentReportedModel, logger } = opts;
|
|
7220
|
+
if (!persistedModel) {
|
|
7221
|
+
return agentReportedModel;
|
|
7222
|
+
}
|
|
7223
|
+
if (persistedModel === agentReportedModel) {
|
|
7224
|
+
return persistedModel;
|
|
7225
|
+
}
|
|
7226
|
+
try {
|
|
7227
|
+
logger?.info(
|
|
7228
|
+
`resurrect: pushing persisted modelId=${JSON.stringify(persistedModel)} to agent (agentReported=${JSON.stringify(agentReportedModel)})`
|
|
7229
|
+
);
|
|
7230
|
+
await agent.connection.request("session/set_model", {
|
|
7231
|
+
sessionId: upstreamSessionId,
|
|
7232
|
+
modelId: persistedModel
|
|
7233
|
+
});
|
|
7234
|
+
logger?.info(
|
|
7235
|
+
`resurrect: session/set_model accepted, effectiveModel=${JSON.stringify(persistedModel)}`
|
|
7236
|
+
);
|
|
7237
|
+
return persistedModel;
|
|
7238
|
+
} catch (err) {
|
|
7239
|
+
logger?.warn(
|
|
7240
|
+
`resurrect: session/set_model rejected by agent for modelId=${JSON.stringify(persistedModel)} (${err.message}); session will use ${JSON.stringify(agentReportedModel)}`
|
|
7241
|
+
);
|
|
7242
|
+
return agentReportedModel;
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7016
7245
|
function parseModesList(list) {
|
|
7017
7246
|
if (!Array.isArray(list)) {
|
|
7018
7247
|
return [];
|
|
@@ -7966,6 +8195,55 @@ function withCode3(err, code) {
|
|
|
7966
8195
|
return err;
|
|
7967
8196
|
}
|
|
7968
8197
|
|
|
8198
|
+
// src/core/extension-commands.ts
|
|
8199
|
+
var ExtensionCommandRegistry = class {
|
|
8200
|
+
entries = /* @__PURE__ */ new Map();
|
|
8201
|
+
changeHandlers = [];
|
|
8202
|
+
register(name, connection, commands) {
|
|
8203
|
+
this.entries.set(name, { connection, commands: [...commands] });
|
|
8204
|
+
this.fireChanged();
|
|
8205
|
+
}
|
|
8206
|
+
clear(name) {
|
|
8207
|
+
if (this.entries.delete(name)) {
|
|
8208
|
+
this.fireChanged();
|
|
8209
|
+
}
|
|
8210
|
+
}
|
|
8211
|
+
get(name) {
|
|
8212
|
+
return this.entries.get(name);
|
|
8213
|
+
}
|
|
8214
|
+
has(name) {
|
|
8215
|
+
return this.entries.has(name);
|
|
8216
|
+
}
|
|
8217
|
+
// Snapshot of every (name, command) pair. Order is stable per-name
|
|
8218
|
+
// (insertion order of the map and the original commands list).
|
|
8219
|
+
list() {
|
|
8220
|
+
const out = [];
|
|
8221
|
+
for (const [name, entry] of this.entries) {
|
|
8222
|
+
for (const command of entry.commands) {
|
|
8223
|
+
out.push({ name, command });
|
|
8224
|
+
}
|
|
8225
|
+
}
|
|
8226
|
+
return out;
|
|
8227
|
+
}
|
|
8228
|
+
onChange(handler) {
|
|
8229
|
+
this.changeHandlers.push(handler);
|
|
8230
|
+
return () => {
|
|
8231
|
+
const i = this.changeHandlers.indexOf(handler);
|
|
8232
|
+
if (i >= 0) {
|
|
8233
|
+
this.changeHandlers.splice(i, 1);
|
|
8234
|
+
}
|
|
8235
|
+
};
|
|
8236
|
+
}
|
|
8237
|
+
fireChanged() {
|
|
8238
|
+
for (const h of this.changeHandlers) {
|
|
8239
|
+
try {
|
|
8240
|
+
h();
|
|
8241
|
+
} catch {
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
8244
|
+
}
|
|
8245
|
+
};
|
|
8246
|
+
|
|
7969
8247
|
// src/core/agent-prune.ts
|
|
7970
8248
|
import * as fsp7 from "fs/promises";
|
|
7971
8249
|
import * as path9 from "path";
|
|
@@ -9131,6 +9409,376 @@ function isLoopbackHost(host) {
|
|
|
9131
9409
|
return host === "127.0.0.1" || host === "::1" || host === "localhost" || host === "[::1]";
|
|
9132
9410
|
}
|
|
9133
9411
|
|
|
9412
|
+
// src/core/history-search.ts
|
|
9413
|
+
function parseQuery(raw) {
|
|
9414
|
+
const trimmed = raw.trim();
|
|
9415
|
+
if (trimmed.length === 0) {
|
|
9416
|
+
return { operator: "OR", terms: [] };
|
|
9417
|
+
}
|
|
9418
|
+
const tokenRe = /\w+:"[^"]*"|"[^"]*"|\S+/g;
|
|
9419
|
+
const tokens = [];
|
|
9420
|
+
let m;
|
|
9421
|
+
while ((m = tokenRe.exec(trimmed)) !== null) {
|
|
9422
|
+
tokens.push(m[0]);
|
|
9423
|
+
}
|
|
9424
|
+
let operator = "OR";
|
|
9425
|
+
let sawAnd = false;
|
|
9426
|
+
let sawOr = false;
|
|
9427
|
+
const termTokens = [];
|
|
9428
|
+
for (const tok of tokens) {
|
|
9429
|
+
const upper = tok.toUpperCase();
|
|
9430
|
+
if (upper === "AND") {
|
|
9431
|
+
sawAnd = true;
|
|
9432
|
+
} else if (upper === "OR") {
|
|
9433
|
+
sawOr = true;
|
|
9434
|
+
} else {
|
|
9435
|
+
termTokens.push(tok);
|
|
9436
|
+
}
|
|
9437
|
+
}
|
|
9438
|
+
if (sawAnd) {
|
|
9439
|
+
operator = "AND";
|
|
9440
|
+
} else if (sawOr) {
|
|
9441
|
+
operator = "OR";
|
|
9442
|
+
}
|
|
9443
|
+
const terms = termTokens.map((tok) => parseTermToken(tok)).filter((t) => t.term.length > 0);
|
|
9444
|
+
return { operator, terms };
|
|
9445
|
+
}
|
|
9446
|
+
function parseTermToken(tok) {
|
|
9447
|
+
const pq = /^(\w+):"([^"]*)"$/.exec(tok);
|
|
9448
|
+
if (pq) {
|
|
9449
|
+
return { scope: prefixToScope(pq[1]), term: pq[2] };
|
|
9450
|
+
}
|
|
9451
|
+
const q = /^"([^"]*)"$/.exec(tok);
|
|
9452
|
+
if (q) {
|
|
9453
|
+
return { scope: "all", term: q[1] };
|
|
9454
|
+
}
|
|
9455
|
+
const pb = /^(prompt|response|tool):([\s\S]*)$/i.exec(tok);
|
|
9456
|
+
if (pb) {
|
|
9457
|
+
return { scope: prefixToScope(pb[1]), term: pb[2].trim() };
|
|
9458
|
+
}
|
|
9459
|
+
return { scope: "all", term: tok.trim() };
|
|
9460
|
+
}
|
|
9461
|
+
function prefixToScope(prefix) {
|
|
9462
|
+
switch (prefix.toLowerCase()) {
|
|
9463
|
+
case "prompt":
|
|
9464
|
+
return "user";
|
|
9465
|
+
case "response":
|
|
9466
|
+
return "agent";
|
|
9467
|
+
case "tool":
|
|
9468
|
+
return "tool";
|
|
9469
|
+
default:
|
|
9470
|
+
return "all";
|
|
9471
|
+
}
|
|
9472
|
+
}
|
|
9473
|
+
function scopeMatchesKind(scope, kind) {
|
|
9474
|
+
if (scope === "all") {
|
|
9475
|
+
return true;
|
|
9476
|
+
}
|
|
9477
|
+
if (scope === "user") {
|
|
9478
|
+
return kind === "user";
|
|
9479
|
+
}
|
|
9480
|
+
if (scope === "agent") {
|
|
9481
|
+
return kind === "agent" || kind === "thought";
|
|
9482
|
+
}
|
|
9483
|
+
return kind === "tool" || kind === "tool-input";
|
|
9484
|
+
}
|
|
9485
|
+
var DEFAULT_MAX_SNIPPETS_PER_SESSION = 5;
|
|
9486
|
+
var DEFAULT_MAX_SESSIONS = 200;
|
|
9487
|
+
var SNIPPET_SIDE = 30;
|
|
9488
|
+
async function searchHistories(manager, query, opts = {}) {
|
|
9489
|
+
const parsed = parseQuery(query);
|
|
9490
|
+
if (parsed.terms.length === 0) {
|
|
9491
|
+
return { query, truncated: false, results: [] };
|
|
9492
|
+
}
|
|
9493
|
+
const maxPerSession = opts.maxSnippetsPerSession ?? DEFAULT_MAX_SNIPPETS_PER_SESSION;
|
|
9494
|
+
const maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
9495
|
+
const allow = opts.sessionIds ? new Set(opts.sessionIds) : null;
|
|
9496
|
+
const all = await manager.list();
|
|
9497
|
+
const candidates = allow ? all.filter((s) => allow.has(s.sessionId)) : all;
|
|
9498
|
+
const results = [];
|
|
9499
|
+
let truncated = false;
|
|
9500
|
+
for (const candidate of candidates) {
|
|
9501
|
+
if (results.length >= maxSessions) {
|
|
9502
|
+
truncated = true;
|
|
9503
|
+
break;
|
|
9504
|
+
}
|
|
9505
|
+
const entries = await manager.loadHistory(candidate.sessionId).catch(
|
|
9506
|
+
() => []
|
|
9507
|
+
);
|
|
9508
|
+
const found = scanSessionEntries(entries, parsed, maxPerSession);
|
|
9509
|
+
if (found.snippets.length === 0) {
|
|
9510
|
+
continue;
|
|
9511
|
+
}
|
|
9512
|
+
const hit = {
|
|
9513
|
+
sessionId: candidate.sessionId,
|
|
9514
|
+
cwd: candidate.cwd,
|
|
9515
|
+
status: candidate.status,
|
|
9516
|
+
updatedAt: candidate.updatedAt,
|
|
9517
|
+
totalMatches: found.totalMatches,
|
|
9518
|
+
snippets: found.snippets
|
|
9519
|
+
};
|
|
9520
|
+
if (candidate.title !== void 0) {
|
|
9521
|
+
hit.title = candidate.title;
|
|
9522
|
+
}
|
|
9523
|
+
results.push(hit);
|
|
9524
|
+
}
|
|
9525
|
+
return { query, truncated, results };
|
|
9526
|
+
}
|
|
9527
|
+
function scanSessionEntries(entries, query, maxSnippets) {
|
|
9528
|
+
if (query.terms.length === 0) {
|
|
9529
|
+
return { totalMatches: 0, snippets: [] };
|
|
9530
|
+
}
|
|
9531
|
+
let totalMatches = 0;
|
|
9532
|
+
const snippets = [];
|
|
9533
|
+
for (const { scope, term } of query.terms) {
|
|
9534
|
+
const result = scanForTerm(entries, term, scope, maxSnippets - snippets.length);
|
|
9535
|
+
if (query.operator === "AND" && result.totalMatches === 0) {
|
|
9536
|
+
return { totalMatches: 0, snippets: [] };
|
|
9537
|
+
}
|
|
9538
|
+
totalMatches += result.totalMatches;
|
|
9539
|
+
snippets.push(...result.snippets);
|
|
9540
|
+
}
|
|
9541
|
+
return { totalMatches, snippets };
|
|
9542
|
+
}
|
|
9543
|
+
function scanForTerm(entries, term, scope, snippetBudget) {
|
|
9544
|
+
const needle = term.toLowerCase();
|
|
9545
|
+
let totalMatches = 0;
|
|
9546
|
+
const snippets = [];
|
|
9547
|
+
for (const entry of entries) {
|
|
9548
|
+
const fragments = extractSearchableFragments(entry).filter(
|
|
9549
|
+
(f) => scopeMatchesKind(scope, f.kind)
|
|
9550
|
+
);
|
|
9551
|
+
for (const frag of fragments) {
|
|
9552
|
+
const hay = frag.text.toLowerCase();
|
|
9553
|
+
let idx = hay.indexOf(needle);
|
|
9554
|
+
if (idx === -1) {
|
|
9555
|
+
continue;
|
|
9556
|
+
}
|
|
9557
|
+
let occurrences = 0;
|
|
9558
|
+
while (idx !== -1) {
|
|
9559
|
+
occurrences++;
|
|
9560
|
+
idx = hay.indexOf(needle, idx + needle.length);
|
|
9561
|
+
}
|
|
9562
|
+
totalMatches += occurrences;
|
|
9563
|
+
if (snippets.length < snippetBudget) {
|
|
9564
|
+
const first = hay.indexOf(needle);
|
|
9565
|
+
const snippet = {
|
|
9566
|
+
kind: frag.kind,
|
|
9567
|
+
text: buildSnippet(frag.text, first, needle.length),
|
|
9568
|
+
recordedAt: entry.recordedAt
|
|
9569
|
+
};
|
|
9570
|
+
if (frag.toolName !== void 0) {
|
|
9571
|
+
snippet.toolName = frag.toolName;
|
|
9572
|
+
}
|
|
9573
|
+
snippets.push(snippet);
|
|
9574
|
+
}
|
|
9575
|
+
}
|
|
9576
|
+
}
|
|
9577
|
+
return { totalMatches, snippets };
|
|
9578
|
+
}
|
|
9579
|
+
function extractSearchableFragments(entry) {
|
|
9580
|
+
if (entry.method !== "session/update") {
|
|
9581
|
+
return [];
|
|
9582
|
+
}
|
|
9583
|
+
const params = entry.params;
|
|
9584
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
9585
|
+
return [];
|
|
9586
|
+
}
|
|
9587
|
+
const update = params.update;
|
|
9588
|
+
if (!update || typeof update !== "object" || Array.isArray(update)) {
|
|
9589
|
+
return [];
|
|
9590
|
+
}
|
|
9591
|
+
const u = update;
|
|
9592
|
+
const tag = typeof u.sessionUpdate === "string" ? u.sessionUpdate : u.kind;
|
|
9593
|
+
if (typeof tag !== "string") {
|
|
9594
|
+
return [];
|
|
9595
|
+
}
|
|
9596
|
+
switch (tag) {
|
|
9597
|
+
case "agent_message_chunk": {
|
|
9598
|
+
const text = readContentText(u.content);
|
|
9599
|
+
return text ? [{ kind: "agent", text }] : [];
|
|
9600
|
+
}
|
|
9601
|
+
case "agent_thought":
|
|
9602
|
+
case "agent_thought_chunk": {
|
|
9603
|
+
const text = typeof u.text === "string" ? sanitizeWireText(u.text) : readContentText(u.content);
|
|
9604
|
+
return text ? [{ kind: "thought", text }] : [];
|
|
9605
|
+
}
|
|
9606
|
+
case "user_message_chunk": {
|
|
9607
|
+
if (isCompatPromptReceived(u)) {
|
|
9608
|
+
return [];
|
|
9609
|
+
}
|
|
9610
|
+
const text = readContentText(u.content);
|
|
9611
|
+
return text ? [{ kind: "user", text }] : [];
|
|
9612
|
+
}
|
|
9613
|
+
case "prompt_received": {
|
|
9614
|
+
const text = readPromptText(u.prompt);
|
|
9615
|
+
return text ? [{ kind: "user", text }] : [];
|
|
9616
|
+
}
|
|
9617
|
+
case "tool_call":
|
|
9618
|
+
case "tool_call_update": {
|
|
9619
|
+
return extractToolFragments(u);
|
|
9620
|
+
}
|
|
9621
|
+
default:
|
|
9622
|
+
return [];
|
|
9623
|
+
}
|
|
9624
|
+
}
|
|
9625
|
+
function extractToolFragments(u) {
|
|
9626
|
+
const toolName = readString2(u, "name");
|
|
9627
|
+
const title = readString2(u, "title");
|
|
9628
|
+
const out = [];
|
|
9629
|
+
if (title !== void 0) {
|
|
9630
|
+
const sanitized = sanitizeSingleLine(title);
|
|
9631
|
+
if (sanitized.length > 0) {
|
|
9632
|
+
const frag = { kind: "tool", text: sanitized };
|
|
9633
|
+
if (toolName !== void 0) {
|
|
9634
|
+
frag.toolName = toolName;
|
|
9635
|
+
}
|
|
9636
|
+
out.push(frag);
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9639
|
+
if (toolName !== void 0 && toolName !== title) {
|
|
9640
|
+
const sanitized = sanitizeSingleLine(toolName);
|
|
9641
|
+
if (sanitized.length > 0) {
|
|
9642
|
+
out.push({ kind: "tool", toolName, text: sanitized });
|
|
9643
|
+
}
|
|
9644
|
+
}
|
|
9645
|
+
const rawInput = u.rawInput;
|
|
9646
|
+
if (rawInput && typeof rawInput === "object") {
|
|
9647
|
+
const serialized = safeStringify(rawInput);
|
|
9648
|
+
if (serialized.length > 0) {
|
|
9649
|
+
const frag = {
|
|
9650
|
+
kind: "tool-input",
|
|
9651
|
+
text: sanitizeSingleLine(serialized)
|
|
9652
|
+
};
|
|
9653
|
+
if (toolName !== void 0) {
|
|
9654
|
+
frag.toolName = toolName;
|
|
9655
|
+
}
|
|
9656
|
+
out.push(frag);
|
|
9657
|
+
}
|
|
9658
|
+
}
|
|
9659
|
+
const locations = u.locations;
|
|
9660
|
+
if (Array.isArray(locations) && locations.length > 0) {
|
|
9661
|
+
const serialized = safeStringify(locations);
|
|
9662
|
+
if (serialized.length > 0) {
|
|
9663
|
+
const frag = {
|
|
9664
|
+
kind: "tool-input",
|
|
9665
|
+
text: sanitizeSingleLine(serialized)
|
|
9666
|
+
};
|
|
9667
|
+
if (toolName !== void 0) {
|
|
9668
|
+
frag.toolName = toolName;
|
|
9669
|
+
}
|
|
9670
|
+
out.push(frag);
|
|
9671
|
+
}
|
|
9672
|
+
}
|
|
9673
|
+
const errorText = extractToolErrorText(u);
|
|
9674
|
+
if (errorText !== null) {
|
|
9675
|
+
const frag = { kind: "tool", text: errorText };
|
|
9676
|
+
if (toolName !== void 0) {
|
|
9677
|
+
frag.toolName = toolName;
|
|
9678
|
+
}
|
|
9679
|
+
out.push(frag);
|
|
9680
|
+
}
|
|
9681
|
+
return out;
|
|
9682
|
+
}
|
|
9683
|
+
function extractToolErrorText(u) {
|
|
9684
|
+
const content = u.content;
|
|
9685
|
+
if (Array.isArray(content)) {
|
|
9686
|
+
for (const block of content) {
|
|
9687
|
+
if (!block || typeof block !== "object") {
|
|
9688
|
+
continue;
|
|
9689
|
+
}
|
|
9690
|
+
const b = block;
|
|
9691
|
+
const inner = b.content;
|
|
9692
|
+
if (!inner || typeof inner !== "object") {
|
|
9693
|
+
continue;
|
|
9694
|
+
}
|
|
9695
|
+
const i = inner;
|
|
9696
|
+
if (i.type === "text" && typeof i.text === "string") {
|
|
9697
|
+
const s = sanitizeSingleLine(i.text);
|
|
9698
|
+
if (s.length > 0) {
|
|
9699
|
+
return s;
|
|
9700
|
+
}
|
|
9701
|
+
}
|
|
9702
|
+
}
|
|
9703
|
+
}
|
|
9704
|
+
const rawOutput = u.rawOutput;
|
|
9705
|
+
if (rawOutput && typeof rawOutput === "object") {
|
|
9706
|
+
const err = rawOutput.error;
|
|
9707
|
+
if (typeof err === "string") {
|
|
9708
|
+
const s = sanitizeSingleLine(err);
|
|
9709
|
+
if (s.length > 0) {
|
|
9710
|
+
return s;
|
|
9711
|
+
}
|
|
9712
|
+
}
|
|
9713
|
+
}
|
|
9714
|
+
return null;
|
|
9715
|
+
}
|
|
9716
|
+
function isCompatPromptReceived(u) {
|
|
9717
|
+
const meta = u._meta;
|
|
9718
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
9719
|
+
return false;
|
|
9720
|
+
}
|
|
9721
|
+
const hydra = meta["hydra-acp"];
|
|
9722
|
+
if (!hydra || typeof hydra !== "object" || Array.isArray(hydra)) {
|
|
9723
|
+
return false;
|
|
9724
|
+
}
|
|
9725
|
+
return hydra.compatFor === "prompt_received";
|
|
9726
|
+
}
|
|
9727
|
+
function readContentText(content) {
|
|
9728
|
+
if (typeof content === "string") {
|
|
9729
|
+
return sanitizeWireText(content);
|
|
9730
|
+
}
|
|
9731
|
+
if (!content || typeof content !== "object" || Array.isArray(content)) {
|
|
9732
|
+
return "";
|
|
9733
|
+
}
|
|
9734
|
+
const c = content;
|
|
9735
|
+
if (typeof c.text === "string") {
|
|
9736
|
+
return sanitizeWireText(c.text);
|
|
9737
|
+
}
|
|
9738
|
+
return "";
|
|
9739
|
+
}
|
|
9740
|
+
function readPromptText(prompt) {
|
|
9741
|
+
if (!Array.isArray(prompt)) {
|
|
9742
|
+
return "";
|
|
9743
|
+
}
|
|
9744
|
+
const parts = [];
|
|
9745
|
+
for (const block of prompt) {
|
|
9746
|
+
const text = readContentText(block);
|
|
9747
|
+
if (text.length > 0) {
|
|
9748
|
+
parts.push(text);
|
|
9749
|
+
}
|
|
9750
|
+
}
|
|
9751
|
+
return parts.join("");
|
|
9752
|
+
}
|
|
9753
|
+
function readString2(u, key) {
|
|
9754
|
+
const v = u[key];
|
|
9755
|
+
return typeof v === "string" ? v : void 0;
|
|
9756
|
+
}
|
|
9757
|
+
function safeStringify(value) {
|
|
9758
|
+
try {
|
|
9759
|
+
return JSON.stringify(value);
|
|
9760
|
+
} catch {
|
|
9761
|
+
return "";
|
|
9762
|
+
}
|
|
9763
|
+
}
|
|
9764
|
+
function buildSnippet(text, matchIdx, matchLen) {
|
|
9765
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
9766
|
+
if (flat.length === 0) {
|
|
9767
|
+
return "";
|
|
9768
|
+
}
|
|
9769
|
+
const flatLower = flat.toLowerCase();
|
|
9770
|
+
const needleSlice = text.slice(matchIdx, matchIdx + matchLen).toLowerCase().replace(/\s+/g, " ").trim();
|
|
9771
|
+
let pos = needleSlice.length > 0 ? flatLower.indexOf(needleSlice) : 0;
|
|
9772
|
+
if (pos === -1) {
|
|
9773
|
+
pos = 0;
|
|
9774
|
+
}
|
|
9775
|
+
const start = Math.max(0, pos - SNIPPET_SIDE);
|
|
9776
|
+
const end = Math.min(flat.length, pos + needleSlice.length + SNIPPET_SIDE);
|
|
9777
|
+
const head = start > 0 ? "\u2026" : "";
|
|
9778
|
+
const tail = end < flat.length ? "\u2026" : "";
|
|
9779
|
+
return `${head}${flat.slice(start, end)}${tail}`;
|
|
9780
|
+
}
|
|
9781
|
+
|
|
9134
9782
|
// src/daemon/routes/sessions.ts
|
|
9135
9783
|
function resolveHydraHost(defaults) {
|
|
9136
9784
|
if (defaults.publicHost && defaults.publicHost.length > 0) {
|
|
@@ -9147,6 +9795,17 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
9147
9795
|
const sessions = await manager.list({ cwd: query?.cwd });
|
|
9148
9796
|
return { sessions };
|
|
9149
9797
|
});
|
|
9798
|
+
app.get("/v1/sessions/search", async (request, reply) => {
|
|
9799
|
+
const query = request.query;
|
|
9800
|
+
const q = query?.q ?? "";
|
|
9801
|
+
if (q.trim().length === 0) {
|
|
9802
|
+
reply.code(400).send({ error: "q is required" });
|
|
9803
|
+
return reply;
|
|
9804
|
+
}
|
|
9805
|
+
const ids = query?.sessionIds ? query.sessionIds.split(",").filter((s) => s.length > 0) : void 0;
|
|
9806
|
+
const out = await searchHistories(manager, q, { sessionIds: ids });
|
|
9807
|
+
return out;
|
|
9808
|
+
});
|
|
9150
9809
|
app.post("/v1/sessions", async (request, reply) => {
|
|
9151
9810
|
const body = request.body ?? {};
|
|
9152
9811
|
const cwd = expandHome(body.cwd ?? defaults.cwd);
|
|
@@ -9978,6 +10637,34 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
9978
10637
|
}
|
|
9979
10638
|
return buildInitializeResult();
|
|
9980
10639
|
});
|
|
10640
|
+
if (processIdentity && deps.extensionCommands) {
|
|
10641
|
+
const registry = deps.extensionCommands;
|
|
10642
|
+
connection.onRequest("hydra-acp/register_commands", async (raw) => {
|
|
10643
|
+
const params = raw ?? {};
|
|
10644
|
+
const commands = Array.isArray(params.commands) ? params.commands.map((c) => {
|
|
10645
|
+
if (!c || typeof c !== "object") {
|
|
10646
|
+
return void 0;
|
|
10647
|
+
}
|
|
10648
|
+
const obj = c;
|
|
10649
|
+
if (typeof obj.verb !== "string" || obj.verb.length === 0) {
|
|
10650
|
+
return void 0;
|
|
10651
|
+
}
|
|
10652
|
+
const spec = { verb: obj.verb };
|
|
10653
|
+
if (typeof obj.argsHint === "string") {
|
|
10654
|
+
spec.argsHint = obj.argsHint;
|
|
10655
|
+
}
|
|
10656
|
+
if (typeof obj.description === "string") {
|
|
10657
|
+
spec.description = obj.description;
|
|
10658
|
+
}
|
|
10659
|
+
return spec;
|
|
10660
|
+
}).filter((s) => s !== void 0) : [];
|
|
10661
|
+
registry.register(processIdentity.name, connection, commands);
|
|
10662
|
+
return { ok: true, registered: commands.length };
|
|
10663
|
+
});
|
|
10664
|
+
connection.onClose(() => {
|
|
10665
|
+
registry.clear(processIdentity.name);
|
|
10666
|
+
});
|
|
10667
|
+
}
|
|
9981
10668
|
if (processIdentity?.kind === "transformer") {
|
|
9982
10669
|
connection.onRequest("transformer/initialize", async (raw) => {
|
|
9983
10670
|
const params = raw ?? {};
|
|
@@ -10525,7 +11212,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
10525
11212
|
return null;
|
|
10526
11213
|
}
|
|
10527
11214
|
app.log.info(decision.logMessage);
|
|
10528
|
-
|
|
11215
|
+
const { modelId } = rawParams;
|
|
11216
|
+
const result = await decision.session.forwardRequest("session/set_model", rawParams);
|
|
11217
|
+
decision.session.applyModelChange(modelId);
|
|
11218
|
+
return result;
|
|
10529
11219
|
});
|
|
10530
11220
|
connection.onRequest("session/set_mode", async (rawParams) => {
|
|
10531
11221
|
const params = rawParams;
|
|
@@ -10916,13 +11606,15 @@ async function startDaemon(config, serviceToken) {
|
|
|
10916
11606
|
stderrTailBytes: config.daemon.agentStderrTailBytes,
|
|
10917
11607
|
logger: agentLogger
|
|
10918
11608
|
});
|
|
11609
|
+
const extensionCommands = new ExtensionCommandRegistry();
|
|
10919
11610
|
const manager = new SessionManager(registry, spawner, void 0, {
|
|
10920
11611
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
10921
11612
|
defaultModels: config.defaultModels,
|
|
10922
11613
|
defaultTransformers: config.defaultTransformers,
|
|
10923
11614
|
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
|
|
10924
11615
|
logger: agentLogger,
|
|
10925
|
-
npmRegistry: config.npmRegistry
|
|
11616
|
+
npmRegistry: config.npmRegistry,
|
|
11617
|
+
extensionCommands
|
|
10926
11618
|
});
|
|
10927
11619
|
const extensions = new ExtensionManager(extensionList(config), void 0, {
|
|
10928
11620
|
tokenRegistry: processRegistry
|
|
@@ -10956,7 +11648,8 @@ async function startDaemon(config, serviceToken) {
|
|
|
10956
11648
|
processRegistry,
|
|
10957
11649
|
onExtensionVersion: (name, version) => extensions.reportVersion(name, version),
|
|
10958
11650
|
onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
|
|
10959
|
-
transformers
|
|
11651
|
+
transformers,
|
|
11652
|
+
extensionCommands
|
|
10960
11653
|
});
|
|
10961
11654
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
10962
11655
|
const address = app.server.address();
|