@hydra-acp/cli 0.1.6 → 0.1.8

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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/daemon/server.ts
2
- import * as fs9 from "fs";
3
- import * as fsp3 from "fs/promises";
2
+ import * as fs10 from "fs";
3
+ import * as fsp4 from "fs/promises";
4
4
  import Fastify from "fastify";
5
5
  import websocketPlugin from "@fastify/websocket";
6
6
  import pino from "pino";
@@ -30,6 +30,9 @@ function hydraHome() {
30
30
  var paths = {
31
31
  home: hydraHome,
32
32
  config: () => path.join(hydraHome(), "config.json"),
33
+ // Auth token lives in its own file so config.json can be version-
34
+ // controlled without leaking the secret. Raw string contents, mode 0600.
35
+ authToken: () => path.join(hydraHome(), "auth-token"),
33
36
  pidFile: () => path.join(hydraHome(), "daemon.pid"),
34
37
  logFile: () => path.join(hydraHome(), "daemon.log"),
35
38
  currentLogFile: () => path.join(hydraHome(), "current.log"),
@@ -40,6 +43,18 @@ var paths = {
40
43
  // machine's binaries cleanly separated. `ls agents/` immediately
41
44
  // shows which platforms have ever installed anything.
42
45
  agentInstallDir: (id, platformKey, version) => path.join(hydraHome(), "agents", platformKey, id, version),
46
+ // npm install cache for npx-distributed agents. The trailing
47
+ // node<ABI> segment keys on process.versions.modules so a Node
48
+ // major bump (different ABI → native modules incompatible) yields
49
+ // a fresh install rather than failing at require() time.
50
+ agentNpmInstallDir: (id, platformKey, version) => path.join(
51
+ hydraHome(),
52
+ "agents",
53
+ platformKey,
54
+ id,
55
+ version,
56
+ `node${process.versions.modules}`
57
+ ),
43
58
  sessionsDir: () => path.join(hydraHome(), "sessions"),
44
59
  // One directory per session id under sessions/. Co-locates the
45
60
  // session record, its transcript, and any future per-session state
@@ -66,7 +81,16 @@ var DaemonConfig = z.object({
66
81
  authToken: z.string().min(16),
67
82
  logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
68
83
  tls: TlsConfig.optional(),
69
- sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600)
84
+ sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600),
85
+ // Cap on entries kept in a session's on-disk replay log (history.jsonl).
86
+ // Compaction trims to this many on a periodic basis; reads also slice
87
+ // to the tail at this length as a defensive measure against older
88
+ // daemons that may have written unbounded files.
89
+ sessionHistoryMaxEntries: z.number().int().positive().default(1e3),
90
+ // Bytes of trailing agent stderr buffered per AgentInstance so the
91
+ // daemon can include it in the diagnostic message when a spawn fails.
92
+ // Bump if your agents emit large tracebacks you want surfaced.
93
+ agentStderrTailBytes: z.number().int().positive().default(4096)
70
94
  });
71
95
  var RegistryConfig = z.object({
72
96
  url: z.string().url().default(REGISTRY_URL_DEFAULT),
@@ -83,7 +107,20 @@ var TuiConfig = z.object({
83
107
  // Cap on logical lines retained in the in-memory scrollback render
84
108
  // buffer. Oldest lines are dropped on overflow. The on-disk session
85
109
  // history is unaffected; this only bounds the TUI's local view buffer.
86
- maxScrollbackLines: z.number().int().positive().default(1e4)
110
+ maxScrollbackLines: z.number().int().positive().default(1e4),
111
+ // When true (default), the TUI captures mouse events so the wheel can
112
+ // drive scrollback. The cost: terminals route clicks to the app, so
113
+ // text selection requires shift+drag to bypass mouse reporting. Set
114
+ // false to disable capture — wheel scrollback stops working, but
115
+ // plain click-drag selects text via the terminal emulator.
116
+ mouse: z.boolean().default(true),
117
+ // Size at which the TUI's session/update debug log (tui.log) rotates
118
+ // to tui.log.0 and resets. Bounds on-disk use at ~2x this value.
119
+ logMaxBytes: z.number().int().positive().default(5 * 1024 * 1024),
120
+ // Width cap on the cwd column in the `sessions list` output and the
121
+ // TUI picker. Set higher if you keep deeply-nested working directories
122
+ // and want them visible; the elastic title column shrinks to make room.
123
+ cwdColumnMaxWidth: z.number().int().positive().default(24)
87
124
  });
88
125
  var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
89
126
  var ExtensionBody = z.object({
@@ -116,7 +153,16 @@ var HydraConfig = z.object({
116
153
  // recency and truncated to this count. `--all` overrides in the CLI.
117
154
  sessionListColdLimit: z.number().int().nonnegative().default(20),
118
155
  extensions: z.record(ExtensionName, ExtensionBody).default({}),
119
- tui: TuiConfig.default({ repaintThrottleMs: 1e3, maxScrollbackLines: 1e4 })
156
+ tui: TuiConfig.default({
157
+ repaintThrottleMs: 1e3,
158
+ maxScrollbackLines: 1e4,
159
+ mouse: true,
160
+ logMaxBytes: 5 * 1024 * 1024,
161
+ cwdColumnMaxWidth: 24
162
+ })
163
+ });
164
+ var HydraConfigReadOnly = HydraConfig.extend({
165
+ daemon: DaemonConfig.omit({ authToken: true })
120
166
  });
121
167
  function extensionList(config) {
122
168
  return Object.entries(config.extensions).map(([name, body]) => ({
@@ -124,56 +170,104 @@ function extensionList(config) {
124
170
  ...body
125
171
  }));
126
172
  }
127
- async function loadConfig() {
128
- const configPath = paths.config();
173
+ async function readConfigFile() {
129
174
  let raw;
130
175
  try {
131
- raw = await fs.readFile(configPath, "utf8");
176
+ raw = await fs.readFile(paths.config(), "utf8");
132
177
  } catch (err) {
133
178
  const e = err;
134
179
  if (e.code === "ENOENT") {
135
- throw new Error(
136
- `No config found at ${configPath}. Run \`hydra-acp init\` to create one.`
137
- );
180
+ return {};
138
181
  }
139
182
  throw err;
140
183
  }
141
- const parsed = JSON.parse(raw);
142
- return HydraConfig.parse(parsed);
184
+ return JSON.parse(raw);
143
185
  }
144
- async function ensureConfig() {
186
+ async function loadAuthToken() {
187
+ let tokenFile;
145
188
  try {
146
- await fs.access(paths.config());
189
+ const text = await fs.readFile(paths.authToken(), "utf8");
190
+ const trimmed = text.trim();
191
+ if (trimmed.length > 0) {
192
+ tokenFile = trimmed;
193
+ }
147
194
  } catch (err) {
148
195
  const e = err;
149
196
  if (e.code !== "ENOENT") {
150
197
  throw err;
151
198
  }
152
- const config = await writeMinimalInitConfig();
153
- process.stderr.write(
154
- `hydra-acp: initialized ${paths.config()} with a fresh auth token.
155
- `
199
+ }
200
+ const raw = await readConfigFile();
201
+ const daemon = raw.daemon;
202
+ const legacy = daemon && typeof daemon.authToken === "string" ? daemon.authToken : void 0;
203
+ if (tokenFile && legacy) {
204
+ throw new Error(
205
+ `Auth token present in both ${paths.authToken()} and ${paths.config()} (daemon.authToken). Remove daemon.authToken from config.json to resolve.`
156
206
  );
157
- return config;
158
207
  }
159
- return loadConfig();
208
+ if (tokenFile) {
209
+ return tokenFile;
210
+ }
211
+ if (legacy) {
212
+ await migrateLegacyAuthToken(raw, daemon, legacy);
213
+ return legacy;
214
+ }
215
+ return void 0;
160
216
  }
161
- async function writeConfig(config) {
217
+ async function migrateLegacyAuthToken(raw, daemon, token) {
218
+ await writeAuthToken(token);
219
+ delete daemon.authToken;
220
+ if (Object.keys(daemon).length === 0) {
221
+ delete raw.daemon;
222
+ }
223
+ await fs.writeFile(paths.config(), JSON.stringify(raw, null, 2) + "\n", {
224
+ encoding: "utf8",
225
+ mode: 384
226
+ });
227
+ process.stderr.write(
228
+ `hydra-acp: migrated auth token from ${paths.config()} to ${paths.authToken()}.
229
+ `
230
+ );
231
+ }
232
+ async function writeAuthToken(token) {
162
233
  await fs.mkdir(paths.home(), { recursive: true });
163
- await fs.writeFile(paths.config(), JSON.stringify(config, null, 2) + "\n", {
234
+ await fs.writeFile(paths.authToken(), token + "\n", {
164
235
  encoding: "utf8",
165
236
  mode: 384
166
237
  });
167
238
  }
168
- async function writeMinimalInitConfig(authToken) {
169
- const token = authToken ?? generateAuthToken();
170
- const minimal = { daemon: { authToken: token } };
239
+ async function loadConfig() {
240
+ const token = await loadAuthToken();
241
+ if (!token) {
242
+ throw new Error(
243
+ `No auth token found at ${paths.authToken()}. Run \`hydra-acp init\` to create one.`
244
+ );
245
+ }
246
+ const raw = await readConfigFile();
247
+ const daemon = raw.daemon ??= {};
248
+ daemon.authToken = token;
249
+ return HydraConfig.parse(raw);
250
+ }
251
+ async function ensureConfig() {
252
+ if (!await loadAuthToken()) {
253
+ const token = generateAuthToken();
254
+ await writeAuthToken(token);
255
+ process.stderr.write(
256
+ `hydra-acp: initialized ${paths.authToken()} with a fresh auth token.
257
+ `
258
+ );
259
+ }
260
+ return loadConfig();
261
+ }
262
+ async function writeConfig(config) {
171
263
  await fs.mkdir(paths.home(), { recursive: true });
172
- await fs.writeFile(paths.config(), JSON.stringify(minimal, null, 2) + "\n", {
264
+ const { daemon, ...rest } = config;
265
+ const { authToken: _authToken, ...daemonRest } = daemon;
266
+ const onDisk = { ...rest, daemon: daemonRest };
267
+ await fs.writeFile(paths.config(), JSON.stringify(onDisk, null, 2) + "\n", {
173
268
  encoding: "utf8",
174
269
  mode: 384
175
270
  });
176
- return HydraConfig.parse(minimal);
177
271
  }
178
272
  function generateAuthToken() {
179
273
  const bytes = new Uint8Array(32);
@@ -411,9 +505,129 @@ async function fileExists(p) {
411
505
  }
412
506
  }
413
507
 
508
+ // src/core/npm-install.ts
509
+ import * as fsp2 from "fs/promises";
510
+ import * as path3 from "path";
511
+ import { spawn as spawn2 } from "child_process";
512
+ var logSink2 = (msg) => {
513
+ process.stderr.write(msg + "\n");
514
+ };
515
+ function setNpmInstallLogger(log) {
516
+ logSink2 = log ?? ((msg) => process.stderr.write(msg + "\n"));
517
+ }
518
+ async function ensureNpmPackage(args) {
519
+ const platformKey = currentPlatformKey();
520
+ if (!platformKey) {
521
+ throw new Error(
522
+ `Agent ${args.agentId}: cannot determine platform key for ${process.platform}/${process.arch}`
523
+ );
524
+ }
525
+ const installDir = paths.agentNpmInstallDir(
526
+ args.agentId,
527
+ platformKey,
528
+ args.version
529
+ );
530
+ const binPath = path3.join(installDir, "node_modules", ".bin", args.bin);
531
+ if (await fileExists2(binPath)) {
532
+ return binPath;
533
+ }
534
+ await installInto({
535
+ agentId: args.agentId,
536
+ packageSpec: args.packageSpec,
537
+ installDir
538
+ });
539
+ if (!await fileExists2(binPath)) {
540
+ throw new Error(
541
+ `Agent ${args.agentId}: npm install of ${args.packageSpec} did not produce bin ${args.bin} (looked in ${installDir}/node_modules/.bin/)`
542
+ );
543
+ }
544
+ return binPath;
545
+ }
546
+ async function installInto(args) {
547
+ await fsp2.mkdir(path3.dirname(args.installDir), { recursive: true });
548
+ const tempDir = await fsp2.mkdtemp(`${args.installDir}.partial-`);
549
+ try {
550
+ logSink2(
551
+ `hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
552
+ );
553
+ await runNpmInstall({
554
+ packageSpec: args.packageSpec,
555
+ cwd: tempDir
556
+ });
557
+ try {
558
+ await fsp2.rename(tempDir, args.installDir);
559
+ } catch (err) {
560
+ const e = err;
561
+ if ((e.code === "EEXIST" || e.code === "ENOTEMPTY") && await fileExists2(args.installDir)) {
562
+ await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
563
+ () => void 0
564
+ );
565
+ return;
566
+ }
567
+ throw err;
568
+ }
569
+ logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
570
+ } catch (err) {
571
+ await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
572
+ () => void 0
573
+ );
574
+ throw err;
575
+ }
576
+ }
577
+ function runNpmInstall(args) {
578
+ return new Promise((resolve3, reject) => {
579
+ const child = spawn2(
580
+ "npm",
581
+ ["install", "--no-audit", "--no-fund", "--silent", args.packageSpec],
582
+ {
583
+ cwd: args.cwd,
584
+ stdio: ["ignore", "pipe", "pipe"]
585
+ }
586
+ );
587
+ let stderrTail = "";
588
+ child.stdout?.on("data", (chunk) => {
589
+ void chunk;
590
+ });
591
+ child.stderr?.setEncoding("utf8");
592
+ child.stderr?.on("data", (chunk) => {
593
+ stderrTail = (stderrTail + chunk).slice(-4096);
594
+ });
595
+ child.on("error", (err) => {
596
+ const msg = err.code === "ENOENT" ? `npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)` : err.message;
597
+ reject(new Error(msg));
598
+ });
599
+ child.on("exit", (code, signal) => {
600
+ if (code === 0) {
601
+ resolve3();
602
+ return;
603
+ }
604
+ const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
605
+ const tail = stderrTail.trim();
606
+ reject(
607
+ new Error(
608
+ tail ? `npm install ${args.packageSpec} failed (${reason})
609
+ stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
610
+ )
611
+ );
612
+ });
613
+ });
614
+ }
615
+ async function fileExists2(p) {
616
+ try {
617
+ await fsp2.access(p);
618
+ return true;
619
+ } catch {
620
+ return false;
621
+ }
622
+ }
623
+
414
624
  // src/core/registry.ts
415
625
  var NpxDistribution = z2.object({
416
626
  package: z2.string(),
627
+ // The bin to invoke after install. Defaults to the package basename
628
+ // (e.g. "claude-code" for "@anthropic-ai/claude-code"). Required when
629
+ // the package exposes a bin name that differs from its basename.
630
+ bin: z2.string().optional(),
417
631
  args: z2.array(z2.string()).optional(),
418
632
  env: z2.record(z2.string()).optional()
419
633
  });
@@ -573,13 +787,27 @@ function npxPackageBasename(agent) {
573
787
  const atIdx = afterSlash.lastIndexOf("@");
574
788
  return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
575
789
  }
576
- async function planSpawn(agent, extraArgs = []) {
790
+ async function planSpawn(agent, callerArgs = []) {
577
791
  if (agent.distribution.npx) {
578
792
  const npx = agent.distribution.npx;
579
- const args = ["-y", npx.package, ...npx.args ?? [], ...extraArgs];
793
+ const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
794
+ if (process.env.HYDRA_ACP_SKIP_NPM_PREFETCH) {
795
+ return {
796
+ command: "npx",
797
+ args: ["-y", npx.package, ...tail],
798
+ env: npx.env ?? {}
799
+ };
800
+ }
801
+ const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
802
+ const binPath = await ensureNpmPackage({
803
+ agentId: agent.id,
804
+ version: agent.version ?? "current",
805
+ packageSpec: npx.package,
806
+ bin
807
+ });
580
808
  return {
581
- command: "npx",
582
- args,
809
+ command: binPath,
810
+ args: tail,
583
811
  env: npx.env ?? {}
584
812
  };
585
813
  }
@@ -595,33 +823,31 @@ async function planSpawn(agent, extraArgs = []) {
595
823
  version: agent.version ?? "current",
596
824
  target
597
825
  });
826
+ const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
598
827
  return {
599
828
  command: cmdPath,
600
- args: [...target.args ?? [], ...extraArgs],
829
+ args: tail,
601
830
  env: target.env ?? {}
602
831
  };
603
832
  }
604
833
  if (agent.distribution.uvx) {
605
834
  const uvx = agent.distribution.uvx;
606
- const args = [uvx.package, ...uvx.args ?? [], ...extraArgs];
835
+ const tail = callerArgs.length > 0 ? callerArgs : uvx.args ?? [];
607
836
  return {
608
837
  command: "uvx",
609
- args,
838
+ args: [uvx.package, ...tail],
610
839
  env: uvx.env ?? {}
611
840
  };
612
841
  }
613
842
  throw new Error(`Agent ${agent.id} has no usable distribution method.`);
614
843
  }
615
844
 
616
- // src/core/session-manager.ts
617
- import * as fs7 from "fs/promises";
618
- import { customAlphabet as customAlphabet3 } from "nanoid";
619
-
620
845
  // src/core/agent-instance.ts
621
- import { spawn as spawn2 } from "child_process";
846
+ import { spawn as spawn3 } from "child_process";
622
847
 
623
848
  // src/acp/types.ts
624
849
  import { z as z3 } from "zod";
850
+ var ACP_PROTOCOL_VERSION = 1;
625
851
  var JsonRpcErrorCodes = {
626
852
  ParseError: -32700,
627
853
  InvalidRequest: -32600,
@@ -865,7 +1091,7 @@ function ndjsonStreamFromStdio(stdout, stdin) {
865
1091
 
866
1092
  // src/acp/connection.ts
867
1093
  import { nanoid } from "nanoid";
868
- var JsonRpcConnection = class {
1094
+ var JsonRpcConnection = class _JsonRpcConnection {
869
1095
  constructor(stream) {
870
1096
  this.stream = stream;
871
1097
  this.stream.onMessage((m) => this.handleIncoming(m));
@@ -875,6 +1101,16 @@ var JsonRpcConnection = class {
875
1101
  requestHandlers = /* @__PURE__ */ new Map();
876
1102
  defaultRequestHandler;
877
1103
  notificationHandlers = /* @__PURE__ */ new Map();
1104
+ // Notifications received before a handler was registered. Some agents
1105
+ // (e.g. claude-acp) advertise their command list in the same chunk as
1106
+ // the `session/new` response, which is processed before the consumer
1107
+ // can attach its `session/update` handler. Without this buffer those
1108
+ // notifications would be silently dropped, so e.g. `/model` would
1109
+ // never appear in the TUI's slash-completion palette. Capped per
1110
+ // method to keep the buffer from growing unboundedly when nothing
1111
+ // ever subscribes.
1112
+ bufferedNotifications = /* @__PURE__ */ new Map();
1113
+ static MAX_BUFFERED_PER_METHOD = 64;
878
1114
  pending = /* @__PURE__ */ new Map();
879
1115
  closed = false;
880
1116
  closeHandlers = [];
@@ -886,6 +1122,17 @@ var JsonRpcConnection = class {
886
1122
  }
887
1123
  onNotification(method, handler) {
888
1124
  this.notificationHandlers.set(method, handler);
1125
+ const queued = this.bufferedNotifications.get(method);
1126
+ if (!queued) {
1127
+ return;
1128
+ }
1129
+ this.bufferedNotifications.delete(method);
1130
+ for (const note of queued) {
1131
+ try {
1132
+ handler(note.params, note.method);
1133
+ } catch {
1134
+ }
1135
+ }
889
1136
  }
890
1137
  onClose(handler) {
891
1138
  this.closeHandlers.push(handler);
@@ -930,6 +1177,13 @@ var JsonRpcConnection = class {
930
1177
  }
931
1178
  await this.stream.close();
932
1179
  }
1180
+ // Force-close with an error. Rejects all pending requests and fires
1181
+ // close handlers carrying `err`. Used by transports that detect a
1182
+ // failure (e.g. child process crash, spawn ENOENT) the stream itself
1183
+ // can't surface as a stdout/stdin error.
1184
+ fail(err) {
1185
+ this.handleClose(err);
1186
+ }
933
1187
  handleIncoming(message) {
934
1188
  if ("method" in message) {
935
1189
  if ("id" in message && message.id !== void 0) {
@@ -971,6 +1225,16 @@ var JsonRpcConnection = class {
971
1225
  const handler = this.notificationHandlers.get(note.method);
972
1226
  if (handler) {
973
1227
  handler(note.params, note.method);
1228
+ return;
1229
+ }
1230
+ let queued = this.bufferedNotifications.get(note.method);
1231
+ if (!queued) {
1232
+ queued = [];
1233
+ this.bufferedNotifications.set(note.method, queued);
1234
+ }
1235
+ queued.push(note);
1236
+ if (queued.length > _JsonRpcConnection.MAX_BUFFERED_PER_METHOD) {
1237
+ queued.shift();
974
1238
  }
975
1239
  }
976
1240
  handleResponse(res) {
@@ -1012,17 +1276,22 @@ var JsonRpcConnection = class {
1012
1276
  };
1013
1277
 
1014
1278
  // src/core/agent-instance.ts
1279
+ var DEFAULT_STDERR_TAIL_BYTES = 4096;
1015
1280
  var AgentInstance = class _AgentInstance {
1016
1281
  agentId;
1017
1282
  cwd;
1018
1283
  connection;
1019
1284
  child;
1020
1285
  exited = false;
1286
+ killed = false;
1287
+ stderrTail = "";
1288
+ stderrTailBytes;
1021
1289
  exitHandlers = [];
1022
1290
  constructor(opts, child) {
1023
1291
  this.agentId = opts.agentId;
1024
1292
  this.cwd = opts.cwd;
1025
1293
  this.child = child;
1294
+ this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
1026
1295
  if (!child.stdout || !child.stdin) {
1027
1296
  throw new Error("agent subprocess missing stdio");
1028
1297
  }
@@ -1030,22 +1299,36 @@ var AgentInstance = class _AgentInstance {
1030
1299
  this.connection = new JsonRpcConnection(stream);
1031
1300
  child.stderr?.setEncoding("utf8");
1032
1301
  child.stderr?.on("data", (chunk) => {
1302
+ this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
1033
1303
  process.stderr.write(`[${opts.agentId}] ${chunk}`);
1034
1304
  });
1305
+ child.on("error", (err) => {
1306
+ const msg = this.formatFailure(err.message);
1307
+ this.connection.fail(new Error(msg));
1308
+ });
1035
1309
  child.on("exit", (code, signal) => {
1036
1310
  this.exited = true;
1311
+ if (!this.killed) {
1312
+ const reason = `agent ${opts.agentId} exited before responding (code=${code} signal=${signal})`;
1313
+ this.connection.fail(new Error(this.formatFailure(reason)));
1314
+ }
1037
1315
  for (const handler of this.exitHandlers) {
1038
1316
  handler(code, signal);
1039
1317
  }
1040
1318
  });
1041
1319
  }
1320
+ formatFailure(reason) {
1321
+ const tail = this.stderrTail.trim();
1322
+ return tail ? `${reason}
1323
+ stderr: ${tail}` : reason;
1324
+ }
1042
1325
  static spawn(opts) {
1043
1326
  const env = {
1044
1327
  ...process.env,
1045
1328
  ...opts.plan.env,
1046
1329
  ...opts.extraEnv ?? {}
1047
1330
  };
1048
- const child = spawn2(opts.plan.command, opts.plan.args, {
1331
+ const child = spawn3(opts.plan.command, opts.plan.args, {
1049
1332
  cwd: opts.cwd,
1050
1333
  env,
1051
1334
  stdio: ["pipe", "pipe", "pipe"]
@@ -1062,11 +1345,17 @@ var AgentInstance = class _AgentInstance {
1062
1345
  if (this.exited) {
1063
1346
  return;
1064
1347
  }
1348
+ this.killed = true;
1065
1349
  await this.connection.close().catch(() => void 0);
1066
1350
  this.child.kill(signal);
1067
1351
  }
1068
1352
  };
1069
1353
 
1354
+ // src/core/session-manager.ts
1355
+ import * as fs8 from "fs/promises";
1356
+ import * as os2 from "os";
1357
+ import { customAlphabet as customAlphabet3 } from "nanoid";
1358
+
1070
1359
  // src/core/session.ts
1071
1360
  import { customAlphabet } from "nanoid";
1072
1361
 
@@ -1074,12 +1363,12 @@ import { customAlphabet } from "nanoid";
1074
1363
  var HYDRA_COMMANDS = [
1075
1364
  {
1076
1365
  verb: "title",
1077
- name: "/hydra title",
1366
+ name: "hydra title",
1078
1367
  description: "Regenerate the session title via the agent (or set manually with an arg)"
1079
1368
  },
1080
1369
  {
1081
1370
  verb: "agent",
1082
- name: "/hydra agent",
1371
+ name: "hydra agent",
1083
1372
  argsHint: "<agent>",
1084
1373
  description: "Swap the agent backing this session, preserving context"
1085
1374
  }
@@ -1096,8 +1385,7 @@ function hydraCommandsAsAdvertised() {
1096
1385
  var HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1097
1386
  var generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
1098
1387
  var HYDRA_SESSION_PREFIX = "hydra_session_";
1099
- var MAX_HISTORY_ENTRIES = 1e3;
1100
- var COMPACT_EVERY = 200;
1388
+ var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
1101
1389
  var Session = class {
1102
1390
  sessionId;
1103
1391
  cwd;
@@ -1139,11 +1427,13 @@ var Session = class {
1139
1427
  // Bumped by broadcastPromptReceived, cleared by broadcastTurnComplete.
1140
1428
  // Drives the mid-turn elapsed counter delivered to fresh attachers.
1141
1429
  promptStartedAt;
1142
- // Counts appends since the last compaction. When it hits COMPACT_EVERY
1430
+ // Counts appends since the last compaction. When it hits compactEvery
1143
1431
  // we ask the history store to trim the file to the most recent
1144
- // MAX_HISTORY_ENTRIES. Keeps file growth bounded without per-append
1432
+ // historyMaxEntries. Keeps file growth bounded without per-append
1145
1433
  // file-size checks.
1146
1434
  appendCount = 0;
1435
+ historyMaxEntries;
1436
+ compactEvery;
1147
1437
  // Permission requests that have been broadcast to one or more
1148
1438
  // clients but have not yet resolved. Replayed to clients that
1149
1439
  // attach mid-flight so a late joiner sees the prompt instead of an
@@ -1200,6 +1490,8 @@ var Session = class {
1200
1490
  this.firstPromptSeeded = true;
1201
1491
  }
1202
1492
  this.historyStore = init.historyStore;
1493
+ this.historyMaxEntries = init.historyMaxEntries ?? DEFAULT_HISTORY_MAX_ENTRIES;
1494
+ this.compactEvery = Math.max(1, Math.floor(this.historyMaxEntries * 0.2));
1203
1495
  this.updatedAt = Date.now();
1204
1496
  this.createdAt = init.createdAt ?? this.updatedAt;
1205
1497
  this.lastRecordedAt = this.updatedAt;
@@ -2038,9 +2330,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2038
2330
  if (this.historyStore) {
2039
2331
  const store = this.historyStore;
2040
2332
  void store.append(this.sessionId, entry).catch(() => void 0);
2041
- if (this.appendCount >= COMPACT_EVERY) {
2333
+ if (this.appendCount >= this.compactEvery) {
2042
2334
  this.appendCount = 0;
2043
- void store.compact(this.sessionId, MAX_HISTORY_ENTRIES).catch(
2335
+ void store.compact(this.sessionId, this.historyMaxEntries).catch(
2044
2336
  () => void 0
2045
2337
  );
2046
2338
  }
@@ -2242,7 +2534,7 @@ function firstLine(text, max) {
2242
2534
 
2243
2535
  // src/core/session-store.ts
2244
2536
  import * as fs4 from "fs/promises";
2245
- import * as path3 from "path";
2537
+ import * as path4 from "path";
2246
2538
  import { customAlphabet as customAlphabet2 } from "nanoid";
2247
2539
  import { z as z4 } from "zod";
2248
2540
  var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -2414,12 +2706,16 @@ function recordFromMemorySession(args) {
2414
2706
  // src/core/history-store.ts
2415
2707
  import * as fs5 from "fs/promises";
2416
2708
  var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
2417
- var MAX_ENTRIES = 1e3;
2709
+ var DEFAULT_MAX_ENTRIES = 1e3;
2418
2710
  var HistoryStore = class {
2419
2711
  // Serialize writes per session id so appends and rewrites don't
2420
2712
  // interleave JSONL lines on disk. The chain swallows errors so one
2421
2713
  // failed append doesn't poison every subsequent write.
2422
2714
  writeQueues = /* @__PURE__ */ new Map();
2715
+ maxEntries;
2716
+ constructor(options = {}) {
2717
+ this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
2718
+ }
2423
2719
  async append(sessionId, entry) {
2424
2720
  if (!SESSION_ID_PATTERN2.test(sessionId)) {
2425
2721
  return;
@@ -2521,8 +2817,8 @@ var HistoryStore = class {
2521
2817
  recordedAt: obj.recordedAt
2522
2818
  });
2523
2819
  }
2524
- if (out.length > MAX_ENTRIES) {
2525
- return out.slice(-MAX_ENTRIES);
2820
+ if (out.length > this.maxEntries) {
2821
+ return out.slice(-this.maxEntries);
2526
2822
  }
2527
2823
  return out;
2528
2824
  }
@@ -2565,13 +2861,40 @@ var HistoryStore = class {
2565
2861
 
2566
2862
  // src/tui/history.ts
2567
2863
  import { promises as fs6 } from "fs";
2568
- import * as path4 from "path";
2864
+ import * as path5 from "path";
2569
2865
  async function saveHistory(file, history) {
2570
- await fs6.mkdir(path4.dirname(file), { recursive: true });
2866
+ await fs6.mkdir(path5.dirname(file), { recursive: true });
2571
2867
  const lines = history.map((entry) => JSON.stringify(entry));
2572
2868
  await fs6.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
2573
2869
  }
2574
2870
 
2871
+ // src/core/hydra-version.ts
2872
+ import { fileURLToPath } from "url";
2873
+ import * as path6 from "path";
2874
+ import * as fs7 from "fs";
2875
+ function resolveVersion() {
2876
+ try {
2877
+ let dir = path6.dirname(fileURLToPath(import.meta.url));
2878
+ for (let i = 0; i < 8; i += 1) {
2879
+ const candidate = path6.join(dir, "package.json");
2880
+ if (fs7.existsSync(candidate)) {
2881
+ const pkg = JSON.parse(fs7.readFileSync(candidate, "utf8"));
2882
+ if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
2883
+ return pkg.version;
2884
+ }
2885
+ }
2886
+ const parent = path6.dirname(dir);
2887
+ if (parent === dir) {
2888
+ break;
2889
+ }
2890
+ dir = parent;
2891
+ }
2892
+ } catch {
2893
+ }
2894
+ return "0.0.0";
2895
+ }
2896
+ var HYDRA_VERSION = resolveVersion();
2897
+
2575
2898
  // src/core/session-manager.ts
2576
2899
  var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2577
2900
  var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
@@ -2580,7 +2903,8 @@ var SessionManager = class {
2580
2903
  this.registry = registry;
2581
2904
  this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
2582
2905
  this.store = store ?? new SessionStore();
2583
- this.histories = new HistoryStore();
2906
+ this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
2907
+ this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
2584
2908
  this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
2585
2909
  this.defaultModels = options.defaultModels ?? {};
2586
2910
  }
@@ -2592,6 +2916,7 @@ var SessionManager = class {
2592
2916
  histories;
2593
2917
  idleTimeoutMs;
2594
2918
  defaultModels;
2919
+ sessionHistoryMaxEntries;
2595
2920
  // Serialize meta.json read-modify-write operations per session id so
2596
2921
  // concurrent snapshot updates (e.g. an agent emitting model + mode
2597
2922
  // back-to-back) don't lose writes via interleaved reads.
@@ -2615,6 +2940,7 @@ var SessionManager = class {
2615
2940
  idleTimeoutMs: this.idleTimeoutMs,
2616
2941
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
2617
2942
  historyStore: this.histories,
2943
+ historyMaxEntries: this.sessionHistoryMaxEntries,
2618
2944
  currentModel: fresh.initialModel
2619
2945
  });
2620
2946
  await this.attachManagerHooks(session);
@@ -2666,11 +2992,16 @@ var SessionManager = class {
2666
2992
  cwd: params.cwd,
2667
2993
  plan
2668
2994
  });
2669
- await agent.connection.request("initialize", {
2670
- protocolVersion: 1,
2671
- clientCapabilities: {},
2672
- clientInfo: { name: "hydra", version: "0.1.0" }
2673
- });
2995
+ try {
2996
+ await agent.connection.request("initialize", {
2997
+ protocolVersion: ACP_PROTOCOL_VERSION,
2998
+ clientCapabilities: {},
2999
+ clientInfo: { name: "hydra", version: HYDRA_VERSION }
3000
+ });
3001
+ } catch (err) {
3002
+ await agent.kill().catch(() => void 0);
3003
+ throw err;
3004
+ }
2674
3005
  let loadResult;
2675
3006
  try {
2676
3007
  loadResult = await agent.connection.request(
@@ -2682,10 +3013,12 @@ var SessionManager = class {
2682
3013
  }
2683
3014
  );
2684
3015
  } catch (err) {
2685
- await agent.kill().catch(() => void 0);
2686
- throw new Error(
2687
- `agent ${params.agentId} failed to load upstream session ${params.upstreamSessionId}: ${err.message}`
3016
+ process.stderr.write(
3017
+ `session/load failed for upstream ${params.upstreamSessionId} on ${params.agentId} (${err.message}); recovering via import-reseed
3018
+ `
2688
3019
  );
3020
+ await agent.kill().catch(() => void 0);
3021
+ return this.doResurrectFromImport(params);
2689
3022
  }
2690
3023
  const session = new Session({
2691
3024
  sessionId: params.hydraSessionId,
@@ -2699,6 +3032,7 @@ var SessionManager = class {
2699
3032
  idleTimeoutMs: this.idleTimeoutMs,
2700
3033
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
2701
3034
  historyStore: this.histories,
3035
+ historyMaxEntries: this.sessionHistoryMaxEntries,
2702
3036
  // Prefer what we previously stored from a current_model_update; if
2703
3037
  // we never captured one (e.g. old opencode sessions on disk before
2704
3038
  // this fix), fall back to the model the agent ships in its
@@ -2725,15 +3059,16 @@ var SessionManager = class {
2725
3059
  // so subsequent resurrects of this session use the normal session/load
2726
3060
  // path.
2727
3061
  async doResurrectFromImport(params) {
3062
+ const cwd = await this.resolveImportCwd(params.cwd);
2728
3063
  const fresh = await this.bootstrapAgent({
2729
3064
  agentId: params.agentId,
2730
- cwd: params.cwd,
3065
+ cwd,
2731
3066
  agentArgs: params.agentArgs,
2732
3067
  mcpServers: []
2733
3068
  });
2734
3069
  const session = new Session({
2735
3070
  sessionId: params.hydraSessionId,
2736
- cwd: params.cwd,
3071
+ cwd,
2737
3072
  agentId: params.agentId,
2738
3073
  agent: fresh.agent,
2739
3074
  upstreamSessionId: fresh.upstreamSessionId,
@@ -2743,6 +3078,7 @@ var SessionManager = class {
2743
3078
  idleTimeoutMs: this.idleTimeoutMs,
2744
3079
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
2745
3080
  historyStore: this.histories,
3081
+ historyMaxEntries: this.sessionHistoryMaxEntries,
2746
3082
  // Prefer the stored value (set by a previous current_model_update);
2747
3083
  // fall back to whatever the agent ships in its session/new response.
2748
3084
  currentModel: params.currentModel ?? fresh.initialModel,
@@ -2756,6 +3092,16 @@ var SessionManager = class {
2756
3092
  void session.seedFromImport().catch(() => void 0);
2757
3093
  return session;
2758
3094
  }
3095
+ async resolveImportCwd(cwd) {
3096
+ try {
3097
+ const stat2 = await fs8.stat(cwd);
3098
+ if (stat2.isDirectory()) {
3099
+ return cwd;
3100
+ }
3101
+ } catch {
3102
+ }
3103
+ return os2.homedir();
3104
+ }
2759
3105
  // Bootstrap a fresh agent process: registry resolve → spawn → initialize
2760
3106
  // → session/new. Shared by create() and the /hydra agent path so both
2761
3107
  // go through the same env / capabilities / error-handling.
@@ -2776,9 +3122,9 @@ var SessionManager = class {
2776
3122
  });
2777
3123
  try {
2778
3124
  await agent.connection.request("initialize", {
2779
- protocolVersion: 1,
3125
+ protocolVersion: ACP_PROTOCOL_VERSION,
2780
3126
  clientCapabilities: {},
2781
- clientInfo: { name: "hydra", version: "0.1.0" }
3127
+ clientInfo: { name: "hydra", version: HYDRA_VERSION }
2782
3128
  });
2783
3129
  const newResult = await agent.connection.request(
2784
3130
  "session/new",
@@ -3058,7 +3404,8 @@ var SessionManager = class {
3058
3404
  await this.writeImportedRecord({
3059
3405
  sessionId: existing.sessionId,
3060
3406
  bundle,
3061
- preservedCreatedAt: existing.createdAt
3407
+ preservedCreatedAt: existing.createdAt,
3408
+ cwd: opts.cwd
3062
3409
  });
3063
3410
  return {
3064
3411
  sessionId: existing.sessionId,
@@ -3067,7 +3414,11 @@ var SessionManager = class {
3067
3414
  };
3068
3415
  }
3069
3416
  const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
3070
- await this.writeImportedRecord({ sessionId: newId, bundle });
3417
+ await this.writeImportedRecord({
3418
+ sessionId: newId,
3419
+ bundle,
3420
+ cwd: opts.cwd
3421
+ });
3071
3422
  return {
3072
3423
  sessionId: newId,
3073
3424
  importedFromSessionId: bundle.session.sessionId,
@@ -3097,7 +3448,7 @@ var SessionManager = class {
3097
3448
  upstreamSessionId: "",
3098
3449
  importedFromSessionId: args.bundle.session.sessionId,
3099
3450
  agentId: args.bundle.session.agentId,
3100
- cwd: args.bundle.session.cwd,
3451
+ cwd: args.cwd ?? args.bundle.session.cwd,
3101
3452
  title: args.bundle.session.title,
3102
3453
  currentModel: args.bundle.session.currentModel,
3103
3454
  currentMode: args.bundle.session.currentMode,
@@ -3290,7 +3641,7 @@ function asString(value) {
3290
3641
  }
3291
3642
  async function loadPromptHistorySafely(sessionId) {
3292
3643
  try {
3293
- const raw = await fs7.readFile(paths.tuiHistoryFile(sessionId), "utf8");
3644
+ const raw = await fs8.readFile(paths.tuiHistoryFile(sessionId), "utf8");
3294
3645
  const out = [];
3295
3646
  for (const line of raw.split("\n")) {
3296
3647
  if (line.length === 0) {
@@ -3311,7 +3662,7 @@ async function loadPromptHistorySafely(sessionId) {
3311
3662
  }
3312
3663
  async function historyMtimeIso(sessionId) {
3313
3664
  try {
3314
- const st = await fs7.stat(paths.historyFile(sessionId));
3665
+ const st = await fs8.stat(paths.historyFile(sessionId));
3315
3666
  return new Date(st.mtimeMs).toISOString();
3316
3667
  } catch {
3317
3668
  return void 0;
@@ -3319,10 +3670,10 @@ async function historyMtimeIso(sessionId) {
3319
3670
  }
3320
3671
 
3321
3672
  // src/core/extensions.ts
3322
- import { spawn as spawn3 } from "child_process";
3323
- import * as fs8 from "fs";
3324
- import * as fsp2 from "fs/promises";
3325
- import * as path5 from "path";
3673
+ import { spawn as spawn4 } from "child_process";
3674
+ import * as fs9 from "fs";
3675
+ import * as fsp3 from "fs/promises";
3676
+ import * as path7 from "path";
3326
3677
  var RESTART_BASE_MS = 1e3;
3327
3678
  var RESTART_CAP_MS = 6e4;
3328
3679
  var STOP_GRACE_MS = 3e3;
@@ -3343,7 +3694,7 @@ var ExtensionManager = class {
3343
3694
  if (!this.context) {
3344
3695
  throw new Error("ExtensionManager: setContext must be called before start");
3345
3696
  }
3346
- await fsp2.mkdir(paths.extensionsDir(), { recursive: true });
3697
+ await fsp3.mkdir(paths.extensionsDir(), { recursive: true });
3347
3698
  await this.reapOrphans();
3348
3699
  for (const entry of this.entries.values()) {
3349
3700
  if (!entry.config.enabled) {
@@ -3552,7 +3903,7 @@ var ExtensionManager = class {
3552
3903
  async reapOrphans() {
3553
3904
  let entries;
3554
3905
  try {
3555
- entries = await fsp2.readdir(paths.extensionsDir());
3906
+ entries = await fsp3.readdir(paths.extensionsDir());
3556
3907
  } catch (err) {
3557
3908
  const e = err;
3558
3909
  if (e.code === "ENOENT") {
@@ -3564,10 +3915,10 @@ var ExtensionManager = class {
3564
3915
  if (!entry.endsWith(".pid")) {
3565
3916
  continue;
3566
3917
  }
3567
- const pidPath = path5.join(paths.extensionsDir(), entry);
3918
+ const pidPath = path7.join(paths.extensionsDir(), entry);
3568
3919
  let pid;
3569
3920
  try {
3570
- const raw = await fsp2.readFile(pidPath, "utf8");
3921
+ const raw = await fsp3.readFile(pidPath, "utf8");
3571
3922
  const parsed = Number.parseInt(raw.trim(), 10);
3572
3923
  if (Number.isInteger(parsed) && parsed > 0) {
3573
3924
  pid = parsed;
@@ -3590,7 +3941,7 @@ var ExtensionManager = class {
3590
3941
  }
3591
3942
  }
3592
3943
  }
3593
- await fsp2.unlink(pidPath).catch(() => void 0);
3944
+ await fsp3.unlink(pidPath).catch(() => void 0);
3594
3945
  }
3595
3946
  }
3596
3947
  spawn(entry, attempt) {
@@ -3603,7 +3954,7 @@ var ExtensionManager = class {
3603
3954
  }
3604
3955
  const ext = entry.config;
3605
3956
  const command = ext.command.length > 0 ? ext.command : [ext.name];
3606
- const logStream = fs8.createWriteStream(paths.extensionLogFile(ext.name), {
3957
+ const logStream = fs9.createWriteStream(paths.extensionLogFile(ext.name), {
3607
3958
  flags: "a"
3608
3959
  });
3609
3960
  logStream.write(
@@ -3631,7 +3982,7 @@ var ExtensionManager = class {
3631
3982
  const args = [...baseArgs, ...ext.args];
3632
3983
  let child;
3633
3984
  try {
3634
- child = spawn3(cmd, args, {
3985
+ child = spawn4(cmd, args, {
3635
3986
  env,
3636
3987
  stdio: ["ignore", "pipe", "pipe"],
3637
3988
  detached: false
@@ -3653,7 +4004,7 @@ var ExtensionManager = class {
3653
4004
  }
3654
4005
  if (typeof child.pid === "number") {
3655
4006
  try {
3656
- fs8.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
4007
+ fs9.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
3657
4008
  `, {
3658
4009
  encoding: "utf8",
3659
4010
  mode: 384
@@ -3678,7 +4029,7 @@ var ExtensionManager = class {
3678
4029
  });
3679
4030
  child.on("exit", (code, signal) => {
3680
4031
  try {
3681
- fs8.unlinkSync(paths.extensionPidFile(ext.name));
4032
+ fs9.unlinkSync(paths.extensionPidFile(ext.name));
3682
4033
  } catch {
3683
4034
  }
3684
4035
  logStream.write(
@@ -3787,7 +4138,7 @@ function constantTimeEqual(a, b) {
3787
4138
  }
3788
4139
 
3789
4140
  // src/daemon/routes/sessions.ts
3790
- import * as os2 from "os";
4141
+ import * as os3 from "os";
3791
4142
 
3792
4143
  // src/core/bundle.ts
3793
4144
  import { z as z5 } from "zod";
@@ -3857,7 +4208,6 @@ function decodeBundle(raw) {
3857
4208
  }
3858
4209
 
3859
4210
  // src/daemon/routes/sessions.ts
3860
- var HYDRA_VERSION = "0.1.0";
3861
4211
  function registerSessionRoutes(app, manager, defaults) {
3862
4212
  app.get("/v1/sessions", async (request) => {
3863
4213
  const query = request.query;
@@ -3928,7 +4278,7 @@ function registerSessionRoutes(app, manager, defaults) {
3928
4278
  history: exported.history,
3929
4279
  promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
3930
4280
  hydraVersion: HYDRA_VERSION,
3931
- machine: os2.hostname()
4281
+ machine: os3.hostname()
3932
4282
  });
3933
4283
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3934
4284
  reply.header(
@@ -3943,6 +4293,14 @@ function registerSessionRoutes(app, manager, defaults) {
3943
4293
  reply.code(400).send({ error: "missing bundle" });
3944
4294
  return;
3945
4295
  }
4296
+ let cwdOverride;
4297
+ if (body.cwd !== void 0) {
4298
+ if (typeof body.cwd !== "string" || body.cwd.length === 0) {
4299
+ reply.code(400).send({ error: "cwd must be a non-empty string" });
4300
+ return;
4301
+ }
4302
+ cwdOverride = body.cwd;
4303
+ }
3946
4304
  let bundle;
3947
4305
  try {
3948
4306
  bundle = decodeBundle(body.bundle);
@@ -3955,7 +4313,8 @@ function registerSessionRoutes(app, manager, defaults) {
3955
4313
  }
3956
4314
  try {
3957
4315
  const result = await manager.importBundle(bundle, {
3958
- replace: body.replace === true
4316
+ replace: body.replace === true,
4317
+ ...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
3959
4318
  });
3960
4319
  reply.code(201).send(result);
3961
4320
  } catch (err) {
@@ -4266,8 +4625,6 @@ function wsToMessageStream(ws) {
4266
4625
  }
4267
4626
 
4268
4627
  // src/daemon/acp-ws.ts
4269
- var HYDRA_VERSION2 = "0.1.0";
4270
- var HYDRA_PROTOCOL_VERSION = 1;
4271
4628
  function registerAcpWsEndpoint(app, deps) {
4272
4629
  app.get("/acp", { websocket: true }, (socket, request) => {
4273
4630
  const token = tokenFromUpgradeRequest({
@@ -4530,8 +4887,8 @@ function buildResponseMeta(session) {
4530
4887
  }
4531
4888
  function buildInitializeResult() {
4532
4889
  return {
4533
- protocolVersion: HYDRA_PROTOCOL_VERSION,
4534
- agentInfo: { name: "hydra", version: HYDRA_VERSION2 },
4890
+ protocolVersion: ACP_PROTOCOL_VERSION,
4891
+ agentInfo: { name: "hydra", version: HYDRA_VERSION },
4535
4892
  agentCapabilities: {
4536
4893
  // hydra is a transparent proxy: prompt blocks and MCP server configs are
4537
4894
  // forwarded to the underlying agent unchanged. We claim the union of
@@ -4570,14 +4927,13 @@ function bindClientToSession(connection, session, state, clientInfo) {
4570
4927
  }
4571
4928
 
4572
4929
  // src/daemon/server.ts
4573
- var HYDRA_VERSION3 = "0.1.0";
4574
4930
  async function startDaemon(config) {
4575
4931
  ensureLoopbackOrTls(config);
4576
4932
  const httpsOptions = config.daemon.tls ? {
4577
- key: await fsp3.readFile(config.daemon.tls.key),
4578
- cert: await fsp3.readFile(config.daemon.tls.cert)
4933
+ key: await fsp4.readFile(config.daemon.tls.key),
4934
+ cert: await fsp4.readFile(config.daemon.tls.cert)
4579
4935
  } : void 0;
4580
- await fsp3.mkdir(paths.home(), { recursive: true });
4936
+ await fsp4.mkdir(paths.home(), { recursive: true });
4581
4937
  const { stream: logStream, fileStream } = await buildLogStream(
4582
4938
  config.daemon.logLevel
4583
4939
  );
@@ -4586,12 +4942,18 @@ async function startDaemon(config) {
4586
4942
  level: config.daemon.logLevel,
4587
4943
  stream: logStream
4588
4944
  },
4589
- https: httpsOptions ?? null
4945
+ https: httpsOptions ?? null,
4946
+ // Session bundles can be large (full history + tool output);
4947
+ // the 1MB Fastify default rejects ordinary imports.
4948
+ bodyLimit: 256 * 1024 * 1024
4590
4949
  });
4591
4950
  await app.register(websocketPlugin);
4592
4951
  setBinaryInstallLogger((msg) => {
4593
4952
  app.log.info(msg);
4594
4953
  });
4954
+ setNpmInstallLogger((msg) => {
4955
+ app.log.info(msg);
4956
+ });
4595
4957
  const auth = bearerAuth({ config });
4596
4958
  app.addHook("onRequest", async (request, reply) => {
4597
4959
  if (request.routeOptions.config?.skipAuth) {
@@ -4603,12 +4965,14 @@ async function startDaemon(config) {
4603
4965
  await auth(request, reply);
4604
4966
  });
4605
4967
  const registry = new Registry(config);
4606
- const manager = new SessionManager(registry, void 0, void 0, {
4968
+ const spawner = (opts) => AgentInstance.spawn({ ...opts, stderrTailBytes: config.daemon.agentStderrTailBytes });
4969
+ const manager = new SessionManager(registry, spawner, void 0, {
4607
4970
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
4608
- defaultModels: config.defaultModels
4971
+ defaultModels: config.defaultModels,
4972
+ sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
4609
4973
  });
4610
4974
  const extensions = new ExtensionManager(extensionList(config));
4611
- registerHealthRoutes(app, HYDRA_VERSION3);
4975
+ registerHealthRoutes(app, HYDRA_VERSION);
4612
4976
  registerSessionRoutes(app, manager, {
4613
4977
  agentId: config.defaultAgent,
4614
4978
  cwd: config.defaultCwd
@@ -4627,8 +4991,8 @@ async function startDaemon(config) {
4627
4991
  await app.listen({ host: config.daemon.host, port: config.daemon.port });
4628
4992
  const address = app.server.address();
4629
4993
  const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
4630
- await fsp3.mkdir(paths.home(), { recursive: true });
4631
- await fsp3.writeFile(
4994
+ await fsp4.mkdir(paths.home(), { recursive: true });
4995
+ await fsp4.writeFile(
4632
4996
  paths.pidFile(),
4633
4997
  JSON.stringify({
4634
4998
  pid: process.pid,
@@ -4654,9 +5018,10 @@ async function startDaemon(config) {
4654
5018
  await manager.closeAll();
4655
5019
  await manager.flushMetaWrites();
4656
5020
  setBinaryInstallLogger(null);
5021
+ setNpmInstallLogger(null);
4657
5022
  await app.close();
4658
5023
  try {
4659
- fs9.unlinkSync(paths.pidFile());
5024
+ fs10.unlinkSync(paths.pidFile());
4660
5025
  } catch {
4661
5026
  }
4662
5027
  try {