@hydra-acp/cli 0.1.25 → 0.1.27

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
@@ -483,12 +483,35 @@ function extractHydraMeta(meta) {
483
483
  out.availableModes = modes;
484
484
  }
485
485
  }
486
+ if (Array.isArray(obj.availableModels)) {
487
+ const models = [];
488
+ for (const raw of obj.availableModels) {
489
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
490
+ continue;
491
+ }
492
+ const m = raw;
493
+ if (typeof m.modelId !== "string") {
494
+ continue;
495
+ }
496
+ const model = { modelId: m.modelId };
497
+ if (typeof m.name === "string") {
498
+ model.name = m.name;
499
+ }
500
+ if (typeof m.description === "string") {
501
+ model.description = m.description;
502
+ }
503
+ models.push(model);
504
+ }
505
+ if (models.length > 0) {
506
+ out.availableModels = models;
507
+ }
508
+ }
486
509
  return out;
487
510
  }
488
511
  function mergeMeta(passthrough, ours) {
489
512
  return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
490
513
  }
491
- var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, PromptOriginatorSchema, PromptQueueAddedParams, PromptQueueUpdatedParams, PromptQueueRemovedParams, CancelPromptParams, CancelPromptResult, UpdatePromptParams, UpdatePromptResult, AmendPromptParams, AmendPromptResult, PromptAmendedParams, ProxyInitializeParams;
514
+ var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, PromptOriginatorSchema, PromptQueueAddedParams, PromptQueueUpdatedParams, PromptQueueRemovedParams, CancelPromptParams, CancelPromptResult, UpdatePromptParams, UpdatePromptResult, AmendPromptParams, AmendPromptResult, PromptAmendedParams, AgentInstallProgressParams, AGENT_INSTALL_PROGRESS_METHOD, ProxyInitializeParams;
492
515
  var init_types = __esm({
493
516
  "src/acp/types.ts"() {
494
517
  "use strict";
@@ -682,6 +705,23 @@ var init_types = __esm({
682
705
  originator: PromptOriginatorSchema,
683
706
  amendedAt: z3.number()
684
707
  });
708
+ AgentInstallProgressParams = z3.object({
709
+ agentId: z3.string(),
710
+ version: z3.string(),
711
+ source: z3.enum(["binary", "npm"]),
712
+ phase: z3.enum([
713
+ "download_start",
714
+ "download_progress",
715
+ "download_done",
716
+ "extract",
717
+ "install_start",
718
+ "installed"
719
+ ]),
720
+ receivedBytes: z3.number().optional(),
721
+ totalBytes: z3.number().optional(),
722
+ packageSpec: z3.string().optional()
723
+ });
724
+ AGENT_INSTALL_PROGRESS_METHOD = "hydra-acp/agent_install_progress";
685
725
  ProxyInitializeParams = z3.object({
686
726
  protocolVersion: z3.number().optional(),
687
727
  proxyInfo: z3.object({
@@ -1020,6 +1060,42 @@ function sameAdvertisedModes(a, b) {
1020
1060
  }
1021
1061
  return true;
1022
1062
  }
1063
+ function sameAdvertisedModels(a, b) {
1064
+ if (a.length !== b.length) {
1065
+ return false;
1066
+ }
1067
+ for (let i = 0; i < a.length; i++) {
1068
+ if (a[i]?.modelId !== b[i]?.modelId || a[i]?.name !== b[i]?.name || a[i]?.description !== b[i]?.description) {
1069
+ return false;
1070
+ }
1071
+ }
1072
+ return true;
1073
+ }
1074
+ function parseModelsList(list) {
1075
+ if (!Array.isArray(list)) {
1076
+ return [];
1077
+ }
1078
+ const out = [];
1079
+ for (const raw of list) {
1080
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1081
+ continue;
1082
+ }
1083
+ const r = raw;
1084
+ const modelId = typeof r.modelId === "string" && r.modelId.trim() || typeof r.value === "string" && r.value.trim() || typeof r.id === "string" && r.id.trim() || void 0;
1085
+ if (!modelId) {
1086
+ continue;
1087
+ }
1088
+ const model = { modelId };
1089
+ if (typeof r.name === "string" && r.name.length > 0) {
1090
+ model.name = r.name;
1091
+ }
1092
+ if (typeof r.description === "string" && r.description.length > 0) {
1093
+ model.description = r.description;
1094
+ }
1095
+ out.push(model);
1096
+ }
1097
+ return out;
1098
+ }
1023
1099
  function extractAdvertisedModes(params) {
1024
1100
  const obj = params ?? {};
1025
1101
  const update = obj.update ?? {};
@@ -1306,11 +1382,19 @@ var init_session = __esm({
1306
1382
  // Last available_modes_update we observed from the agent. Same
1307
1383
  // pattern as commands: cache, persist, broadcast on change.
1308
1384
  agentAdvertisedModes = [];
1385
+ // Last availableModels payload we observed (from current_model_update,
1386
+ // a session/new / session/load response, or — for opencode — a
1387
+ // config_option_update where configOptions[i].id === "model").
1388
+ // Cached so a mid-session attach can synthesize a model picker
1389
+ // snapshot, and so session/set_model can validate the requested id
1390
+ // against what the agent claims to support.
1391
+ agentAdvertisedModels = [];
1309
1392
  // Persist hooks for snapshot-shaped state. SessionManager hooks these
1310
1393
  // to mirror changes into meta.json so cold-resurrect attaches can
1311
1394
  // surface the latest snapshot via the attach response _meta.
1312
1395
  agentCommandsHandlers = [];
1313
1396
  agentModesHandlers = [];
1397
+ agentModelsHandlers = [];
1314
1398
  modelHandlers = [];
1315
1399
  modeHandlers = [];
1316
1400
  usageHandlers = [];
@@ -1346,6 +1430,9 @@ var init_session = __esm({
1346
1430
  if (init.agentModes && init.agentModes.length > 0) {
1347
1431
  this.agentAdvertisedModes = [...init.agentModes];
1348
1432
  }
1433
+ if (init.agentModels && init.agentModels.length > 0) {
1434
+ this.agentAdvertisedModels = [...init.agentModels];
1435
+ }
1349
1436
  this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
1350
1437
  this.spawnReplacementAgent = init.spawnReplacementAgent;
1351
1438
  this.logger = init.logger;
@@ -1383,6 +1470,23 @@ var init_session = __esm({
1383
1470
  }
1384
1471
  });
1385
1472
  }
1473
+ // Re-broadcast our cached availableModels via current_model_update.
1474
+ // Spec shape: { currentModel, availableModels } — we only include the
1475
+ // currentModel field when we know it, so this broadcast can also fire
1476
+ // model-list updates standalone before any current model is set.
1477
+ broadcastAvailableModels() {
1478
+ const update = {
1479
+ sessionUpdate: "current_model_update",
1480
+ availableModels: [...this.agentAdvertisedModels]
1481
+ };
1482
+ if (this.currentModel !== void 0 && this.currentModel.length > 0) {
1483
+ update.currentModel = this.currentModel;
1484
+ }
1485
+ this.recordAndBroadcast("session/update", {
1486
+ sessionId: this.upstreamSessionId,
1487
+ update
1488
+ });
1489
+ }
1386
1490
  // Register session/update, session/request_permission, and onExit
1387
1491
  // handlers on an agent connection. Re-run on every /hydra agent so
1388
1492
  // the new agent is plumbed identically. The exit handler's identity
@@ -1413,6 +1517,10 @@ var init_session = __esm({
1413
1517
  this.recordAndBroadcast("session/update", params);
1414
1518
  return;
1415
1519
  }
1520
+ if (this.maybeApplyAgentConfigOption(params)) {
1521
+ this.recordAndBroadcast("session/update", params);
1522
+ return;
1523
+ }
1416
1524
  if (this.maybeApplyAgentUsage(params)) {
1417
1525
  this.recordAndBroadcast("session/update", params);
1418
1526
  return;
@@ -1561,16 +1669,19 @@ var init_session = __esm({
1561
1669
  recordedAt
1562
1670
  });
1563
1671
  }
1564
- if (this.currentModel !== void 0 && this.currentModel.length > 0) {
1672
+ if (this.currentModel !== void 0 && this.currentModel.length > 0 || this.agentAdvertisedModels.length > 0) {
1673
+ const update = {
1674
+ sessionUpdate: "current_model_update"
1675
+ };
1676
+ if (this.currentModel !== void 0 && this.currentModel.length > 0) {
1677
+ update.currentModel = this.currentModel;
1678
+ }
1679
+ if (this.agentAdvertisedModels.length > 0) {
1680
+ update.availableModels = [...this.agentAdvertisedModels];
1681
+ }
1565
1682
  out.push({
1566
1683
  method: "session/update",
1567
- params: {
1568
- sessionId,
1569
- update: {
1570
- sessionUpdate: "current_model_update",
1571
- currentModel: this.currentModel
1572
- }
1573
- },
1684
+ params: { sessionId, update },
1574
1685
  recordedAt
1575
1686
  });
1576
1687
  }
@@ -2205,6 +2316,18 @@ var init_session = __esm({
2205
2316
  onTitleChange(handler) {
2206
2317
  this.titleHandlers.push(handler);
2207
2318
  }
2319
+ // External entry point for retitling a live session from outside the
2320
+ // ACP slash-command path (e.g. PATCH /v1/sessions/:id from the picker).
2321
+ // Goes through the same enqueuePrompt path as /hydra title so it
2322
+ // serializes after any in-flight turn and shares broadcast/persistence.
2323
+ retitle(title) {
2324
+ return this.runTitleCommand(title);
2325
+ }
2326
+ // External entry point for the LLM-regen title path (T in the picker,
2327
+ // equivalent to bare /hydra title with no arg).
2328
+ retitleFromAgent() {
2329
+ return this.runTitleCommand("");
2330
+ }
2208
2331
  // Update the canonical title and broadcast a session_info_update to
2209
2332
  // every attached client. Clients that already speak the spec's
2210
2333
  // session_info_update need no hydra-specific wiring to pick this up.
@@ -2252,12 +2375,19 @@ var init_session = __esm({
2252
2375
  // Apply an agent-emitted current_model_update. Returns true if the
2253
2376
  // notification was a model update (caller still needs to broadcast
2254
2377
  // it). Returns false otherwise so the caller can try the next kind.
2378
+ // current_model_update can carry availableModels in the same payload
2379
+ // (per ACP spec); we cache that list too so session/set_model can
2380
+ // validate against it.
2255
2381
  maybeApplyAgentModel(params) {
2256
2382
  const obj = params ?? {};
2257
2383
  const update = obj.update ?? {};
2258
2384
  if (update.sessionUpdate !== "current_model_update") {
2259
2385
  return false;
2260
2386
  }
2387
+ const models = parseModelsList(update.availableModels);
2388
+ if (models.length > 0) {
2389
+ this.setAgentAdvertisedModels(models);
2390
+ }
2261
2391
  const raw = typeof update.currentModel === "string" ? update.currentModel : typeof update.model === "string" ? update.model : void 0;
2262
2392
  if (raw === void 0) {
2263
2393
  return true;
@@ -2275,6 +2405,55 @@ var init_session = __esm({
2275
2405
  }
2276
2406
  return true;
2277
2407
  }
2408
+ // Apply an opencode-style config_option_update. opencode emits this
2409
+ // (not the spec-shaped current_model_update / available_models_update)
2410
+ // to carry both the current model and the list of available models.
2411
+ // The payload is `configOptions: [{ id: "model", currentValue, options:
2412
+ // [{ value, name }] }, ...]`. We harvest only the entry whose id is
2413
+ // "model" — other ids ("mode", "effort", etc.) are opencode-internal
2414
+ // and not consumed by hydra. Returns true when we recognized and
2415
+ // handled the notification so the wireAgent loop can stop trying
2416
+ // further extractors (the broadcast still fires; clients that grok
2417
+ // config_option_update render it directly).
2418
+ maybeApplyAgentConfigOption(params) {
2419
+ const obj = params ?? {};
2420
+ const update = obj.update ?? {};
2421
+ if (update.sessionUpdate !== "config_option_update") {
2422
+ return false;
2423
+ }
2424
+ const list = update.configOptions;
2425
+ if (!Array.isArray(list)) {
2426
+ return true;
2427
+ }
2428
+ for (const raw of list) {
2429
+ if (!raw || typeof raw !== "object") {
2430
+ continue;
2431
+ }
2432
+ const opt = raw;
2433
+ if (opt.id !== "model") {
2434
+ continue;
2435
+ }
2436
+ const models = parseModelsList(opt.options);
2437
+ if (models.length > 0) {
2438
+ this.setAgentAdvertisedModels(models);
2439
+ }
2440
+ const cv = opt.currentValue;
2441
+ if (typeof cv === "string") {
2442
+ const trimmed = cv.trim();
2443
+ if (trimmed && trimmed !== this.currentModel) {
2444
+ this.currentModel = trimmed;
2445
+ for (const handler of this.modelHandlers) {
2446
+ try {
2447
+ handler(trimmed);
2448
+ } catch {
2449
+ }
2450
+ }
2451
+ }
2452
+ }
2453
+ break;
2454
+ }
2455
+ return true;
2456
+ }
2278
2457
  maybeApplyAgentMode(params) {
2279
2458
  const obj = params ?? {};
2280
2459
  const update = obj.update ?? {};
@@ -2373,6 +2552,20 @@ var init_session = __esm({
2373
2552
  }
2374
2553
  this.broadcastAvailableModes();
2375
2554
  }
2555
+ setAgentAdvertisedModels(models) {
2556
+ if (sameAdvertisedModels(this.agentAdvertisedModels, models)) {
2557
+ this.broadcastAvailableModels();
2558
+ return;
2559
+ }
2560
+ this.agentAdvertisedModels = models;
2561
+ for (const handler of this.agentModelsHandlers) {
2562
+ try {
2563
+ handler(models);
2564
+ } catch {
2565
+ }
2566
+ }
2567
+ this.broadcastAvailableModels();
2568
+ }
2376
2569
  // Subscribe to snapshot-state updates. SessionManager wires these to
2377
2570
  // persist the new value into meta.json so cold resurrect can restore
2378
2571
  // them via the attach response _meta.
@@ -2382,6 +2575,9 @@ var init_session = __esm({
2382
2575
  onAgentModesChange(handler) {
2383
2576
  this.agentModesHandlers.push(handler);
2384
2577
  }
2578
+ onAgentModelsChange(handler) {
2579
+ this.agentModelsHandlers.push(handler);
2580
+ }
2385
2581
  onModelChange(handler) {
2386
2582
  this.modelHandlers.push(handler);
2387
2583
  }
@@ -2407,6 +2603,15 @@ var init_session = __esm({
2407
2603
  availableModes() {
2408
2604
  return [...this.agentAdvertisedModes];
2409
2605
  }
2606
+ // The agent's advertised models list. Used by acp-ws.ts's dedicated
2607
+ // session/set_model handler to validate the requested modelId before
2608
+ // forwarding to the agent (catches cross-agent set_model storms from
2609
+ // clients that assume a different agent is on the other end). When
2610
+ // the agent never advertised any models, returns [] and the
2611
+ // set_model handler falls back to pass-through.
2612
+ availableModels() {
2613
+ return [...this.agentAdvertisedModels];
2614
+ }
2410
2615
  // Pick up an agent-emitted session_info_update and store its title
2411
2616
  // as our canonical record. The notification is also forwarded to
2412
2617
  // clients via the surrounding recordAndBroadcast call. Authoritative
@@ -2554,6 +2759,12 @@ var init_session = __esm({
2554
2759
  this.agentMeta = fresh.agentMeta;
2555
2760
  this.agentAdvertisedCommands = [];
2556
2761
  this.broadcastMergedCommands();
2762
+ if (this.agentAdvertisedModels.length > 0) {
2763
+ this.setAgentAdvertisedModels([]);
2764
+ }
2765
+ if (this.agentAdvertisedModes.length > 0) {
2766
+ this.setAgentAdvertisedModes([]);
2767
+ }
2557
2768
  await oldAgent.kill().catch(() => void 0);
2558
2769
  if (transcript) {
2559
2770
  await this.runInternalPrompt(transcript).catch(() => void 0);
@@ -3120,11 +3331,12 @@ function recordFromMemorySession(args) {
3120
3331
  currentUsage: args.currentUsage,
3121
3332
  agentCommands: args.agentCommands,
3122
3333
  agentModes: args.agentModes,
3334
+ agentModels: args.agentModels,
3123
3335
  createdAt: args.createdAt ?? now,
3124
3336
  updatedAt: args.updatedAt ?? now
3125
3337
  };
3126
3338
  }
3127
- var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedAgentMode, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
3339
+ var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedAgentMode, PersistedAgentModel, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
3128
3340
  var init_session_store = __esm({
3129
3341
  "src/core/session-store.ts"() {
3130
3342
  "use strict";
@@ -3141,6 +3353,11 @@ var init_session_store = __esm({
3141
3353
  name: z4.string().optional(),
3142
3354
  description: z4.string().optional()
3143
3355
  });
3356
+ PersistedAgentModel = z4.object({
3357
+ modelId: z4.string(),
3358
+ name: z4.string().optional(),
3359
+ description: z4.string().optional()
3360
+ });
3144
3361
  PersistedUsage = z4.object({
3145
3362
  used: z4.number().optional(),
3146
3363
  size: z4.number().optional(),
@@ -3187,6 +3404,7 @@ var init_session_store = __esm({
3187
3404
  currentUsage: PersistedUsage.optional(),
3188
3405
  agentCommands: z4.array(PersistedAgentCommand).optional(),
3189
3406
  agentModes: z4.array(PersistedAgentMode).optional(),
3407
+ agentModels: z4.array(PersistedAgentModel).optional(),
3190
3408
  createdAt: z4.string(),
3191
3409
  updatedAt: z4.string()
3192
3410
  });
@@ -3682,8 +3900,55 @@ function mapToolCallUpdate(u) {
3682
3900
  if (status !== void 0) {
3683
3901
  event.status = status;
3684
3902
  }
3903
+ if (status === "failed") {
3904
+ const errorText = extractToolFailureText(u);
3905
+ if (errorText !== null) {
3906
+ event.errorText = errorText;
3907
+ }
3908
+ if (isUpstreamInterrupted(u, errorText)) {
3909
+ event.upstreamInterrupted = true;
3910
+ }
3911
+ }
3685
3912
  return event;
3686
3913
  }
3914
+ function extractToolFailureText(u) {
3915
+ const content = u.content;
3916
+ if (Array.isArray(content)) {
3917
+ for (const block of content) {
3918
+ if (!block || typeof block !== "object") {
3919
+ continue;
3920
+ }
3921
+ const b = block;
3922
+ const text = extractContentText(b.content);
3923
+ if (text !== null && text.length > 0) {
3924
+ return text;
3925
+ }
3926
+ }
3927
+ }
3928
+ const rawOutput = u.rawOutput;
3929
+ if (rawOutput && typeof rawOutput === "object") {
3930
+ const err = rawOutput.error;
3931
+ if (typeof err === "string" && err.length > 0) {
3932
+ return sanitizeWireText(err);
3933
+ }
3934
+ }
3935
+ return null;
3936
+ }
3937
+ function isUpstreamInterrupted(u, errorText) {
3938
+ const rawOutput = u.rawOutput;
3939
+ if (rawOutput && typeof rawOutput === "object") {
3940
+ const meta = rawOutput.metadata;
3941
+ if (meta && typeof meta === "object") {
3942
+ if (meta.interrupted === true) {
3943
+ return true;
3944
+ }
3945
+ }
3946
+ }
3947
+ if (errorText !== null && errorText.toLowerCase().includes("tool execution aborted")) {
3948
+ return true;
3949
+ }
3950
+ return false;
3951
+ }
3687
3952
  function mapPlan(u) {
3688
3953
  const entries = u.entries;
3689
3954
  if (!Array.isArray(entries)) {
@@ -5069,6 +5334,34 @@ async function killSession(config, serviceToken, id, fetchImpl = fetch) {
5069
5334
  throw new Error(`daemon returned HTTP ${response.status}`);
5070
5335
  }
5071
5336
  }
5337
+ async function renameSession(config, serviceToken, id, title, fetchImpl = fetch) {
5338
+ const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
5339
+ const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
5340
+ method: "PATCH",
5341
+ headers: {
5342
+ Authorization: `Bearer ${serviceToken}`,
5343
+ "Content-Type": "application/json"
5344
+ },
5345
+ body: JSON.stringify({ title })
5346
+ });
5347
+ if (!response.ok && response.status !== 204 && response.status !== 404) {
5348
+ throw new Error(`daemon returned HTTP ${response.status}`);
5349
+ }
5350
+ }
5351
+ async function regenSessionTitle(config, serviceToken, id, fetchImpl = fetch) {
5352
+ const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
5353
+ const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
5354
+ method: "PATCH",
5355
+ headers: {
5356
+ Authorization: `Bearer ${serviceToken}`,
5357
+ "Content-Type": "application/json"
5358
+ },
5359
+ body: JSON.stringify({ regen: true })
5360
+ });
5361
+ if (!response.ok && response.status !== 202 && response.status !== 204 && response.status !== 404 && response.status !== 409) {
5362
+ throw new Error(`daemon returned HTTP ${response.status}`);
5363
+ }
5364
+ }
5072
5365
  async function deleteSession(config, serviceToken, id, fetchImpl = fetch) {
5073
5366
  const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
5074
5367
  const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
@@ -5139,6 +5432,7 @@ async function pickSession(term, opts) {
5139
5432
  let cwdOnly = false;
5140
5433
  let mode = "normal";
5141
5434
  let pendingAction = null;
5435
+ let renameBuffer = "";
5142
5436
  let transientStatus = null;
5143
5437
  let termHeight = readTermHeight(term);
5144
5438
  let termWidth = readTermWidth(term);
@@ -5247,6 +5541,12 @@ async function pickSession(term, opts) {
5247
5541
  term.dim.noFormat(` working on ${shortId2(pendingAction.sessionId)}\u2026`);
5248
5542
  return;
5249
5543
  }
5544
+ if (mode === "rename" && pendingAction) {
5545
+ term.brightYellow.noFormat(` title: ${renameBuffer}`);
5546
+ term.bgBrightYellow(" ");
5547
+ term.dim.noFormat(" Enter saves \xB7 Esc cancels");
5548
+ return;
5549
+ }
5250
5550
  if (transientStatus !== null) {
5251
5551
  term.dim.noFormat(` ${transientStatus}`);
5252
5552
  return;
@@ -5364,6 +5664,37 @@ async function pickSession(term, opts) {
5364
5664
  renderFromScratch();
5365
5665
  }
5366
5666
  };
5667
+ const performRename = async (title) => {
5668
+ if (!pendingAction) {
5669
+ return;
5670
+ }
5671
+ const target = pendingAction;
5672
+ mode = "busy";
5673
+ paintIndicator();
5674
+ try {
5675
+ await renameSession(opts.config, opts.serviceToken, target.sessionId, title);
5676
+ mode = "normal";
5677
+ pendingAction = null;
5678
+ renameBuffer = "";
5679
+ await refresh(target.sessionId);
5680
+ } catch (err) {
5681
+ mode = "normal";
5682
+ pendingAction = null;
5683
+ renameBuffer = "";
5684
+ transientStatus = `rename failed: ${err.message}`;
5685
+ paintIndicator();
5686
+ }
5687
+ };
5688
+ const performRegen = async (target) => {
5689
+ try {
5690
+ await regenSessionTitle(opts.config, opts.serviceToken, target.sessionId);
5691
+ transientStatus = "title regen queued (press r to refresh)";
5692
+ paintIndicator();
5693
+ } catch (err) {
5694
+ transientStatus = `regen failed: ${err.message}`;
5695
+ paintIndicator();
5696
+ }
5697
+ };
5367
5698
  const performAction = async (kind) => {
5368
5699
  if (!pendingAction) {
5369
5700
  return;
@@ -5436,6 +5767,52 @@ async function pickSession(term, opts) {
5436
5767
  renderFromScratch();
5437
5768
  return;
5438
5769
  }
5770
+ if (mode === "rename") {
5771
+ if (name === "ENTER" || name === "KP_ENTER") {
5772
+ const trimmed = renameBuffer.trim();
5773
+ if (trimmed.length === 0) {
5774
+ mode = "normal";
5775
+ pendingAction = null;
5776
+ renameBuffer = "";
5777
+ paintIndicator();
5778
+ return;
5779
+ }
5780
+ void performRename(trimmed);
5781
+ return;
5782
+ }
5783
+ if (name === "ESCAPE" || name === "CTRL_C") {
5784
+ mode = "normal";
5785
+ pendingAction = null;
5786
+ renameBuffer = "";
5787
+ paintIndicator();
5788
+ return;
5789
+ }
5790
+ if (name === "BACKSPACE") {
5791
+ if (renameBuffer.length > 0) {
5792
+ renameBuffer = renameBuffer.slice(0, -1);
5793
+ paintIndicator();
5794
+ }
5795
+ return;
5796
+ }
5797
+ if (name === "CTRL_U") {
5798
+ renameBuffer = "";
5799
+ paintIndicator();
5800
+ return;
5801
+ }
5802
+ if (name === "CTRL_W") {
5803
+ const trimmedRight = renameBuffer.replace(/\s+$/, "");
5804
+ const lastSpace = trimmedRight.lastIndexOf(" ");
5805
+ renameBuffer = lastSpace >= 0 ? trimmedRight.slice(0, lastSpace) : "";
5806
+ paintIndicator();
5807
+ return;
5808
+ }
5809
+ if (data?.isCharacter) {
5810
+ renameBuffer += name;
5811
+ paintIndicator();
5812
+ return;
5813
+ }
5814
+ return;
5815
+ }
5439
5816
  if (mode === "confirm-kill" || mode === "confirm-delete") {
5440
5817
  if (data?.isCharacter && (name === "y" || name === "Y")) {
5441
5818
  const kind = mode === "confirm-kill" ? "kill" : "delete";
@@ -5538,6 +5915,29 @@ async function pickSession(term, opts) {
5538
5915
  paintIndicator();
5539
5916
  return;
5540
5917
  }
5918
+ if (name === "t" && selectedIdx > 0) {
5919
+ const session = visible[selectedIdx - 1];
5920
+ if (!session) {
5921
+ return;
5922
+ }
5923
+ pendingAction = {
5924
+ sessionId: session.sessionId,
5925
+ cwd: session.cwd,
5926
+ status: session.status
5927
+ };
5928
+ renameBuffer = session.title ?? "";
5929
+ mode = "rename";
5930
+ paintIndicator();
5931
+ return;
5932
+ }
5933
+ if (name === "T" && selectedIdx > 0) {
5934
+ const session = visible[selectedIdx - 1];
5935
+ if (!session || session.status !== "live") {
5936
+ return;
5937
+ }
5938
+ void performRegen({ sessionId: session.sessionId });
5939
+ return;
5940
+ }
5541
5941
  if ((name === "d" || name === "D") && selectedIdx > 0) {
5542
5942
  const session = visible[selectedIdx - 1];
5543
5943
  if (!session) {
@@ -5668,6 +6068,8 @@ var init_picker = __esm({
5668
6068
  null,
5669
6069
  ["k", "kill the selected live session"],
5670
6070
  ["d", "delete the selected cold session"],
6071
+ ["t", "retitle the selected session"],
6072
+ ["T", "regenerate title via agent (live session)"],
5671
6073
  null,
5672
6074
  ["c", "create new session"],
5673
6075
  ["?", "toggle this help"],
@@ -6589,6 +6991,42 @@ var init_screen = __esm({
6589
6991
  return;
6590
6992
  }
6591
6993
  }
6994
+ const csiUCtrlMap = {
6995
+ 97: "ctrl-a",
6996
+ 98: "ctrl-b",
6997
+ 99: "ctrl-c",
6998
+ 100: "ctrl-d",
6999
+ 101: "ctrl-e",
7000
+ 102: "ctrl-f",
7001
+ 103: "ctrl-g",
7002
+ 107: "ctrl-k",
7003
+ 108: "ctrl-l",
7004
+ 110: "ctrl-n",
7005
+ 111: "ctrl-o",
7006
+ 112: "ctrl-p",
7007
+ 114: "ctrl-r",
7008
+ 115: "ctrl-s",
7009
+ 116: "ctrl-t",
7010
+ 117: "ctrl-u",
7011
+ 118: "ctrl-v",
7012
+ 119: "ctrl-w",
7013
+ 121: "ctrl-y"
7014
+ };
7015
+ const csiUCtrlRe = /\x1b\[(\d+);5u/;
7016
+ const m = csiUCtrlRe.exec(text);
7017
+ if (m !== null) {
7018
+ const keyName = csiUCtrlMap[parseInt(m[1], 10)];
7019
+ if (keyName !== void 0) {
7020
+ const parts = text.split(m[0]);
7021
+ for (let i = 0; i < parts.length; i++) {
7022
+ if (parts[i].length > 0)
7023
+ this.handleRawStdin(Buffer.from(parts[i], "binary"));
7024
+ if (i < parts.length - 1)
7025
+ this.onKey([{ type: "key", name: keyName }]);
7026
+ }
7027
+ return;
7028
+ }
7029
+ }
6592
7030
  }
6593
7031
  this.handleRawStdinSegment(text);
6594
7032
  }
@@ -7886,11 +8324,16 @@ var init_screen = __esm({
7886
8324
  const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
7887
8325
  const right = this.bannerRightContent();
7888
8326
  const rightSig = right ? `${right.kind}|${right.text}` : "";
7889
- const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${this.banner.queued}|${this.scrollOffset}|${this.banner.currentMode ?? ""}|${this.banner.hint}|` + rightSig;
8327
+ const stalled = this.banner.status === "busy" && this.banner.stalled === true;
8328
+ const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${stalled ? "1" : "0"}|${this.banner.queued}|${this.scrollOffset}|${this.banner.currentMode ?? ""}|${this.banner.hint}|` + rightSig;
7890
8329
  this.paintRow(row, sig, () => {
7891
8330
  const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
7892
8331
  if (this.banner.status === "busy") {
7893
- this.term.brightYellow(`${dot} ${this.banner.status}`);
8332
+ if (stalled) {
8333
+ this.term.brightRed(`${dot} stalled`);
8334
+ } else {
8335
+ this.term.brightYellow(`${dot} ${this.banner.status}`);
8336
+ }
7894
8337
  if (elapsedStr) {
7895
8338
  this.term(" ").dim(elapsedStr);
7896
8339
  }
@@ -9267,8 +9710,29 @@ var init_completion = __esm({
9267
9710
  }
9268
9711
  });
9269
9712
 
9713
+ // src/tui/reconnect-state.ts
9714
+ function parseReattachResponse(result) {
9715
+ const out = {};
9716
+ if (!result || typeof result !== "object") {
9717
+ return out;
9718
+ }
9719
+ const r = result;
9720
+ if (typeof r.historyPolicy === "string") {
9721
+ out.appliedPolicy = r.historyPolicy;
9722
+ }
9723
+ if (typeof r.clientId === "string" && r.clientId.length > 0) {
9724
+ out.clientId = r.clientId;
9725
+ }
9726
+ return out;
9727
+ }
9728
+ var init_reconnect_state = __esm({
9729
+ "src/tui/reconnect-state.ts"() {
9730
+ "use strict";
9731
+ }
9732
+ });
9733
+
9270
9734
  // src/tui/format.ts
9271
- import chalk from "chalk";
9735
+ import chalk2 from "chalk";
9272
9736
  import { highlight, supportsLanguage } from "cli-highlight";
9273
9737
  function formatEvent(event) {
9274
9738
  switch (event.kind) {
@@ -9561,12 +10025,22 @@ function formatToolLine2(state) {
9561
10025
  } else {
9562
10026
  title = `${initial} \xB7 ${latest}`;
9563
10027
  }
9564
- return {
9565
- prefix: ` ${toolStatusIcon(state.status)} `,
9566
- prefixStyle: toolIconStyle(state.status),
9567
- body: title,
9568
- bodyStyle: toolStatusStyle(state.status)
9569
- };
10028
+ const lines = [
10029
+ {
10030
+ prefix: ` ${toolStatusIcon(state.status)} `,
10031
+ prefixStyle: toolIconStyle(state.status),
10032
+ body: title,
10033
+ bodyStyle: toolStatusStyle(state.status)
10034
+ }
10035
+ ];
10036
+ if (state.status === "failed" && state.errorText) {
10037
+ lines.push({
10038
+ prefix: " ",
10039
+ body: sanitizeSingleLine(state.errorText),
10040
+ bodyStyle: "tool-status-fail"
10041
+ });
10042
+ }
10043
+ return lines;
9570
10044
  }
9571
10045
  function toolStatusIcon(status) {
9572
10046
  switch (status) {
@@ -9665,7 +10139,8 @@ var highlightChalk, HIGHLIGHT_THEME;
9665
10139
  var init_format = __esm({
9666
10140
  "src/tui/format.ts"() {
9667
10141
  "use strict";
9668
- highlightChalk = new chalk.Instance({ level: 3 });
10142
+ init_render_update();
10143
+ highlightChalk = new chalk2.Instance({ level: 3 });
9669
10144
  HIGHLIGHT_THEME = {
9670
10145
  keyword: highlightChalk.blueBright,
9671
10146
  built_in: highlightChalk.cyan,
@@ -9727,8 +10202,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9727
10202
  term.grabInput(false);
9728
10203
  process.exit(0);
9729
10204
  }
9730
- const launchLabel = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
9731
- term.brightYellow(launchLabel)("\n");
10205
+ const launchLabelBase = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
10206
+ const installStatus = createInstallStatusLine(term, launchLabelBase);
10207
+ installStatus.write(launchLabelBase);
9732
10208
  const protocol = config.daemon.tls ? "wss" : "ws";
9733
10209
  const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
9734
10210
  const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
@@ -9754,6 +10230,13 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9754
10230
  });
9755
10231
  const conn = new JsonRpcConnection(stream);
9756
10232
  await stream.start();
10233
+ conn.onNotification(AGENT_INSTALL_PROGRESS_METHOD, (raw) => {
10234
+ const parsed = AgentInstallProgressParams.safeParse(raw);
10235
+ if (!parsed.success) {
10236
+ return;
10237
+ }
10238
+ installStatus.applyProgress(parsed.data);
10239
+ });
9757
10240
  let bufferedEvents = [];
9758
10241
  let applyRenderEvent = null;
9759
10242
  let teardownStarted = false;
@@ -9771,34 +10254,46 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9771
10254
  let currentHeadMessageId;
9772
10255
  let sessionBusySince = null;
9773
10256
  let sessionElapsedTimer = null;
10257
+ let lastUpdateAt = null;
10258
+ let upstreamInterruptedSeen = false;
9774
10259
  const adjustPendingTurns = (delta) => {
9775
10260
  const before = pendingTurns;
9776
10261
  pendingTurns = Math.max(0, pendingTurns + delta);
9777
10262
  const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
9778
10263
  if (before === 0 && pendingTurns > 0) {
9779
10264
  sessionBusySince = Date.now();
10265
+ lastUpdateAt = Date.now();
9780
10266
  dispatcherRef?.setTurnRunning(true);
9781
10267
  if (screenReady) {
9782
- screenRef.setBanner({ status: "busy", elapsedMs: 0 });
10268
+ screenRef.setBanner({ status: "busy", elapsedMs: 0, stalled: false });
9783
10269
  }
9784
10270
  if (sessionElapsedTimer === null && screenReady) {
9785
10271
  sessionElapsedTimer = setInterval(() => {
9786
10272
  if (sessionBusySince === null || screenRef === null) {
9787
10273
  return;
9788
10274
  }
9789
- screenRef.setBanner({ elapsedMs: Date.now() - sessionBusySince });
10275
+ const idleMs = lastUpdateAt === null ? 0 : Date.now() - lastUpdateAt;
10276
+ screenRef.setBanner({
10277
+ elapsedMs: Date.now() - sessionBusySince,
10278
+ stalled: idleMs >= STALL_THRESHOLD_MS
10279
+ });
9790
10280
  renderToolsBlock();
9791
10281
  }, 1e3);
9792
10282
  }
9793
10283
  } else if (before > 0 && pendingTurns === 0) {
9794
10284
  sessionBusySince = null;
10285
+ lastUpdateAt = null;
9795
10286
  dispatcherRef?.setTurnRunning(false);
9796
10287
  if (sessionElapsedTimer !== null) {
9797
10288
  clearInterval(sessionElapsedTimer);
9798
10289
  sessionElapsedTimer = null;
9799
10290
  }
9800
10291
  if (screenReady) {
9801
- screenRef.setBanner({ status: "ready", elapsedMs: void 0 });
10292
+ screenRef.setBanner({
10293
+ status: "ready",
10294
+ elapsedMs: void 0,
10295
+ stalled: false
10296
+ });
9802
10297
  }
9803
10298
  }
9804
10299
  void delta;
@@ -9819,6 +10314,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
9819
10314
  const { update } = params ?? {};
9820
10315
  const event = mapUpdate(update);
9821
10316
  debugLogUpdate(update, event);
10317
+ lastUpdateAt = Date.now();
9822
10318
  const rawTag = update?.sessionUpdate;
9823
10319
  if (typeof rawTag === "string" && !STATE_UPDATE_KINDS2.has(rawTag)) {
9824
10320
  const u = update ?? {};
@@ -10388,6 +10884,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
10388
10884
  };
10389
10885
  const sessionbarAgent = resolvedAgentId || agentInfoName || "?";
10390
10886
  const usage = { ...initialUsage ?? {} };
10887
+ installStatus.finalize();
10391
10888
  screen.start();
10392
10889
  screen.setSessionbar({
10393
10890
  agent: sessionbarAgent,
@@ -11270,7 +11767,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11270
11767
  for (const id of visibleIds) {
11271
11768
  const state = toolStates.get(id);
11272
11769
  if (state) {
11273
- lines.push(formatToolLine2(state));
11770
+ lines.push(...formatToolLine2(state));
11274
11771
  }
11275
11772
  }
11276
11773
  screen.upsertLines("tools", lines);
@@ -11281,7 +11778,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11281
11778
  toolsBlockStopReason = null;
11282
11779
  renderToolsBlock();
11283
11780
  };
11284
- const recordToolCall = (id, title, status) => {
11781
+ const recordToolCall = (id, title, status, errorText) => {
11285
11782
  const wasNew = !toolStates.has(id);
11286
11783
  const existing = toolStates.get(id);
11287
11784
  const state = existing ?? {
@@ -11298,6 +11795,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11298
11795
  if (!existing) {
11299
11796
  state.status = status ?? "pending";
11300
11797
  }
11798
+ if (errorText !== void 0) {
11799
+ state.errorText = errorText;
11800
+ }
11301
11801
  toolStates.set(id, state);
11302
11802
  if (wasNew) {
11303
11803
  if (toolsBlockStartedAt === null) {
@@ -11389,7 +11889,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11389
11889
  }
11390
11890
  if (event.kind === "tool-call") {
11391
11891
  closeAgentText();
11392
- recordToolCall(event.toolCallId, event.title, event.status);
11892
+ recordToolCall(event.toolCallId, event.title, event.status, void 0);
11393
11893
  renderToolsBlock();
11394
11894
  return;
11395
11895
  }
@@ -11404,7 +11904,15 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11404
11904
  }
11405
11905
  if (event.kind === "tool-call-update") {
11406
11906
  closeAgentText();
11407
- recordToolCall(event.toolCallId, event.title, event.status);
11907
+ recordToolCall(
11908
+ event.toolCallId,
11909
+ event.title,
11910
+ event.status,
11911
+ event.errorText
11912
+ );
11913
+ if (event.upstreamInterrupted) {
11914
+ upstreamInterruptedSeen = true;
11915
+ }
11408
11916
  renderToolsBlock();
11409
11917
  return;
11410
11918
  }
@@ -11418,7 +11926,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11418
11926
  if (event.kind === "turn-complete") {
11419
11927
  currentHeadMessageId = void 0;
11420
11928
  closeAgentText();
11421
- const effectiveStopReason = event.amended ? "amended" : event.stopReason;
11929
+ let effectiveStopReason = event.amended ? "amended" : event.stopReason;
11930
+ if (!event.amended && upstreamInterruptedSeen && (effectiveStopReason === void 0 || effectiveStopReason === "end_turn")) {
11931
+ effectiveStopReason = "error";
11932
+ }
11422
11933
  if (lastPlanEvent !== null && effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
11423
11934
  const lines = formatEvent({ ...lastPlanEvent, stopped: true });
11424
11935
  if (lines.length > 0) {
@@ -11448,6 +11959,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11448
11959
  toolsBlockEndedAt = null;
11449
11960
  toolsBlockStopReason = null;
11450
11961
  toolsExpanded = false;
11962
+ upstreamInterruptedSeen = false;
11451
11963
  screen.ensureSeparator();
11452
11964
  }
11453
11965
  };
@@ -11557,9 +12069,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
11557
12069
  if (resp.error) {
11558
12070
  throw new Error(resp.error.message);
11559
12071
  }
11560
- const result = resp.result ?? {};
11561
- if (typeof result.historyPolicy === "string") {
11562
- appliedPolicy = result.historyPolicy;
12072
+ const fields = parseReattachResponse(resp.result);
12073
+ appliedPolicy = fields.appliedPolicy;
12074
+ if (fields.clientId !== void 0) {
12075
+ ownClientId = fields.clientId;
11563
12076
  }
11564
12077
  } catch (err) {
11565
12078
  attachErr = err;
@@ -11685,6 +12198,95 @@ function writeDebugLine(payload) {
11685
12198
  } catch {
11686
12199
  }
11687
12200
  }
12201
+ function createInstallStatusLine(term, baseLabel) {
12202
+ let finalized = false;
12203
+ let lastText = "";
12204
+ let osc94Active = false;
12205
+ const writeOsc94 = (state) => {
12206
+ if (finalized) {
12207
+ return;
12208
+ }
12209
+ if (state === 3 && osc94Active) {
12210
+ return;
12211
+ }
12212
+ if (state === 0 && !osc94Active) {
12213
+ return;
12214
+ }
12215
+ osc94Active = state === 3;
12216
+ process.stdout.write(`\x1B]9;4;${state}\x1B\\`);
12217
+ };
12218
+ const redraw = (text) => {
12219
+ if (finalized) {
12220
+ return;
12221
+ }
12222
+ process.stdout.write("\r");
12223
+ term.eraseLineAfter();
12224
+ term.brightYellow(text);
12225
+ lastText = text;
12226
+ };
12227
+ const formatProgressText = (event) => {
12228
+ const idVer = `${event.agentId}@${event.version}`;
12229
+ if (event.source === "npm") {
12230
+ if (event.phase === "install_start" || event.phase === "download_start") {
12231
+ return `${baseLabel} installing ${idVer} via npm\u2026`;
12232
+ }
12233
+ if (event.phase === "installed") {
12234
+ return `${baseLabel} ${idVer} installed`;
12235
+ }
12236
+ return `${baseLabel} installing ${idVer} via npm\u2026`;
12237
+ }
12238
+ if (event.phase === "download_start" || event.phase === "download_progress") {
12239
+ const received = event.receivedBytes ?? 0;
12240
+ const total = event.totalBytes ?? 0;
12241
+ const rxMb = (received / 1e6).toFixed(1);
12242
+ if (total > 0) {
12243
+ const totalMb = (total / 1e6).toFixed(1);
12244
+ const pct = Math.min(100, Math.floor(received / total * 100));
12245
+ return `${baseLabel} downloading ${idVer} ${rxMb}/${totalMb} MB (${pct}%)`;
12246
+ }
12247
+ return `${baseLabel} downloading ${idVer} ${rxMb} MB`;
12248
+ }
12249
+ if (event.phase === "download_done") {
12250
+ return `${baseLabel} downloaded ${idVer}, verifying\u2026`;
12251
+ }
12252
+ if (event.phase === "extract") {
12253
+ return `${baseLabel} extracting ${idVer}\u2026`;
12254
+ }
12255
+ if (event.phase === "installed") {
12256
+ return `${baseLabel} ${idVer} installed`;
12257
+ }
12258
+ return lastText || baseLabel;
12259
+ };
12260
+ return {
12261
+ write(text) {
12262
+ if (finalized) {
12263
+ return;
12264
+ }
12265
+ term.brightYellow(text);
12266
+ lastText = text;
12267
+ },
12268
+ applyProgress(event) {
12269
+ if (finalized) {
12270
+ return;
12271
+ }
12272
+ const isActive = event.phase === "download_start" || event.phase === "download_progress" || event.phase === "install_start" || event.phase === "extract" || event.phase === "download_done";
12273
+ if (isActive) {
12274
+ writeOsc94(3);
12275
+ } else if (event.phase === "installed") {
12276
+ writeOsc94(0);
12277
+ }
12278
+ redraw(formatProgressText(event));
12279
+ },
12280
+ finalize() {
12281
+ if (finalized) {
12282
+ return;
12283
+ }
12284
+ finalized = true;
12285
+ writeOsc94(0);
12286
+ process.stdout.write("\n");
12287
+ }
12288
+ };
12289
+ }
11688
12290
  function rotateIfBig(target) {
11689
12291
  try {
11690
12292
  const stat4 = statSync(target);
@@ -11695,7 +12297,7 @@ function rotateIfBig(target) {
11695
12297
  } catch {
11696
12298
  }
11697
12299
  }
11698
- var HELP_ENTRIES_TAIL, logMaxBytes;
12300
+ var STALL_THRESHOLD_MS, HELP_ENTRIES_TAIL, logMaxBytes;
11699
12301
  var init_app = __esm({
11700
12302
  "src/tui/app.ts"() {
11701
12303
  "use strict";
@@ -11717,8 +12319,10 @@ var init_app = __esm({
11717
12319
  init_attachments();
11718
12320
  init_clipboard();
11719
12321
  init_completion();
12322
+ init_reconnect_state();
11720
12323
  init_render_update();
11721
12324
  init_format();
12325
+ STALL_THRESHOLD_MS = 12e4;
11722
12326
  HELP_ENTRIES_TAIL = [
11723
12327
  ["Alt+Enter", "newline in prompt"],
11724
12328
  ["Shift+Tab", "cycle agent modes (plan / accept-edits / etc.)"],
@@ -11880,6 +12484,7 @@ init_config();
11880
12484
  init_service_token();
11881
12485
  import * as fsp7 from "fs/promises";
11882
12486
  import { setTimeout as sleep2 } from "timers/promises";
12487
+ import chalk from "chalk";
11883
12488
 
11884
12489
  // src/daemon/server.ts
11885
12490
  init_config();
@@ -11948,8 +12553,10 @@ async function ensureBinary(args) {
11948
12553
  }
11949
12554
  await downloadAndExtract({
11950
12555
  agentId: args.agentId,
12556
+ version: args.version,
11951
12557
  archiveUrl: args.target.archive,
11952
- installDir
12558
+ installDir,
12559
+ onProgress: args.onProgress
11953
12560
  });
11954
12561
  if (!await fileExists(cmdPath)) {
11955
12562
  throw new Error(
@@ -11969,9 +12576,16 @@ async function downloadAndExtract(args) {
11969
12576
  const archivePath = await downloadTo({
11970
12577
  url: args.archiveUrl,
11971
12578
  dir: tempDir,
11972
- agentId: args.agentId
12579
+ agentId: args.agentId,
12580
+ version: args.version,
12581
+ onProgress: args.onProgress
11973
12582
  });
11974
12583
  logSink(`hydra-acp: extracting ${args.agentId}`);
12584
+ safeEmit(args.onProgress, {
12585
+ phase: "extract",
12586
+ agentId: args.agentId,
12587
+ version: args.version
12588
+ });
11975
12589
  await extract(archivePath, tempDir);
11976
12590
  await fsp.unlink(archivePath).catch(() => void 0);
11977
12591
  try {
@@ -11982,16 +12596,35 @@ async function downloadAndExtract(args) {
11982
12596
  await fsp.rm(tempDir, { recursive: true, force: true }).catch(
11983
12597
  () => void 0
11984
12598
  );
12599
+ safeEmit(args.onProgress, {
12600
+ phase: "installed",
12601
+ agentId: args.agentId,
12602
+ version: args.version
12603
+ });
11985
12604
  return;
11986
12605
  }
11987
12606
  throw err;
11988
12607
  }
11989
12608
  logSink(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
12609
+ safeEmit(args.onProgress, {
12610
+ phase: "installed",
12611
+ agentId: args.agentId,
12612
+ version: args.version
12613
+ });
11990
12614
  } catch (err) {
11991
12615
  await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
11992
12616
  throw err;
11993
12617
  }
11994
12618
  }
12619
+ function safeEmit(cb, event) {
12620
+ if (!cb) {
12621
+ return;
12622
+ }
12623
+ try {
12624
+ cb(event);
12625
+ } catch {
12626
+ }
12627
+ }
11995
12628
  async function downloadTo(args) {
11996
12629
  const filename = inferArchiveName(args.url);
11997
12630
  const dest = path2.join(args.dir, filename);
@@ -12004,17 +12637,34 @@ async function downloadTo(args) {
12004
12637
  const total = Number(response.headers.get("content-length") ?? "0");
12005
12638
  const out = fs4.createWriteStream(dest);
12006
12639
  const nodeStream = Readable.fromWeb(response.body);
12640
+ safeEmit(args.onProgress, {
12641
+ phase: "download_start",
12642
+ agentId: args.agentId,
12643
+ version: args.version,
12644
+ totalBytes: total
12645
+ });
12007
12646
  let received = 0;
12008
- let lastEmit = Date.now();
12009
- const EMIT_INTERVAL_MS = 2e3;
12647
+ let lastLogEmit = Date.now();
12648
+ let lastCbEmit = 0;
12649
+ const LOG_INTERVAL_MS = 2e3;
12650
+ const CB_INTERVAL_MS = 150;
12010
12651
  nodeStream.on("data", (chunk) => {
12011
12652
  received += chunk.length;
12012
12653
  const now = Date.now();
12013
- if (now - lastEmit < EMIT_INTERVAL_MS) {
12014
- return;
12654
+ if (now - lastCbEmit >= CB_INTERVAL_MS) {
12655
+ lastCbEmit = now;
12656
+ safeEmit(args.onProgress, {
12657
+ phase: "download_progress",
12658
+ agentId: args.agentId,
12659
+ version: args.version,
12660
+ receivedBytes: received,
12661
+ totalBytes: total
12662
+ });
12663
+ }
12664
+ if (now - lastLogEmit >= LOG_INTERVAL_MS) {
12665
+ lastLogEmit = now;
12666
+ logSink(formatProgress(args.agentId, received, total));
12015
12667
  }
12016
- lastEmit = now;
12017
- logSink(formatProgress(args.agentId, received, total));
12018
12668
  });
12019
12669
  await new Promise((resolve5, reject) => {
12020
12670
  nodeStream.on("error", reject);
@@ -12029,6 +12679,13 @@ async function downloadTo(args) {
12029
12679
  /* done */
12030
12680
  true
12031
12681
  ));
12682
+ safeEmit(args.onProgress, {
12683
+ phase: "download_done",
12684
+ agentId: args.agentId,
12685
+ version: args.version,
12686
+ receivedBytes: received,
12687
+ totalBytes: total
12688
+ });
12032
12689
  return dest;
12033
12690
  }
12034
12691
  function formatProgress(agentId, received, total, done = false) {
@@ -12128,9 +12785,11 @@ async function ensureNpmPackage(args) {
12128
12785
  }
12129
12786
  await installInto({
12130
12787
  agentId: args.agentId,
12788
+ version: args.version,
12131
12789
  packageSpec: args.packageSpec,
12132
12790
  installDir,
12133
- registry: args.registry
12791
+ registry: args.registry,
12792
+ onProgress: args.onProgress
12134
12793
  });
12135
12794
  if (!await fileExists2(binPath)) {
12136
12795
  throw new Error(
@@ -12146,6 +12805,12 @@ async function installInto(args) {
12146
12805
  logSink2(
12147
12806
  `hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
12148
12807
  );
12808
+ safeEmit2(args.onProgress, {
12809
+ phase: "install_start",
12810
+ agentId: args.agentId,
12811
+ version: args.version,
12812
+ packageSpec: args.packageSpec
12813
+ });
12149
12814
  await runNpmInstall({
12150
12815
  packageSpec: args.packageSpec,
12151
12816
  cwd: tempDir,
@@ -12159,11 +12824,21 @@ async function installInto(args) {
12159
12824
  await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
12160
12825
  () => void 0
12161
12826
  );
12827
+ safeEmit2(args.onProgress, {
12828
+ phase: "installed",
12829
+ agentId: args.agentId,
12830
+ version: args.version
12831
+ });
12162
12832
  return;
12163
12833
  }
12164
12834
  throw err;
12165
12835
  }
12166
12836
  logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
12837
+ safeEmit2(args.onProgress, {
12838
+ phase: "installed",
12839
+ agentId: args.agentId,
12840
+ version: args.version
12841
+ });
12167
12842
  } catch (err) {
12168
12843
  await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
12169
12844
  () => void 0
@@ -12171,44 +12846,87 @@ async function installInto(args) {
12171
12846
  throw err;
12172
12847
  }
12173
12848
  }
12849
+ function safeEmit2(cb, event) {
12850
+ if (!cb) {
12851
+ return;
12852
+ }
12853
+ try {
12854
+ cb(event);
12855
+ } catch {
12856
+ }
12857
+ }
12858
+ var ETXTBSY_RETRIES = 5;
12859
+ var ETXTBSY_BACKOFF_MS = 25;
12174
12860
  function runNpmInstall(args) {
12175
- return new Promise((resolve5, reject) => {
12176
- const registryArgs = args.registry ? ["--registry", args.registry] : [];
12177
- const child = spawn2(
12178
- "npm",
12179
- ["install", "--no-audit", "--no-fund", "--silent", ...registryArgs, args.packageSpec],
12180
- {
12181
- cwd: args.cwd,
12182
- stdio: ["ignore", "pipe", "pipe"]
12183
- }
12184
- );
12185
- let stderrTail = "";
12186
- child.stdout?.on("data", (chunk) => {
12187
- void chunk;
12188
- });
12189
- child.stderr?.setEncoding("utf8");
12190
- child.stderr?.on("data", (chunk) => {
12191
- stderrTail = (stderrTail + chunk).slice(-4096);
12192
- });
12193
- child.on("error", (err) => {
12194
- const msg = err.code === "ENOENT" ? `npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)` : err.message;
12195
- reject(new Error(msg));
12196
- });
12197
- child.on("exit", (code, signal) => {
12198
- if (code === 0) {
12199
- resolve5();
12861
+ return runNpmInstallOnce(args, 0);
12862
+ }
12863
+ async function runNpmInstallOnce(args, attempt) {
12864
+ try {
12865
+ await new Promise((resolve5, reject) => {
12866
+ const registryArgs = args.registry ? ["--registry", args.registry] : [];
12867
+ let child;
12868
+ try {
12869
+ child = spawn2(
12870
+ "npm",
12871
+ [
12872
+ "install",
12873
+ "--no-audit",
12874
+ "--no-fund",
12875
+ "--silent",
12876
+ ...registryArgs,
12877
+ args.packageSpec
12878
+ ],
12879
+ { cwd: args.cwd, stdio: ["ignore", "pipe", "pipe"] }
12880
+ );
12881
+ } catch (err) {
12882
+ reject(err);
12200
12883
  return;
12201
12884
  }
12202
- const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
12203
- const tail = stderrTail.trim();
12204
- reject(
12205
- new Error(
12206
- tail ? `npm install ${args.packageSpec} failed (${reason})
12885
+ let stderrTail = "";
12886
+ child.stdout?.on("data", (chunk) => {
12887
+ void chunk;
12888
+ });
12889
+ child.stderr?.setEncoding("utf8");
12890
+ child.stderr?.on("data", (chunk) => {
12891
+ stderrTail = (stderrTail + chunk).slice(-4096);
12892
+ });
12893
+ child.on("error", (err) => {
12894
+ const e = err;
12895
+ if (e.code === "ENOENT") {
12896
+ reject(
12897
+ new Error(
12898
+ `npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)`
12899
+ )
12900
+ );
12901
+ return;
12902
+ }
12903
+ reject(err);
12904
+ });
12905
+ child.on("exit", (code, signal) => {
12906
+ if (code === 0) {
12907
+ resolve5();
12908
+ return;
12909
+ }
12910
+ const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
12911
+ const tail = stderrTail.trim();
12912
+ reject(
12913
+ new Error(
12914
+ tail ? `npm install ${args.packageSpec} failed (${reason})
12207
12915
  stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
12208
- )
12209
- );
12916
+ )
12917
+ );
12918
+ });
12210
12919
  });
12211
- });
12920
+ } catch (err) {
12921
+ const code = err.code;
12922
+ if (code === "ETXTBSY" && attempt < ETXTBSY_RETRIES) {
12923
+ await new Promise(
12924
+ (r) => setTimeout(r, ETXTBSY_BACKOFF_MS * (attempt + 1))
12925
+ );
12926
+ return runNpmInstallOnce(args, attempt + 1);
12927
+ }
12928
+ throw err;
12929
+ }
12212
12930
  }
12213
12931
  async function fileExists2(p) {
12214
12932
  try {
@@ -12406,12 +13124,14 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
12406
13124
  };
12407
13125
  }
12408
13126
  const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
13127
+ const npmCb = options.onInstallProgress;
12409
13128
  const binPath = await ensureNpmPackage({
12410
13129
  agentId: agent.id,
12411
13130
  version,
12412
13131
  packageSpec: npx.package,
12413
13132
  bin,
12414
- registry: options.npmRegistry
13133
+ registry: options.npmRegistry,
13134
+ onProgress: npmCb ? (e) => npmCb({ source: "npm", ...e }) : void 0
12415
13135
  });
12416
13136
  return {
12417
13137
  command: binPath,
@@ -12427,10 +13147,12 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
12427
13147
  `Agent ${agent.id} has no binary distribution for ${currentPlatformKey() ?? "this platform"}.`
12428
13148
  );
12429
13149
  }
13150
+ const binCb = options.onInstallProgress;
12430
13151
  const cmdPath = await ensureBinary({
12431
13152
  agentId: agent.id,
12432
13153
  version,
12433
- target
13154
+ target,
13155
+ onProgress: binCb ? (e) => binCb({ source: "binary", ...e }) : void 0
12434
13156
  });
12435
13157
  const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
12436
13158
  return {
@@ -12848,7 +13570,8 @@ var SessionManager = class {
12848
13570
  cwd: params.cwd,
12849
13571
  agentArgs: params.agentArgs,
12850
13572
  mcpServers: params.mcpServers,
12851
- model: params.model
13573
+ model: params.model,
13574
+ onInstallProgress: params.onInstallProgress
12852
13575
  });
12853
13576
  const session = new Session({
12854
13577
  cwd: params.cwd,
@@ -12865,7 +13588,8 @@ var SessionManager = class {
12865
13588
  historyMaxEntries: this.sessionHistoryMaxEntries,
12866
13589
  currentModel: fresh.initialModel,
12867
13590
  currentMode: fresh.initialMode,
12868
- agentModes: fresh.initialModes
13591
+ agentModes: fresh.initialModes,
13592
+ agentModels: fresh.initialModels
12869
13593
  });
12870
13594
  await this.attachManagerHooks(session);
12871
13595
  return session;
@@ -12910,7 +13634,10 @@ var SessionManager = class {
12910
13634
  if (params.upstreamSessionId === "") {
12911
13635
  return this.doResurrectFromImport(params);
12912
13636
  }
12913
- const plan = await planSpawn(agentDef, params.agentArgs ?? [], { npmRegistry: this.npmRegistry });
13637
+ const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
13638
+ npmRegistry: this.npmRegistry,
13639
+ onInstallProgress: params.onInstallProgress
13640
+ });
12914
13641
  const agent = this.spawner({
12915
13642
  agentId: params.agentId,
12916
13643
  cwd: params.cwd,
@@ -12968,6 +13695,7 @@ var SessionManager = class {
12968
13695
  currentUsage: params.currentUsage,
12969
13696
  agentCommands: params.agentCommands,
12970
13697
  agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
13698
+ agentModels: params.agentModels ?? nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})),
12971
13699
  // Only gate the first-prompt title heuristic when we actually have
12972
13700
  // a title to preserve. A title-less session (lost to a write race
12973
13701
  // or never seeded) should re-derive from the next prompt rather
@@ -12991,7 +13719,8 @@ var SessionManager = class {
12991
13719
  agentId: params.agentId,
12992
13720
  cwd,
12993
13721
  agentArgs: params.agentArgs,
12994
- mcpServers: []
13722
+ mcpServers: [],
13723
+ onInstallProgress: params.onInstallProgress
12995
13724
  });
12996
13725
  const session = new Session({
12997
13726
  sessionId: params.hydraSessionId,
@@ -13014,6 +13743,7 @@ var SessionManager = class {
13014
13743
  currentUsage: params.currentUsage,
13015
13744
  agentCommands: params.agentCommands,
13016
13745
  agentModes: params.agentModes ?? fresh.initialModes,
13746
+ agentModels: params.agentModels ?? fresh.initialModels,
13017
13747
  firstPromptSeeded: !!params.title,
13018
13748
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
13019
13749
  });
@@ -13043,7 +13773,10 @@ var SessionManager = class {
13043
13773
  err.code = JsonRpcErrorCodes.AgentNotInstalled;
13044
13774
  throw err;
13045
13775
  }
13046
- const plan = await planSpawn(agentDef, params.agentArgs ?? [], { npmRegistry: this.npmRegistry });
13776
+ const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
13777
+ npmRegistry: this.npmRegistry,
13778
+ onInstallProgress: params.onInstallProgress
13779
+ });
13047
13780
  const agent = this.spawner({
13048
13781
  agentId: params.agentId,
13049
13782
  cwd: params.cwd,
@@ -13069,15 +13802,25 @@ var SessionManager = class {
13069
13802
  );
13070
13803
  }
13071
13804
  let initialModel = extractInitialModel(newResult);
13805
+ const initialModels = extractInitialModels(newResult);
13072
13806
  const desired = params.model ?? this.defaultModels[params.agentId];
13073
13807
  if (desired && desired !== initialModel) {
13074
- try {
13075
- await agent.connection.request("session/set_model", {
13076
- sessionId: sessionIdRaw,
13077
- modelId: desired
13078
- });
13079
- initialModel = desired;
13080
- } catch {
13808
+ const validates = initialModels.length === 0 || initialModels.some((m) => m.modelId === desired);
13809
+ if (validates) {
13810
+ try {
13811
+ await agent.connection.request("session/set_model", {
13812
+ sessionId: sessionIdRaw,
13813
+ modelId: desired
13814
+ });
13815
+ initialModel = desired;
13816
+ } catch {
13817
+ }
13818
+ } else {
13819
+ const known = initialModels.map((m) => m.modelId).join(", ");
13820
+ process.stderr.write(
13821
+ `hydra-acp: defaultModels[${params.agentId}]=${JSON.stringify(desired)} is not in the agent's availableModels (${known}); skipping session/set_model
13822
+ `
13823
+ );
13081
13824
  }
13082
13825
  }
13083
13826
  const initialModes = extractInitialModes(newResult);
@@ -13087,6 +13830,7 @@ var SessionManager = class {
13087
13830
  upstreamSessionId: sessionIdRaw,
13088
13831
  agentMeta: newResult._meta,
13089
13832
  initialModel,
13833
+ initialModels: initialModels.length > 0 ? initialModels : void 0,
13090
13834
  initialModes: initialModes.length > 0 ? initialModes : void 0,
13091
13835
  initialMode
13092
13836
  };
@@ -13149,6 +13893,15 @@ var SessionManager = class {
13149
13893
  }))
13150
13894
  }).catch(() => void 0);
13151
13895
  });
13896
+ session.onAgentModelsChange((models) => {
13897
+ void this.persistSnapshot(session.sessionId, {
13898
+ agentModels: models.map((m) => ({
13899
+ modelId: m.modelId,
13900
+ ...m.name !== void 0 ? { name: m.name } : {},
13901
+ ...m.description !== void 0 ? { description: m.description } : {}
13902
+ }))
13903
+ }).catch(() => void 0);
13904
+ });
13152
13905
  this.sessions.set(session.sessionId, session);
13153
13906
  await this.enqueueMetaWrite(session.sessionId, async () => {
13154
13907
  const existing = await this.store.read(session.sessionId);
@@ -13192,6 +13945,7 @@ var SessionManager = class {
13192
13945
  currentUsage: persistedUsageToSnapshot(record.currentUsage),
13193
13946
  agentCommands: record.agentCommands,
13194
13947
  agentModes: record.agentModes,
13948
+ agentModels: record.agentModels,
13195
13949
  createdAt: record.createdAt
13196
13950
  };
13197
13951
  }
@@ -13435,6 +14189,26 @@ var SessionManager = class {
13435
14189
  const record = await this.store.read(sessionId).catch(() => void 0);
13436
14190
  return record !== void 0;
13437
14191
  }
14192
+ // Public retitle entry point that works on live AND cold sessions.
14193
+ // - Live: routes through Session.retitle so attached clients receive
14194
+ // a session_info_update broadcast (and persistTitle fires from the
14195
+ // onTitleChange handler, just like /hydra title).
14196
+ // - Cold: writes the new title straight into meta.json — there's
14197
+ // nothing in memory to broadcast to, but a later resurrect / list
14198
+ // will pick up the new title.
14199
+ // Returns false when no record exists at all (live or on disk).
14200
+ async setTitle(sessionId, title) {
14201
+ const live = this.get(sessionId);
14202
+ if (live) {
14203
+ await live.retitle(title);
14204
+ return true;
14205
+ }
14206
+ if (!await this.hasRecord(sessionId)) {
14207
+ return false;
14208
+ }
14209
+ await this.persistTitle(sessionId, title);
14210
+ return true;
14211
+ }
13438
14212
  // Persist a title update from Session.setTitle. The on-disk record
13439
14213
  // was written at create time; updating it here keeps the session
13440
14214
  // record's title in sync with what was broadcast to clients so a
@@ -13487,6 +14261,7 @@ var SessionManager = class {
13487
14261
  ...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
13488
14262
  ...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
13489
14263
  ...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
14264
+ ...update.agentModels !== void 0 ? { agentModels: update.agentModels } : {},
13490
14265
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13491
14266
  });
13492
14267
  });
@@ -13588,6 +14363,18 @@ function mergeForPersistence(session, existing) {
13588
14363
  return out;
13589
14364
  }) : void 0;
13590
14365
  const agentModes = persistedModes ?? existing?.agentModes;
14366
+ const sessionModels = session.availableModels();
14367
+ const persistedModels = sessionModels.length > 0 ? sessionModels.map((m) => {
14368
+ const out = { modelId: m.modelId };
14369
+ if (m.name !== void 0) {
14370
+ out.name = m.name;
14371
+ }
14372
+ if (m.description !== void 0) {
14373
+ out.description = m.description;
14374
+ }
14375
+ return out;
14376
+ }) : void 0;
14377
+ const agentModels = persistedModels ?? existing?.agentModels;
13591
14378
  return recordFromMemorySession({
13592
14379
  sessionId: session.sessionId,
13593
14380
  lineageId: existing?.lineageId ?? generateLineageId(),
@@ -13604,6 +14391,7 @@ function mergeForPersistence(session, existing) {
13604
14391
  currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
13605
14392
  agentCommands,
13606
14393
  agentModes,
14394
+ agentModels,
13607
14395
  createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
13608
14396
  });
13609
14397
  }
@@ -13669,6 +14457,40 @@ function asString(value) {
13669
14457
  function nonEmptyOrUndefined(arr) {
13670
14458
  return arr.length > 0 ? arr : void 0;
13671
14459
  }
14460
+ function extractInitialModels(result) {
14461
+ const direct = parseModelsList(result.availableModels);
14462
+ if (direct.length > 0) {
14463
+ return direct;
14464
+ }
14465
+ const models = result.models;
14466
+ if (models && typeof models === "object" && !Array.isArray(models)) {
14467
+ const fromModelsObj = parseModelsList(
14468
+ models.availableModels
14469
+ );
14470
+ if (fromModelsObj.length > 0) {
14471
+ return fromModelsObj;
14472
+ }
14473
+ }
14474
+ const meta = result._meta;
14475
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
14476
+ for (const [key, value] of Object.entries(
14477
+ meta
14478
+ )) {
14479
+ if (key === "hydra-acp") {
14480
+ continue;
14481
+ }
14482
+ if (value && typeof value === "object" && !Array.isArray(value)) {
14483
+ const fromMeta = parseModelsList(
14484
+ value.availableModels
14485
+ );
14486
+ if (fromMeta.length > 0) {
14487
+ return fromMeta;
14488
+ }
14489
+ }
14490
+ }
14491
+ }
14492
+ return [];
14493
+ }
13672
14494
  function extractInitialModes(result) {
13673
14495
  const direct = parseModesList(result.availableModes);
13674
14496
  if (direct.length > 0) {
@@ -14646,6 +15468,35 @@ function registerSessionRoutes(app, manager, defaults) {
14646
15468
  }
14647
15469
  reply.code(204).send();
14648
15470
  });
15471
+ app.patch("/v1/sessions/:id", async (request, reply) => {
15472
+ const raw = request.params.id;
15473
+ const id = await manager.resolveCanonicalId(raw) ?? raw;
15474
+ const body = request.body ?? {};
15475
+ if (body.regen === true) {
15476
+ const session = manager.get(id);
15477
+ if (!session) {
15478
+ reply.code(409).send({ error: "regen requires a live session" });
15479
+ return;
15480
+ }
15481
+ void session.retitleFromAgent().catch((err) => {
15482
+ app.log.warn(
15483
+ `title regen failed for ${id}: ${err.message}`
15484
+ );
15485
+ });
15486
+ reply.code(202).send();
15487
+ return;
15488
+ }
15489
+ if (typeof body.title !== "string" || body.title.trim().length === 0) {
15490
+ reply.code(400).send({ error: "title must be a non-empty string" });
15491
+ return;
15492
+ }
15493
+ const ok = await manager.setTitle(id, body.title);
15494
+ if (!ok) {
15495
+ reply.code(404).send({ error: "session not found" });
15496
+ return;
15497
+ }
15498
+ reply.code(204).send();
15499
+ });
14649
15500
  app.delete("/v1/sessions/:id", async (request, reply) => {
14650
15501
  const raw = request.params.id;
14651
15502
  const id = await manager.resolveCanonicalId(raw) ?? raw;
@@ -15188,7 +16039,8 @@ function registerAcpWsEndpoint(app, deps) {
15188
16039
  mcpServers: params.mcpServers,
15189
16040
  title: hydraMeta.name,
15190
16041
  agentArgs: hydraMeta.agentArgs,
15191
- model: hydraMeta.model
16042
+ model: hydraMeta.model,
16043
+ onInstallProgress: makeInstallProgressForwarder(connection)
15192
16044
  });
15193
16045
  const client = bindClientToSession(connection, session, state);
15194
16046
  const { entries: replay } = await session.attach(client, "full");
@@ -15204,6 +16056,7 @@ function registerAcpWsEndpoint(app, deps) {
15204
16056
  })();
15205
16057
  });
15206
16058
  const modesPayload = buildModesPayload(session);
16059
+ const modelsPayload = buildModelsPayload(session);
15207
16060
  return {
15208
16061
  sessionId: session.sessionId,
15209
16062
  // session/new is implicitly an attach; mirror session/attach's
@@ -15212,6 +16065,7 @@ function registerAcpWsEndpoint(app, deps) {
15212
16065
  // events without an extra round-trip.
15213
16066
  clientId: client.clientId,
15214
16067
  ...modesPayload ? { modes: modesPayload } : {},
16068
+ ...modelsPayload ? { models: modelsPayload } : {},
15215
16069
  _meta: buildResponseMeta(session)
15216
16070
  };
15217
16071
  });
@@ -15247,7 +16101,10 @@ function registerAcpWsEndpoint(app, deps) {
15247
16101
  err.code = JsonRpcErrorCodes.SessionNotFound;
15248
16102
  throw err;
15249
16103
  }
15250
- session = await deps.manager.resurrect(resurrectParams);
16104
+ session = await deps.manager.resurrect({
16105
+ ...resurrectParams,
16106
+ onInstallProgress: makeInstallProgressForwarder(connection)
16107
+ });
15251
16108
  }
15252
16109
  const client = bindClientToSession(
15253
16110
  connection,
@@ -15273,6 +16130,7 @@ function registerAcpWsEndpoint(app, deps) {
15273
16130
  }
15274
16131
  session.replayPendingPermissions(client);
15275
16132
  const modesPayload = buildModesPayload(session);
16133
+ const modelsPayload = buildModelsPayload(session);
15276
16134
  return {
15277
16135
  sessionId: session.sessionId,
15278
16136
  clientId: client.clientId,
@@ -15284,6 +16142,7 @@ function registerAcpWsEndpoint(app, deps) {
15284
16142
  historyPolicy: appliedPolicy,
15285
16143
  replayed: replay.length,
15286
16144
  ...modesPayload ? { modes: modesPayload } : {},
16145
+ ...modelsPayload ? { models: modelsPayload } : {},
15287
16146
  _meta: buildResponseMeta(session)
15288
16147
  };
15289
16148
  });
@@ -15439,15 +16298,39 @@ function registerAcpWsEndpoint(app, deps) {
15439
16298
  }
15440
16299
  session.replayPendingPermissions(client);
15441
16300
  const modesPayload = buildModesPayload(session);
16301
+ const modelsPayload = buildModelsPayload(session);
15442
16302
  return {
15443
16303
  sessionId: session.sessionId,
15444
16304
  // Same as session/new: include clientId so the deferred-echo
15445
16305
  // path in queue-aware clients can recognize own broadcasts.
15446
16306
  clientId: client.clientId,
15447
16307
  ...modesPayload ? { modes: modesPayload } : {},
16308
+ ...modelsPayload ? { models: modelsPayload } : {},
15448
16309
  _meta: buildResponseMeta(session)
15449
16310
  };
15450
16311
  });
16312
+ connection.onRequest("session/set_model", async (rawParams) => {
16313
+ const decision = decideSetModel(rawParams, deps.manager);
16314
+ if (decision.kind === "error") {
16315
+ app.log.warn(decision.logMessage);
16316
+ const err = new Error(decision.message);
16317
+ err.code = decision.code;
16318
+ throw err;
16319
+ }
16320
+ if (decision.kind === "no_op") {
16321
+ app.log.warn(decision.logMessage);
16322
+ await connection.notify("session/update", {
16323
+ sessionId: decision.sessionId,
16324
+ update: {
16325
+ sessionUpdate: "current_model_update",
16326
+ currentModel: decision.currentModel
16327
+ }
16328
+ }).catch(() => void 0);
16329
+ return null;
16330
+ }
16331
+ app.log.info(decision.logMessage);
16332
+ return decision.session.forwardRequest("session/set_model", rawParams);
16333
+ });
15451
16334
  connection.setDefaultHandler(async (rawParams, method) => {
15452
16335
  if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
15453
16336
  const err = new Error(`Method not found: ${method}`);
@@ -15470,6 +16353,26 @@ function registerAcpWsEndpoint(app, deps) {
15470
16353
  });
15471
16354
  });
15472
16355
  }
16356
+ function makeInstallProgressForwarder(connection) {
16357
+ return (event) => {
16358
+ const payload = {
16359
+ agentId: event.agentId,
16360
+ version: event.version,
16361
+ source: event.source,
16362
+ phase: event.phase
16363
+ };
16364
+ if ("receivedBytes" in event) {
16365
+ payload.receivedBytes = event.receivedBytes;
16366
+ }
16367
+ if ("totalBytes" in event) {
16368
+ payload.totalBytes = event.totalBytes;
16369
+ }
16370
+ if ("packageSpec" in event) {
16371
+ payload.packageSpec = event.packageSpec;
16372
+ }
16373
+ void connection.notify(AGENT_INSTALL_PROGRESS_METHOD, payload).catch(() => void 0);
16374
+ };
16375
+ }
15473
16376
  function buildModesPayload(session) {
15474
16377
  const modes = session.availableModes();
15475
16378
  if (modes.length === 0) {
@@ -15490,6 +16393,94 @@ function buildModesPayload(session) {
15490
16393
  const currentModeId = session.currentMode ?? modes[0].id;
15491
16394
  return { currentModeId, availableModes };
15492
16395
  }
16396
+ function buildModelsPayload(session) {
16397
+ const models = session.availableModels();
16398
+ if (models.length === 0) {
16399
+ return void 0;
16400
+ }
16401
+ const availableModels = models.map((m) => {
16402
+ const out = {
16403
+ modelId: m.modelId
16404
+ };
16405
+ if (m.name !== void 0) {
16406
+ out.name = m.name;
16407
+ }
16408
+ if (m.description !== void 0) {
16409
+ out.description = m.description;
16410
+ }
16411
+ return out;
16412
+ });
16413
+ const currentModelId = session.currentModel ?? models[0].modelId;
16414
+ return { currentModelId, availableModels };
16415
+ }
16416
+ function decideSetModel(rawParams, manager) {
16417
+ if (!rawParams || typeof rawParams !== "object") {
16418
+ return {
16419
+ kind: "error",
16420
+ code: JsonRpcErrorCodes.InvalidParams,
16421
+ message: "session/set_model requires params",
16422
+ logMessage: "session/set_model rejected: params not an object"
16423
+ };
16424
+ }
16425
+ const params = rawParams;
16426
+ if (typeof params.sessionId !== "string") {
16427
+ return {
16428
+ kind: "error",
16429
+ code: JsonRpcErrorCodes.InvalidParams,
16430
+ message: "session/set_model requires string sessionId",
16431
+ logMessage: "session/set_model rejected: missing/non-string sessionId"
16432
+ };
16433
+ }
16434
+ if (typeof params.modelId !== "string") {
16435
+ return {
16436
+ kind: "error",
16437
+ code: JsonRpcErrorCodes.InvalidParams,
16438
+ message: "session/set_model requires string modelId",
16439
+ logMessage: `session/set_model rejected: missing/non-string modelId sessionId=${params.sessionId}`
16440
+ };
16441
+ }
16442
+ const session = manager.get(params.sessionId);
16443
+ if (!session) {
16444
+ return {
16445
+ kind: "error",
16446
+ code: JsonRpcErrorCodes.SessionNotFound,
16447
+ message: `session ${params.sessionId} not found`,
16448
+ logMessage: `session/set_model rejected: session not found sessionId=${params.sessionId}`
16449
+ };
16450
+ }
16451
+ const advertised = session.availableModels();
16452
+ if (advertised.length === 0) {
16453
+ return {
16454
+ kind: "ok",
16455
+ session,
16456
+ logMessage: `session/set_model passthrough (no availableModels) sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
16457
+ };
16458
+ }
16459
+ const match = advertised.find((m) => m.modelId === params.modelId);
16460
+ if (!match) {
16461
+ const known = advertised.map((m) => m.modelId).join(", ");
16462
+ if (session.currentModel !== void 0 && session.currentModel.length > 0) {
16463
+ return {
16464
+ kind: "no_op",
16465
+ session,
16466
+ sessionId: params.sessionId,
16467
+ currentModel: session.currentModel,
16468
+ logMessage: `session/set_model no_op (resyncing client) sessionId=${params.sessionId} requested=${JSON.stringify(params.modelId)} actual=${JSON.stringify(session.currentModel)} agentId=${session.agentId} known=[${known}]`
16469
+ };
16470
+ }
16471
+ return {
16472
+ kind: "error",
16473
+ code: JsonRpcErrorCodes.InvalidParams,
16474
+ message: `model "${params.modelId}" is not in this session's availableModels (agent ${session.agentId}); known models: ${known}`,
16475
+ logMessage: `session/set_model rejected sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)} agentId=${session.agentId} known=[${known}] (no current model to fall back to)`
16476
+ };
16477
+ }
16478
+ return {
16479
+ kind: "ok",
16480
+ session,
16481
+ logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
16482
+ };
16483
+ }
15493
16484
  function buildResponseMeta(session) {
15494
16485
  const ours = {
15495
16486
  upstreamSessionId: session.upstreamSessionId,
@@ -15519,6 +16510,10 @@ function buildResponseMeta(session) {
15519
16510
  if (modes.length > 0) {
15520
16511
  ours.availableModes = modes;
15521
16512
  }
16513
+ const models = session.availableModels();
16514
+ if (models.length > 0) {
16515
+ ours.availableModels = models;
16516
+ }
15522
16517
  if (session.turnStartedAt !== void 0) {
15523
16518
  ours.turnStartedAt = session.turnStartedAt;
15524
16519
  }
@@ -15754,6 +16749,7 @@ function ensureLoopbackOrTls(config) {
15754
16749
 
15755
16750
  // src/cli/commands/daemon.ts
15756
16751
  init_daemon_bootstrap();
16752
+ init_hydra_version();
15757
16753
 
15758
16754
  // src/cli/commands/log-tail.ts
15759
16755
  import * as fs16 from "fs";
@@ -15984,6 +16980,8 @@ async function runDaemonStatus() {
15984
16980
  const info = await readPidFile();
15985
16981
  if (!info) {
15986
16982
  process.stdout.write("Daemon: not running\n");
16983
+ process.stdout.write(`CLI version: ${HYDRA_VERSION}
16984
+ `);
15987
16985
  return;
15988
16986
  }
15989
16987
  const alive = isProcessAlive(info.pid);
@@ -15991,6 +16989,52 @@ async function runDaemonStatus() {
15991
16989
  `Daemon: ${alive ? "running" : "stale pid file"} pid=${info.pid} host=${info.host} port=${info.port} started=${info.startedAt}
15992
16990
  `
15993
16991
  );
16992
+ let daemonVersion;
16993
+ if (alive) {
16994
+ try {
16995
+ const config = await loadConfig();
16996
+ daemonVersion = await fetchDaemonVersion(config);
16997
+ } catch {
16998
+ }
16999
+ }
17000
+ if (daemonVersion === void 0) {
17001
+ process.stdout.write(`CLI version: ${HYDRA_VERSION}
17002
+ `);
17003
+ if (alive) {
17004
+ process.stdout.write(
17005
+ "Daemon version: unknown (health endpoint unreachable)\n"
17006
+ );
17007
+ }
17008
+ return;
17009
+ }
17010
+ if (daemonVersion === HYDRA_VERSION) {
17011
+ process.stdout.write(`Version: ${HYDRA_VERSION}
17012
+ `);
17013
+ return;
17014
+ }
17015
+ process.stdout.write(`CLI version: ${HYDRA_VERSION}
17016
+ `);
17017
+ process.stdout.write(`Daemon version: ${daemonVersion}
17018
+ `);
17019
+ process.stdout.write(
17020
+ chalk.yellow(
17021
+ "Version mismatch \u2014 run `hydra-acp daemon restart` to upgrade the daemon.\n"
17022
+ )
17023
+ );
17024
+ }
17025
+ async function fetchDaemonVersion(config) {
17026
+ const protocol = config.daemon.tls ? "https" : "http";
17027
+ const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/v1/health`;
17028
+ try {
17029
+ const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
17030
+ if (!response.ok) {
17031
+ return void 0;
17032
+ }
17033
+ const body = await response.json();
17034
+ return typeof body.version === "string" ? body.version : void 0;
17035
+ } catch {
17036
+ return void 0;
17037
+ }
15994
17038
  }
15995
17039
  async function readPidFile() {
15996
17040
  try {