@hydra-acp/cli 0.1.7 → 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";
@@ -43,6 +43,18 @@ var paths = {
43
43
  // machine's binaries cleanly separated. `ls agents/` immediately
44
44
  // shows which platforms have ever installed anything.
45
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
+ ),
46
58
  sessionsDir: () => path.join(hydraHome(), "sessions"),
47
59
  // One directory per session id under sessions/. Co-locates the
48
60
  // session record, its transcript, and any future per-session state
@@ -69,7 +81,16 @@ var DaemonConfig = z.object({
69
81
  authToken: z.string().min(16),
70
82
  logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
71
83
  tls: TlsConfig.optional(),
72
- 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)
73
94
  });
74
95
  var RegistryConfig = z.object({
75
96
  url: z.string().url().default(REGISTRY_URL_DEFAULT),
@@ -92,7 +113,14 @@ var TuiConfig = z.object({
92
113
  // text selection requires shift+drag to bypass mouse reporting. Set
93
114
  // false to disable capture — wheel scrollback stops working, but
94
115
  // plain click-drag selects text via the terminal emulator.
95
- mouse: z.boolean().default(true)
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)
96
124
  });
97
125
  var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
98
126
  var ExtensionBody = z.object({
@@ -128,9 +156,14 @@ var HydraConfig = z.object({
128
156
  tui: TuiConfig.default({
129
157
  repaintThrottleMs: 1e3,
130
158
  maxScrollbackLines: 1e4,
131
- mouse: true
159
+ mouse: true,
160
+ logMaxBytes: 5 * 1024 * 1024,
161
+ cwdColumnMaxWidth: 24
132
162
  })
133
163
  });
164
+ var HydraConfigReadOnly = HydraConfig.extend({
165
+ daemon: DaemonConfig.omit({ authToken: true })
166
+ });
134
167
  function extensionList(config) {
135
168
  return Object.entries(config.extensions).map(([name, body]) => ({
136
169
  name,
@@ -472,9 +505,129 @@ async function fileExists(p) {
472
505
  }
473
506
  }
474
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
+
475
624
  // src/core/registry.ts
476
625
  var NpxDistribution = z2.object({
477
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(),
478
631
  args: z2.array(z2.string()).optional(),
479
632
  env: z2.record(z2.string()).optional()
480
633
  });
@@ -638,9 +791,23 @@ async function planSpawn(agent, callerArgs = []) {
638
791
  if (agent.distribution.npx) {
639
792
  const npx = agent.distribution.npx;
640
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
+ });
641
808
  return {
642
- command: "npx",
643
- args: ["-y", npx.package, ...tail],
809
+ command: binPath,
810
+ args: tail,
644
811
  env: npx.env ?? {}
645
812
  };
646
813
  }
@@ -675,15 +842,12 @@ async function planSpawn(agent, callerArgs = []) {
675
842
  throw new Error(`Agent ${agent.id} has no usable distribution method.`);
676
843
  }
677
844
 
678
- // src/core/session-manager.ts
679
- import * as fs7 from "fs/promises";
680
- import { customAlphabet as customAlphabet3 } from "nanoid";
681
-
682
845
  // src/core/agent-instance.ts
683
- import { spawn as spawn2 } from "child_process";
846
+ import { spawn as spawn3 } from "child_process";
684
847
 
685
848
  // src/acp/types.ts
686
849
  import { z as z3 } from "zod";
850
+ var ACP_PROTOCOL_VERSION = 1;
687
851
  var JsonRpcErrorCodes = {
688
852
  ParseError: -32700,
689
853
  InvalidRequest: -32600,
@@ -1013,6 +1177,13 @@ var JsonRpcConnection = class _JsonRpcConnection {
1013
1177
  }
1014
1178
  await this.stream.close();
1015
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
+ }
1016
1187
  handleIncoming(message) {
1017
1188
  if ("method" in message) {
1018
1189
  if ("id" in message && message.id !== void 0) {
@@ -1105,17 +1276,22 @@ var JsonRpcConnection = class _JsonRpcConnection {
1105
1276
  };
1106
1277
 
1107
1278
  // src/core/agent-instance.ts
1279
+ var DEFAULT_STDERR_TAIL_BYTES = 4096;
1108
1280
  var AgentInstance = class _AgentInstance {
1109
1281
  agentId;
1110
1282
  cwd;
1111
1283
  connection;
1112
1284
  child;
1113
1285
  exited = false;
1286
+ killed = false;
1287
+ stderrTail = "";
1288
+ stderrTailBytes;
1114
1289
  exitHandlers = [];
1115
1290
  constructor(opts, child) {
1116
1291
  this.agentId = opts.agentId;
1117
1292
  this.cwd = opts.cwd;
1118
1293
  this.child = child;
1294
+ this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
1119
1295
  if (!child.stdout || !child.stdin) {
1120
1296
  throw new Error("agent subprocess missing stdio");
1121
1297
  }
@@ -1123,22 +1299,36 @@ var AgentInstance = class _AgentInstance {
1123
1299
  this.connection = new JsonRpcConnection(stream);
1124
1300
  child.stderr?.setEncoding("utf8");
1125
1301
  child.stderr?.on("data", (chunk) => {
1302
+ this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
1126
1303
  process.stderr.write(`[${opts.agentId}] ${chunk}`);
1127
1304
  });
1305
+ child.on("error", (err) => {
1306
+ const msg = this.formatFailure(err.message);
1307
+ this.connection.fail(new Error(msg));
1308
+ });
1128
1309
  child.on("exit", (code, signal) => {
1129
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
+ }
1130
1315
  for (const handler of this.exitHandlers) {
1131
1316
  handler(code, signal);
1132
1317
  }
1133
1318
  });
1134
1319
  }
1320
+ formatFailure(reason) {
1321
+ const tail = this.stderrTail.trim();
1322
+ return tail ? `${reason}
1323
+ stderr: ${tail}` : reason;
1324
+ }
1135
1325
  static spawn(opts) {
1136
1326
  const env = {
1137
1327
  ...process.env,
1138
1328
  ...opts.plan.env,
1139
1329
  ...opts.extraEnv ?? {}
1140
1330
  };
1141
- const child = spawn2(opts.plan.command, opts.plan.args, {
1331
+ const child = spawn3(opts.plan.command, opts.plan.args, {
1142
1332
  cwd: opts.cwd,
1143
1333
  env,
1144
1334
  stdio: ["pipe", "pipe", "pipe"]
@@ -1155,11 +1345,17 @@ var AgentInstance = class _AgentInstance {
1155
1345
  if (this.exited) {
1156
1346
  return;
1157
1347
  }
1348
+ this.killed = true;
1158
1349
  await this.connection.close().catch(() => void 0);
1159
1350
  this.child.kill(signal);
1160
1351
  }
1161
1352
  };
1162
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
+
1163
1359
  // src/core/session.ts
1164
1360
  import { customAlphabet } from "nanoid";
1165
1361
 
@@ -1189,8 +1385,7 @@ function hydraCommandsAsAdvertised() {
1189
1385
  var HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1190
1386
  var generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
1191
1387
  var HYDRA_SESSION_PREFIX = "hydra_session_";
1192
- var MAX_HISTORY_ENTRIES = 1e3;
1193
- var COMPACT_EVERY = 200;
1388
+ var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
1194
1389
  var Session = class {
1195
1390
  sessionId;
1196
1391
  cwd;
@@ -1232,11 +1427,13 @@ var Session = class {
1232
1427
  // Bumped by broadcastPromptReceived, cleared by broadcastTurnComplete.
1233
1428
  // Drives the mid-turn elapsed counter delivered to fresh attachers.
1234
1429
  promptStartedAt;
1235
- // Counts appends since the last compaction. When it hits COMPACT_EVERY
1430
+ // Counts appends since the last compaction. When it hits compactEvery
1236
1431
  // we ask the history store to trim the file to the most recent
1237
- // MAX_HISTORY_ENTRIES. Keeps file growth bounded without per-append
1432
+ // historyMaxEntries. Keeps file growth bounded without per-append
1238
1433
  // file-size checks.
1239
1434
  appendCount = 0;
1435
+ historyMaxEntries;
1436
+ compactEvery;
1240
1437
  // Permission requests that have been broadcast to one or more
1241
1438
  // clients but have not yet resolved. Replayed to clients that
1242
1439
  // attach mid-flight so a late joiner sees the prompt instead of an
@@ -1293,6 +1490,8 @@ var Session = class {
1293
1490
  this.firstPromptSeeded = true;
1294
1491
  }
1295
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));
1296
1495
  this.updatedAt = Date.now();
1297
1496
  this.createdAt = init.createdAt ?? this.updatedAt;
1298
1497
  this.lastRecordedAt = this.updatedAt;
@@ -2131,9 +2330,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2131
2330
  if (this.historyStore) {
2132
2331
  const store = this.historyStore;
2133
2332
  void store.append(this.sessionId, entry).catch(() => void 0);
2134
- if (this.appendCount >= COMPACT_EVERY) {
2333
+ if (this.appendCount >= this.compactEvery) {
2135
2334
  this.appendCount = 0;
2136
- void store.compact(this.sessionId, MAX_HISTORY_ENTRIES).catch(
2335
+ void store.compact(this.sessionId, this.historyMaxEntries).catch(
2137
2336
  () => void 0
2138
2337
  );
2139
2338
  }
@@ -2335,7 +2534,7 @@ function firstLine(text, max) {
2335
2534
 
2336
2535
  // src/core/session-store.ts
2337
2536
  import * as fs4 from "fs/promises";
2338
- import * as path3 from "path";
2537
+ import * as path4 from "path";
2339
2538
  import { customAlphabet as customAlphabet2 } from "nanoid";
2340
2539
  import { z as z4 } from "zod";
2341
2540
  var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -2507,12 +2706,16 @@ function recordFromMemorySession(args) {
2507
2706
  // src/core/history-store.ts
2508
2707
  import * as fs5 from "fs/promises";
2509
2708
  var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
2510
- var MAX_ENTRIES = 1e3;
2709
+ var DEFAULT_MAX_ENTRIES = 1e3;
2511
2710
  var HistoryStore = class {
2512
2711
  // Serialize writes per session id so appends and rewrites don't
2513
2712
  // interleave JSONL lines on disk. The chain swallows errors so one
2514
2713
  // failed append doesn't poison every subsequent write.
2515
2714
  writeQueues = /* @__PURE__ */ new Map();
2715
+ maxEntries;
2716
+ constructor(options = {}) {
2717
+ this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
2718
+ }
2516
2719
  async append(sessionId, entry) {
2517
2720
  if (!SESSION_ID_PATTERN2.test(sessionId)) {
2518
2721
  return;
@@ -2614,8 +2817,8 @@ var HistoryStore = class {
2614
2817
  recordedAt: obj.recordedAt
2615
2818
  });
2616
2819
  }
2617
- if (out.length > MAX_ENTRIES) {
2618
- return out.slice(-MAX_ENTRIES);
2820
+ if (out.length > this.maxEntries) {
2821
+ return out.slice(-this.maxEntries);
2619
2822
  }
2620
2823
  return out;
2621
2824
  }
@@ -2658,13 +2861,40 @@ var HistoryStore = class {
2658
2861
 
2659
2862
  // src/tui/history.ts
2660
2863
  import { promises as fs6 } from "fs";
2661
- import * as path4 from "path";
2864
+ import * as path5 from "path";
2662
2865
  async function saveHistory(file, history) {
2663
- await fs6.mkdir(path4.dirname(file), { recursive: true });
2866
+ await fs6.mkdir(path5.dirname(file), { recursive: true });
2664
2867
  const lines = history.map((entry) => JSON.stringify(entry));
2665
2868
  await fs6.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
2666
2869
  }
2667
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
+
2668
2898
  // src/core/session-manager.ts
2669
2899
  var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2670
2900
  var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
@@ -2673,7 +2903,8 @@ var SessionManager = class {
2673
2903
  this.registry = registry;
2674
2904
  this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
2675
2905
  this.store = store ?? new SessionStore();
2676
- this.histories = new HistoryStore();
2906
+ this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
2907
+ this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
2677
2908
  this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
2678
2909
  this.defaultModels = options.defaultModels ?? {};
2679
2910
  }
@@ -2685,6 +2916,7 @@ var SessionManager = class {
2685
2916
  histories;
2686
2917
  idleTimeoutMs;
2687
2918
  defaultModels;
2919
+ sessionHistoryMaxEntries;
2688
2920
  // Serialize meta.json read-modify-write operations per session id so
2689
2921
  // concurrent snapshot updates (e.g. an agent emitting model + mode
2690
2922
  // back-to-back) don't lose writes via interleaved reads.
@@ -2708,6 +2940,7 @@ var SessionManager = class {
2708
2940
  idleTimeoutMs: this.idleTimeoutMs,
2709
2941
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
2710
2942
  historyStore: this.histories,
2943
+ historyMaxEntries: this.sessionHistoryMaxEntries,
2711
2944
  currentModel: fresh.initialModel
2712
2945
  });
2713
2946
  await this.attachManagerHooks(session);
@@ -2759,11 +2992,16 @@ var SessionManager = class {
2759
2992
  cwd: params.cwd,
2760
2993
  plan
2761
2994
  });
2762
- await agent.connection.request("initialize", {
2763
- protocolVersion: 1,
2764
- clientCapabilities: {},
2765
- clientInfo: { name: "hydra", version: "0.1.0" }
2766
- });
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
+ }
2767
3005
  let loadResult;
2768
3006
  try {
2769
3007
  loadResult = await agent.connection.request(
@@ -2775,10 +3013,12 @@ var SessionManager = class {
2775
3013
  }
2776
3014
  );
2777
3015
  } catch (err) {
2778
- await agent.kill().catch(() => void 0);
2779
- throw new Error(
2780
- `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
+ `
2781
3019
  );
3020
+ await agent.kill().catch(() => void 0);
3021
+ return this.doResurrectFromImport(params);
2782
3022
  }
2783
3023
  const session = new Session({
2784
3024
  sessionId: params.hydraSessionId,
@@ -2792,6 +3032,7 @@ var SessionManager = class {
2792
3032
  idleTimeoutMs: this.idleTimeoutMs,
2793
3033
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
2794
3034
  historyStore: this.histories,
3035
+ historyMaxEntries: this.sessionHistoryMaxEntries,
2795
3036
  // Prefer what we previously stored from a current_model_update; if
2796
3037
  // we never captured one (e.g. old opencode sessions on disk before
2797
3038
  // this fix), fall back to the model the agent ships in its
@@ -2818,15 +3059,16 @@ var SessionManager = class {
2818
3059
  // so subsequent resurrects of this session use the normal session/load
2819
3060
  // path.
2820
3061
  async doResurrectFromImport(params) {
3062
+ const cwd = await this.resolveImportCwd(params.cwd);
2821
3063
  const fresh = await this.bootstrapAgent({
2822
3064
  agentId: params.agentId,
2823
- cwd: params.cwd,
3065
+ cwd,
2824
3066
  agentArgs: params.agentArgs,
2825
3067
  mcpServers: []
2826
3068
  });
2827
3069
  const session = new Session({
2828
3070
  sessionId: params.hydraSessionId,
2829
- cwd: params.cwd,
3071
+ cwd,
2830
3072
  agentId: params.agentId,
2831
3073
  agent: fresh.agent,
2832
3074
  upstreamSessionId: fresh.upstreamSessionId,
@@ -2836,6 +3078,7 @@ var SessionManager = class {
2836
3078
  idleTimeoutMs: this.idleTimeoutMs,
2837
3079
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
2838
3080
  historyStore: this.histories,
3081
+ historyMaxEntries: this.sessionHistoryMaxEntries,
2839
3082
  // Prefer the stored value (set by a previous current_model_update);
2840
3083
  // fall back to whatever the agent ships in its session/new response.
2841
3084
  currentModel: params.currentModel ?? fresh.initialModel,
@@ -2849,6 +3092,16 @@ var SessionManager = class {
2849
3092
  void session.seedFromImport().catch(() => void 0);
2850
3093
  return session;
2851
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
+ }
2852
3105
  // Bootstrap a fresh agent process: registry resolve → spawn → initialize
2853
3106
  // → session/new. Shared by create() and the /hydra agent path so both
2854
3107
  // go through the same env / capabilities / error-handling.
@@ -2869,9 +3122,9 @@ var SessionManager = class {
2869
3122
  });
2870
3123
  try {
2871
3124
  await agent.connection.request("initialize", {
2872
- protocolVersion: 1,
3125
+ protocolVersion: ACP_PROTOCOL_VERSION,
2873
3126
  clientCapabilities: {},
2874
- clientInfo: { name: "hydra", version: "0.1.0" }
3127
+ clientInfo: { name: "hydra", version: HYDRA_VERSION }
2875
3128
  });
2876
3129
  const newResult = await agent.connection.request(
2877
3130
  "session/new",
@@ -3151,7 +3404,8 @@ var SessionManager = class {
3151
3404
  await this.writeImportedRecord({
3152
3405
  sessionId: existing.sessionId,
3153
3406
  bundle,
3154
- preservedCreatedAt: existing.createdAt
3407
+ preservedCreatedAt: existing.createdAt,
3408
+ cwd: opts.cwd
3155
3409
  });
3156
3410
  return {
3157
3411
  sessionId: existing.sessionId,
@@ -3160,7 +3414,11 @@ var SessionManager = class {
3160
3414
  };
3161
3415
  }
3162
3416
  const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
3163
- await this.writeImportedRecord({ sessionId: newId, bundle });
3417
+ await this.writeImportedRecord({
3418
+ sessionId: newId,
3419
+ bundle,
3420
+ cwd: opts.cwd
3421
+ });
3164
3422
  return {
3165
3423
  sessionId: newId,
3166
3424
  importedFromSessionId: bundle.session.sessionId,
@@ -3190,7 +3448,7 @@ var SessionManager = class {
3190
3448
  upstreamSessionId: "",
3191
3449
  importedFromSessionId: args.bundle.session.sessionId,
3192
3450
  agentId: args.bundle.session.agentId,
3193
- cwd: args.bundle.session.cwd,
3451
+ cwd: args.cwd ?? args.bundle.session.cwd,
3194
3452
  title: args.bundle.session.title,
3195
3453
  currentModel: args.bundle.session.currentModel,
3196
3454
  currentMode: args.bundle.session.currentMode,
@@ -3383,7 +3641,7 @@ function asString(value) {
3383
3641
  }
3384
3642
  async function loadPromptHistorySafely(sessionId) {
3385
3643
  try {
3386
- const raw = await fs7.readFile(paths.tuiHistoryFile(sessionId), "utf8");
3644
+ const raw = await fs8.readFile(paths.tuiHistoryFile(sessionId), "utf8");
3387
3645
  const out = [];
3388
3646
  for (const line of raw.split("\n")) {
3389
3647
  if (line.length === 0) {
@@ -3404,7 +3662,7 @@ async function loadPromptHistorySafely(sessionId) {
3404
3662
  }
3405
3663
  async function historyMtimeIso(sessionId) {
3406
3664
  try {
3407
- const st = await fs7.stat(paths.historyFile(sessionId));
3665
+ const st = await fs8.stat(paths.historyFile(sessionId));
3408
3666
  return new Date(st.mtimeMs).toISOString();
3409
3667
  } catch {
3410
3668
  return void 0;
@@ -3412,10 +3670,10 @@ async function historyMtimeIso(sessionId) {
3412
3670
  }
3413
3671
 
3414
3672
  // src/core/extensions.ts
3415
- import { spawn as spawn3 } from "child_process";
3416
- import * as fs8 from "fs";
3417
- import * as fsp2 from "fs/promises";
3418
- 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";
3419
3677
  var RESTART_BASE_MS = 1e3;
3420
3678
  var RESTART_CAP_MS = 6e4;
3421
3679
  var STOP_GRACE_MS = 3e3;
@@ -3436,7 +3694,7 @@ var ExtensionManager = class {
3436
3694
  if (!this.context) {
3437
3695
  throw new Error("ExtensionManager: setContext must be called before start");
3438
3696
  }
3439
- await fsp2.mkdir(paths.extensionsDir(), { recursive: true });
3697
+ await fsp3.mkdir(paths.extensionsDir(), { recursive: true });
3440
3698
  await this.reapOrphans();
3441
3699
  for (const entry of this.entries.values()) {
3442
3700
  if (!entry.config.enabled) {
@@ -3645,7 +3903,7 @@ var ExtensionManager = class {
3645
3903
  async reapOrphans() {
3646
3904
  let entries;
3647
3905
  try {
3648
- entries = await fsp2.readdir(paths.extensionsDir());
3906
+ entries = await fsp3.readdir(paths.extensionsDir());
3649
3907
  } catch (err) {
3650
3908
  const e = err;
3651
3909
  if (e.code === "ENOENT") {
@@ -3657,10 +3915,10 @@ var ExtensionManager = class {
3657
3915
  if (!entry.endsWith(".pid")) {
3658
3916
  continue;
3659
3917
  }
3660
- const pidPath = path5.join(paths.extensionsDir(), entry);
3918
+ const pidPath = path7.join(paths.extensionsDir(), entry);
3661
3919
  let pid;
3662
3920
  try {
3663
- const raw = await fsp2.readFile(pidPath, "utf8");
3921
+ const raw = await fsp3.readFile(pidPath, "utf8");
3664
3922
  const parsed = Number.parseInt(raw.trim(), 10);
3665
3923
  if (Number.isInteger(parsed) && parsed > 0) {
3666
3924
  pid = parsed;
@@ -3683,7 +3941,7 @@ var ExtensionManager = class {
3683
3941
  }
3684
3942
  }
3685
3943
  }
3686
- await fsp2.unlink(pidPath).catch(() => void 0);
3944
+ await fsp3.unlink(pidPath).catch(() => void 0);
3687
3945
  }
3688
3946
  }
3689
3947
  spawn(entry, attempt) {
@@ -3696,7 +3954,7 @@ var ExtensionManager = class {
3696
3954
  }
3697
3955
  const ext = entry.config;
3698
3956
  const command = ext.command.length > 0 ? ext.command : [ext.name];
3699
- const logStream = fs8.createWriteStream(paths.extensionLogFile(ext.name), {
3957
+ const logStream = fs9.createWriteStream(paths.extensionLogFile(ext.name), {
3700
3958
  flags: "a"
3701
3959
  });
3702
3960
  logStream.write(
@@ -3724,7 +3982,7 @@ var ExtensionManager = class {
3724
3982
  const args = [...baseArgs, ...ext.args];
3725
3983
  let child;
3726
3984
  try {
3727
- child = spawn3(cmd, args, {
3985
+ child = spawn4(cmd, args, {
3728
3986
  env,
3729
3987
  stdio: ["ignore", "pipe", "pipe"],
3730
3988
  detached: false
@@ -3746,7 +4004,7 @@ var ExtensionManager = class {
3746
4004
  }
3747
4005
  if (typeof child.pid === "number") {
3748
4006
  try {
3749
- fs8.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
4007
+ fs9.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
3750
4008
  `, {
3751
4009
  encoding: "utf8",
3752
4010
  mode: 384
@@ -3771,7 +4029,7 @@ var ExtensionManager = class {
3771
4029
  });
3772
4030
  child.on("exit", (code, signal) => {
3773
4031
  try {
3774
- fs8.unlinkSync(paths.extensionPidFile(ext.name));
4032
+ fs9.unlinkSync(paths.extensionPidFile(ext.name));
3775
4033
  } catch {
3776
4034
  }
3777
4035
  logStream.write(
@@ -3880,7 +4138,7 @@ function constantTimeEqual(a, b) {
3880
4138
  }
3881
4139
 
3882
4140
  // src/daemon/routes/sessions.ts
3883
- import * as os2 from "os";
4141
+ import * as os3 from "os";
3884
4142
 
3885
4143
  // src/core/bundle.ts
3886
4144
  import { z as z5 } from "zod";
@@ -3950,7 +4208,6 @@ function decodeBundle(raw) {
3950
4208
  }
3951
4209
 
3952
4210
  // src/daemon/routes/sessions.ts
3953
- var HYDRA_VERSION = "0.1.0";
3954
4211
  function registerSessionRoutes(app, manager, defaults) {
3955
4212
  app.get("/v1/sessions", async (request) => {
3956
4213
  const query = request.query;
@@ -4021,7 +4278,7 @@ function registerSessionRoutes(app, manager, defaults) {
4021
4278
  history: exported.history,
4022
4279
  promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
4023
4280
  hydraVersion: HYDRA_VERSION,
4024
- machine: os2.hostname()
4281
+ machine: os3.hostname()
4025
4282
  });
4026
4283
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4027
4284
  reply.header(
@@ -4036,6 +4293,14 @@ function registerSessionRoutes(app, manager, defaults) {
4036
4293
  reply.code(400).send({ error: "missing bundle" });
4037
4294
  return;
4038
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
+ }
4039
4304
  let bundle;
4040
4305
  try {
4041
4306
  bundle = decodeBundle(body.bundle);
@@ -4048,7 +4313,8 @@ function registerSessionRoutes(app, manager, defaults) {
4048
4313
  }
4049
4314
  try {
4050
4315
  const result = await manager.importBundle(bundle, {
4051
- replace: body.replace === true
4316
+ replace: body.replace === true,
4317
+ ...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
4052
4318
  });
4053
4319
  reply.code(201).send(result);
4054
4320
  } catch (err) {
@@ -4359,8 +4625,6 @@ function wsToMessageStream(ws) {
4359
4625
  }
4360
4626
 
4361
4627
  // src/daemon/acp-ws.ts
4362
- var HYDRA_VERSION2 = "0.1.0";
4363
- var HYDRA_PROTOCOL_VERSION = 1;
4364
4628
  function registerAcpWsEndpoint(app, deps) {
4365
4629
  app.get("/acp", { websocket: true }, (socket, request) => {
4366
4630
  const token = tokenFromUpgradeRequest({
@@ -4623,8 +4887,8 @@ function buildResponseMeta(session) {
4623
4887
  }
4624
4888
  function buildInitializeResult() {
4625
4889
  return {
4626
- protocolVersion: HYDRA_PROTOCOL_VERSION,
4627
- agentInfo: { name: "hydra", version: HYDRA_VERSION2 },
4890
+ protocolVersion: ACP_PROTOCOL_VERSION,
4891
+ agentInfo: { name: "hydra", version: HYDRA_VERSION },
4628
4892
  agentCapabilities: {
4629
4893
  // hydra is a transparent proxy: prompt blocks and MCP server configs are
4630
4894
  // forwarded to the underlying agent unchanged. We claim the union of
@@ -4663,14 +4927,13 @@ function bindClientToSession(connection, session, state, clientInfo) {
4663
4927
  }
4664
4928
 
4665
4929
  // src/daemon/server.ts
4666
- var HYDRA_VERSION3 = "0.1.0";
4667
4930
  async function startDaemon(config) {
4668
4931
  ensureLoopbackOrTls(config);
4669
4932
  const httpsOptions = config.daemon.tls ? {
4670
- key: await fsp3.readFile(config.daemon.tls.key),
4671
- 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)
4672
4935
  } : void 0;
4673
- await fsp3.mkdir(paths.home(), { recursive: true });
4936
+ await fsp4.mkdir(paths.home(), { recursive: true });
4674
4937
  const { stream: logStream, fileStream } = await buildLogStream(
4675
4938
  config.daemon.logLevel
4676
4939
  );
@@ -4679,12 +4942,18 @@ async function startDaemon(config) {
4679
4942
  level: config.daemon.logLevel,
4680
4943
  stream: logStream
4681
4944
  },
4682
- 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
4683
4949
  });
4684
4950
  await app.register(websocketPlugin);
4685
4951
  setBinaryInstallLogger((msg) => {
4686
4952
  app.log.info(msg);
4687
4953
  });
4954
+ setNpmInstallLogger((msg) => {
4955
+ app.log.info(msg);
4956
+ });
4688
4957
  const auth = bearerAuth({ config });
4689
4958
  app.addHook("onRequest", async (request, reply) => {
4690
4959
  if (request.routeOptions.config?.skipAuth) {
@@ -4696,12 +4965,14 @@ async function startDaemon(config) {
4696
4965
  await auth(request, reply);
4697
4966
  });
4698
4967
  const registry = new Registry(config);
4699
- 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, {
4700
4970
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
4701
- defaultModels: config.defaultModels
4971
+ defaultModels: config.defaultModels,
4972
+ sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
4702
4973
  });
4703
4974
  const extensions = new ExtensionManager(extensionList(config));
4704
- registerHealthRoutes(app, HYDRA_VERSION3);
4975
+ registerHealthRoutes(app, HYDRA_VERSION);
4705
4976
  registerSessionRoutes(app, manager, {
4706
4977
  agentId: config.defaultAgent,
4707
4978
  cwd: config.defaultCwd
@@ -4720,8 +4991,8 @@ async function startDaemon(config) {
4720
4991
  await app.listen({ host: config.daemon.host, port: config.daemon.port });
4721
4992
  const address = app.server.address();
4722
4993
  const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
4723
- await fsp3.mkdir(paths.home(), { recursive: true });
4724
- await fsp3.writeFile(
4994
+ await fsp4.mkdir(paths.home(), { recursive: true });
4995
+ await fsp4.writeFile(
4725
4996
  paths.pidFile(),
4726
4997
  JSON.stringify({
4727
4998
  pid: process.pid,
@@ -4747,9 +5018,10 @@ async function startDaemon(config) {
4747
5018
  await manager.closeAll();
4748
5019
  await manager.flushMetaWrites();
4749
5020
  setBinaryInstallLogger(null);
5021
+ setNpmInstallLogger(null);
4750
5022
  await app.close();
4751
5023
  try {
4752
- fs9.unlinkSync(paths.pidFile());
5024
+ fs10.unlinkSync(paths.pidFile());
4753
5025
  } catch {
4754
5026
  }
4755
5027
  try {