@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/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(1e3),
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
- currentUsage;
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.currentUsage = init.currentUsage;
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
- let envelope = params;
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(envelope)) {
2499
- this.recordAndBroadcast("session/update", this.injectCumulativeCost(envelope));
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 || this.cumulativeCost > 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" || this.cumulativeCost) {
2706
- cost.amount = this.cumulativeCost + (u.costAmount ?? 0);
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.currentUsage ?? {} };
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.currentUsage = next;
3669
+ this._currentUsage = next;
3670
+ const total = this.currentUsage ?? next;
3613
3671
  for (const handler of this.usageHandlers) {
3614
3672
  try {
3615
- handler(next);
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.currentUsage?.costAmount;
3683
+ const amount = this._currentUsage?.costAmount;
3626
3684
  if (!amount)
3627
3685
  return;
3628
3686
  this.cumulativeCost += amount;
3629
3687
  const next = {
3630
- ...this.currentUsage ?? {},
3688
+ ...this._currentUsage ?? {},
3631
3689
  cumulativeCost: this.cumulativeCost,
3632
3690
  costAmount: void 0
3633
3691
  };
3634
- this.currentUsage = next;
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
- // Total cost across all agent lives for this hydra session. Used by
3670
- // session/list so list rows show the accumulated figure.
3727
+ // Deprecated alias currentUsage already returns the total.
3671
3728
  get totalUsage() {
3672
- if (!this.currentUsage && !this.cumulativeCost)
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
- var HISTORY_CAP, GLOBAL_HISTORY_CAP;
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
- return { kind: "model-changed", model: sanitizeSingleLine(model) };
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. Editing a queued slot
6677
- // delegates to the existing queue-edit / queue-remove path Shift+Enter
6678
- // there has no special meaning since the entry is already queued (not
6679
- // running). With an empty draft and no attachments we emit nothing
6680
- // (no-op). Otherwise emit an "amend" effect; the app decides whether
6681
- // to route through amend_prompt or fall through to a regular send.
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 (text.trim().length === 0) {
6945
+ if (empty) {
6689
6946
  return [{ type: "queue-remove", index }];
6690
6947
  }
6691
- return [{ type: "queue-edit", index, text, attachments: attachments2 }];
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 > GLOBAL_HISTORY_CAP) {
12771
- globalHistory = globalHistory.slice(globalHistory.length - GLOBAL_HISTORY_CAP);
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, GLOBAL_HISTORY_CAP);
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
- const fields = parseReattachResponse(resp.result);
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 / ^S", "history reverse / forward search"],
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: params.currentMode ?? extractInitialCurrentMode(loadResult ?? {}),
16635
+ currentMode: effectiveMode,
16359
16636
  currentUsage: params.currentUsage,
16360
16637
  agentCommands: params.agentCommands,
16361
- agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
16362
- agentModels: params.agentModels ?? nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})),
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: params.currentMode ?? fresh.initialMode,
16698
+ currentMode: effectiveMode,
16408
16699
  currentUsage: params.currentUsage,
16409
16700
  agentCommands: params.agentCommands,
16410
- agentModes: params.agentModes ?? fresh.initialModes,
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
- process.stderr.write(
16617
- `hydra-acp: defaultModels[${params.agentId}]=${JSON.stringify(desired)} is not in the agent's availableModels (${known}); skipping session/set_model
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.totalUsage,
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 ?? fromDisk?.title,
20150
- agentArgs: hydraHints.agentArgs ?? fromDisk?.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(name, argv) {
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 logs <name> [--tail N] [--follow]\n"
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, argv, "No log file (extension never ran?)");
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(name, argv) {
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 logs <name> [--tail N] [--follow]\n"
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, argv, "No log file (transformer never ran?)");
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(name2, rest);
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(name2, rest);
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 logs <name> [-f] [-n N] Tail or follow an extension's log",
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 logs <name> [-f] [-n N] Tail or follow a transformer's log",
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)",