@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/cli.js +1403 -396
- package/dist/index.d.ts +34 -3
- package/dist/index.js +763 -132
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/daemon/server.ts
|
|
2
|
-
import * as
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
2456
|
-
const stream =
|
|
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
|
|
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
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
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
|
|
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
|
|
3278
|
+
await fs8.unlink(file).catch(() => void 0);
|
|
3081
3279
|
return;
|
|
3082
3280
|
}
|
|
3083
|
-
await
|
|
3281
|
+
await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
3084
3282
|
const body = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
3085
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7049
|
+
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
6831
7050
|
function assertSafeId(id) {
|
|
6832
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
7078
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6860
7079
|
return;
|
|
6861
7080
|
}
|
|
6862
7081
|
try {
|
|
6863
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
7111
|
-
const pkg = JSON.parse(
|
|
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
|
|
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
|
|
7739
|
-
|
|
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 (!
|
|
8179
|
+
if (!SESSION_ID_PATTERN3.test(sessionId)) {
|
|
7752
8180
|
return;
|
|
7753
8181
|
}
|
|
7754
8182
|
return this.enqueue(sessionId, async () => {
|
|
7755
|
-
await
|
|
7756
|
-
const
|
|
7757
|
-
|
|
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 (!
|
|
8196
|
+
if (!SESSION_ID_PATTERN3.test(sessionId)) {
|
|
7765
8197
|
return;
|
|
7766
8198
|
}
|
|
7767
8199
|
return this.enqueue(sessionId, async () => {
|
|
7768
|
-
await
|
|
7769
|
-
const
|
|
7770
|
-
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
-
|
|
7807
|
-
|
|
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
|
|
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
|
-
|
|
7852
|
-
|
|
8293
|
+
const kept = out.length > this.maxEntries ? out.slice(-this.maxEntries) : out;
|
|
8294
|
+
if (!expand) {
|
|
8295
|
+
return kept;
|
|
7853
8296
|
}
|
|
7854
|
-
|
|
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 (!
|
|
8327
|
+
if (!SESSION_ID_PATTERN3.test(sessionId)) {
|
|
7871
8328
|
return;
|
|
7872
8329
|
}
|
|
7873
8330
|
return this.enqueue(sessionId, async () => {
|
|
7874
8331
|
try {
|
|
7875
|
-
await
|
|
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
|
|
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
|
|
8365
|
+
import { promises as fs14 } from "fs";
|
|
7908
8366
|
import * as path8 from "path";
|
|
7909
8367
|
async function saveHistory(file, history) {
|
|
7910
|
-
await
|
|
8368
|
+
await fs14.mkdir(path8.dirname(file), { recursive: true });
|
|
7911
8369
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
7912
|
-
await
|
|
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
|
|
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
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
11308
|
-
const
|
|
11309
|
-
if (
|
|
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:
|
|
11316
|
-
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
13615
|
-
|
|
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 } =
|
|
13873
|
-
const result = await decision.session.forwardRequest("session/set_model",
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
14116
|
-
|
|
14117
|
-
|
|
14118
|
-
logMessage: `session/set_model
|
|
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: "
|
|
14123
|
-
|
|
14124
|
-
|
|
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
|
-
|
|
15686
|
+
fs18.unlinkSync(paths.pidFile());
|
|
15056
15687
|
} catch {
|
|
15057
15688
|
}
|
|
15058
15689
|
try {
|