@hydra-acp/cli 0.1.45 → 0.1.47
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/README.md +4 -4
- package/dist/cli.js +526 -108
- package/dist/index.d.ts +22 -1
- package/dist/index.js +402 -44
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -271,7 +271,7 @@ var init_config = __esm({
|
|
|
271
271
|
// Compaction trims to this many on a periodic basis; reads also slice
|
|
272
272
|
// to the tail at this length as a defensive measure against older
|
|
273
273
|
// daemons that may have written unbounded files.
|
|
274
|
-
sessionHistoryMaxEntries: z.number().int().positive().default(
|
|
274
|
+
sessionHistoryMaxEntries: z.number().int().positive().default(1e4),
|
|
275
275
|
// Bytes of trailing agent stderr buffered per AgentInstance so the
|
|
276
276
|
// daemon can include it in the diagnostic message when a spawn fails.
|
|
277
277
|
// Bump if your agents emit large tracebacks you want surfaced.
|
|
@@ -331,7 +331,13 @@ var init_config = __esm({
|
|
|
331
331
|
// streaming lines beneath the live thinking block. Set false to
|
|
332
332
|
// suppress them — the TUI hotkey ^T toggles this at runtime without
|
|
333
333
|
// persisting back to config.
|
|
334
|
-
showThoughts: z.boolean().default(true)
|
|
334
|
+
showThoughts: z.boolean().default(true),
|
|
335
|
+
// Cap on entries kept in the cross-session global prompt-history file
|
|
336
|
+
// (~/.hydra-acp/prompt-history). This is the ^P / ^R recall list
|
|
337
|
+
// shared across all sessions; it's append-only on disk, so long-lived
|
|
338
|
+
// installs can grow past this — it's enforced at load time and per
|
|
339
|
+
// append in memory.
|
|
340
|
+
promptHistoryMaxEntries: z.number().int().positive().default(2e3)
|
|
335
341
|
});
|
|
336
342
|
ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
337
343
|
ExtensionBody = z.object({
|
|
@@ -385,7 +391,8 @@ var init_config = __esm({
|
|
|
385
391
|
cwdColumnMaxWidth: 24,
|
|
386
392
|
progressIndicator: true,
|
|
387
393
|
defaultEnterAction: "amend",
|
|
388
|
-
showThoughts: true
|
|
394
|
+
showThoughts: true,
|
|
395
|
+
promptHistoryMaxEntries: 2e3
|
|
389
396
|
})
|
|
390
397
|
});
|
|
391
398
|
}
|
|
@@ -1826,6 +1833,11 @@ var init_hydra_commands = __esm({
|
|
|
1826
1833
|
verb: "kill",
|
|
1827
1834
|
name: "hydra kill",
|
|
1828
1835
|
description: "Close this session (kills the agent; record is kept so it can be resumed later)"
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
verb: "restart",
|
|
1839
|
+
name: "hydra restart",
|
|
1840
|
+
description: "Restart the agent with a fresh session/new while preserving conversation history (useful when the proxy has changed available models)"
|
|
1829
1841
|
}
|
|
1830
1842
|
];
|
|
1831
1843
|
VERB_INDEX = new Map(HYDRA_COMMANDS.map((c) => [c.verb, c]));
|
|
@@ -2182,7 +2194,9 @@ var init_session = __esm({
|
|
|
2182
2194
|
// stale-prone for snapshot-shaped events).
|
|
2183
2195
|
currentModel;
|
|
2184
2196
|
currentMode;
|
|
2185
|
-
|
|
2197
|
+
// Raw per-agent-life usage. Never read directly outside this class —
|
|
2198
|
+
// always access via the currentUsage getter which adds cumulativeCost.
|
|
2199
|
+
_currentUsage;
|
|
2186
2200
|
updatedAt;
|
|
2187
2201
|
createdAt;
|
|
2188
2202
|
clients = /* @__PURE__ */ new Map();
|
|
@@ -2248,6 +2262,7 @@ var init_session = __esm({
|
|
|
2248
2262
|
// and noisy state churn keep a quiet session alive forever.
|
|
2249
2263
|
lastRecordedAt;
|
|
2250
2264
|
spawnReplacementAgent;
|
|
2265
|
+
listSessions;
|
|
2251
2266
|
logger;
|
|
2252
2267
|
transformChain;
|
|
2253
2268
|
// Outstanding "processing" claims: token → claim waiting for respondsTo discharge.
|
|
@@ -2279,6 +2294,23 @@ var init_session = __esm({
|
|
|
2279
2294
|
modeHandlers = [];
|
|
2280
2295
|
usageHandlers = [];
|
|
2281
2296
|
cumulativeCost = 0;
|
|
2297
|
+
// Total cost across all agent lives. costAmount in the returned snapshot
|
|
2298
|
+
// is cumulativeCost + the current agent's raw amount so every consumer
|
|
2299
|
+
// gets the right figure without knowing about the internal split.
|
|
2300
|
+
// cumulativeCost is stripped from the return value so it never leaks
|
|
2301
|
+
// into persistence paths via session.currentUsage.
|
|
2302
|
+
get currentUsage() {
|
|
2303
|
+
if (!this._currentUsage && !this.cumulativeCost) {
|
|
2304
|
+
return void 0;
|
|
2305
|
+
}
|
|
2306
|
+
const base = this._currentUsage ?? {};
|
|
2307
|
+
const total = this.cumulativeCost + (base.costAmount ?? 0);
|
|
2308
|
+
return {
|
|
2309
|
+
...base,
|
|
2310
|
+
costAmount: total || void 0,
|
|
2311
|
+
cumulativeCost: void 0
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2282
2314
|
// Set by amendPrompt at the start of a cancel-and-resubmit dance.
|
|
2283
2315
|
// broadcastTurnComplete reads it to attach the _meta.amended marker
|
|
2284
2316
|
// to the cancelled turn's turn_complete notification, and to fire the
|
|
@@ -2311,7 +2343,7 @@ var init_session = __esm({
|
|
|
2311
2343
|
this.title = init.title;
|
|
2312
2344
|
this.currentModel = init.currentModel;
|
|
2313
2345
|
this.currentMode = init.currentMode;
|
|
2314
|
-
this.
|
|
2346
|
+
this._currentUsage = init.currentUsage;
|
|
2315
2347
|
this.cumulativeCost = init.currentUsage?.cumulativeCost ?? 0;
|
|
2316
2348
|
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
2317
2349
|
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
@@ -2325,6 +2357,7 @@ var init_session = __esm({
|
|
|
2325
2357
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
2326
2358
|
this.idleEventTimeoutMs = init.idleEventTimeoutMs ?? 3e4;
|
|
2327
2359
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
2360
|
+
this.listSessions = init.listSessions;
|
|
2328
2361
|
this.logger = init.logger;
|
|
2329
2362
|
this.transformChain = init.transformChain ?? [];
|
|
2330
2363
|
if (init.firstPromptSeeded) {
|
|
@@ -2343,6 +2376,9 @@ var init_session = __esm({
|
|
|
2343
2376
|
broadcastMergedCommands() {
|
|
2344
2377
|
const merged = [
|
|
2345
2378
|
...hydraCommandsAsAdvertised(),
|
|
2379
|
+
{ name: "model <model-id>", description: "Switch model; omit arg to list available models" },
|
|
2380
|
+
{ name: "sessions", description: "List all sessions" },
|
|
2381
|
+
{ name: "help", description: "Show available commands" },
|
|
2346
2382
|
...this.agentAdvertisedCommands
|
|
2347
2383
|
];
|
|
2348
2384
|
this.recordAndBroadcast("session/update", {
|
|
@@ -2407,7 +2443,8 @@ var init_session = __esm({
|
|
|
2407
2443
|
// then recordAndBroadcast. All state mutation happens after the chain exits.
|
|
2408
2444
|
// See forwardRequest for originatedBy / startIdx semantics.
|
|
2409
2445
|
async runResponseChain(params, originatedBy = /* @__PURE__ */ new Set(), startIdx = 0) {
|
|
2410
|
-
|
|
2446
|
+
const rawParams = params;
|
|
2447
|
+
let envelope = this.injectCumulativeCost(params);
|
|
2411
2448
|
for (let i = startIdx; i < this.transformChain.length; i++) {
|
|
2412
2449
|
const t = this.transformChain[i];
|
|
2413
2450
|
if (originatedBy.has(t.name)) {
|
|
@@ -2495,8 +2532,8 @@ var init_session = __esm({
|
|
|
2495
2532
|
this.recordAndBroadcast("session/update", envelope);
|
|
2496
2533
|
return;
|
|
2497
2534
|
}
|
|
2498
|
-
if (this.maybeApplyAgentUsage(
|
|
2499
|
-
this.recordAndBroadcast("session/update",
|
|
2535
|
+
if (this.maybeApplyAgentUsage(rawParams)) {
|
|
2536
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2500
2537
|
return;
|
|
2501
2538
|
}
|
|
2502
2539
|
this.maybeApplyAgentSessionInfo(envelope);
|
|
@@ -2689,8 +2726,8 @@ var init_session = __esm({
|
|
|
2689
2726
|
recordedAt
|
|
2690
2727
|
});
|
|
2691
2728
|
}
|
|
2692
|
-
if (this.currentUsage !== void 0
|
|
2693
|
-
const u = this.currentUsage
|
|
2729
|
+
if (this.currentUsage !== void 0) {
|
|
2730
|
+
const u = this.currentUsage;
|
|
2694
2731
|
const update = {
|
|
2695
2732
|
sessionUpdate: "usage_update"
|
|
2696
2733
|
};
|
|
@@ -2702,8 +2739,8 @@ var init_session = __esm({
|
|
|
2702
2739
|
}
|
|
2703
2740
|
if (typeof u.costAmount === "number" || typeof u.costCurrency === "string") {
|
|
2704
2741
|
const cost = {};
|
|
2705
|
-
if (typeof u.costAmount === "number"
|
|
2706
|
-
cost.amount =
|
|
2742
|
+
if (typeof u.costAmount === "number") {
|
|
2743
|
+
cost.amount = u.costAmount;
|
|
2707
2744
|
}
|
|
2708
2745
|
if (typeof u.costCurrency === "string") {
|
|
2709
2746
|
cost.currency = u.costCurrency;
|
|
@@ -3242,6 +3279,23 @@ var init_session = __esm({
|
|
|
3242
3279
|
sessionId: this.upstreamSessionId
|
|
3243
3280
|
});
|
|
3244
3281
|
}
|
|
3282
|
+
// Add a transformer to this session's chain retroactively. No-ops if it's
|
|
3283
|
+
// already present. Fires session.opened on the new transformer so it gets
|
|
3284
|
+
// the same lifecycle signal it would have received at session creation.
|
|
3285
|
+
addTransformer(ref) {
|
|
3286
|
+
const existing = this.transformChain.findIndex((t) => t.name === ref.name);
|
|
3287
|
+
if (existing >= 0) {
|
|
3288
|
+
this.transformChain[existing] = ref;
|
|
3289
|
+
} else {
|
|
3290
|
+
this.transformChain.push(ref);
|
|
3291
|
+
}
|
|
3292
|
+
if (ref.intercepts.has("lifecycle:session.opened")) {
|
|
3293
|
+
void ref.connection.notify("transformer/session_event", {
|
|
3294
|
+
event: "session.opened",
|
|
3295
|
+
sessionId: this.sessionId
|
|
3296
|
+
}).catch(() => void 0);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3245
3299
|
// Walk the request-side chain then forward to the agent.
|
|
3246
3300
|
// originatedBy: transformer names already in the lineage — skipped for loop
|
|
3247
3301
|
// prevention and to implement resume-routing on re-entry from emit_message.
|
|
@@ -3566,6 +3620,9 @@ var init_session = __esm({
|
|
|
3566
3620
|
if (!trimmed || trimmed === this.currentMode) {
|
|
3567
3621
|
return true;
|
|
3568
3622
|
}
|
|
3623
|
+
this.logger?.info(
|
|
3624
|
+
`current_mode_update: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
|
|
3625
|
+
);
|
|
3569
3626
|
this.currentMode = trimmed;
|
|
3570
3627
|
for (const handler of this.modeHandlers) {
|
|
3571
3628
|
try {
|
|
@@ -3585,7 +3642,7 @@ var init_session = __esm({
|
|
|
3585
3642
|
if (update.sessionUpdate !== "usage_update") {
|
|
3586
3643
|
return false;
|
|
3587
3644
|
}
|
|
3588
|
-
const next = { ...this.
|
|
3645
|
+
const next = { ...this._currentUsage ?? {} };
|
|
3589
3646
|
let changed = false;
|
|
3590
3647
|
if (typeof update.used === "number" && next.used !== update.used) {
|
|
3591
3648
|
next.used = update.used;
|
|
@@ -3609,10 +3666,11 @@ var init_session = __esm({
|
|
|
3609
3666
|
if (!changed) {
|
|
3610
3667
|
return true;
|
|
3611
3668
|
}
|
|
3612
|
-
this.
|
|
3669
|
+
this._currentUsage = next;
|
|
3670
|
+
const total = this.currentUsage ?? next;
|
|
3613
3671
|
for (const handler of this.usageHandlers) {
|
|
3614
3672
|
try {
|
|
3615
|
-
handler(
|
|
3673
|
+
handler(total);
|
|
3616
3674
|
} catch {
|
|
3617
3675
|
}
|
|
3618
3676
|
}
|
|
@@ -3622,16 +3680,16 @@ var init_session = __esm({
|
|
|
3622
3680
|
// next agent life starts accumulating from $0. Fires usageHandlers so
|
|
3623
3681
|
// meta.json is updated before the new agent starts emitting.
|
|
3624
3682
|
accumulateAndResetCost() {
|
|
3625
|
-
const amount = this.
|
|
3683
|
+
const amount = this._currentUsage?.costAmount;
|
|
3626
3684
|
if (!amount)
|
|
3627
3685
|
return;
|
|
3628
3686
|
this.cumulativeCost += amount;
|
|
3629
3687
|
const next = {
|
|
3630
|
-
...this.
|
|
3688
|
+
...this._currentUsage ?? {},
|
|
3631
3689
|
cumulativeCost: this.cumulativeCost,
|
|
3632
3690
|
costAmount: void 0
|
|
3633
3691
|
};
|
|
3634
|
-
this.
|
|
3692
|
+
this._currentUsage = next;
|
|
3635
3693
|
for (const handler of this.usageHandlers) {
|
|
3636
3694
|
try {
|
|
3637
3695
|
handler(next);
|
|
@@ -3666,16 +3724,9 @@ var init_session = __esm({
|
|
|
3666
3724
|
}
|
|
3667
3725
|
};
|
|
3668
3726
|
}
|
|
3669
|
-
//
|
|
3670
|
-
// session/list so list rows show the accumulated figure.
|
|
3727
|
+
// Deprecated alias — currentUsage already returns the total.
|
|
3671
3728
|
get totalUsage() {
|
|
3672
|
-
|
|
3673
|
-
return void 0;
|
|
3674
|
-
const base = this.currentUsage ?? {};
|
|
3675
|
-
return {
|
|
3676
|
-
...base,
|
|
3677
|
-
costAmount: this.cumulativeCost + (this.currentUsage?.costAmount ?? 0)
|
|
3678
|
-
};
|
|
3729
|
+
return this.currentUsage;
|
|
3679
3730
|
}
|
|
3680
3731
|
// Update the cached agent command list, fire persist handlers, and
|
|
3681
3732
|
// broadcast the merged list to attached clients. Idempotent on a
|
|
@@ -3741,6 +3792,26 @@ var init_session = __esm({
|
|
|
3741
3792
|
onModeChange(handler) {
|
|
3742
3793
|
this.modeHandlers.push(handler);
|
|
3743
3794
|
}
|
|
3795
|
+
// Apply a mode change initiated by a client request (session/set_mode)
|
|
3796
|
+
// when the agent doesn't emit a current_mode_update notification on its
|
|
3797
|
+
// own. Fires modeHandlers so the persistence hook and any other listeners
|
|
3798
|
+
// see the change, identical to the agent-notification path.
|
|
3799
|
+
applyModeChange(modeId) {
|
|
3800
|
+
const trimmed = modeId.trim();
|
|
3801
|
+
if (!trimmed || trimmed === this.currentMode) {
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3804
|
+
this.logger?.info(
|
|
3805
|
+
`applyModeChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
|
|
3806
|
+
);
|
|
3807
|
+
this.currentMode = trimmed;
|
|
3808
|
+
for (const handler of this.modeHandlers) {
|
|
3809
|
+
try {
|
|
3810
|
+
handler(trimmed);
|
|
3811
|
+
} catch {
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3744
3815
|
onUsageChange(handler) {
|
|
3745
3816
|
this.usageHandlers.push(handler);
|
|
3746
3817
|
}
|
|
@@ -3823,6 +3894,8 @@ var init_session = __esm({
|
|
|
3823
3894
|
return this.runAgentCommand(arg);
|
|
3824
3895
|
case "kill":
|
|
3825
3896
|
return this.runKillCommand();
|
|
3897
|
+
case "restart":
|
|
3898
|
+
return this.runRestartCommand();
|
|
3826
3899
|
default: {
|
|
3827
3900
|
const err = new Error(
|
|
3828
3901
|
`no dispatcher for /hydra verb ${verb}`
|
|
@@ -3832,6 +3905,92 @@ var init_session = __esm({
|
|
|
3832
3905
|
}
|
|
3833
3906
|
}
|
|
3834
3907
|
}
|
|
3908
|
+
async handleSessionsCommand() {
|
|
3909
|
+
let text;
|
|
3910
|
+
if (!this.listSessions) {
|
|
3911
|
+
text = "_(session listing not available)_";
|
|
3912
|
+
} else {
|
|
3913
|
+
const sessions = await this.listSessions();
|
|
3914
|
+
if (sessions.length === 0) {
|
|
3915
|
+
text = "_(no sessions)_";
|
|
3916
|
+
} else {
|
|
3917
|
+
const lines = sessions.map((s) => {
|
|
3918
|
+
const id = s.sessionId.replace(/^hydra_session_/, "").slice(0, 12);
|
|
3919
|
+
const model = s.currentModel ? ` \xB7 ${s.currentModel}` : "";
|
|
3920
|
+
const marker = s.sessionId === this.sessionId ? " \u25C0" : "";
|
|
3921
|
+
const title = s.title ? ` ${s.title}` : "";
|
|
3922
|
+
return `\`${id}\` ${s.cwd}${model}${marker}${title}`;
|
|
3923
|
+
});
|
|
3924
|
+
text = `Sessions (${sessions.length}):
|
|
3925
|
+
${lines.join("\n")}`;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
this.recordAndBroadcast("session/update", {
|
|
3929
|
+
sessionId: this.upstreamSessionId,
|
|
3930
|
+
update: {
|
|
3931
|
+
sessionUpdate: "agent_message_chunk",
|
|
3932
|
+
content: { type: "text", text: `
|
|
3933
|
+
${text}
|
|
3934
|
+
` },
|
|
3935
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
3936
|
+
}
|
|
3937
|
+
});
|
|
3938
|
+
return { stopReason: "end_turn" };
|
|
3939
|
+
}
|
|
3940
|
+
handleHelpCommand() {
|
|
3941
|
+
const commands = this.mergedAvailableCommands();
|
|
3942
|
+
const lines = commands.map((c) => {
|
|
3943
|
+
const desc = c.description ? ` ${c.description}` : "";
|
|
3944
|
+
return `\`/${c.name}\`${desc}`;
|
|
3945
|
+
});
|
|
3946
|
+
const text = lines.length > 0 ? `Available commands:
|
|
3947
|
+
${lines.join("\n")}` : "_(no commands advertised)_";
|
|
3948
|
+
this.recordAndBroadcast("session/update", {
|
|
3949
|
+
sessionId: this.upstreamSessionId,
|
|
3950
|
+
update: {
|
|
3951
|
+
sessionUpdate: "agent_message_chunk",
|
|
3952
|
+
content: { type: "text", text: `
|
|
3953
|
+
${text}
|
|
3954
|
+
` },
|
|
3955
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
3956
|
+
}
|
|
3957
|
+
});
|
|
3958
|
+
return Promise.resolve({ stopReason: "end_turn" });
|
|
3959
|
+
}
|
|
3960
|
+
async handleModelCommand(text) {
|
|
3961
|
+
const arg = text.slice("/model".length).trim();
|
|
3962
|
+
if (arg === "") {
|
|
3963
|
+
const models = this.agentAdvertisedModels;
|
|
3964
|
+
const current = this.currentModel;
|
|
3965
|
+
let body;
|
|
3966
|
+
if (models.length === 0) {
|
|
3967
|
+
body = current ? `Current model: ${current}` : "_(no models advertised yet)_";
|
|
3968
|
+
} else {
|
|
3969
|
+
const lines = models.map((m) => {
|
|
3970
|
+
const marker = m.modelId === current ? " \u25C0" : "";
|
|
3971
|
+
const desc = m.name && m.name !== m.modelId ? ` ${m.name}` : "";
|
|
3972
|
+
return `${m.modelId}${marker}${desc}`;
|
|
3973
|
+
});
|
|
3974
|
+
body = lines.join("\n");
|
|
3975
|
+
}
|
|
3976
|
+
this.recordAndBroadcast("session/update", {
|
|
3977
|
+
sessionId: this.upstreamSessionId,
|
|
3978
|
+
update: {
|
|
3979
|
+
sessionUpdate: "agent_message_chunk",
|
|
3980
|
+
content: { type: "text", text: `
|
|
3981
|
+
${body}
|
|
3982
|
+
` },
|
|
3983
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
3984
|
+
}
|
|
3985
|
+
});
|
|
3986
|
+
return { stopReason: "end_turn" };
|
|
3987
|
+
}
|
|
3988
|
+
await this.forwardRequest("session/set_model", {
|
|
3989
|
+
sessionId: this.sessionId,
|
|
3990
|
+
modelId: arg
|
|
3991
|
+
});
|
|
3992
|
+
return { stopReason: "end_turn" };
|
|
3993
|
+
}
|
|
3835
3994
|
// Runs as a normal queued prompt (so it serializes after any in-flight
|
|
3836
3995
|
// turn). With an arg, sets the title directly. Without one, runs a
|
|
3837
3996
|
// suppressed sub-prompt to the agent and uses its reply as the title.
|
|
@@ -3953,6 +4112,57 @@ var init_session = __esm({
|
|
|
3953
4112
|
await this.close({ deleteRecord: false });
|
|
3954
4113
|
return { stopReason: "end_turn" };
|
|
3955
4114
|
}
|
|
4115
|
+
// Restart the underlying agent with a fresh session/new, preserving the
|
|
4116
|
+
// conversation history. Unlike /hydra agent, this allows the same agentId
|
|
4117
|
+
// — useful when the proxy has changed available models (e.g. opus became
|
|
4118
|
+
// available after a quota reset) and the resumed session is locked to a
|
|
4119
|
+
// stale model list.
|
|
4120
|
+
runRestartCommand() {
|
|
4121
|
+
if (!this.spawnReplacementAgent) {
|
|
4122
|
+
throw withCode(
|
|
4123
|
+
new Error("agent restart not configured for this session"),
|
|
4124
|
+
JsonRpcErrorCodes.InternalError
|
|
4125
|
+
);
|
|
4126
|
+
}
|
|
4127
|
+
const spawnAgent = this.spawnReplacementAgent;
|
|
4128
|
+
const agentId = this.agentId;
|
|
4129
|
+
return this.enqueuePrompt(async () => {
|
|
4130
|
+
const transcript = await this.buildSwitchTranscript(agentId);
|
|
4131
|
+
const fresh = await spawnAgent({
|
|
4132
|
+
agentId,
|
|
4133
|
+
cwd: this.cwd,
|
|
4134
|
+
agentArgs: this.agentArgs
|
|
4135
|
+
});
|
|
4136
|
+
this.accumulateAndResetCost();
|
|
4137
|
+
this.wireAgent(fresh.agent);
|
|
4138
|
+
const oldAgent = this.agent;
|
|
4139
|
+
this.agent = fresh.agent;
|
|
4140
|
+
this.upstreamSessionId = fresh.upstreamSessionId;
|
|
4141
|
+
this.agentMeta = fresh.agentMeta;
|
|
4142
|
+
this.agentCapabilities = fresh.agentCapabilities;
|
|
4143
|
+
this.agentAdvertisedCommands = [];
|
|
4144
|
+
this.broadcastMergedCommands();
|
|
4145
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
4146
|
+
this.setAgentAdvertisedModels([]);
|
|
4147
|
+
}
|
|
4148
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
4149
|
+
this.setAgentAdvertisedModes([]);
|
|
4150
|
+
}
|
|
4151
|
+
await oldAgent.kill().catch(() => void 0);
|
|
4152
|
+
if (transcript) {
|
|
4153
|
+
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
4154
|
+
}
|
|
4155
|
+
this.broadcastAgentSwitch(agentId, agentId);
|
|
4156
|
+
const info = { agentId, upstreamSessionId: this.upstreamSessionId };
|
|
4157
|
+
for (const handler of this.agentChangeHandlers) {
|
|
4158
|
+
try {
|
|
4159
|
+
handler(info);
|
|
4160
|
+
} catch {
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
return { stopReason: "end_turn" };
|
|
4164
|
+
});
|
|
4165
|
+
}
|
|
3956
4166
|
// Walk the persisted history and produce a labeled transcript suitable
|
|
3957
4167
|
// for handing to a fresh agent. Includes user prompts, agent replies,
|
|
3958
4168
|
// and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
|
|
@@ -4569,6 +4779,22 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
4569
4779
|
return entry.task();
|
|
4570
4780
|
}
|
|
4571
4781
|
this.broadcastPromptReceived(entry);
|
|
4782
|
+
const promptText = extractPromptText(entry.prompt).trim();
|
|
4783
|
+
if (promptText === "/model" || promptText.startsWith("/model ") || promptText === "/sessions" || promptText === "/help") {
|
|
4784
|
+
let result;
|
|
4785
|
+
if (promptText === "/sessions") {
|
|
4786
|
+
result = await this.handleSessionsCommand();
|
|
4787
|
+
} else if (promptText === "/help") {
|
|
4788
|
+
result = await this.handleHelpCommand();
|
|
4789
|
+
} else {
|
|
4790
|
+
result = await this.handleModelCommand(promptText);
|
|
4791
|
+
}
|
|
4792
|
+
if (!this.closed) {
|
|
4793
|
+
this.broadcastTurnComplete(entry.clientId, result, entry.messageId, entry.wasAmend);
|
|
4794
|
+
}
|
|
4795
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
4796
|
+
return result;
|
|
4797
|
+
}
|
|
4572
4798
|
let response;
|
|
4573
4799
|
try {
|
|
4574
4800
|
response = await this.agent.connection.request(
|
|
@@ -4690,12 +4916,30 @@ function buildCombinedHistory(global, session) {
|
|
|
4690
4916
|
const filteredGlobal = global.filter((e) => !sessionSet.has(e));
|
|
4691
4917
|
return [...filteredGlobal, ...session];
|
|
4692
4918
|
}
|
|
4693
|
-
|
|
4919
|
+
function mergeReplayedEntries(existing, replayed, cap = HISTORY_CAP) {
|
|
4920
|
+
if (replayed.length === 0) {
|
|
4921
|
+
return existing;
|
|
4922
|
+
}
|
|
4923
|
+
const seen = new Set(existing);
|
|
4924
|
+
let out = existing;
|
|
4925
|
+
for (const raw of replayed) {
|
|
4926
|
+
const trimmed = raw.replace(/\n+$/, "");
|
|
4927
|
+
if (trimmed.length === 0) {
|
|
4928
|
+
continue;
|
|
4929
|
+
}
|
|
4930
|
+
if (seen.has(trimmed)) {
|
|
4931
|
+
continue;
|
|
4932
|
+
}
|
|
4933
|
+
seen.add(trimmed);
|
|
4934
|
+
out = appendEntry(out, trimmed, cap);
|
|
4935
|
+
}
|
|
4936
|
+
return out;
|
|
4937
|
+
}
|
|
4938
|
+
var HISTORY_CAP;
|
|
4694
4939
|
var init_history = __esm({
|
|
4695
4940
|
"src/tui/history.ts"() {
|
|
4696
4941
|
"use strict";
|
|
4697
4942
|
HISTORY_CAP = 500;
|
|
4698
|
-
GLOBAL_HISTORY_CAP = 2e3;
|
|
4699
4943
|
}
|
|
4700
4944
|
});
|
|
4701
4945
|
|
|
@@ -5083,7 +5327,15 @@ function mapModel(u) {
|
|
|
5083
5327
|
if (!model) {
|
|
5084
5328
|
return null;
|
|
5085
5329
|
}
|
|
5086
|
-
|
|
5330
|
+
const raw = u.availableModels;
|
|
5331
|
+
const availableModels = Array.isArray(raw) ? raw.map(
|
|
5332
|
+
(m) => typeof m === "object" && m !== null ? m.modelId : typeof m === "string" ? m : void 0
|
|
5333
|
+
).filter((id) => typeof id === "string" && id.length > 0) : void 0;
|
|
5334
|
+
return {
|
|
5335
|
+
kind: "model-changed",
|
|
5336
|
+
model: sanitizeSingleLine(model),
|
|
5337
|
+
...availableModels && availableModels.length > 0 ? { availableModels } : {}
|
|
5338
|
+
};
|
|
5087
5339
|
}
|
|
5088
5340
|
function mapTurnComplete(u) {
|
|
5089
5341
|
const stopReason = readString(u, "stopReason");
|
|
@@ -6155,7 +6407,7 @@ var init_input = __esm({
|
|
|
6155
6407
|
case "ctrl-r":
|
|
6156
6408
|
return this.startHistorySearch();
|
|
6157
6409
|
case "ctrl-s":
|
|
6158
|
-
return
|
|
6410
|
+
return this.amend();
|
|
6159
6411
|
case "ctrl-u":
|
|
6160
6412
|
this.killLine();
|
|
6161
6413
|
return [];
|
|
@@ -6673,22 +6925,30 @@ var init_input = __esm({
|
|
|
6673
6925
|
this.clearBuffer();
|
|
6674
6926
|
return [{ type: "send", text, planMode, attachments }];
|
|
6675
6927
|
}
|
|
6676
|
-
// Shift+Enter: amend the in-flight turn.
|
|
6677
|
-
//
|
|
6678
|
-
//
|
|
6679
|
-
//
|
|
6680
|
-
//
|
|
6681
|
-
//
|
|
6928
|
+
// Shift+Enter (also Ctrl+Enter / ^S): amend the in-flight turn.
|
|
6929
|
+
// While editing a queued slot, this is the "drop and amend" chord:
|
|
6930
|
+
// emit queue-remove for the slot AND amend with the loaded (possibly
|
|
6931
|
+
// edited) text, so the queued prompt becomes the amendment for the
|
|
6932
|
+
// running turn in a single keystroke. Empty buffer + no attachments
|
|
6933
|
+
// on a slot collapses to just queue-remove (matches empty-Enter).
|
|
6934
|
+
// Outside queue editing, an empty draft is a no-op. The app decides
|
|
6935
|
+
// whether to route the amend through amend_prompt or fall through to
|
|
6936
|
+
// a regular send when no turn is in flight.
|
|
6682
6937
|
amend() {
|
|
6683
6938
|
const text = this.bufferText();
|
|
6684
6939
|
if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
|
|
6685
6940
|
const index = this.queueIndex;
|
|
6941
|
+
const planMode2 = this.planMode;
|
|
6686
6942
|
const attachments2 = [...this.attachments];
|
|
6943
|
+
const empty = text.trim().length === 0 && attachments2.length === 0;
|
|
6687
6944
|
this.clearBuffer();
|
|
6688
|
-
if (
|
|
6945
|
+
if (empty) {
|
|
6689
6946
|
return [{ type: "queue-remove", index }];
|
|
6690
6947
|
}
|
|
6691
|
-
return [
|
|
6948
|
+
return [
|
|
6949
|
+
{ type: "queue-remove", index },
|
|
6950
|
+
{ type: "amend", text, planMode: planMode2, attachments: attachments2 }
|
|
6951
|
+
];
|
|
6692
6952
|
}
|
|
6693
6953
|
if (text.trim().length === 0 && this.attachments.length === 0) {
|
|
6694
6954
|
return [];
|
|
@@ -11681,6 +11941,16 @@ function parseReattachResponse(result) {
|
|
|
11681
11941
|
if (typeof r.clientId === "string" && r.clientId.length > 0) {
|
|
11682
11942
|
out.clientId = r.clientId;
|
|
11683
11943
|
}
|
|
11944
|
+
const meta = r._meta;
|
|
11945
|
+
if (meta && typeof meta === "object") {
|
|
11946
|
+
const hydra = meta["hydra-acp"];
|
|
11947
|
+
if (hydra && typeof hydra === "object") {
|
|
11948
|
+
const ts = hydra.turnStartedAt;
|
|
11949
|
+
if (typeof ts === "number") {
|
|
11950
|
+
out.turnStartedAt = ts;
|
|
11951
|
+
}
|
|
11952
|
+
}
|
|
11953
|
+
}
|
|
11684
11954
|
return out;
|
|
11685
11955
|
}
|
|
11686
11956
|
var init_reconnect_state = __esm({
|
|
@@ -12767,13 +13037,14 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12767
13037
|
const globalHistoryFile = paths.globalTuiHistoryFile();
|
|
12768
13038
|
let history = await loadHistory(historyFile).catch(() => []);
|
|
12769
13039
|
let globalHistory = await loadHistory(globalHistoryFile).catch(() => []);
|
|
12770
|
-
if (globalHistory.length >
|
|
12771
|
-
globalHistory = globalHistory.slice(globalHistory.length -
|
|
13040
|
+
if (globalHistory.length > config.tui.promptHistoryMaxEntries) {
|
|
13041
|
+
globalHistory = globalHistory.slice(globalHistory.length - config.tui.promptHistoryMaxEntries);
|
|
12772
13042
|
}
|
|
12773
13043
|
const dispatcher = new InputDispatcher({
|
|
12774
13044
|
history: buildCombinedHistory(globalHistory, history)
|
|
12775
13045
|
});
|
|
12776
13046
|
dispatcherRef = dispatcher;
|
|
13047
|
+
let livePeerHistoryRecording = false;
|
|
12777
13048
|
const recordHistoryEntry = (entry) => {
|
|
12778
13049
|
const trimmed = entry.replace(/\n+$/, "");
|
|
12779
13050
|
if (trimmed.length === 0) {
|
|
@@ -12782,7 +13053,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
12782
13053
|
const nextSession = appendEntry(history, trimmed);
|
|
12783
13054
|
const sessionChanged = nextSession !== history;
|
|
12784
13055
|
history = nextSession;
|
|
12785
|
-
const nextGlobal = appendEntry(globalHistory, trimmed,
|
|
13056
|
+
const nextGlobal = appendEntry(globalHistory, trimmed, config.tui.promptHistoryMaxEntries);
|
|
12786
13057
|
const globalChanged = nextGlobal !== globalHistory;
|
|
12787
13058
|
globalHistory = nextGlobal;
|
|
12788
13059
|
dispatcher.setHistory(buildCombinedHistory(globalHistory, history));
|
|
@@ -13112,10 +13383,10 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
13112
13383
|
const amendDesc = "amend the in-flight turn (cancel + replace)";
|
|
13113
13384
|
const head = config.tui.defaultEnterAction === "amend" ? [
|
|
13114
13385
|
["Enter", amendDesc],
|
|
13115
|
-
["Ctrl+Enter / Shift+Enter", enqueueDesc]
|
|
13386
|
+
["Ctrl+Enter / Shift+Enter / ^S", enqueueDesc]
|
|
13116
13387
|
] : [
|
|
13117
13388
|
["Enter", enqueueDesc],
|
|
13118
|
-
["Ctrl+Enter / Shift+Enter", amendDesc]
|
|
13389
|
+
["Ctrl+Enter / Shift+Enter / ^S", amendDesc]
|
|
13119
13390
|
];
|
|
13120
13391
|
return [...head, ...HELP_ENTRIES_TAIL];
|
|
13121
13392
|
};
|
|
@@ -13754,40 +14025,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
13754
14025
|
}
|
|
13755
14026
|
]);
|
|
13756
14027
|
return true;
|
|
13757
|
-
case "/model": {
|
|
13758
|
-
const arg = space === -1 ? "" : trimmed.slice(space + 1).trim();
|
|
13759
|
-
if (arg === "") {
|
|
13760
|
-
screen.appendLines([
|
|
13761
|
-
{
|
|
13762
|
-
prefix: " ",
|
|
13763
|
-
body: "Usage: /model <model-id>",
|
|
13764
|
-
bodyStyle: "info"
|
|
13765
|
-
}
|
|
13766
|
-
]);
|
|
13767
|
-
return true;
|
|
13768
|
-
}
|
|
13769
|
-
conn.request("session/set_model", {
|
|
13770
|
-
sessionId: resolvedSessionId,
|
|
13771
|
-
modelId: arg
|
|
13772
|
-
}).then(() => {
|
|
13773
|
-
screen.appendLines([
|
|
13774
|
-
{
|
|
13775
|
-
prefix: " ",
|
|
13776
|
-
body: `model set to ${arg}`,
|
|
13777
|
-
bodyStyle: "system"
|
|
13778
|
-
}
|
|
13779
|
-
]);
|
|
13780
|
-
}).catch((err) => {
|
|
13781
|
-
screen.appendLines([
|
|
13782
|
-
{
|
|
13783
|
-
prefix: " ",
|
|
13784
|
-
body: `set_model failed: ${err.message}`,
|
|
13785
|
-
bodyStyle: "tool-status-fail"
|
|
13786
|
-
}
|
|
13787
|
-
]);
|
|
13788
|
-
});
|
|
13789
|
-
return true;
|
|
13790
|
-
}
|
|
13791
14028
|
default:
|
|
13792
14029
|
return false;
|
|
13793
14030
|
}
|
|
@@ -14047,6 +14284,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
14047
14284
|
return;
|
|
14048
14285
|
}
|
|
14049
14286
|
if (event.kind === "user-text") {
|
|
14287
|
+
if (livePeerHistoryRecording) {
|
|
14288
|
+
recordHistoryEntry(event.text);
|
|
14289
|
+
}
|
|
14050
14290
|
closeAgentText();
|
|
14051
14291
|
if (toolsBlockStartedAt !== null) {
|
|
14052
14292
|
toolsBlockEndedAt = Date.now();
|
|
@@ -14179,6 +14419,12 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
14179
14419
|
};
|
|
14180
14420
|
const buffered = bufferedEvents;
|
|
14181
14421
|
bufferedEvents = [];
|
|
14422
|
+
const replayedPromptTexts = [];
|
|
14423
|
+
for (const event of buffered) {
|
|
14424
|
+
if (event.kind === "user-text" && typeof event.text === "string") {
|
|
14425
|
+
replayedPromptTexts.push(event.text);
|
|
14426
|
+
}
|
|
14427
|
+
}
|
|
14182
14428
|
screen.pauseRepaint();
|
|
14183
14429
|
try {
|
|
14184
14430
|
for (const event of buffered) {
|
|
@@ -14187,6 +14433,15 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
14187
14433
|
} finally {
|
|
14188
14434
|
screen.resumeRepaint();
|
|
14189
14435
|
}
|
|
14436
|
+
if (replayedPromptTexts.length > 0) {
|
|
14437
|
+
const merged = mergeReplayedEntries(history, replayedPromptTexts);
|
|
14438
|
+
if (merged !== history) {
|
|
14439
|
+
history = merged;
|
|
14440
|
+
dispatcher.setHistory(buildCombinedHistory(globalHistory, history));
|
|
14441
|
+
saveHistory(historyFile, history).catch(() => void 0);
|
|
14442
|
+
}
|
|
14443
|
+
}
|
|
14444
|
+
livePeerHistoryRecording = true;
|
|
14190
14445
|
if (initialTurnStartedAt !== void 0 && pendingTurns > 0) {
|
|
14191
14446
|
sessionBusySince = initialTurnStartedAt;
|
|
14192
14447
|
screen.setBanner({
|
|
@@ -14279,12 +14534,13 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
14279
14534
|
reconnectReplayBuffer = [];
|
|
14280
14535
|
let appliedPolicy;
|
|
14281
14536
|
let attachErr;
|
|
14537
|
+
let fields;
|
|
14282
14538
|
try {
|
|
14283
14539
|
const resp = await stream.request(attachReq);
|
|
14284
14540
|
if (resp.error) {
|
|
14285
14541
|
throw new Error(resp.error.message);
|
|
14286
14542
|
}
|
|
14287
|
-
|
|
14543
|
+
fields = parseReattachResponse(resp.result);
|
|
14288
14544
|
appliedPolicy = fields.appliedPolicy;
|
|
14289
14545
|
if (fields.clientId !== void 0) {
|
|
14290
14546
|
ownClientId = fields.clientId;
|
|
@@ -14318,6 +14574,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs) {
|
|
|
14318
14574
|
handleSessionUpdate(params);
|
|
14319
14575
|
}
|
|
14320
14576
|
}
|
|
14577
|
+
if (fields && fields.turnStartedAt === void 0 && pendingTurns > 0) {
|
|
14578
|
+
adjustPendingTurns(-pendingTurns);
|
|
14579
|
+
}
|
|
14321
14580
|
screen.setBanner({
|
|
14322
14581
|
status: pendingTurns > 0 ? "busy" : "ready",
|
|
14323
14582
|
elapsedMs: pendingTurns > 0 ? 0 : void 0
|
|
@@ -14621,7 +14880,7 @@ var init_app = __esm({
|
|
|
14621
14880
|
["^V", "paste image from clipboard"],
|
|
14622
14881
|
["^O", "expand / collapse tools block"],
|
|
14623
14882
|
null,
|
|
14624
|
-
["^R
|
|
14883
|
+
["^R", "history reverse search (^S walks forward once engaged)"],
|
|
14625
14884
|
["PgUp / PgDn", "scroll scrollback"],
|
|
14626
14885
|
["Mouse wheel", "scroll scrollback (when mouse capture is on)"],
|
|
14627
14886
|
["^X", "toggle mouse capture (wheel scroll vs. text selection)"],
|
|
@@ -16234,6 +16493,7 @@ var SessionManager = class {
|
|
|
16234
16493
|
idleEventTimeoutMs: this.idleEventTimeoutMs,
|
|
16235
16494
|
logger: this.logger,
|
|
16236
16495
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
16496
|
+
listSessions: () => this.list(),
|
|
16237
16497
|
historyStore: this.histories,
|
|
16238
16498
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
16239
16499
|
currentModel: fresh.initialModel,
|
|
@@ -16335,6 +16595,22 @@ var SessionManager = class {
|
|
|
16335
16595
|
} else {
|
|
16336
16596
|
agent.connection.drainBuffered("session/update");
|
|
16337
16597
|
}
|
|
16598
|
+
const agentReportedMode = extractInitialCurrentMode(loadResult ?? {});
|
|
16599
|
+
const advertisedModes = params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {}));
|
|
16600
|
+
this.logger?.info(
|
|
16601
|
+
`resurrect: sessionId=${params.hydraSessionId} persistedMode=${JSON.stringify(params.currentMode)} agentReportedMode=${JSON.stringify(agentReportedMode)} advertisedModes=${JSON.stringify(advertisedModes?.map((m) => m.id))}`
|
|
16602
|
+
);
|
|
16603
|
+
const effectiveMode = await restoreCurrentMode({
|
|
16604
|
+
agent,
|
|
16605
|
+
upstreamSessionId: params.upstreamSessionId,
|
|
16606
|
+
persistedMode: params.currentMode,
|
|
16607
|
+
agentReportedMode,
|
|
16608
|
+
advertisedModes,
|
|
16609
|
+
logger: this.logger
|
|
16610
|
+
});
|
|
16611
|
+
this.logger?.info(
|
|
16612
|
+
`resurrect: effectiveMode=${JSON.stringify(effectiveMode)} for sessionId=${params.hydraSessionId}`
|
|
16613
|
+
);
|
|
16338
16614
|
const session = new Session({
|
|
16339
16615
|
sessionId: params.hydraSessionId,
|
|
16340
16616
|
cwd: params.cwd,
|
|
@@ -16348,6 +16624,7 @@ var SessionManager = class {
|
|
|
16348
16624
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
16349
16625
|
logger: this.logger,
|
|
16350
16626
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
16627
|
+
listSessions: () => this.list(),
|
|
16351
16628
|
historyStore: this.histories,
|
|
16352
16629
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
16353
16630
|
// Prefer what we previously stored from a current_model_update; if
|
|
@@ -16355,11 +16632,15 @@ var SessionManager = class {
|
|
|
16355
16632
|
// this fix), fall back to the model the agent ships in its
|
|
16356
16633
|
// session/load response body.
|
|
16357
16634
|
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
16358
|
-
currentMode:
|
|
16635
|
+
currentMode: effectiveMode,
|
|
16359
16636
|
currentUsage: params.currentUsage,
|
|
16360
16637
|
agentCommands: params.agentCommands,
|
|
16361
|
-
agentModes:
|
|
16362
|
-
|
|
16638
|
+
agentModes: advertisedModes,
|
|
16639
|
+
// Always prefer the fresh list from session/load over the persisted
|
|
16640
|
+
// snapshot — the proxy's available models can change between daemon
|
|
16641
|
+
// restarts (quota resets, rollouts), so meta.json is intentionally
|
|
16642
|
+
// treated as a cold fallback here, not the authoritative source.
|
|
16643
|
+
agentModels: nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})) ?? params.agentModels,
|
|
16363
16644
|
// Only gate the first-prompt title heuristic when we actually have
|
|
16364
16645
|
// a title to preserve. A title-less session (lost to a write race
|
|
16365
16646
|
// or never seeded) should re-derive from the next prompt rather
|
|
@@ -16386,6 +16667,15 @@ var SessionManager = class {
|
|
|
16386
16667
|
mcpServers: [],
|
|
16387
16668
|
onInstallProgress: params.onInstallProgress
|
|
16388
16669
|
});
|
|
16670
|
+
const advertisedModes = params.agentModes ?? fresh.initialModes;
|
|
16671
|
+
const effectiveMode = await restoreCurrentMode({
|
|
16672
|
+
agent: fresh.agent,
|
|
16673
|
+
upstreamSessionId: fresh.upstreamSessionId,
|
|
16674
|
+
persistedMode: params.currentMode,
|
|
16675
|
+
agentReportedMode: fresh.initialMode,
|
|
16676
|
+
advertisedModes,
|
|
16677
|
+
logger: this.logger
|
|
16678
|
+
});
|
|
16389
16679
|
const session = new Session({
|
|
16390
16680
|
sessionId: params.hydraSessionId,
|
|
16391
16681
|
cwd,
|
|
@@ -16399,15 +16689,16 @@ var SessionManager = class {
|
|
|
16399
16689
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
16400
16690
|
logger: this.logger,
|
|
16401
16691
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
16692
|
+
listSessions: () => this.list(),
|
|
16402
16693
|
historyStore: this.histories,
|
|
16403
16694
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
16404
16695
|
// Prefer the stored value (set by a previous current_model_update);
|
|
16405
16696
|
// fall back to whatever the agent ships in its session/new response.
|
|
16406
16697
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
16407
|
-
currentMode:
|
|
16698
|
+
currentMode: effectiveMode,
|
|
16408
16699
|
currentUsage: params.currentUsage,
|
|
16409
16700
|
agentCommands: params.agentCommands,
|
|
16410
|
-
agentModes:
|
|
16701
|
+
agentModes: advertisedModes,
|
|
16411
16702
|
agentModels: params.agentModels ?? fresh.initialModels,
|
|
16412
16703
|
firstPromptSeeded: !!params.title,
|
|
16413
16704
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
@@ -16609,13 +16900,15 @@ var SessionManager = class {
|
|
|
16609
16900
|
modelId: desired
|
|
16610
16901
|
});
|
|
16611
16902
|
initialModel = desired;
|
|
16612
|
-
} catch {
|
|
16903
|
+
} catch (err) {
|
|
16904
|
+
this.logger?.warn(
|
|
16905
|
+
`defaultModels[${params.agentId}]=${JSON.stringify(desired)} rejected by agent (${err.message}); session will use ${JSON.stringify(initialModel)}`
|
|
16906
|
+
);
|
|
16613
16907
|
}
|
|
16614
16908
|
} else {
|
|
16615
16909
|
const known = initialModels.map((m) => m.modelId).join(", ");
|
|
16616
|
-
|
|
16617
|
-
`
|
|
16618
|
-
`
|
|
16910
|
+
this.logger?.warn(
|
|
16911
|
+
`defaultModels[${params.agentId}]=${JSON.stringify(desired)} not in agent's availableModels ([${known}]); skipping session/set_model, session will use ${JSON.stringify(initialModel)}`
|
|
16619
16912
|
);
|
|
16620
16913
|
}
|
|
16621
16914
|
}
|
|
@@ -16792,6 +17085,9 @@ var SessionManager = class {
|
|
|
16792
17085
|
get(sessionId) {
|
|
16793
17086
|
return this.sessions.get(sessionId);
|
|
16794
17087
|
}
|
|
17088
|
+
liveSessions() {
|
|
17089
|
+
return this.sessions.values();
|
|
17090
|
+
}
|
|
16795
17091
|
// Snapshot of which agent versions are currently in use by live
|
|
16796
17092
|
// sessions, keyed by agentId. Read by the registry-fetch prune sweep
|
|
16797
17093
|
// so it can skip install dirs that still back a running process.
|
|
@@ -16853,7 +17149,7 @@ var SessionManager = class {
|
|
|
16853
17149
|
title: session.title,
|
|
16854
17150
|
agentId: session.agentId,
|
|
16855
17151
|
currentModel: session.currentModel,
|
|
16856
|
-
currentUsage: session.
|
|
17152
|
+
currentUsage: session.currentUsage,
|
|
16857
17153
|
parentSessionId: session.parentSessionId,
|
|
16858
17154
|
updatedAt: used,
|
|
16859
17155
|
attachedClients: session.attachedCount,
|
|
@@ -17005,6 +17301,7 @@ var SessionManager = class {
|
|
|
17005
17301
|
currentMode: args.bundle.session.currentMode,
|
|
17006
17302
|
currentUsage: args.bundle.session.currentUsage,
|
|
17007
17303
|
agentCommands: args.bundle.session.agentCommands,
|
|
17304
|
+
agentModes: args.bundle.session.agentModes,
|
|
17008
17305
|
createdAt: args.preservedCreatedAt ?? now,
|
|
17009
17306
|
// Fallback path for historyMtimeIso (used when the history file
|
|
17010
17307
|
// is missing). Keep this consistent with the utimes stamp above.
|
|
@@ -17394,6 +17691,47 @@ function extractInitialCurrentMode(result) {
|
|
|
17394
17691
|
}
|
|
17395
17692
|
return void 0;
|
|
17396
17693
|
}
|
|
17694
|
+
async function restoreCurrentMode(opts) {
|
|
17695
|
+
const {
|
|
17696
|
+
agent,
|
|
17697
|
+
upstreamSessionId,
|
|
17698
|
+
persistedMode,
|
|
17699
|
+
agentReportedMode,
|
|
17700
|
+
advertisedModes,
|
|
17701
|
+
logger
|
|
17702
|
+
} = opts;
|
|
17703
|
+
if (!persistedMode) {
|
|
17704
|
+
return agentReportedMode;
|
|
17705
|
+
}
|
|
17706
|
+
if (persistedMode === agentReportedMode) {
|
|
17707
|
+
return persistedMode;
|
|
17708
|
+
}
|
|
17709
|
+
if (advertisedModes && advertisedModes.length > 0 && !advertisedModes.some((m) => m.id === persistedMode)) {
|
|
17710
|
+
const known = advertisedModes.map((m) => m.id).join(", ");
|
|
17711
|
+
logger?.warn(
|
|
17712
|
+
`resurrect: persisted currentMode=${JSON.stringify(persistedMode)} not in agent's availableModes ([${known}]); skipping session/set_mode, session will use ${JSON.stringify(agentReportedMode)}`
|
|
17713
|
+
);
|
|
17714
|
+
return agentReportedMode;
|
|
17715
|
+
}
|
|
17716
|
+
try {
|
|
17717
|
+
logger?.info(
|
|
17718
|
+
`resurrect: pushing persisted modeId=${JSON.stringify(persistedMode)} to agent (agentReported=${JSON.stringify(agentReportedMode)})`
|
|
17719
|
+
);
|
|
17720
|
+
await agent.connection.request("session/set_mode", {
|
|
17721
|
+
sessionId: upstreamSessionId,
|
|
17722
|
+
modeId: persistedMode
|
|
17723
|
+
});
|
|
17724
|
+
logger?.info(
|
|
17725
|
+
`resurrect: session/set_mode accepted, effectiveMode=${JSON.stringify(persistedMode)}`
|
|
17726
|
+
);
|
|
17727
|
+
return persistedMode;
|
|
17728
|
+
} catch (err) {
|
|
17729
|
+
logger?.warn(
|
|
17730
|
+
`resurrect: session/set_mode rejected by agent for modeId=${JSON.stringify(persistedMode)} (${err.message}); session will use ${JSON.stringify(agentReportedMode)}`
|
|
17731
|
+
);
|
|
17732
|
+
return agentReportedMode;
|
|
17733
|
+
}
|
|
17734
|
+
}
|
|
17397
17735
|
function parseModesList(list) {
|
|
17398
17736
|
if (!Array.isArray(list)) {
|
|
17399
17737
|
return [];
|
|
@@ -19927,6 +20265,14 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
19927
20265
|
connection,
|
|
19928
20266
|
intercepts
|
|
19929
20267
|
);
|
|
20268
|
+
if (deps.manager?.defaultTransformers.includes(processIdentity.name)) {
|
|
20269
|
+
const ref = deps.transformers.resolveChain([processIdentity.name])[0];
|
|
20270
|
+
if (ref) {
|
|
20271
|
+
for (const session of deps.manager.liveSessions()) {
|
|
20272
|
+
session.addTransformer(ref);
|
|
20273
|
+
}
|
|
20274
|
+
}
|
|
20275
|
+
}
|
|
19930
20276
|
}
|
|
19931
20277
|
return { ack: true };
|
|
19932
20278
|
});
|
|
@@ -20142,16 +20488,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
20142
20488
|
let resurrectParams = fromDisk;
|
|
20143
20489
|
if (hydraHints) {
|
|
20144
20490
|
resurrectParams = {
|
|
20491
|
+
...fromDisk,
|
|
20145
20492
|
hydraSessionId: params.sessionId,
|
|
20146
20493
|
upstreamSessionId: hydraHints.upstreamSessionId,
|
|
20147
20494
|
agentId: hydraHints.agentId,
|
|
20148
20495
|
cwd: hydraHints.cwd,
|
|
20149
|
-
title: hydraHints.title
|
|
20150
|
-
agentArgs: hydraHints.agentArgs
|
|
20151
|
-
currentModel: fromDisk?.currentModel,
|
|
20152
|
-
currentMode: fromDisk?.currentMode,
|
|
20153
|
-
agentCommands: fromDisk?.agentCommands,
|
|
20154
|
-
createdAt: fromDisk?.createdAt
|
|
20496
|
+
...hydraHints.title !== void 0 ? { title: hydraHints.title } : {},
|
|
20497
|
+
...hydraHints.agentArgs !== void 0 ? { agentArgs: hydraHints.agentArgs } : {}
|
|
20155
20498
|
};
|
|
20156
20499
|
}
|
|
20157
20500
|
if (!resurrectParams) {
|
|
@@ -20165,6 +20508,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
20165
20508
|
...resurrectParams,
|
|
20166
20509
|
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
20167
20510
|
});
|
|
20511
|
+
wireDefaultTransformers(session, deps);
|
|
20168
20512
|
}
|
|
20169
20513
|
const client = bindClientToSession(
|
|
20170
20514
|
connection,
|
|
@@ -20254,6 +20598,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
20254
20598
|
`session/prompt auto-resurrecting cold sessionId=${params.sessionId}`
|
|
20255
20599
|
);
|
|
20256
20600
|
session = await deps.manager.resurrect(fromDisk);
|
|
20601
|
+
wireDefaultTransformers(session, deps);
|
|
20257
20602
|
const client = bindClientToSession(
|
|
20258
20603
|
connection,
|
|
20259
20604
|
session,
|
|
@@ -20406,6 +20751,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
20406
20751
|
throw err;
|
|
20407
20752
|
}
|
|
20408
20753
|
session = await deps.manager.resurrect(fromDisk);
|
|
20754
|
+
wireDefaultTransformers(session, deps);
|
|
20409
20755
|
}
|
|
20410
20756
|
const client = bindClientToSession(connection, session, state);
|
|
20411
20757
|
const { entries: replay } = await session.attach(client, "pending_only");
|
|
@@ -20456,6 +20802,32 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
20456
20802
|
app.log.info(decision.logMessage);
|
|
20457
20803
|
return decision.session.forwardRequest("session/set_model", rawParams);
|
|
20458
20804
|
});
|
|
20805
|
+
connection.onRequest("session/set_mode", async (rawParams) => {
|
|
20806
|
+
const params = rawParams;
|
|
20807
|
+
const sessionIdField = params?.sessionId;
|
|
20808
|
+
if (typeof sessionIdField === "string") {
|
|
20809
|
+
denyIfReadonly(sessionIdField, "session/set_mode");
|
|
20810
|
+
}
|
|
20811
|
+
if (!params || typeof params.sessionId !== "string") {
|
|
20812
|
+
const err = new Error("session/set_mode requires string sessionId");
|
|
20813
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
20814
|
+
throw err;
|
|
20815
|
+
}
|
|
20816
|
+
if (typeof params.modeId !== "string") {
|
|
20817
|
+
const err = new Error("session/set_mode requires string modeId");
|
|
20818
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
20819
|
+
throw err;
|
|
20820
|
+
}
|
|
20821
|
+
const session = deps.manager.get(params.sessionId);
|
|
20822
|
+
if (!session) {
|
|
20823
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
20824
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
20825
|
+
throw err;
|
|
20826
|
+
}
|
|
20827
|
+
const result = await session.forwardRequest("session/set_mode", rawParams);
|
|
20828
|
+
session.applyModeChange(params.modeId);
|
|
20829
|
+
return result;
|
|
20830
|
+
});
|
|
20459
20831
|
connection.setDefaultHandler(async (rawParams, method) => {
|
|
20460
20832
|
if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
|
|
20461
20833
|
const err = new Error(`Method not found: ${method}`);
|
|
@@ -20728,6 +21100,17 @@ function buildInitializeResult() {
|
|
|
20728
21100
|
})
|
|
20729
21101
|
};
|
|
20730
21102
|
}
|
|
21103
|
+
function wireDefaultTransformers(session, deps) {
|
|
21104
|
+
if (!deps.transformers || !deps.manager) {
|
|
21105
|
+
return;
|
|
21106
|
+
}
|
|
21107
|
+
for (const name of deps.manager.defaultTransformers) {
|
|
21108
|
+
const ref = deps.transformers.resolveChain([name])[0];
|
|
21109
|
+
if (ref) {
|
|
21110
|
+
session.addTransformer(ref);
|
|
21111
|
+
}
|
|
21112
|
+
}
|
|
21113
|
+
}
|
|
20731
21114
|
function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
|
|
20732
21115
|
void state;
|
|
20733
21116
|
void session;
|
|
@@ -21026,6 +21409,39 @@ async function printTail(logPath, fileSize, lines) {
|
|
|
21026
21409
|
}
|
|
21027
21410
|
return fileSize;
|
|
21028
21411
|
}
|
|
21412
|
+
function splitNameFromLogTailArgs(args) {
|
|
21413
|
+
const flagsWithValue = /* @__PURE__ */ new Set(["--tail", "-n"]);
|
|
21414
|
+
let name;
|
|
21415
|
+
const rest = [];
|
|
21416
|
+
let i = 0;
|
|
21417
|
+
while (i < args.length) {
|
|
21418
|
+
const tok = args[i];
|
|
21419
|
+
if (tok === void 0) {
|
|
21420
|
+
break;
|
|
21421
|
+
}
|
|
21422
|
+
if (tok.startsWith("-")) {
|
|
21423
|
+
rest.push(tok);
|
|
21424
|
+
if (flagsWithValue.has(tok) && i + 1 < args.length) {
|
|
21425
|
+
const v = args[i + 1];
|
|
21426
|
+
if (v !== void 0) {
|
|
21427
|
+
rest.push(v);
|
|
21428
|
+
}
|
|
21429
|
+
i += 2;
|
|
21430
|
+
continue;
|
|
21431
|
+
}
|
|
21432
|
+
i += 1;
|
|
21433
|
+
continue;
|
|
21434
|
+
}
|
|
21435
|
+
if (name === void 0) {
|
|
21436
|
+
name = tok;
|
|
21437
|
+
i += 1;
|
|
21438
|
+
continue;
|
|
21439
|
+
}
|
|
21440
|
+
rest.push(tok);
|
|
21441
|
+
i += 1;
|
|
21442
|
+
}
|
|
21443
|
+
return { name, rest };
|
|
21444
|
+
}
|
|
21029
21445
|
function parseLogTailFlags(argv) {
|
|
21030
21446
|
let tail = 50;
|
|
21031
21447
|
let follow = false;
|
|
@@ -22025,16 +22441,17 @@ async function postLifecycleAll(verb) {
|
|
|
22025
22441
|
process.exit(1);
|
|
22026
22442
|
}
|
|
22027
22443
|
}
|
|
22028
|
-
async function runExtensionsLogs(
|
|
22444
|
+
async function runExtensionsLogs(argv) {
|
|
22445
|
+
const { name, rest } = splitNameFromLogTailArgs(argv);
|
|
22029
22446
|
if (!name) {
|
|
22030
22447
|
process.stderr.write(
|
|
22031
|
-
"Usage: hydra-acp extensions
|
|
22448
|
+
"Usage: hydra-acp extensions log <name> [--tail N] [--follow]\n"
|
|
22032
22449
|
);
|
|
22033
22450
|
process.exit(2);
|
|
22034
22451
|
return;
|
|
22035
22452
|
}
|
|
22036
22453
|
const logPath = paths.extensionLogFile(name);
|
|
22037
|
-
await runLogTail(logPath,
|
|
22454
|
+
await runLogTail(logPath, rest, "No log file (extension never ran?)");
|
|
22038
22455
|
}
|
|
22039
22456
|
function parseAddFlags(argv) {
|
|
22040
22457
|
let command = [];
|
|
@@ -22480,16 +22897,17 @@ async function postLifecycleAll2(verb) {
|
|
|
22480
22897
|
process.exit(1);
|
|
22481
22898
|
}
|
|
22482
22899
|
}
|
|
22483
|
-
async function runTransformersLogs(
|
|
22900
|
+
async function runTransformersLogs(argv) {
|
|
22901
|
+
const { name, rest } = splitNameFromLogTailArgs(argv);
|
|
22484
22902
|
if (!name) {
|
|
22485
22903
|
process.stderr.write(
|
|
22486
|
-
"Usage: hydra-acp transformers
|
|
22904
|
+
"Usage: hydra-acp transformers log <name> [--tail N] [--follow]\n"
|
|
22487
22905
|
);
|
|
22488
22906
|
process.exit(2);
|
|
22489
22907
|
return;
|
|
22490
22908
|
}
|
|
22491
22909
|
const logPath = paths.transformerLogFile(name);
|
|
22492
|
-
await runLogTail(logPath,
|
|
22910
|
+
await runLogTail(logPath, rest, "No log file (transformer never ran?)");
|
|
22493
22911
|
}
|
|
22494
22912
|
async function readRawConfig2() {
|
|
22495
22913
|
const raw = await fsp12.readFile(paths.config(), "utf8");
|
|
@@ -24123,8 +24541,8 @@ async function main() {
|
|
|
24123
24541
|
await runExtensionsRestart(name2);
|
|
24124
24542
|
return;
|
|
24125
24543
|
}
|
|
24126
|
-
if (sub === "logs") {
|
|
24127
|
-
await runExtensionsLogs(
|
|
24544
|
+
if (sub === "log" || sub === "logs") {
|
|
24545
|
+
await runExtensionsLogs(tail.slice(1));
|
|
24128
24546
|
return;
|
|
24129
24547
|
}
|
|
24130
24548
|
process.stderr.write(`Unknown extension subcommand: ${sub}
|
|
@@ -24163,8 +24581,8 @@ async function main() {
|
|
|
24163
24581
|
await runTransformersRestart(name2);
|
|
24164
24582
|
return;
|
|
24165
24583
|
}
|
|
24166
|
-
if (sub === "logs") {
|
|
24167
|
-
await runTransformersLogs(
|
|
24584
|
+
if (sub === "log" || sub === "logs") {
|
|
24585
|
+
await runTransformersLogs(tail.slice(1));
|
|
24168
24586
|
return;
|
|
24169
24587
|
}
|
|
24170
24588
|
process.stderr.write(`Unknown transformer subcommand: ${sub}
|
|
@@ -24384,12 +24802,12 @@ function printHelp() {
|
|
|
24384
24802
|
" hydra-acp extension add <name> [opts] Add an extension to config",
|
|
24385
24803
|
" hydra-acp extension remove <name> Remove an extension from config",
|
|
24386
24804
|
" hydra-acp extension start|stop|restart <n>|all Lifecycle on one or all",
|
|
24387
|
-
" hydra-acp extension
|
|
24805
|
+
" hydra-acp extension log <name> [-f] [-n N] Tail or follow an extension's log",
|
|
24388
24806
|
" hydra-acp transformer list List configured transformers and live state",
|
|
24389
24807
|
" hydra-acp transformer add <name> [opts] Add a transformer to config (--command, --args, --env, --disabled)",
|
|
24390
24808
|
" hydra-acp transformer remove <name> Remove a transformer from config",
|
|
24391
24809
|
" hydra-acp transformer start|stop|restart <n>|all Lifecycle on one or all",
|
|
24392
|
-
" hydra-acp transformer
|
|
24810
|
+
" hydra-acp transformer log <name> [-f] [-n N] Tail or follow a transformer's log",
|
|
24393
24811
|
" hydra-acp agent [list] List agents in the cached registry",
|
|
24394
24812
|
" hydra-acp agent refresh Force a registry re-fetch",
|
|
24395
24813
|
" hydra-acp agent install <id> Pre-install <id> from the registry (else lazy on first session)",
|