@hydra-acp/cli 0.1.62 → 0.1.63

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,5 +1,5 @@
1
1
  // src/daemon/server.ts
2
- import * as fs17 from "fs";
2
+ import * as fs18 from "fs";
3
3
  import * as fsp7 from "fs/promises";
4
4
  import Fastify from "fastify";
5
5
  import websocketPlugin from "@fastify/websocket";
@@ -26,6 +26,19 @@ import { z } from "zod";
26
26
  import * as path from "path";
27
27
  import * as os from "os";
28
28
  var ROOT_ENV = "HYDRA_ACP_HOME";
29
+ function shortenHomePath(p) {
30
+ const home = os.homedir();
31
+ if (!home) {
32
+ return p;
33
+ }
34
+ if (p === home) {
35
+ return "~";
36
+ }
37
+ if (p.startsWith(home + "/")) {
38
+ return "~" + p.slice(home.length);
39
+ }
40
+ return p;
41
+ }
29
42
  function hydraHome() {
30
43
  const override = process.env[ROOT_ENV];
31
44
  if (override && override.length > 0) {
@@ -82,6 +95,12 @@ var paths = {
82
95
  sessionDir: (id) => path.join(hydraHome(), "sessions", id),
83
96
  sessionFile: (id) => path.join(hydraHome(), "sessions", id, "meta.json"),
84
97
  historyFile: (id) => path.join(hydraHome(), "sessions", id, "history.jsonl"),
98
+ // Content-addressed store for heavy tool payload (diff bodies, stdout)
99
+ // externalized out of history.jsonl. One file per unique blob, named by
100
+ // its sha256, so repeated identical content (e.g. an agent re-emitting
101
+ // the same full-file diff on every status tick) dedupes to one file.
102
+ toolsDir: (id) => path.join(hydraHome(), "sessions", id, "tools"),
103
+ toolBlobFile: (id, hash) => path.join(hydraHome(), "sessions", id, "tools", hash),
85
104
  // Persisted prompt queue for a session. ndjson, one record per
86
105
  // entry. Survives daemon restarts so queued prompts get a chance to
87
106
  // run rather than being silently lost. Entries are removed BEFORE
@@ -350,6 +369,29 @@ var TuiConfig = z.object({
350
369
  // suppress them — the TUI hotkey ^T toggles this at runtime without
351
370
  // persisting back to config.
352
371
  showThoughts: z.boolean().default(true),
372
+ // How the terminal renders East-Asian "Ambiguous" width glyphs (em-dash
373
+ // —, smart quotes “ ”, ellipsis …, middle-dot ·). Most modern terminals
374
+ // draw them 1 col wide ("narrow"); CJK-locale / legacy setups draw them 2
375
+ // cols wide ("wide"). Defaults to "wide": counting ambiguous glyphs as 2
376
+ // cols never overflows the right margin (the worst case is wrapping a
377
+ // column early on narrow terminals, which is benign), whereas "narrow"
378
+ // bleeds past the margin on wide terminals. The thought gutter uses an
379
+ // ASCII marker, so this no longer affects marker alignment either way.
380
+ ambiguousWidth: z.enum(["narrow", "wide"]).default("wide"),
381
+ // How the TUI receives tool payload on attach/replay.
382
+ // "references" — the lean path (default): the daemon ships blob refs and
383
+ // the TUI fetches a diff/output body on demand when
384
+ // expanded, cutting replay size on tool-heavy sessions.
385
+ // Collapsed rows never fetch (they show a size hint), and
386
+ // old inline sessions/live turns are unaffected (no refs).
387
+ // "inline" — full content up front (the pre-externalization shape).
388
+ toolContent: z.enum(["inline", "references"]).default("references"),
389
+ // Unchanged context lines shown around each change in an expanded Edited
390
+ // diff. Some agents (e.g. pi) report edits as full-file old/new text via
391
+ // ACP "diff" content blocks; without hunking a 1-line edit would render
392
+ // the entire file. This bounds the context so only the changed region (±N
393
+ // lines) shows, with runs of unchanged lines collapsed to a marker.
394
+ diffContextLines: z.number().int().min(0).default(3),
353
395
  // Cap on entries kept in the cross-session global prompt-history file
354
396
  // (~/.hydra-acp/prompt-history). This is the ^P / ^R recall list
355
397
  // shared across all sessions; it's append-only on disk, so long-lived
@@ -465,6 +507,12 @@ var HydraConfig = z.object({
465
507
  // a literal string ("~", "~/dev", "$HOME/work") so the config file is
466
508
  // portable across machines; expanded via expandHome at use time.
467
509
  defaultCwd: z.string().default("~"),
510
+ // Gzip externalized tool-content blobs at rest (tools/<sha256>.gz).
511
+ // Default true — text diffs/output compress ~3.5x and decompression is
512
+ // lazy (only on diff expand in references mode). Set false to write plain
513
+ // blobs instead, as an escape hatch if gzip CPU is ever a problem; reads
514
+ // transparently handle both, so flipping it only affects new writes.
515
+ compressToolContent: z.boolean().default(true),
468
516
  // Cap on cold sessions shown in CLI `sessions` listing and the TUI
469
517
  // picker. Live sessions are always included; cold are sorted by
470
518
  // recency and truncated to this count. `--all` overrides in the CLI.
@@ -486,6 +534,9 @@ var HydraConfig = z.object({
486
534
  progressIndicator: true,
487
535
  defaultEnterAction: "amend",
488
536
  showThoughts: true,
537
+ ambiguousWidth: "wide",
538
+ toolContent: "references",
539
+ diffContextLines: 3,
489
540
  promptHistoryMaxEntries: 2e3,
490
541
  maxToolItems: 5,
491
542
  maxPlanItems: 5,
@@ -564,13 +615,113 @@ function expandHome(p) {
564
615
  return p;
565
616
  }
566
617
 
618
+ // src/core/tool-store.ts
619
+ import * as fs4 from "fs/promises";
620
+ import { createHash } from "crypto";
621
+ import { gzip as gzipCb, gunzip as gunzipCb } from "zlib";
622
+ import { promisify } from "util";
623
+ var gzip = promisify(gzipCb);
624
+ var gunzip = promisify(gunzipCb);
625
+ var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
626
+ var HASH_PATTERN = /^[a-f0-9]{64}$/;
627
+ var compressBlobs = true;
628
+ function setToolBlobCompression(enabled) {
629
+ compressBlobs = enabled;
630
+ }
631
+ function safe(sessionId, hash) {
632
+ if (!SESSION_ID_PATTERN.test(sessionId)) {
633
+ return false;
634
+ }
635
+ return hash === void 0 || HASH_PATTERN.test(hash);
636
+ }
637
+ function gzPath(sessionId, hash) {
638
+ return `${paths.toolBlobFile(sessionId, hash)}.gz`;
639
+ }
640
+ async function putToolBlob(sessionId, text) {
641
+ if (!safe(sessionId)) {
642
+ return null;
643
+ }
644
+ const hash = createHash("sha256").update(text, "utf8").digest("hex");
645
+ const gzFile = gzPath(sessionId, hash);
646
+ const plainFile = paths.toolBlobFile(sessionId, hash);
647
+ for (const existing of [gzFile, plainFile]) {
648
+ try {
649
+ await fs4.access(existing);
650
+ return hash;
651
+ } catch {
652
+ }
653
+ }
654
+ await fs4.mkdir(paths.toolsDir(sessionId), { recursive: true });
655
+ const file = compressBlobs ? gzFile : plainFile;
656
+ const data = compressBlobs ? await gzip(Buffer.from(text, "utf8")) : Buffer.from(text, "utf8");
657
+ await fs4.writeFile(file, data, { mode: 384, flag: "wx" }).catch(async (err) => {
658
+ if (err.code !== "EEXIST") {
659
+ throw err;
660
+ }
661
+ });
662
+ return hash;
663
+ }
664
+ async function getToolBlob(sessionId, hash) {
665
+ if (!safe(sessionId, hash)) {
666
+ return null;
667
+ }
668
+ try {
669
+ const buf = await fs4.readFile(gzPath(sessionId, hash));
670
+ return (await gunzip(buf)).toString("utf8");
671
+ } catch {
672
+ }
673
+ try {
674
+ return await fs4.readFile(paths.toolBlobFile(sessionId, hash), "utf8");
675
+ } catch {
676
+ return null;
677
+ }
678
+ }
679
+ async function readToolBlobGz(sessionId, hash) {
680
+ if (!safe(sessionId, hash)) {
681
+ return null;
682
+ }
683
+ try {
684
+ return await fs4.readFile(gzPath(sessionId, hash));
685
+ } catch {
686
+ }
687
+ try {
688
+ const plain = await fs4.readFile(paths.toolBlobFile(sessionId, hash));
689
+ return await gzip(plain);
690
+ } catch {
691
+ return null;
692
+ }
693
+ }
694
+ async function writeToolBlobGz(sessionId, hash, gzBytes) {
695
+ if (!safe(sessionId, hash)) {
696
+ return;
697
+ }
698
+ const file = gzPath(sessionId, hash);
699
+ try {
700
+ await fs4.access(file);
701
+ return;
702
+ } catch {
703
+ }
704
+ await fs4.mkdir(paths.toolsDir(sessionId), { recursive: true });
705
+ await fs4.writeFile(file, gzBytes, { mode: 384, flag: "wx" }).catch((err) => {
706
+ if (err.code !== "EEXIST") {
707
+ throw err;
708
+ }
709
+ });
710
+ }
711
+ async function deleteToolBlobs(sessionId) {
712
+ if (!SESSION_ID_PATTERN.test(sessionId)) {
713
+ return;
714
+ }
715
+ await fs4.rm(paths.toolsDir(sessionId), { recursive: true, force: true }).catch(() => void 0);
716
+ }
717
+
567
718
  // src/core/registry.ts
568
- import * as fs5 from "fs/promises";
719
+ import * as fs6 from "fs/promises";
569
720
  import * as path4 from "path";
570
721
  import { z as z2 } from "zod";
571
722
 
572
723
  // src/core/binary-install.ts
573
- import * as fs4 from "fs";
724
+ import * as fs5 from "fs";
574
725
  import * as fsp from "fs/promises";
575
726
  import * as path2 from "path";
576
727
  import { spawn } from "child_process";
@@ -703,7 +854,7 @@ async function downloadTo(args) {
703
854
  );
704
855
  }
705
856
  const total = Number(response.headers.get("content-length") ?? "0");
706
- const out = fs4.createWriteStream(dest);
857
+ const out = fs5.createWriteStream(dest);
707
858
  const nodeStream = Readable.fromWeb(response.body);
708
859
  safeEmit(args.onProgress, {
709
860
  phase: "download_start",
@@ -1357,7 +1508,7 @@ async function agentInstallState(agent) {
1357
1508
  }
1358
1509
  async function fileExists3(p) {
1359
1510
  try {
1360
- await fs5.access(p);
1511
+ await fs6.access(p);
1361
1512
  return true;
1362
1513
  } catch {
1363
1514
  return false;
@@ -1440,7 +1591,7 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
1440
1591
 
1441
1592
  // src/core/agent-instance.ts
1442
1593
  import { spawn as spawn3 } from "child_process";
1443
- import * as fs6 from "fs";
1594
+ import * as fs7 from "fs";
1444
1595
  import * as path5 from "path";
1445
1596
 
1446
1597
  // src/acp/types.ts
@@ -1559,6 +1710,9 @@ function extractHydraMeta(meta) {
1559
1710
  if (typeof obj.dripSpeed === "number" && obj.dripSpeed > 0) {
1560
1711
  out.dripSpeed = obj.dripSpeed;
1561
1712
  }
1713
+ if (obj.toolContent === "inline" || obj.toolContent === "references") {
1714
+ out.toolContent = obj.toolContent;
1715
+ }
1562
1716
  if (obj.detachStatus === "detached") {
1563
1717
  out.detachStatus = obj.detachStatus;
1564
1718
  }
@@ -2452,8 +2606,8 @@ stderr: ${tail}` : reason;
2452
2606
  function openAgentLog(agentId) {
2453
2607
  try {
2454
2608
  const logPath = paths.agentLogFile(agentId);
2455
- fs6.mkdirSync(path5.dirname(logPath), { recursive: true });
2456
- const stream = fs6.createWriteStream(logPath, { flags: "a" });
2609
+ fs7.mkdirSync(path5.dirname(logPath), { recursive: true });
2610
+ const stream = fs7.createWriteStream(logPath, { flags: "a" });
2457
2611
  stream.on("error", () => void 0);
2458
2612
  return stream;
2459
2613
  } catch {
@@ -2462,7 +2616,7 @@ function openAgentLog(agentId) {
2462
2616
  }
2463
2617
 
2464
2618
  // src/core/session-manager.ts
2465
- import * as fs14 from "fs/promises";
2619
+ import * as fs15 from "fs/promises";
2466
2620
  import * as os2 from "os";
2467
2621
  import * as path9 from "path";
2468
2622
  import { customAlphabet as customAlphabet3 } from "nanoid";
@@ -2936,6 +3090,30 @@ var HYDRA_COMMANDS = [
2936
3090
  ];
2937
3091
  var VERB_INDEX = new Map(HYDRA_COMMANDS.map((c) => [c.verb, c]));
2938
3092
 
3093
+ // src/core/model-resolve.ts
3094
+ function trailingSegment(modelId) {
3095
+ const slash = modelId.lastIndexOf("/");
3096
+ const tail = slash === -1 ? modelId : modelId.slice(slash + 1);
3097
+ return tail.toLowerCase();
3098
+ }
3099
+ function resolveModelId(requested, advertised) {
3100
+ if (advertised.length === 0) {
3101
+ return { kind: "none", requested };
3102
+ }
3103
+ if (advertised.some((m) => m.modelId === requested)) {
3104
+ return { kind: "exact", modelId: requested };
3105
+ }
3106
+ const wantKey = trailingSegment(requested);
3107
+ const candidates = advertised.map((m) => m.modelId).filter((id) => trailingSegment(id) === wantKey);
3108
+ if (candidates.length === 1) {
3109
+ return { kind: "resolved", modelId: candidates[0], requested };
3110
+ }
3111
+ if (candidates.length > 1) {
3112
+ return { kind: "ambiguous", requested, candidates };
3113
+ }
3114
+ return { kind: "unknown", requested };
3115
+ }
3116
+
2939
3117
  // src/core/coalesce-replay.ts
2940
3118
  function coalesceReplay(entries) {
2941
3119
  if (entries.length === 0) {
@@ -2943,6 +3121,7 @@ function coalesceReplay(entries) {
2943
3121
  }
2944
3122
  const lastToolUpdateIndex = /* @__PURE__ */ new Map();
2945
3123
  const mergedToolContent = /* @__PURE__ */ new Map();
3124
+ const carriedRawInput = /* @__PURE__ */ new Map();
2946
3125
  for (let i = 0; i < entries.length; i++) {
2947
3126
  const entry = entries[i];
2948
3127
  if (entry === void 0) {
@@ -2957,6 +3136,9 @@ function coalesceReplay(entries) {
2957
3136
  continue;
2958
3137
  }
2959
3138
  lastToolUpdateIndex.set(id, i);
3139
+ if (upd.rawInput && typeof upd.rawInput === "object" && !Array.isArray(upd.rawInput) && Object.keys(upd.rawInput).length > 0) {
3140
+ carriedRawInput.set(id, upd.rawInput);
3141
+ }
2960
3142
  if (Array.isArray(upd.content) && upd.content.length > 0) {
2961
3143
  const buf = mergedToolContent.get(id);
2962
3144
  if (buf) {
@@ -2996,11 +3178,11 @@ function coalesceReplay(entries) {
2996
3178
  if (id !== void 0 && lastToolUpdateIndex.get(id) !== i) {
2997
3179
  continue;
2998
3180
  }
2999
- if (id !== void 0 && mergedToolContent.has(id)) {
3000
- out.push(withReplacedContent(entry, mergedToolContent.get(id) ?? []));
3001
- } else {
3002
- out.push(entry);
3181
+ let emitted = id !== void 0 && mergedToolContent.has(id) ? withReplacedContent(entry, mergedToolContent.get(id) ?? []) : entry;
3182
+ if (id !== void 0 && carriedRawInput.has(id) && !hasRawInput(emitted)) {
3183
+ emitted = withRawInput(emitted, carriedRawInput.get(id));
3003
3184
  }
3185
+ out.push(emitted);
3004
3186
  continue;
3005
3187
  }
3006
3188
  if (kind === "plan") {
@@ -3071,24 +3253,40 @@ function withReplacedContent(entry, content) {
3071
3253
  }
3072
3254
  };
3073
3255
  }
3256
+ function hasRawInput(entry) {
3257
+ const update = readUpdate(entry);
3258
+ const ri = update?.rawInput;
3259
+ return !!ri && typeof ri === "object" && !Array.isArray(ri) && Object.keys(ri).length > 0;
3260
+ }
3261
+ function withRawInput(entry, rawInput) {
3262
+ const params = entry.params ?? {};
3263
+ const update = params.update ?? {};
3264
+ return {
3265
+ ...entry,
3266
+ params: {
3267
+ ...params,
3268
+ update: { ...update, rawInput }
3269
+ }
3270
+ };
3271
+ }
3074
3272
 
3075
3273
  // src/core/queue-store.ts
3076
- import * as fs7 from "fs/promises";
3274
+ import * as fs8 from "fs/promises";
3077
3275
  async function rewriteQueue(sessionId, entries) {
3078
3276
  const file = paths.queueFile(sessionId);
3079
3277
  if (entries.length === 0) {
3080
- await fs7.unlink(file).catch(() => void 0);
3278
+ await fs8.unlink(file).catch(() => void 0);
3081
3279
  return;
3082
3280
  }
3083
- await fs7.mkdir(paths.sessionDir(sessionId), { recursive: true });
3281
+ await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
3084
3282
  const body = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
3085
- await fs7.writeFile(file, body, "utf8");
3283
+ await fs8.writeFile(file, body, "utf8");
3086
3284
  }
3087
3285
  async function loadQueue(sessionId) {
3088
3286
  const file = paths.queueFile(sessionId);
3089
3287
  let text;
3090
3288
  try {
3091
- text = await fs7.readFile(file, "utf8");
3289
+ text = await fs8.readFile(file, "utf8");
3092
3290
  } catch (err) {
3093
3291
  if (err.code === "ENOENT") {
3094
3292
  return [];
@@ -3110,7 +3308,7 @@ async function loadQueue(sessionId) {
3110
3308
  }
3111
3309
  async function deleteQueue(sessionId) {
3112
3310
  const file = paths.queueFile(sessionId);
3113
- await fs7.unlink(file).catch(() => void 0);
3311
+ await fs8.unlink(file).catch(() => void 0);
3114
3312
  }
3115
3313
 
3116
3314
  // src/core/session.ts
@@ -3626,11 +3824,11 @@ var Session = class _Session {
3626
3824
  // Read the persisted history from disk. Returns [] if no history
3627
3825
  // file exists (fresh session, never prompted). Used by attach() and
3628
3826
  // the HTTP /history endpoint.
3629
- async getHistorySnapshot() {
3827
+ async getHistorySnapshot(tools = "inline") {
3630
3828
  if (!this.historyStore) {
3631
3829
  return [];
3632
3830
  }
3633
- return this.historyStore.load(this.sessionId).catch(() => []);
3831
+ return this.historyStore.load(this.sessionId, { tools }).catch(() => []);
3634
3832
  }
3635
3833
  // Subscribe to recordable broadcast entries — fires once per entry
3636
3834
  // that lands in history (so snapshot-shaped session_info/model/mode/
@@ -3680,7 +3878,7 @@ var Session = class _Session {
3680
3878
  }
3681
3879
  async loadReplay(historyPolicy, opts) {
3682
3880
  const maybeCoalesce = (entries) => opts.raw ? entries : coalesceReplay(entries);
3683
- const raw = await this.getHistorySnapshot();
3881
+ const raw = await this.getHistorySnapshot(opts.toolContent ?? "inline");
3684
3882
  const state = this.buildStateSnapshotReplay();
3685
3883
  if (historyPolicy === "after_message") {
3686
3884
  const cutoff = opts.afterMessageId ? findMessageIdIndex(raw, opts.afterMessageId) : -1;
@@ -5349,6 +5547,27 @@ ${text}
5349
5547
  sessionUpdate: "agent_message_chunk",
5350
5548
  content: { type: "text", text: `
5351
5549
  ${body}
5550
+ ` },
5551
+ _meta: { "hydra-acp": { synthetic: true } }
5552
+ }
5553
+ });
5554
+ return { stopReason: "end_turn" };
5555
+ }
5556
+ const resolution = resolveModelId(arg, this.agentAdvertisedModels);
5557
+ let modelId = arg;
5558
+ if (resolution.kind === "resolved") {
5559
+ modelId = resolution.modelId;
5560
+ } else if (resolution.kind === "ambiguous" || resolution.kind === "unknown") {
5561
+ const known = this.agentAdvertisedModels.map((m) => m.modelId).join("\n ");
5562
+ const reason = resolution.kind === "ambiguous" ? `"${arg}" matches multiple models: ${resolution.candidates.join(", ")}` : `"${arg}" is not an available model`;
5563
+ this.recordAndBroadcast("session/update", {
5564
+ sessionId: this.upstreamSessionId,
5565
+ update: {
5566
+ sessionUpdate: "agent_message_chunk",
5567
+ content: { type: "text", text: `
5568
+ ${reason}.
5569
+ Available models:
5570
+ ${known}
5352
5571
  ` },
5353
5572
  _meta: { "hydra-acp": { synthetic: true } }
5354
5573
  }
@@ -5357,7 +5576,7 @@ ${body}
5357
5576
  }
5358
5577
  await this.forwardRequest("session/set_model", {
5359
5578
  sessionId: this.sessionId,
5360
- modelId: arg
5579
+ modelId
5361
5580
  });
5362
5581
  return { stopReason: "end_turn" };
5363
5582
  }
@@ -6620,7 +6839,7 @@ function firstLine(text, max) {
6620
6839
  }
6621
6840
 
6622
6841
  // src/core/session-store.ts
6623
- import * as fs8 from "fs/promises";
6842
+ import * as fs9 from "fs/promises";
6624
6843
  import * as path6 from "path";
6625
6844
  import { customAlphabet as customAlphabet2 } from "nanoid";
6626
6845
  import { z as z5 } from "zod";
@@ -6827,9 +7046,9 @@ var SessionRecord = z5.object({
6827
7046
  createdAt: z5.string(),
6828
7047
  updatedAt: z5.string()
6829
7048
  });
6830
- var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
7049
+ var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
6831
7050
  function assertSafeId(id) {
6832
- if (!SESSION_ID_PATTERN.test(id)) {
7051
+ if (!SESSION_ID_PATTERN2.test(id)) {
6833
7052
  throw new Error(`unsafe session id: ${id}`);
6834
7053
  }
6835
7054
  }
@@ -6842,7 +7061,7 @@ var SessionStore = class {
6842
7061
  });
6843
7062
  }
6844
7063
  async read(sessionId) {
6845
- if (!SESSION_ID_PATTERN.test(sessionId)) {
7064
+ if (!SESSION_ID_PATTERN2.test(sessionId)) {
6846
7065
  return void 0;
6847
7066
  }
6848
7067
  const parsed = await readJsonSafe(paths.sessionFile(sessionId));
@@ -6856,11 +7075,11 @@ var SessionStore = class {
6856
7075
  }
6857
7076
  }
6858
7077
  async delete(sessionId) {
6859
- if (!SESSION_ID_PATTERN.test(sessionId)) {
7078
+ if (!SESSION_ID_PATTERN2.test(sessionId)) {
6860
7079
  return;
6861
7080
  }
6862
7081
  try {
6863
- await fs8.unlink(paths.sessionFile(sessionId));
7082
+ await fs9.unlink(paths.sessionFile(sessionId));
6864
7083
  } catch (err) {
6865
7084
  const e = err;
6866
7085
  if (e.code !== "ENOENT") {
@@ -6868,7 +7087,7 @@ var SessionStore = class {
6868
7087
  }
6869
7088
  }
6870
7089
  try {
6871
- await fs8.rmdir(paths.sessionDir(sessionId));
7090
+ await fs9.rmdir(paths.sessionDir(sessionId));
6872
7091
  } catch (err) {
6873
7092
  const e = err;
6874
7093
  if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
@@ -6898,7 +7117,7 @@ var SessionStore = class {
6898
7117
  async list() {
6899
7118
  let entries;
6900
7119
  try {
6901
- entries = await fs8.readdir(paths.sessionsDir());
7120
+ entries = await fs9.readdir(paths.sessionsDir());
6902
7121
  } catch (err) {
6903
7122
  const e = err;
6904
7123
  if (e.code === "ENOENT") {
@@ -6949,7 +7168,7 @@ function recordFromMemorySession(args) {
6949
7168
  }
6950
7169
 
6951
7170
  // src/core/tombstone-store.ts
6952
- import * as fs9 from "fs/promises";
7171
+ import * as fs10 from "fs/promises";
6953
7172
  import { z as z6 } from "zod";
6954
7173
  var Tombstone = z6.object({
6955
7174
  version: z6.literal(1),
@@ -6978,7 +7197,7 @@ var TombstoneStore = class {
6978
7197
  }
6979
7198
  async has(agentId, upstreamSessionId) {
6980
7199
  try {
6981
- await fs9.access(paths.tombstoneFile(agentId, upstreamSessionId));
7200
+ await fs10.access(paths.tombstoneFile(agentId, upstreamSessionId));
6982
7201
  return true;
6983
7202
  } catch {
6984
7203
  return false;
@@ -7016,7 +7235,7 @@ var TombstoneStore = class {
7016
7235
  }
7017
7236
  async remove(agentId, upstreamSessionId) {
7018
7237
  try {
7019
- await fs9.unlink(paths.tombstoneFile(agentId, upstreamSessionId));
7238
+ await fs10.unlink(paths.tombstoneFile(agentId, upstreamSessionId));
7020
7239
  } catch (err) {
7021
7240
  const e = err;
7022
7241
  if (e.code !== "ENOENT") {
@@ -7024,7 +7243,7 @@ var TombstoneStore = class {
7024
7243
  }
7025
7244
  }
7026
7245
  try {
7027
- await fs9.rmdir(paths.tombstoneAgentDir(agentId));
7246
+ await fs10.rmdir(paths.tombstoneAgentDir(agentId));
7028
7247
  } catch (err) {
7029
7248
  const e = err;
7030
7249
  if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
@@ -7038,7 +7257,7 @@ var TombstoneStore = class {
7038
7257
  }
7039
7258
  let agents;
7040
7259
  try {
7041
- agents = await fs9.readdir(paths.tombstonesDir());
7260
+ agents = await fs10.readdir(paths.tombstonesDir());
7042
7261
  } catch (err) {
7043
7262
  const e = err;
7044
7263
  if (e.code === "ENOENT") {
@@ -7061,7 +7280,7 @@ var TombstoneStore = class {
7061
7280
  async listForAgent(agentId) {
7062
7281
  let files;
7063
7282
  try {
7064
- files = await fs9.readdir(paths.tombstoneAgentDir(agentId));
7283
+ files = await fs10.readdir(paths.tombstoneAgentDir(agentId));
7065
7284
  } catch (err) {
7066
7285
  const e = err;
7067
7286
  if (e.code === "ENOENT") {
@@ -7096,19 +7315,19 @@ function shouldResurrectFromUpstream(tombstone, listingUpdatedAt) {
7096
7315
  }
7097
7316
 
7098
7317
  // src/core/synopsis-coordinator.ts
7099
- import * as fs11 from "fs/promises";
7318
+ import * as fs12 from "fs/promises";
7100
7319
 
7101
7320
  // src/core/hydra-version.ts
7102
7321
  import { fileURLToPath } from "url";
7103
7322
  import * as path7 from "path";
7104
- import * as fs10 from "fs";
7323
+ import * as fs11 from "fs";
7105
7324
  function resolveVersion() {
7106
7325
  try {
7107
7326
  let dir = path7.dirname(fileURLToPath(import.meta.url));
7108
7327
  for (let i = 0; i < 8; i += 1) {
7109
7328
  const candidate = path7.join(dir, "package.json");
7110
- if (fs10.existsSync(candidate)) {
7111
- const pkg = JSON.parse(fs10.readFileSync(candidate, "utf8"));
7329
+ if (fs11.existsSync(candidate)) {
7330
+ const pkg = JSON.parse(fs11.readFileSync(candidate, "utf8"));
7112
7331
  if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
7113
7332
  return pkg.version;
7114
7333
  }
@@ -7632,7 +7851,7 @@ var SynopsisCoordinator = class {
7632
7851
  });
7633
7852
  const modelId = this.opts.synopsisModel;
7634
7853
  const synopsisCwd = paths.sessionDir(sessionId);
7635
- await fs11.mkdir(synopsisCwd, { recursive: true }).catch(() => void 0);
7854
+ await fs12.mkdir(synopsisCwd, { recursive: true }).catch(() => void 0);
7636
7855
  this.opts.logger?.info(
7637
7856
  `synopsis: start sessionId=${sessionId} agentId=${synopsisAgentId} historyLen=${history.length} model=${JSON.stringify(modelId ?? "(default)")} cwd=${synopsisCwd}`
7638
7857
  );
@@ -7735,8 +7954,217 @@ function describeFields(s) {
7735
7954
  }
7736
7955
 
7737
7956
  // src/core/history-store.ts
7738
- import * as fs12 from "fs/promises";
7739
- var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
7957
+ import * as fs13 from "fs/promises";
7958
+
7959
+ // src/core/tool-content.ts
7960
+ function parseToolContentMode(raw) {
7961
+ return raw === "summary" ? "summary" : "inline";
7962
+ }
7963
+ var SUMMARY_TEXT_CAP = 256;
7964
+ function applyToolContentMode(entries, mode) {
7965
+ if (mode !== "summary") {
7966
+ return entries;
7967
+ }
7968
+ return entries.map(summarizeEntry);
7969
+ }
7970
+ function summarizeEntry(entry) {
7971
+ if (entry.method !== "session/update") {
7972
+ return entry;
7973
+ }
7974
+ const params = entry.params;
7975
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
7976
+ return entry;
7977
+ }
7978
+ const p = params;
7979
+ const update = p.update;
7980
+ if (!update || typeof update !== "object" || Array.isArray(update)) {
7981
+ return entry;
7982
+ }
7983
+ const u = update;
7984
+ if (u.sessionUpdate !== "tool_call" && u.sessionUpdate !== "tool_call_update") {
7985
+ return entry;
7986
+ }
7987
+ const newUpdate = { ...u };
7988
+ if (Array.isArray(u.content)) {
7989
+ newUpdate.content = u.content.map(summarizeBlock);
7990
+ }
7991
+ const rawOutput = u.rawOutput;
7992
+ if (rawOutput && typeof rawOutput === "object" && !Array.isArray(rawOutput)) {
7993
+ const ro = rawOutput;
7994
+ const slim = {};
7995
+ if (ro.error !== void 0) {
7996
+ slim.error = clip(ro.error);
7997
+ }
7998
+ if (ro.metadata !== void 0) {
7999
+ slim.metadata = ro.metadata;
8000
+ }
8001
+ newUpdate.rawOutput = slim;
8002
+ }
8003
+ return { ...entry, params: { ...p, update: newUpdate } };
8004
+ }
8005
+ function isDiffBlock(block) {
8006
+ return !!block && typeof block === "object" && !Array.isArray(block) && block.type === "diff";
8007
+ }
8008
+ function summarizeBlock(block) {
8009
+ if (isDiffBlock(block)) {
8010
+ const b2 = block;
8011
+ const out2 = {
8012
+ type: "diff",
8013
+ oldText: "",
8014
+ newText: ""
8015
+ };
8016
+ if (typeof b2.path === "string") {
8017
+ out2.path = b2.path;
8018
+ }
8019
+ return out2;
8020
+ }
8021
+ if (!block || typeof block !== "object" || Array.isArray(block)) {
8022
+ return block;
8023
+ }
8024
+ const b = block;
8025
+ const out = { ...b };
8026
+ if (typeof b.text === "string") {
8027
+ out.text = clip(b.text);
8028
+ }
8029
+ if (typeof b.content === "string") {
8030
+ out.content = clip(b.content);
8031
+ } else if (b.content && typeof b.content === "object") {
8032
+ out.content = summarizeBlock(b.content);
8033
+ }
8034
+ return out;
8035
+ }
8036
+ function clip(value) {
8037
+ if (typeof value === "string" && value.length > SUMMARY_TEXT_CAP) {
8038
+ const elided = value.length - SUMMARY_TEXT_CAP;
8039
+ return `${value.slice(0, SUMMARY_TEXT_CAP)}\u2026[+${elided} chars omitted from summary export]`;
8040
+ }
8041
+ return value;
8042
+ }
8043
+ var TOOL_BLOB_THRESHOLD = 2048;
8044
+ function collectToolBlobHashes(entries) {
8045
+ const out = /* @__PURE__ */ new Set();
8046
+ const walk = (value) => {
8047
+ if (isToolBlobRef(value)) {
8048
+ out.add(value.__hydraBlob);
8049
+ return;
8050
+ }
8051
+ if (Array.isArray(value)) {
8052
+ for (const item of value) {
8053
+ walk(item);
8054
+ }
8055
+ return;
8056
+ }
8057
+ if (value && typeof value === "object") {
8058
+ for (const v of Object.values(value)) {
8059
+ walk(v);
8060
+ }
8061
+ }
8062
+ };
8063
+ for (const entry of entries) {
8064
+ walk(entry.params);
8065
+ }
8066
+ return out;
8067
+ }
8068
+ function isToolBlobRef(value) {
8069
+ return !!value && typeof value === "object" && !Array.isArray(value) && typeof value.__hydraBlob === "string";
8070
+ }
8071
+ function isToolEntry(entry) {
8072
+ if (entry.method !== "session/update") {
8073
+ return false;
8074
+ }
8075
+ const params = entry.params;
8076
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
8077
+ return false;
8078
+ }
8079
+ const update = params.update;
8080
+ if (!update || typeof update !== "object" || Array.isArray(update)) {
8081
+ return false;
8082
+ }
8083
+ const kind = update.sessionUpdate;
8084
+ return kind === "tool_call" || kind === "tool_call_update";
8085
+ }
8086
+ async function externalizeToolEntry(entry, put) {
8087
+ if (!isToolEntry(entry)) {
8088
+ return entry;
8089
+ }
8090
+ const p = entry.params;
8091
+ const update = p.update;
8092
+ const newUpdate = await deepExternalize(update, put);
8093
+ return { ...entry, params: { ...p, update: newUpdate } };
8094
+ }
8095
+ async function deepExternalize(value, put) {
8096
+ if (typeof value === "string") {
8097
+ if (value.length <= TOOL_BLOB_THRESHOLD) {
8098
+ return value;
8099
+ }
8100
+ const hash = await put(value);
8101
+ if (hash === null) {
8102
+ return value;
8103
+ }
8104
+ const ref = { __hydraBlob: hash, bytes: value.length };
8105
+ return ref;
8106
+ }
8107
+ if (Array.isArray(value)) {
8108
+ const out = [];
8109
+ for (const item of value) {
8110
+ out.push(await deepExternalize(item, put));
8111
+ }
8112
+ return out;
8113
+ }
8114
+ if (value && typeof value === "object") {
8115
+ const out = {};
8116
+ for (const [k, v] of Object.entries(value)) {
8117
+ out[k] = await deepExternalize(v, put);
8118
+ }
8119
+ return out;
8120
+ }
8121
+ return value;
8122
+ }
8123
+ async function expandToolRefs(entry, get) {
8124
+ const params = entry.params;
8125
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
8126
+ return entry;
8127
+ }
8128
+ const expanded = await deepExpand(params, get);
8129
+ if (expanded === params) {
8130
+ return entry;
8131
+ }
8132
+ return { ...entry, params: expanded };
8133
+ }
8134
+ async function deepExpand(value, get) {
8135
+ if (isToolBlobRef(value)) {
8136
+ const text = await get(value.__hydraBlob);
8137
+ return text ?? "";
8138
+ }
8139
+ if (Array.isArray(value)) {
8140
+ let changed = false;
8141
+ const out = [];
8142
+ for (const item of value) {
8143
+ const next = await deepExpand(item, get);
8144
+ if (next !== item) {
8145
+ changed = true;
8146
+ }
8147
+ out.push(next);
8148
+ }
8149
+ return changed ? out : value;
8150
+ }
8151
+ if (value && typeof value === "object") {
8152
+ let changed = false;
8153
+ const out = {};
8154
+ for (const [k, v] of Object.entries(value)) {
8155
+ const next = await deepExpand(v, get);
8156
+ if (next !== v) {
8157
+ changed = true;
8158
+ }
8159
+ out[k] = next;
8160
+ }
8161
+ return changed ? out : value;
8162
+ }
8163
+ return value;
8164
+ }
8165
+
8166
+ // src/core/history-store.ts
8167
+ var SESSION_ID_PATTERN3 = /^[A-Za-z0-9_-]+$/;
7740
8168
  var DEFAULT_MAX_ENTRIES = 1e3;
7741
8169
  var HistoryStore = class {
7742
8170
  // Serialize writes per session id so appends and rewrites don't
@@ -7748,26 +8176,34 @@ var HistoryStore = class {
7748
8176
  this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
7749
8177
  }
7750
8178
  async append(sessionId, entry) {
7751
- if (!SESSION_ID_PATTERN2.test(sessionId)) {
8179
+ if (!SESSION_ID_PATTERN3.test(sessionId)) {
7752
8180
  return;
7753
8181
  }
7754
8182
  return this.enqueue(sessionId, async () => {
7755
- await fs12.mkdir(paths.sessionDir(sessionId), { recursive: true });
7756
- const line = JSON.stringify(entry) + "\n";
7757
- await fs12.appendFile(paths.historyFile(sessionId), line, {
8183
+ await fs13.mkdir(paths.sessionDir(sessionId), { recursive: true });
8184
+ const stored = await externalizeToolEntry(
8185
+ entry,
8186
+ (t) => putToolBlob(sessionId, t)
8187
+ );
8188
+ const line = JSON.stringify(stored) + "\n";
8189
+ await fs13.appendFile(paths.historyFile(sessionId), line, {
7758
8190
  encoding: "utf8",
7759
8191
  mode: 384
7760
8192
  });
7761
8193
  });
7762
8194
  }
7763
8195
  async rewrite(sessionId, entries) {
7764
- if (!SESSION_ID_PATTERN2.test(sessionId)) {
8196
+ if (!SESSION_ID_PATTERN3.test(sessionId)) {
7765
8197
  return;
7766
8198
  }
7767
8199
  return this.enqueue(sessionId, async () => {
7768
- await fs12.mkdir(paths.sessionDir(sessionId), { recursive: true });
7769
- const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
7770
- await fs12.writeFile(paths.historyFile(sessionId), body, {
8200
+ await fs13.mkdir(paths.sessionDir(sessionId), { recursive: true });
8201
+ const stored = [];
8202
+ for (const e of entries) {
8203
+ stored.push(await externalizeToolEntry(e, (t) => putToolBlob(sessionId, t)));
8204
+ }
8205
+ const body = stored.length === 0 ? "" : stored.map((e) => JSON.stringify(e)).join("\n") + "\n";
8206
+ await fs13.writeFile(paths.historyFile(sessionId), body, {
7771
8207
  encoding: "utf8",
7772
8208
  mode: 384
7773
8209
  });
@@ -7778,13 +8214,13 @@ var HistoryStore = class {
7778
8214
  // it's safe to invoke alongside ongoing writes; a no-op if the file is
7779
8215
  // already at or below the cap.
7780
8216
  async compact(sessionId, maxEntries) {
7781
- if (!SESSION_ID_PATTERN2.test(sessionId)) {
8217
+ if (!SESSION_ID_PATTERN3.test(sessionId)) {
7782
8218
  return;
7783
8219
  }
7784
8220
  return this.enqueue(sessionId, async () => {
7785
8221
  let raw;
7786
8222
  try {
7787
- raw = await fs12.readFile(paths.historyFile(sessionId), "utf8");
8223
+ raw = await fs13.readFile(paths.historyFile(sessionId), "utf8");
7788
8224
  } catch (err) {
7789
8225
  const e = err;
7790
8226
  if (e.code === "ENOENT") {
@@ -7797,23 +8233,29 @@ var HistoryStore = class {
7797
8233
  return;
7798
8234
  }
7799
8235
  const trimmed = lines.slice(-maxEntries);
7800
- await fs12.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
8236
+ await fs13.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
7801
8237
  encoding: "utf8",
7802
8238
  mode: 384
7803
8239
  });
7804
8240
  });
7805
8241
  }
7806
- async load(sessionId) {
7807
- if (!SESSION_ID_PATTERN2.test(sessionId)) {
8242
+ // `tools` selects how externalized tool content is materialized:
8243
+ // "inline" (default) — expand blob refs back to full content, so every
8244
+ // consumer sees the original recorded shape.
8245
+ // "references" — leave references in place (the lean form) for
8246
+ // clients that fetch tool content on demand.
8247
+ async load(sessionId, opts = {}) {
8248
+ if (!SESSION_ID_PATTERN3.test(sessionId)) {
7808
8249
  return [];
7809
8250
  }
8251
+ const expand = (opts.tools ?? "inline") === "inline";
7810
8252
  const pending = this.writeQueues.get(sessionId);
7811
8253
  if (pending) {
7812
8254
  await pending;
7813
8255
  }
7814
8256
  let raw;
7815
8257
  try {
7816
- raw = await fs12.readFile(paths.historyFile(sessionId), "utf8");
8258
+ raw = await fs13.readFile(paths.historyFile(sessionId), "utf8");
7817
8259
  } catch (err) {
7818
8260
  const e = err;
7819
8261
  if (e.code === "ENOENT") {
@@ -7848,10 +8290,25 @@ var HistoryStore = class {
7848
8290
  recordedAt: obj.recordedAt
7849
8291
  });
7850
8292
  }
7851
- if (out.length > this.maxEntries) {
7852
- return out.slice(-this.maxEntries);
8293
+ const kept = out.length > this.maxEntries ? out.slice(-this.maxEntries) : out;
8294
+ if (!expand) {
8295
+ return kept;
7853
8296
  }
7854
- return out;
8297
+ const blobCache = /* @__PURE__ */ new Map();
8298
+ const get = async (hash) => {
8299
+ const cached = blobCache.get(hash);
8300
+ if (cached !== void 0) {
8301
+ return cached;
8302
+ }
8303
+ const value = await getToolBlob(sessionId, hash);
8304
+ blobCache.set(hash, value);
8305
+ return value;
8306
+ };
8307
+ const inlined = [];
8308
+ for (const entry of kept) {
8309
+ inlined.push(await expandToolRefs(entry, get));
8310
+ }
8311
+ return inlined;
7855
8312
  }
7856
8313
  // Wait for every pending append/rewrite/compact across all sessions to
7857
8314
  // settle. Daemon shutdown calls this after closing sessions so the final
@@ -7867,20 +8324,21 @@ var HistoryStore = class {
7867
8324
  await Promise.allSettled(pending);
7868
8325
  }
7869
8326
  async delete(sessionId) {
7870
- if (!SESSION_ID_PATTERN2.test(sessionId)) {
8327
+ if (!SESSION_ID_PATTERN3.test(sessionId)) {
7871
8328
  return;
7872
8329
  }
7873
8330
  return this.enqueue(sessionId, async () => {
7874
8331
  try {
7875
- await fs12.unlink(paths.historyFile(sessionId));
8332
+ await fs13.unlink(paths.historyFile(sessionId));
7876
8333
  } catch (err) {
7877
8334
  const e = err;
7878
8335
  if (e.code !== "ENOENT") {
7879
8336
  throw err;
7880
8337
  }
7881
8338
  }
8339
+ await deleteToolBlobs(sessionId);
7882
8340
  try {
7883
- await fs12.rmdir(paths.sessionDir(sessionId));
8341
+ await fs13.rmdir(paths.sessionDir(sessionId));
7884
8342
  } catch (err) {
7885
8343
  const e = err;
7886
8344
  if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
@@ -7904,12 +8362,12 @@ var HistoryStore = class {
7904
8362
  };
7905
8363
 
7906
8364
  // src/tui/history.ts
7907
- import { promises as fs13 } from "fs";
8365
+ import { promises as fs14 } from "fs";
7908
8366
  import * as path8 from "path";
7909
8367
  async function saveHistory(file, history) {
7910
- await fs13.mkdir(path8.dirname(file), { recursive: true });
8368
+ await fs14.mkdir(path8.dirname(file), { recursive: true });
7911
8369
  const lines = history.map((entry) => JSON.stringify(entry));
7912
- await fs13.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
8370
+ await fs14.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
7913
8371
  }
7914
8372
 
7915
8373
  // src/core/bundle.ts
@@ -7971,7 +8429,12 @@ var Bundle = z7.object({
7971
8429
  }),
7972
8430
  session: BundleSession,
7973
8431
  history: z7.array(HistoryEntrySchema),
7974
- promptHistory: z7.array(z7.string()).optional()
8432
+ promptHistory: z7.array(z7.string()).optional(),
8433
+ // Externalized tool-content blobs referenced by the history, present when
8434
+ // the bundle was exported with tools=references. Map of sha256 → base64
8435
+ // of the gzipped content. Restored to the session's tools/ store on
8436
+ // import; the ref-form history hydrates from them.
8437
+ toolBlobs: z7.record(z7.string()).optional()
7975
8438
  });
7976
8439
  function encodeBundle(params) {
7977
8440
  const bundle = {
@@ -8006,6 +8469,9 @@ function encodeBundle(params) {
8006
8469
  if (params.promptHistory !== void 0) {
8007
8470
  bundle.promptHistory = params.promptHistory;
8008
8471
  }
8472
+ if (params.toolBlobs !== void 0 && Object.keys(params.toolBlobs).length > 0) {
8473
+ bundle.toolBlobs = params.toolBlobs;
8474
+ }
8009
8475
  return bundle;
8010
8476
  }
8011
8477
  function decodeBundle(raw) {
@@ -8420,7 +8886,7 @@ var SessionManager = class {
8420
8886
  }
8421
8887
  async dirExists(cwd) {
8422
8888
  try {
8423
- return (await fs14.stat(cwd)).isDirectory();
8889
+ return (await fs15.stat(cwd)).isDirectory();
8424
8890
  } catch {
8425
8891
  return false;
8426
8892
  }
@@ -8610,6 +9076,25 @@ var SessionManager = class {
8610
9076
  }
8611
9077
  return out;
8612
9078
  }
9079
+ // Issue session/set_model for a seed model (defaultModels / --model) at
9080
+ // bootstrap, logging success or a non-fatal rejection. `where` is the
9081
+ // human-readable provenance string used in log lines. A bad id in config
9082
+ // shouldn't break session creation, so a rejection is swallowed.
9083
+ async applySeedModel(agent, sessionId, modelId, where) {
9084
+ try {
9085
+ await agent.connection.request("session/set_model", {
9086
+ sessionId,
9087
+ modelId
9088
+ });
9089
+ this.logger?.info(`${where}: session/set_model accepted`);
9090
+ return true;
9091
+ } catch (err) {
9092
+ this.logger?.warn(
9093
+ `${where} rejected by agent (${err.message}); session will use the agent's own default`
9094
+ );
9095
+ return false;
9096
+ }
9097
+ }
8613
9098
  // Bootstrap a fresh agent process: registry resolve → spawn → initialize
8614
9099
  // → session/new. Shared by create() and the /hydra agent path so both
8615
9100
  // go through the same env / capabilities / error-handling.
@@ -8658,23 +9143,31 @@ var SessionManager = class {
8658
9143
  const initialModels = extractInitialModels(newResult);
8659
9144
  const desired = params.model ?? this.defaultModels[params.agentId];
8660
9145
  if (desired && desired !== initialModel) {
8661
- const validates = initialModels.length === 0 || initialModels.some((m) => m.modelId === desired);
8662
- if (validates) {
8663
- try {
8664
- await agent.connection.request("session/set_model", {
8665
- sessionId: sessionIdRaw,
8666
- modelId: desired
8667
- });
9146
+ const resolution = resolveModelId(desired, initialModels);
9147
+ const where = params.model !== void 0 ? `model=${JSON.stringify(desired)}` : `defaultModels[${params.agentId}]=${JSON.stringify(desired)}`;
9148
+ if (resolution.kind === "exact" || resolution.kind === "none") {
9149
+ if (await this.applySeedModel(agent, sessionIdRaw, desired, where)) {
8668
9150
  initialModel = desired;
8669
- } catch (err) {
8670
- this.logger?.warn(
8671
- `defaultModels[${params.agentId}]=${JSON.stringify(desired)} rejected by agent (${err.message}); session will use ${JSON.stringify(initialModel)}`
8672
- );
8673
9151
  }
9152
+ } else if (resolution.kind === "resolved") {
9153
+ if (resolution.modelId === initialModel) {
9154
+ initialModel = resolution.modelId;
9155
+ } else if (await this.applySeedModel(
9156
+ agent,
9157
+ sessionIdRaw,
9158
+ resolution.modelId,
9159
+ `${where} resolved to ${JSON.stringify(resolution.modelId)}`
9160
+ )) {
9161
+ initialModel = resolution.modelId;
9162
+ }
9163
+ } else if (resolution.kind === "ambiguous") {
9164
+ this.logger?.warn(
9165
+ `${where} is ambiguous (trailing-segment matches [${resolution.candidates.join(", ")}]); skipping session/set_model, session will use ${JSON.stringify(initialModel)}`
9166
+ );
8674
9167
  } else {
8675
9168
  const known = initialModels.map((m) => m.modelId).join(", ");
8676
9169
  this.logger?.warn(
8677
- `defaultModels[${params.agentId}]=${JSON.stringify(desired)} not in agent's availableModels ([${known}]); skipping session/set_model, session will use ${JSON.stringify(initialModel)}`
9170
+ `${where} not in agent's availableModels ([${known}]); skipping session/set_model, session will use ${JSON.stringify(initialModel)}`
8678
9171
  );
8679
9172
  }
8680
9173
  }
@@ -8807,6 +9300,12 @@ var SessionManager = class {
8807
9300
  async loadHistory(sessionId) {
8808
9301
  return this.histories.load(sessionId);
8809
9302
  }
9303
+ // Read a single externalized tool-content blob by sha256 (the lean
9304
+ // `tools: "references"` fetch-on-expand path). Null if the session id or
9305
+ // hash is malformed, or the blob isn't present.
9306
+ async loadToolBlob(sessionId, hash) {
9307
+ return getToolBlob(sessionId, hash);
9308
+ }
8810
9309
  async loadFromDisk(sessionId) {
8811
9310
  const record = await this.store.read(sessionId);
8812
9311
  if (!record) {
@@ -9044,7 +9543,7 @@ var SessionManager = class {
9044
9543
  // disk. Backfills lineageId if the on-disk record pre-dates that
9045
9544
  // field. Returns undefined if the session doesn't exist. Callers
9046
9545
  // populate the bundle's exportedFrom metadata themselves.
9047
- async exportBundle(sessionId) {
9546
+ async exportBundle(sessionId, opts = {}) {
9048
9547
  const record = await this.store.read(sessionId);
9049
9548
  if (!record) {
9050
9549
  return void 0;
@@ -9067,9 +9566,20 @@ var SessionManager = class {
9067
9566
  }).catch(() => void 0);
9068
9567
  withLineage = backfilled;
9069
9568
  }
9070
- const history = await this.histories.load(sessionId).catch(() => []);
9569
+ const tools = opts.tools ?? "inline";
9570
+ const history = await this.histories.load(sessionId, tools === "references" ? { tools: "references" } : {}).catch(() => []);
9071
9571
  const promptHistory = await loadPromptHistorySafely(sessionId);
9072
- return { record: withLineage, history, promptHistory };
9572
+ if (tools !== "references") {
9573
+ return { record: withLineage, history, promptHistory };
9574
+ }
9575
+ const toolBlobs = {};
9576
+ for (const hash of collectToolBlobHashes(history)) {
9577
+ const gz = await readToolBlobGz(sessionId, hash);
9578
+ if (gz) {
9579
+ toolBlobs[hash] = gz.toString("base64");
9580
+ }
9581
+ }
9582
+ return { record: withLineage, history, promptHistory, toolBlobs };
9073
9583
  }
9074
9584
  // Create a local session from an imported bundle. Without `replace`,
9075
9585
  // a bundle with a lineageId we already have on disk throws
@@ -9230,9 +9740,18 @@ var SessionManager = class {
9230
9740
  args.sessionId,
9231
9741
  args.bundle.history
9232
9742
  );
9743
+ if (args.bundle.toolBlobs) {
9744
+ for (const [hash, b64] of Object.entries(args.bundle.toolBlobs)) {
9745
+ await writeToolBlobGz(
9746
+ args.sessionId,
9747
+ hash,
9748
+ Buffer.from(b64, "base64")
9749
+ ).catch(() => void 0);
9750
+ }
9751
+ }
9233
9752
  const sourceMtime = new Date(args.bundle.session.updatedAt);
9234
9753
  if (!Number.isNaN(sourceMtime.getTime())) {
9235
- await fs14.utimes(paths.historyFile(args.sessionId), sourceMtime, sourceMtime).catch(() => void 0);
9754
+ await fs15.utimes(paths.historyFile(args.sessionId), sourceMtime, sourceMtime).catch(() => void 0);
9236
9755
  }
9237
9756
  if (args.bundle.promptHistory && args.bundle.promptHistory.length > 0) {
9238
9757
  await saveHistory(
@@ -9879,7 +10398,7 @@ function findLastTurnComplete(history) {
9879
10398
  }
9880
10399
  async function loadPromptHistorySafely(sessionId) {
9881
10400
  try {
9882
- const raw = await fs14.readFile(paths.tuiHistoryFile(sessionId), "utf8");
10401
+ const raw = await fs15.readFile(paths.tuiHistoryFile(sessionId), "utf8");
9883
10402
  const out = [];
9884
10403
  for (const line of raw.split("\n")) {
9885
10404
  if (line.length === 0) {
@@ -9900,7 +10419,7 @@ async function loadPromptHistorySafely(sessionId) {
9900
10419
  }
9901
10420
  async function historyStatus(sessionId) {
9902
10421
  try {
9903
- const st = await fs14.stat(paths.historyFile(sessionId));
10422
+ const st = await fs15.stat(paths.historyFile(sessionId));
9904
10423
  return {
9905
10424
  mtime: new Date(st.mtimeMs).toISOString(),
9906
10425
  hasContent: st.size > 0
@@ -9921,7 +10440,7 @@ function effectiveInteractive(record, hasContent) {
9921
10440
 
9922
10441
  // src/core/child-supervisor.ts
9923
10442
  import { spawn as spawn4 } from "child_process";
9924
- import * as fs15 from "fs";
10443
+ import * as fs16 from "fs";
9925
10444
  import * as fsp5 from "fs/promises";
9926
10445
  import * as path10 from "path";
9927
10446
 
@@ -10294,7 +10813,7 @@ var ChildSupervisor = class {
10294
10813
  }
10295
10814
  const cfg = entry.config;
10296
10815
  const command = cfg.command.length > 0 ? cfg.command : [cfg.name];
10297
- const logStream = fs15.createWriteStream(
10816
+ const logStream = fs16.createWriteStream(
10298
10817
  this.adapter.paths.logFile(cfg.name),
10299
10818
  { flags: "a" }
10300
10819
  );
@@ -10350,7 +10869,7 @@ var ChildSupervisor = class {
10350
10869
  }
10351
10870
  if (typeof child.pid === "number") {
10352
10871
  try {
10353
- fs15.writeFileSync(
10872
+ fs16.writeFileSync(
10354
10873
  this.adapter.paths.pidFile(cfg.name),
10355
10874
  `${child.pid}
10356
10875
  `,
@@ -10376,7 +10895,7 @@ var ChildSupervisor = class {
10376
10895
  });
10377
10896
  child.on("exit", (code, signal) => {
10378
10897
  try {
10379
- fs15.unlinkSync(this.adapter.paths.pidFile(cfg.name));
10898
+ fs16.unlinkSync(this.adapter.paths.pidFile(cfg.name));
10380
10899
  } catch {
10381
10900
  }
10382
10901
  logStream.write(
@@ -10716,7 +11235,7 @@ function startAgentSyncScheduler(opts) {
10716
11235
 
10717
11236
  // src/core/session-tokens.ts
10718
11237
  import * as path12 from "path";
10719
- import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
11238
+ import { createHash as createHash2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
10720
11239
  var TOKEN_PREFIX = "hydra_session_";
10721
11240
  var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
10722
11241
  var ID_LENGTH = 12;
@@ -10726,7 +11245,7 @@ function tokensFilePath() {
10726
11245
  return path12.join(paths.home(), "session-tokens.json");
10727
11246
  }
10728
11247
  function sha256Hex(input) {
10729
- return createHash("sha256").update(input).digest("hex");
11248
+ return createHash2("sha256").update(input).digest("hex");
10730
11249
  }
10731
11250
  function randomHex(bytes) {
10732
11251
  return randomBytes2(bytes).toString("hex");
@@ -11293,6 +11812,23 @@ function isExitPlanModeTool(name) {
11293
11812
  const normalised = name.toLowerCase().replace(/[_\s-]/g, "");
11294
11813
  return normalised === "exitplanmode";
11295
11814
  }
11815
+ function readDiffField(value) {
11816
+ if (typeof value === "string") {
11817
+ return { text: value };
11818
+ }
11819
+ if (value && typeof value === "object" && !Array.isArray(value)) {
11820
+ const v = value;
11821
+ if (typeof v.__hydraBlob === "string") {
11822
+ return {
11823
+ ref: {
11824
+ hash: v.__hydraBlob,
11825
+ bytes: typeof v.bytes === "number" ? v.bytes : 0
11826
+ }
11827
+ };
11828
+ }
11829
+ }
11830
+ return void 0;
11831
+ }
11296
11832
  function extractEditDiff(u) {
11297
11833
  const content = u.content;
11298
11834
  if (Array.isArray(content)) {
@@ -11304,16 +11840,18 @@ function extractEditDiff(u) {
11304
11840
  if (b.type !== "diff") {
11305
11841
  continue;
11306
11842
  }
11307
- const oldText = typeof b.oldText === "string" ? b.oldText : void 0;
11308
- const newText = typeof b.newText === "string" ? b.newText : void 0;
11309
- if (oldText === void 0 && newText === void 0) {
11843
+ const oldField = readDiffField(b.oldText);
11844
+ const newField = readDiffField(b.newText);
11845
+ if (oldField === void 0 && newField === void 0) {
11310
11846
  continue;
11311
11847
  }
11312
11848
  const path15 = typeof b.path === "string" ? b.path : void 0;
11313
11849
  return {
11314
11850
  ...path15 !== void 0 ? { path: path15 } : {},
11315
- oldText: oldText ?? "",
11316
- newText: newText ?? ""
11851
+ oldText: oldField?.text ?? "",
11852
+ newText: newField?.text ?? "",
11853
+ ...oldField?.ref ? { oldRef: oldField.ref } : {},
11854
+ ...newField?.ref ? { newRef: newField.ref } : {}
11317
11855
  };
11318
11856
  }
11319
11857
  }
@@ -11381,8 +11919,36 @@ function mapToolCall(u) {
11381
11919
  if (diff !== null) {
11382
11920
  event.editDiff = diff;
11383
11921
  }
11922
+ const detail = extractToolDetail(u);
11923
+ if (detail !== void 0) {
11924
+ event.detail = detail;
11925
+ }
11384
11926
  return event;
11385
11927
  }
11928
+ var TOOL_DETAIL_MAX = 64;
11929
+ function extractToolDetail(u) {
11930
+ const rawInput = u.rawInput;
11931
+ if (!rawInput || typeof rawInput !== "object" || Array.isArray(rawInput)) {
11932
+ return void 0;
11933
+ }
11934
+ const r = rawInput;
11935
+ if (typeof r.command === "string" && r.command.trim().length > 0) {
11936
+ const firstLine2 = sanitizeSingleLine(r.command).trim();
11937
+ const cmd = firstLine2.replace(/^cd\s+\S+\s+&&\s+/, "");
11938
+ return clipHead(cmd, TOOL_DETAIL_MAX);
11939
+ }
11940
+ const path15 = typeof r.file_path === "string" ? r.file_path : typeof r.path === "string" ? r.path : void 0;
11941
+ if (path15 !== void 0 && path15.length > 0) {
11942
+ return clipTail(shortenHomePath(sanitizeSingleLine(path15)), TOOL_DETAIL_MAX);
11943
+ }
11944
+ return void 0;
11945
+ }
11946
+ function clipHead(s, max) {
11947
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
11948
+ }
11949
+ function clipTail(s, max) {
11950
+ return s.length > max ? `\u2026${s.slice(-(max - 1))}` : s;
11951
+ }
11386
11952
  function mapToolCallUpdate(u) {
11387
11953
  const toolCallId = readString(u, "toolCallId") ?? readString(u, "id");
11388
11954
  if (!toolCallId) {
@@ -11392,7 +11958,8 @@ function mapToolCallUpdate(u) {
11392
11958
  const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
11393
11959
  const status = readString(u, "status");
11394
11960
  const diff = extractEditDiff(u);
11395
- const meaningful = title !== void 0 || diff !== null || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
11961
+ const detail = extractToolDetail(u);
11962
+ const meaningful = title !== void 0 || diff !== null || detail !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
11396
11963
  if (!meaningful) {
11397
11964
  return null;
11398
11965
  }
@@ -11412,6 +11979,9 @@ function mapToolCallUpdate(u) {
11412
11979
  if (title !== void 0) {
11413
11980
  event.title = title;
11414
11981
  }
11982
+ if (detail !== void 0) {
11983
+ event.detail = detail;
11984
+ }
11415
11985
  if (status !== void 0) {
11416
11986
  event.status = status;
11417
11987
  }
@@ -12345,15 +12915,21 @@ function registerSessionRoutes(app, manager, defaults) {
12345
12915
  app.get("/v1/sessions/:id/export", async (request, reply) => {
12346
12916
  const raw = request.params.id;
12347
12917
  const id = await manager.resolveCanonicalId(raw) ?? raw;
12348
- const exported = await manager.exportBundle(id);
12918
+ const toolsRaw = request.query?.tools;
12919
+ const toolMode = toolsRaw === "references" ? "references" : parseToolContentMode(toolsRaw);
12920
+ const exported = await manager.exportBundle(
12921
+ id,
12922
+ toolMode === "references" ? { tools: "references" } : {}
12923
+ );
12349
12924
  if (!exported) {
12350
12925
  reply.code(404).send({ error: "session not found" });
12351
12926
  return;
12352
12927
  }
12353
12928
  const bundle = encodeBundle({
12354
12929
  record: exported.record,
12355
- history: exported.history,
12930
+ history: toolMode === "summary" ? applyToolContentMode(exported.history, "summary") : exported.history,
12356
12931
  promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
12932
+ ...exported.toolBlobs !== void 0 ? { toolBlobs: exported.toolBlobs } : {},
12357
12933
  hydraVersion: HYDRA_VERSION,
12358
12934
  machine: os3.hostname(),
12359
12935
  hydraHost: resolveHydraHost(defaults)
@@ -12365,6 +12941,17 @@ function registerSessionRoutes(app, manager, defaults) {
12365
12941
  );
12366
12942
  reply.code(200).send(bundle);
12367
12943
  });
12944
+ app.get("/v1/sessions/:id/tools/:hash", async (request, reply) => {
12945
+ const params = request.params;
12946
+ const id = await manager.resolveCanonicalId(params.id) ?? params.id;
12947
+ const blob = await manager.loadToolBlob(id, params.hash);
12948
+ if (blob === null) {
12949
+ reply.code(404).send({ error: "tool blob not found" });
12950
+ return;
12951
+ }
12952
+ reply.header("Content-Type", "text/plain; charset=utf-8");
12953
+ reply.code(200).send(blob);
12954
+ });
12368
12955
  app.get("/v1/sessions/:id/transcript", async (request, reply) => {
12369
12956
  const raw = request.params.id;
12370
12957
  const id = await manager.resolveCanonicalId(raw) ?? raw;
@@ -12849,11 +13436,11 @@ function registerConfigRoutes(app, snapshot) {
12849
13436
  import { z as z8 } from "zod";
12850
13437
 
12851
13438
  // src/core/password.ts
12852
- import * as fs16 from "fs/promises";
13439
+ import * as fs17 from "fs/promises";
12853
13440
  import * as path14 from "path";
12854
13441
  import { randomBytes as randomBytes3, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
12855
- import { promisify } from "util";
12856
- var scryptAsync = promisify(scrypt);
13442
+ import { promisify as promisify2 } from "util";
13443
+ var scryptAsync = promisify2(scrypt);
12857
13444
  function passwordHashPath() {
12858
13445
  return path14.join(paths.home(), "password-hash");
12859
13446
  }
@@ -12861,7 +13448,7 @@ var DEFAULT_N = 1 << 15;
12861
13448
  var MAX_MEM = 128 * 1024 * 1024;
12862
13449
  async function hasPassword() {
12863
13450
  try {
12864
- const text = await fs16.readFile(passwordHashPath(), "utf8");
13451
+ const text = await fs17.readFile(passwordHashPath(), "utf8");
12865
13452
  return text.trim().length > 0;
12866
13453
  } catch (err) {
12867
13454
  const e = err;
@@ -12877,7 +13464,7 @@ async function verifyPassword(plaintext) {
12877
13464
  }
12878
13465
  let line;
12879
13466
  try {
12880
- line = (await fs16.readFile(passwordHashPath(), "utf8")).trim();
13467
+ line = (await fs17.readFile(passwordHashPath(), "utf8")).trim();
12881
13468
  } catch (err) {
12882
13469
  const e = err;
12883
13470
  if (e.code === "ENOENT") {
@@ -13371,6 +13958,24 @@ function registerAcpWsEndpoint(app, deps) {
13371
13958
  return { ok: true };
13372
13959
  });
13373
13960
  }
13961
+ connection.onRequest("hydra-acp/session/tool_content", async (raw) => {
13962
+ const params = raw ?? {};
13963
+ if (typeof params.sessionId !== "string" || typeof params.hash !== "string") {
13964
+ throw Object.assign(
13965
+ new Error("hydra-acp/session/tool_content requires sessionId and hash"),
13966
+ { code: JsonRpcErrorCodes.InvalidParams }
13967
+ );
13968
+ }
13969
+ const id = await deps.manager.resolveCanonicalId(params.sessionId) ?? params.sessionId;
13970
+ const content = await deps.manager.loadToolBlob(id, params.hash);
13971
+ if (content === null) {
13972
+ throw Object.assign(
13973
+ new Error("tool content not found"),
13974
+ { code: JsonRpcErrorCodes.SessionNotFound }
13975
+ );
13976
+ }
13977
+ return { content };
13978
+ });
13374
13979
  connection.onRequest("session/new", async (raw) => {
13375
13980
  const params = SessionNewParams.parse(raw);
13376
13981
  const hydraMeta = extractHydraMeta(
@@ -13577,7 +14182,13 @@ function registerAcpWsEndpoint(app, deps) {
13577
14182
  const { entries: replay, appliedPolicy } = await session.attach(
13578
14183
  client,
13579
14184
  params.historyPolicy,
13580
- { afterMessageId: params.afterMessageId, raw: drip }
14185
+ {
14186
+ afterMessageId: params.afterMessageId,
14187
+ raw: drip,
14188
+ // Lean clients opt into ref-form tool content via _meta; default
14189
+ // stays inline so existing/third-party clients are unaffected.
14190
+ ...hydraAttach.toolContent !== void 0 ? { toolContent: hydraAttach.toolContent } : {}
14191
+ }
13581
14192
  );
13582
14193
  state.attached.set(session.sessionId, {
13583
14194
  sessionId: session.sessionId,
@@ -13611,8 +14222,13 @@ function registerAcpWsEndpoint(app, deps) {
13611
14222
  }
13612
14223
  })();
13613
14224
  } else {
13614
- for (const note of replay) {
13615
- await connection.notify(note.method, note.params);
14225
+ const REPLAY_FLUSH_EVERY = 200;
14226
+ for (let i = 0; i < replay.length; i++) {
14227
+ const note = replay[i];
14228
+ const pending = connection.notify(note.method, note.params).catch(() => void 0);
14229
+ if ((i + 1) % REPLAY_FLUSH_EVERY === 0) {
14230
+ await pending;
14231
+ }
13616
14232
  }
13617
14233
  }
13618
14234
  session.replayPendingPermissions(client);
@@ -13869,8 +14485,11 @@ function registerAcpWsEndpoint(app, deps) {
13869
14485
  return null;
13870
14486
  }
13871
14487
  app.log.info(decision.logMessage);
13872
- const { modelId } = rawParams;
13873
- const result = await decision.session.forwardRequest("session/set_model", rawParams);
14488
+ const { modelId } = decision;
14489
+ const result = await decision.session.forwardRequest("session/set_model", {
14490
+ ...rawParams,
14491
+ modelId
14492
+ });
13874
14493
  decision.session.applyModelChange(modelId);
13875
14494
  return result;
13876
14495
  });
@@ -14092,36 +14711,47 @@ function decideSetModel(rawParams, manager) {
14092
14711
  };
14093
14712
  }
14094
14713
  const advertised = session.availableModels();
14095
- if (advertised.length === 0) {
14714
+ const resolution = resolveModelId(params.modelId, advertised);
14715
+ if (resolution.kind === "none") {
14096
14716
  return {
14097
14717
  kind: "ok",
14098
14718
  session,
14719
+ modelId: params.modelId,
14099
14720
  logMessage: `session/set_model passthrough (no availableModels) sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
14100
14721
  };
14101
14722
  }
14102
- const match = advertised.find((m) => m.modelId === params.modelId);
14103
- if (!match) {
14104
- const known = advertised.map((m) => m.modelId).join(", ");
14105
- if (session.currentModel !== void 0 && session.currentModel.length > 0) {
14106
- return {
14107
- kind: "no_op",
14108
- session,
14109
- sessionId: params.sessionId,
14110
- currentModel: session.currentModel,
14111
- 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}]`
14112
- };
14113
- }
14723
+ if (resolution.kind === "exact") {
14114
14724
  return {
14115
- kind: "error",
14116
- code: JsonRpcErrorCodes.InvalidParams,
14117
- message: `model "${params.modelId}" is not in this session's availableModels (agent ${session.agentId}); known models: ${known}`,
14118
- 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)`
14725
+ kind: "ok",
14726
+ session,
14727
+ modelId: params.modelId,
14728
+ logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
14729
+ };
14730
+ }
14731
+ if (resolution.kind === "resolved") {
14732
+ return {
14733
+ kind: "ok",
14734
+ session,
14735
+ modelId: resolution.modelId,
14736
+ logMessage: `session/set_model resolved sessionId=${params.sessionId} requested=${JSON.stringify(params.modelId)} \u2192 ${JSON.stringify(resolution.modelId)}`
14737
+ };
14738
+ }
14739
+ const known = advertised.map((m) => m.modelId).join(", ");
14740
+ const detail = resolution.kind === "ambiguous" ? `ambiguous (trailing-segment matches [${resolution.candidates.join(", ")}])` : `not in availableModels`;
14741
+ if (session.currentModel !== void 0 && session.currentModel.length > 0) {
14742
+ return {
14743
+ kind: "no_op",
14744
+ session,
14745
+ sessionId: params.sessionId,
14746
+ currentModel: session.currentModel,
14747
+ logMessage: `session/set_model no_op (resyncing client) sessionId=${params.sessionId} requested=${JSON.stringify(params.modelId)} ${detail} actual=${JSON.stringify(session.currentModel)} agentId=${session.agentId} known=[${known}]`
14119
14748
  };
14120
14749
  }
14121
14750
  return {
14122
- kind: "ok",
14123
- session,
14124
- logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
14751
+ kind: "error",
14752
+ code: JsonRpcErrorCodes.InvalidParams,
14753
+ message: `model "${params.modelId}" is ${detail === "not in availableModels" ? "not in this session's availableModels" : detail} (agent ${session.agentId}); known models: ${known}`,
14754
+ logMessage: `session/set_model rejected sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)} ${detail} agentId=${session.agentId} known=[${known}] (no current model to fall back to)`
14125
14755
  };
14126
14756
  }
14127
14757
  function buildViewerResponseMeta(fromDisk) {
@@ -14921,6 +15551,7 @@ async function startDaemon(config, serviceToken) {
14921
15551
  stderrTailBytes: config.daemon.agentStderrTailBytes,
14922
15552
  logger: agentLogger
14923
15553
  });
15554
+ setToolBlobCompression(config.compressToolContent);
14924
15555
  const extensionCommands = new ExtensionCommandRegistry();
14925
15556
  const manager = new SessionManager(registry, spawner, void 0, {
14926
15557
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
@@ -15052,7 +15683,7 @@ async function startDaemon(config, serviceToken) {
15052
15683
  setAgentPruneLogger(null);
15053
15684
  await app.close();
15054
15685
  try {
15055
- fs17.unlinkSync(paths.pidFile());
15686
+ fs18.unlinkSync(paths.pidFile());
15056
15687
  } catch {
15057
15688
  }
15058
15689
  try {