@hydra-acp/cli 0.1.61 → 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/README.md +123 -107
- package/dist/cli.js +3634 -1107
- package/dist/index.d.ts +265 -25
- package/dist/index.js +2103 -641
- 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,12 +95,35 @@ 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
|
|
88
107
|
// the agent invocation (see Session.drainQueue) so a crash mid-
|
|
89
108
|
// generation doesn't double-run on restart.
|
|
90
109
|
queueFile: (id) => path.join(hydraHome(), "sessions", id, "queue.ndjson"),
|
|
110
|
+
// Tombstones for sessions that were deleted locally but might still
|
|
111
|
+
// be reported by an agent's session/list at the next periodic sync.
|
|
112
|
+
// One file per (agentId, upstreamSessionId); existence is the source
|
|
113
|
+
// of truth, contents are a small JSON blob for diagnostics and the
|
|
114
|
+
// "agent advanced past our snapshot → resurrect" decision. Hidden
|
|
115
|
+
// under sessions/ because SessionStore.read() filters non-conforming
|
|
116
|
+
// dir names (the leading dot fails SESSION_ID_PATTERN) so the
|
|
117
|
+
// directory cohabits safely with real session directories.
|
|
118
|
+
tombstonesDir: () => path.join(hydraHome(), "sessions", ".tombstones"),
|
|
119
|
+
tombstoneAgentDir: (agentId) => path.join(hydraHome(), "sessions", ".tombstones", encodeURIComponent(agentId)),
|
|
120
|
+
tombstoneFile: (agentId, upstreamSessionId) => path.join(
|
|
121
|
+
hydraHome(),
|
|
122
|
+
"sessions",
|
|
123
|
+
".tombstones",
|
|
124
|
+
encodeURIComponent(agentId),
|
|
125
|
+
encodeURIComponent(upstreamSessionId)
|
|
126
|
+
),
|
|
91
127
|
extensionsDir: () => path.join(hydraHome(), "extensions"),
|
|
92
128
|
extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
|
|
93
129
|
extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
|
|
@@ -267,7 +303,27 @@ var DaemonConfig = z.object({
|
|
|
267
303
|
});
|
|
268
304
|
var RegistryConfig = z.object({
|
|
269
305
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
270
|
-
ttlHours: z.number().positive().default(24)
|
|
306
|
+
ttlHours: z.number().positive().default(24),
|
|
307
|
+
// When true, the daemon never re-fetches the registry over the network:
|
|
308
|
+
// it serves whatever is in the on-disk cache (~/.hydra-acp/registry.json)
|
|
309
|
+
// indefinitely, ignoring ttlHours. An escape hatch for when a bad registry
|
|
310
|
+
// push breaks an agent — pin to the last-known-good cache until upstream
|
|
311
|
+
// is fixed. `hydra registry refresh` still forces a one-off fetch.
|
|
312
|
+
pinned: z.boolean().default(false)
|
|
313
|
+
});
|
|
314
|
+
var LocalAgentConfig = z.object({
|
|
315
|
+
name: z.string().optional(),
|
|
316
|
+
description: z.string().optional(),
|
|
317
|
+
// Optional: defaults to the agent id (the config.agents key), mirroring
|
|
318
|
+
// how extensions default their command to the extension name. Set it
|
|
319
|
+
// when the executable differs from the id, or to point at an absolute
|
|
320
|
+
// path / wrapper script.
|
|
321
|
+
command: z.string().optional(),
|
|
322
|
+
args: z.array(z.string()).optional(),
|
|
323
|
+
env: z.record(z.string()).optional()
|
|
324
|
+
});
|
|
325
|
+
var AgentOverrideConfig = z.object({
|
|
326
|
+
packageSpec: z.string().optional()
|
|
271
327
|
});
|
|
272
328
|
var TuiConfig = z.object({
|
|
273
329
|
// Minimum interval (ms) between full-screen repaints driven by content
|
|
@@ -313,6 +369,29 @@ var TuiConfig = z.object({
|
|
|
313
369
|
// suppress them — the TUI hotkey ^T toggles this at runtime without
|
|
314
370
|
// persisting back to config.
|
|
315
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),
|
|
316
395
|
// Cap on entries kept in the cross-session global prompt-history file
|
|
317
396
|
// (~/.hydra-acp/prompt-history). This is the ^P / ^R recall list
|
|
318
397
|
// shared across all sessions; it's append-only on disk, so long-lived
|
|
@@ -385,7 +464,18 @@ var TransformerBody = z.object({
|
|
|
385
464
|
});
|
|
386
465
|
var HydraConfig = z.object({
|
|
387
466
|
daemon: DaemonConfig.default({}),
|
|
388
|
-
registry: RegistryConfig.default({
|
|
467
|
+
registry: RegistryConfig.default({
|
|
468
|
+
url: REGISTRY_URL_DEFAULT,
|
|
469
|
+
ttlHours: 24,
|
|
470
|
+
pinned: false
|
|
471
|
+
}),
|
|
472
|
+
// User-defined agents that bypass the network registry. Keyed by agent
|
|
473
|
+
// id; each is spawned via its `command`/`args` directly. Shadow registry
|
|
474
|
+
// agents of the same id.
|
|
475
|
+
agents: z.record(z.string(), LocalAgentConfig).default({}),
|
|
476
|
+
// Per-agent pin overrides applied to registry agents (e.g. force a
|
|
477
|
+
// specific npm version of opencode). Keyed by agent id.
|
|
478
|
+
agentOverrides: z.record(z.string(), AgentOverrideConfig).default({}),
|
|
389
479
|
defaultAgent: z.string().default("opencode"),
|
|
390
480
|
// Optional per-agent default model id. When a brand-new agent process
|
|
391
481
|
// is spawned (session/new path), hydra issues session/set_model with
|
|
@@ -417,6 +507,12 @@ var HydraConfig = z.object({
|
|
|
417
507
|
// a literal string ("~", "~/dev", "$HOME/work") so the config file is
|
|
418
508
|
// portable across machines; expanded via expandHome at use time.
|
|
419
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),
|
|
420
516
|
// Cap on cold sessions shown in CLI `sessions` listing and the TUI
|
|
421
517
|
// picker. Live sessions are always included; cold are sorted by
|
|
422
518
|
// recency and truncated to this count. `--all` overrides in the CLI.
|
|
@@ -438,6 +534,9 @@ var HydraConfig = z.object({
|
|
|
438
534
|
progressIndicator: true,
|
|
439
535
|
defaultEnterAction: "amend",
|
|
440
536
|
showThoughts: true,
|
|
537
|
+
ambiguousWidth: "wide",
|
|
538
|
+
toolContent: "references",
|
|
539
|
+
diffContextLines: 3,
|
|
441
540
|
promptHistoryMaxEntries: 2e3,
|
|
442
541
|
maxToolItems: 5,
|
|
443
542
|
maxPlanItems: 5,
|
|
@@ -516,13 +615,113 @@ function expandHome(p) {
|
|
|
516
615
|
return p;
|
|
517
616
|
}
|
|
518
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
|
+
|
|
519
718
|
// src/core/registry.ts
|
|
520
|
-
import * as
|
|
719
|
+
import * as fs6 from "fs/promises";
|
|
521
720
|
import * as path4 from "path";
|
|
522
721
|
import { z as z2 } from "zod";
|
|
523
722
|
|
|
524
723
|
// src/core/binary-install.ts
|
|
525
|
-
import * as
|
|
724
|
+
import * as fs5 from "fs";
|
|
526
725
|
import * as fsp from "fs/promises";
|
|
527
726
|
import * as path2 from "path";
|
|
528
727
|
import { spawn } from "child_process";
|
|
@@ -655,7 +854,7 @@ async function downloadTo(args) {
|
|
|
655
854
|
);
|
|
656
855
|
}
|
|
657
856
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
658
|
-
const out =
|
|
857
|
+
const out = fs5.createWriteStream(dest);
|
|
659
858
|
const nodeStream = Readable.fromWeb(response.body);
|
|
660
859
|
safeEmit(args.onProgress, {
|
|
661
860
|
phase: "download_start",
|
|
@@ -1062,10 +1261,16 @@ var UvxDistribution = z2.object({
|
|
|
1062
1261
|
args: z2.array(z2.string()).optional(),
|
|
1063
1262
|
env: z2.record(z2.string()).optional()
|
|
1064
1263
|
});
|
|
1264
|
+
var ExecDistribution = z2.object({
|
|
1265
|
+
command: z2.string(),
|
|
1266
|
+
args: z2.array(z2.string()).optional(),
|
|
1267
|
+
env: z2.record(z2.string()).optional()
|
|
1268
|
+
});
|
|
1065
1269
|
var Distribution = z2.object({
|
|
1066
1270
|
npx: NpxDistribution.optional(),
|
|
1067
1271
|
binary: BinaryDistribution.optional(),
|
|
1068
|
-
uvx: UvxDistribution.optional()
|
|
1272
|
+
uvx: UvxDistribution.optional(),
|
|
1273
|
+
exec: ExecDistribution.optional()
|
|
1069
1274
|
});
|
|
1070
1275
|
var RegistryAgent = z2.object({
|
|
1071
1276
|
id: z2.string(),
|
|
@@ -1093,11 +1298,11 @@ var Registry = class {
|
|
|
1093
1298
|
options;
|
|
1094
1299
|
cache;
|
|
1095
1300
|
async load() {
|
|
1096
|
-
if (this.cache && this.isFresh(this.cache.fetchedAt)) {
|
|
1301
|
+
if (this.cache && (this.isPinned() || this.isFresh(this.cache.fetchedAt))) {
|
|
1097
1302
|
return this.cache.data;
|
|
1098
1303
|
}
|
|
1099
1304
|
const onDisk = await this.readDiskCache();
|
|
1100
|
-
if (onDisk && this.isFresh(onDisk.fetchedAt)) {
|
|
1305
|
+
if (onDisk && (this.isPinned() || this.isFresh(onDisk.fetchedAt))) {
|
|
1101
1306
|
this.cache = onDisk;
|
|
1102
1307
|
return onDisk.data;
|
|
1103
1308
|
}
|
|
@@ -1128,12 +1333,57 @@ var Registry = class {
|
|
|
1128
1333
|
return this.cache?.fetchedAt;
|
|
1129
1334
|
}
|
|
1130
1335
|
async getAgent(id) {
|
|
1336
|
+
const local = this.localAgents().find((a) => a.id === id);
|
|
1337
|
+
if (local) {
|
|
1338
|
+
return local;
|
|
1339
|
+
}
|
|
1131
1340
|
const doc = await this.load();
|
|
1132
1341
|
const exact = doc.agents.find((a) => a.id === id);
|
|
1133
1342
|
if (exact) {
|
|
1134
|
-
return exact;
|
|
1343
|
+
return this.applyOverride(exact);
|
|
1344
|
+
}
|
|
1345
|
+
const byBasename = doc.agents.find((a) => npxPackageBasename(a) === id);
|
|
1346
|
+
return byBasename ? this.applyOverride(byBasename) : void 0;
|
|
1347
|
+
}
|
|
1348
|
+
// Synthesize RegistryAgent entries from config.agents. These carry an
|
|
1349
|
+
// `exec` distribution and a fixed "local" version key (no install dir).
|
|
1350
|
+
localAgents() {
|
|
1351
|
+
return Object.entries(this.config.agents ?? {}).map(([id, def]) => ({
|
|
1352
|
+
id,
|
|
1353
|
+
name: def.name ?? id,
|
|
1354
|
+
description: def.description,
|
|
1355
|
+
version: "local",
|
|
1356
|
+
distribution: {
|
|
1357
|
+
exec: {
|
|
1358
|
+
// Default the command to the agent id (like extensions default
|
|
1359
|
+
// theirs to the extension name) — resolved off PATH at spawn.
|
|
1360
|
+
command: def.command ?? id,
|
|
1361
|
+
args: def.args,
|
|
1362
|
+
env: def.env
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}));
|
|
1366
|
+
}
|
|
1367
|
+
// Apply a config.agentOverrides[id] pin to a registry agent: swap the
|
|
1368
|
+
// npx package spec and key the install dir on the pinned version so it
|
|
1369
|
+
// never collides with the floating "current" install. No-op when the
|
|
1370
|
+
// agent has no override or isn't npx-distributed.
|
|
1371
|
+
applyOverride(agent) {
|
|
1372
|
+
const override = this.config.agentOverrides?.[agent.id];
|
|
1373
|
+
if (!override?.packageSpec || !agent.distribution.npx) {
|
|
1374
|
+
return agent;
|
|
1135
1375
|
}
|
|
1136
|
-
return
|
|
1376
|
+
return {
|
|
1377
|
+
...agent,
|
|
1378
|
+
version: versionKeyFromSpec(override.packageSpec),
|
|
1379
|
+
distribution: {
|
|
1380
|
+
...agent.distribution,
|
|
1381
|
+
npx: { ...agent.distribution.npx, package: override.packageSpec }
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
isPinned() {
|
|
1386
|
+
return this.config.registry?.pinned === true;
|
|
1137
1387
|
}
|
|
1138
1388
|
isFresh(fetchedAt) {
|
|
1139
1389
|
const ageMs = Date.now() - fetchedAt;
|
|
@@ -1175,6 +1425,12 @@ var Registry = class {
|
|
|
1175
1425
|
});
|
|
1176
1426
|
}
|
|
1177
1427
|
};
|
|
1428
|
+
function versionKeyFromSpec(spec) {
|
|
1429
|
+
const lastAt = spec.lastIndexOf("@");
|
|
1430
|
+
const version = lastAt > 0 ? spec.slice(lastAt + 1) : "";
|
|
1431
|
+
const sanitized = version.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1432
|
+
return sanitized.length > 0 ? `pin-${sanitized}` : "pinned";
|
|
1433
|
+
}
|
|
1178
1434
|
function npxPackageBasename(agent) {
|
|
1179
1435
|
const pkg = agent.distribution.npx?.package;
|
|
1180
1436
|
if (!pkg) {
|
|
@@ -1185,12 +1441,45 @@ function npxPackageBasename(agent) {
|
|
|
1185
1441
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
1186
1442
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
1187
1443
|
}
|
|
1444
|
+
async function listAgents(registry) {
|
|
1445
|
+
const local = typeof registry.localAgents === "function" ? registry.localAgents() : [];
|
|
1446
|
+
let doc;
|
|
1447
|
+
try {
|
|
1448
|
+
doc = await registry.load();
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
if (local.length === 0) {
|
|
1451
|
+
throw err;
|
|
1452
|
+
}
|
|
1453
|
+
doc = { version: "local-only", agents: [] };
|
|
1454
|
+
}
|
|
1455
|
+
const localIds = new Set(local.map((a) => a.id));
|
|
1456
|
+
const merged = [...local, ...doc.agents.filter((a) => !localIds.has(a.id))];
|
|
1457
|
+
const agents = await Promise.all(
|
|
1458
|
+
merged.map(async (a) => ({
|
|
1459
|
+
id: a.id,
|
|
1460
|
+
name: a.name,
|
|
1461
|
+
version: a.version,
|
|
1462
|
+
description: a.description,
|
|
1463
|
+
distributions: Object.keys(a.distribution),
|
|
1464
|
+
installed: await agentInstallState(a),
|
|
1465
|
+
source: localIds.has(a.id) ? "local" : "registry"
|
|
1466
|
+
}))
|
|
1467
|
+
);
|
|
1468
|
+
return {
|
|
1469
|
+
version: doc.version,
|
|
1470
|
+
fetchedAt: registry.lastFetchedAt(),
|
|
1471
|
+
agents
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1188
1474
|
async function agentInstallState(agent) {
|
|
1189
1475
|
const platformKey = currentPlatformKey();
|
|
1190
1476
|
if (!platformKey) {
|
|
1191
1477
|
return "no";
|
|
1192
1478
|
}
|
|
1193
1479
|
const version = agent.version ?? "current";
|
|
1480
|
+
if (agent.distribution.exec) {
|
|
1481
|
+
return "yes";
|
|
1482
|
+
}
|
|
1194
1483
|
if (agent.distribution.binary) {
|
|
1195
1484
|
const target = pickBinaryTarget(agent.distribution.binary, platformKey);
|
|
1196
1485
|
if (target?.cmd) {
|
|
@@ -1219,7 +1508,7 @@ async function agentInstallState(agent) {
|
|
|
1219
1508
|
}
|
|
1220
1509
|
async function fileExists3(p) {
|
|
1221
1510
|
try {
|
|
1222
|
-
await
|
|
1511
|
+
await fs6.access(p);
|
|
1223
1512
|
return true;
|
|
1224
1513
|
} catch {
|
|
1225
1514
|
return false;
|
|
@@ -1287,12 +1576,22 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
1287
1576
|
version
|
|
1288
1577
|
};
|
|
1289
1578
|
}
|
|
1579
|
+
if (agent.distribution.exec) {
|
|
1580
|
+
const exec = agent.distribution.exec;
|
|
1581
|
+
const tail = callerArgs.length > 0 ? callerArgs : exec.args ?? [];
|
|
1582
|
+
return {
|
|
1583
|
+
command: exec.command,
|
|
1584
|
+
args: tail,
|
|
1585
|
+
env: exec.env ?? {},
|
|
1586
|
+
version
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1290
1589
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
1291
1590
|
}
|
|
1292
1591
|
|
|
1293
1592
|
// src/core/agent-instance.ts
|
|
1294
1593
|
import { spawn as spawn3 } from "child_process";
|
|
1295
|
-
import * as
|
|
1594
|
+
import * as fs7 from "fs";
|
|
1296
1595
|
import * as path5 from "path";
|
|
1297
1596
|
|
|
1298
1597
|
// src/acp/types.ts
|
|
@@ -1344,8 +1643,8 @@ var HistoryPolicy = z3.enum([
|
|
|
1344
1643
|
]);
|
|
1345
1644
|
var SessionNewParams = z3.object({
|
|
1346
1645
|
cwd: z3.string(),
|
|
1347
|
-
|
|
1348
|
-
|
|
1646
|
+
mcpServers: z3.array(z3.unknown()).optional(),
|
|
1647
|
+
_meta: z3.record(z3.unknown()).optional()
|
|
1349
1648
|
});
|
|
1350
1649
|
var SessionResumeHints = z3.object({
|
|
1351
1650
|
upstreamSessionId: z3.string(),
|
|
@@ -1373,23 +1672,10 @@ var SessionAttachParams = z3.object({
|
|
|
1373
1672
|
name: z3.string(),
|
|
1374
1673
|
version: z3.string().optional()
|
|
1375
1674
|
}).optional(),
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
// session/
|
|
1379
|
-
//
|
|
1380
|
-
// streams history from disk. Used by the TUI's view-only mode.
|
|
1381
|
-
readonly: z3.boolean().optional(),
|
|
1382
|
-
// Debug-only replay pacing. When "drip", the daemon skips chunk
|
|
1383
|
-
// coalescing and re-emits each recorded session/update individually,
|
|
1384
|
-
// spacing them by their original recordedAt deltas (scaled by
|
|
1385
|
-
// dripSpeed, with a per-gap cap) so a session's streaming render can
|
|
1386
|
-
// be reproduced at its real granularity for flicker investigation.
|
|
1387
|
-
// Omitted/"instant" preserves the normal coalesced, as-fast-as-possible
|
|
1388
|
-
// replay.
|
|
1389
|
-
replayMode: z3.enum(["instant", "drip"]).optional(),
|
|
1390
|
-
// Multiplier applied to original inter-entry gaps in drip mode. >1
|
|
1391
|
-
// compresses time (faster), <1 stretches it. Defaults to 1.
|
|
1392
|
-
dripSpeed: z3.number().positive().optional(),
|
|
1675
|
+
// Hydra-specific attach options (readonly, replayMode, dripSpeed) are
|
|
1676
|
+
// NOT top-level — they ride under `_meta["hydra-acp"]` (read via
|
|
1677
|
+
// extractHydraMeta) so session/attach carries only RFD #533's own
|
|
1678
|
+
// fields at the top level.
|
|
1393
1679
|
_meta: z3.record(z3.unknown()).optional()
|
|
1394
1680
|
});
|
|
1395
1681
|
var HYDRA_META_KEY = "hydra-acp";
|
|
@@ -1412,8 +1698,26 @@ function extractHydraMeta(meta) {
|
|
|
1412
1698
|
if (typeof obj.cwd === "string") {
|
|
1413
1699
|
out.cwd = obj.cwd;
|
|
1414
1700
|
}
|
|
1415
|
-
if (typeof obj.
|
|
1416
|
-
out.
|
|
1701
|
+
if (typeof obj.clientId === "string") {
|
|
1702
|
+
out.clientId = obj.clientId;
|
|
1703
|
+
}
|
|
1704
|
+
if (typeof obj.readonly === "boolean") {
|
|
1705
|
+
out.readonly = obj.readonly;
|
|
1706
|
+
}
|
|
1707
|
+
if (obj.replayMode === "instant" || obj.replayMode === "drip") {
|
|
1708
|
+
out.replayMode = obj.replayMode;
|
|
1709
|
+
}
|
|
1710
|
+
if (typeof obj.dripSpeed === "number" && obj.dripSpeed > 0) {
|
|
1711
|
+
out.dripSpeed = obj.dripSpeed;
|
|
1712
|
+
}
|
|
1713
|
+
if (obj.toolContent === "inline" || obj.toolContent === "references") {
|
|
1714
|
+
out.toolContent = obj.toolContent;
|
|
1715
|
+
}
|
|
1716
|
+
if (obj.detachStatus === "detached") {
|
|
1717
|
+
out.detachStatus = obj.detachStatus;
|
|
1718
|
+
}
|
|
1719
|
+
if (typeof obj.title === "string") {
|
|
1720
|
+
out.title = obj.title;
|
|
1417
1721
|
}
|
|
1418
1722
|
if (Array.isArray(obj.agentArgs) && obj.agentArgs.every((a) => typeof a === "string")) {
|
|
1419
1723
|
out.agentArgs = obj.agentArgs;
|
|
@@ -1465,14 +1769,22 @@ function extractHydraMeta(meta) {
|
|
|
1465
1769
|
out.availableCommands = cmds;
|
|
1466
1770
|
}
|
|
1467
1771
|
}
|
|
1468
|
-
if (typeof obj.
|
|
1469
|
-
|
|
1772
|
+
if (obj.prompt && typeof obj.prompt === "object" && !Array.isArray(obj.prompt)) {
|
|
1773
|
+
const p = obj.prompt;
|
|
1774
|
+
const caps = {};
|
|
1775
|
+
if (typeof p.queueing === "boolean") caps.queueing = p.queueing;
|
|
1776
|
+
if (typeof p.cancelling === "boolean") caps.cancelling = p.cancelling;
|
|
1777
|
+
if (typeof p.updating === "boolean") caps.updating = p.updating;
|
|
1778
|
+
if (typeof p.amending === "boolean") caps.amending = p.amending;
|
|
1779
|
+
if (typeof p.pipelining === "boolean") caps.pipelining = p.pipelining;
|
|
1780
|
+
out.prompt = caps;
|
|
1470
1781
|
}
|
|
1471
|
-
if (typeof obj.
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1782
|
+
if (obj.agents && typeof obj.agents === "object" && !Array.isArray(obj.agents)) {
|
|
1783
|
+
const a = obj.agents;
|
|
1784
|
+
const caps = {};
|
|
1785
|
+
if (typeof a.list === "boolean") caps.list = a.list;
|
|
1786
|
+
if (typeof a.installProgress === "boolean") caps.installProgress = a.installProgress;
|
|
1787
|
+
out.agents = caps;
|
|
1476
1788
|
}
|
|
1477
1789
|
if (typeof obj.mcpStdin === "boolean") {
|
|
1478
1790
|
out.mcpStdin = obj.mcpStdin;
|
|
@@ -1483,12 +1795,6 @@ function extractHydraMeta(meta) {
|
|
|
1483
1795
|
if (typeof obj.ancillary === "boolean") {
|
|
1484
1796
|
out.ancillary = obj.ancillary;
|
|
1485
1797
|
}
|
|
1486
|
-
if (typeof obj.promptAmending === "boolean") {
|
|
1487
|
-
out.promptAmending = obj.promptAmending;
|
|
1488
|
-
}
|
|
1489
|
-
if (typeof obj.promptPipelining === "boolean") {
|
|
1490
|
-
out.promptPipelining = obj.promptPipelining;
|
|
1491
|
-
}
|
|
1492
1798
|
if (Array.isArray(obj.queue)) {
|
|
1493
1799
|
const entries = [];
|
|
1494
1800
|
for (const raw of obj.queue) {
|
|
@@ -1561,6 +1867,43 @@ function extractHydraMeta(meta) {
|
|
|
1561
1867
|
out.availableModels = models;
|
|
1562
1868
|
}
|
|
1563
1869
|
}
|
|
1870
|
+
if (obj.status === "live" || obj.status === "cold") {
|
|
1871
|
+
out.status = obj.status;
|
|
1872
|
+
}
|
|
1873
|
+
if (typeof obj.busy === "boolean") {
|
|
1874
|
+
out.busy = obj.busy;
|
|
1875
|
+
}
|
|
1876
|
+
if (typeof obj.awaitingInput === "boolean") {
|
|
1877
|
+
out.awaitingInput = obj.awaitingInput;
|
|
1878
|
+
}
|
|
1879
|
+
if (typeof obj.attachedClients === "number") {
|
|
1880
|
+
out.attachedClients = obj.attachedClients;
|
|
1881
|
+
}
|
|
1882
|
+
if (typeof obj.importedFromMachine === "string") {
|
|
1883
|
+
out.importedFromMachine = obj.importedFromMachine;
|
|
1884
|
+
}
|
|
1885
|
+
if (typeof obj.importedFromUpstreamSessionId === "string") {
|
|
1886
|
+
out.importedFromUpstreamSessionId = obj.importedFromUpstreamSessionId;
|
|
1887
|
+
}
|
|
1888
|
+
if (typeof obj.parentSessionId === "string") {
|
|
1889
|
+
out.parentSessionId = obj.parentSessionId;
|
|
1890
|
+
}
|
|
1891
|
+
if (typeof obj.forkedFromSessionId === "string") {
|
|
1892
|
+
out.forkedFromSessionId = obj.forkedFromSessionId;
|
|
1893
|
+
}
|
|
1894
|
+
if (typeof obj.forkedFromMessageId === "string") {
|
|
1895
|
+
out.forkedFromMessageId = obj.forkedFromMessageId;
|
|
1896
|
+
}
|
|
1897
|
+
if (obj.originatingClient && typeof obj.originatingClient === "object" && !Array.isArray(obj.originatingClient) && typeof obj.originatingClient.name === "string") {
|
|
1898
|
+
const oc = obj.originatingClient;
|
|
1899
|
+
out.originatingClient = {
|
|
1900
|
+
name: oc.name,
|
|
1901
|
+
...typeof oc.version === "string" ? { version: oc.version } : {}
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
if (obj.agentCapabilities !== void 0) {
|
|
1905
|
+
out.agentCapabilities = obj.agentCapabilities;
|
|
1906
|
+
}
|
|
1564
1907
|
return out;
|
|
1565
1908
|
}
|
|
1566
1909
|
function mergeMeta(passthrough, ours) {
|
|
@@ -1598,7 +1941,7 @@ var SessionListEntry = z3.object({
|
|
|
1598
1941
|
importedFromUpstreamSessionId: z3.string().optional(),
|
|
1599
1942
|
// Set when this session was spawned as a child by a transformer.
|
|
1600
1943
|
parentSessionId: z3.string().optional(),
|
|
1601
|
-
// Local-fork breadcrumbs set by hydra-acp/
|
|
1944
|
+
// Local-fork breadcrumbs set by hydra-acp/session/fork. Distinct from
|
|
1602
1945
|
// the imported* family above: a fork is a local branch off another
|
|
1603
1946
|
// local session, an import is a cross-machine takeover.
|
|
1604
1947
|
forkedFromSessionId: z3.string().optional(),
|
|
@@ -1636,36 +1979,89 @@ var SessionListResult = z3.object({
|
|
|
1636
1979
|
sessions: z3.array(SessionListEntryWire),
|
|
1637
1980
|
nextCursor: z3.string().optional()
|
|
1638
1981
|
});
|
|
1639
|
-
function
|
|
1640
|
-
const
|
|
1982
|
+
function buildHydraSessionMeta(entry, extras) {
|
|
1983
|
+
const meta = {
|
|
1641
1984
|
attachedClients: entry.attachedClients,
|
|
1642
1985
|
status: entry.status,
|
|
1643
1986
|
busy: entry.busy,
|
|
1644
1987
|
awaitingInput: entry.awaitingInput
|
|
1645
1988
|
};
|
|
1989
|
+
if (entry.cwd !== void 0) {
|
|
1990
|
+
meta.cwd = entry.cwd;
|
|
1991
|
+
}
|
|
1992
|
+
if (entry.title !== void 0) {
|
|
1993
|
+
meta.title = entry.title;
|
|
1994
|
+
}
|
|
1646
1995
|
if (entry.agentId !== void 0) {
|
|
1647
|
-
|
|
1996
|
+
meta.agentId = entry.agentId;
|
|
1648
1997
|
}
|
|
1649
1998
|
if (entry.upstreamSessionId !== void 0) {
|
|
1650
|
-
|
|
1999
|
+
meta.upstreamSessionId = entry.upstreamSessionId;
|
|
1651
2000
|
}
|
|
1652
2001
|
if (entry.currentModel !== void 0) {
|
|
1653
|
-
|
|
2002
|
+
meta.currentModel = entry.currentModel;
|
|
1654
2003
|
}
|
|
1655
2004
|
if (entry.currentUsage !== void 0) {
|
|
1656
|
-
|
|
2005
|
+
meta.currentUsage = entry.currentUsage;
|
|
1657
2006
|
}
|
|
1658
2007
|
if (entry.importedFromMachine !== void 0) {
|
|
1659
|
-
|
|
2008
|
+
meta.importedFromMachine = entry.importedFromMachine;
|
|
1660
2009
|
}
|
|
1661
2010
|
if (entry.importedFromUpstreamSessionId !== void 0) {
|
|
1662
|
-
|
|
2011
|
+
meta.importedFromUpstreamSessionId = entry.importedFromUpstreamSessionId;
|
|
2012
|
+
}
|
|
2013
|
+
if (entry.parentSessionId !== void 0) {
|
|
2014
|
+
meta.parentSessionId = entry.parentSessionId;
|
|
2015
|
+
}
|
|
2016
|
+
if (entry.forkedFromSessionId !== void 0) {
|
|
2017
|
+
meta.forkedFromSessionId = entry.forkedFromSessionId;
|
|
2018
|
+
}
|
|
2019
|
+
if (entry.forkedFromMessageId !== void 0) {
|
|
2020
|
+
meta.forkedFromMessageId = entry.forkedFromMessageId;
|
|
2021
|
+
}
|
|
2022
|
+
if (entry.originatingClient !== void 0) {
|
|
2023
|
+
meta.originatingClient = entry.originatingClient;
|
|
2024
|
+
}
|
|
2025
|
+
if (entry.interactive !== void 0) {
|
|
2026
|
+
meta.interactive = entry.interactive;
|
|
2027
|
+
}
|
|
2028
|
+
if (extras) {
|
|
2029
|
+
if (extras.clientId !== void 0) {
|
|
2030
|
+
meta.clientId = extras.clientId;
|
|
2031
|
+
}
|
|
2032
|
+
if (extras.currentMode !== void 0) {
|
|
2033
|
+
meta.currentMode = extras.currentMode;
|
|
2034
|
+
}
|
|
2035
|
+
if (extras.agentArgs !== void 0 && extras.agentArgs.length > 0) {
|
|
2036
|
+
meta.agentArgs = extras.agentArgs;
|
|
2037
|
+
}
|
|
2038
|
+
if (extras.availableCommands !== void 0 && extras.availableCommands.length > 0) {
|
|
2039
|
+
meta.availableCommands = extras.availableCommands;
|
|
2040
|
+
}
|
|
2041
|
+
if (extras.availableModes !== void 0 && extras.availableModes.length > 0) {
|
|
2042
|
+
meta.availableModes = extras.availableModes;
|
|
2043
|
+
}
|
|
2044
|
+
if (extras.availableModels !== void 0 && extras.availableModels.length > 0) {
|
|
2045
|
+
meta.availableModels = extras.availableModels;
|
|
2046
|
+
}
|
|
2047
|
+
if (extras.turnStartedAt !== void 0) {
|
|
2048
|
+
meta.turnStartedAt = extras.turnStartedAt;
|
|
2049
|
+
}
|
|
2050
|
+
if (extras.agentCapabilities !== void 0) {
|
|
2051
|
+
meta.agentCapabilities = extras.agentCapabilities;
|
|
2052
|
+
}
|
|
2053
|
+
if (extras.queue !== void 0 && extras.queue.length > 0) {
|
|
2054
|
+
meta.queue = extras.queue;
|
|
2055
|
+
}
|
|
1663
2056
|
}
|
|
2057
|
+
return meta;
|
|
2058
|
+
}
|
|
2059
|
+
function sessionListEntryToWire(entry) {
|
|
1664
2060
|
const wire = {
|
|
1665
2061
|
sessionId: entry.sessionId,
|
|
1666
2062
|
cwd: entry.cwd,
|
|
1667
2063
|
updatedAt: entry.updatedAt,
|
|
1668
|
-
_meta: mergeMeta(entry._meta,
|
|
2064
|
+
_meta: mergeMeta(entry._meta, buildHydraSessionMeta(entry))
|
|
1669
2065
|
};
|
|
1670
2066
|
if (entry.title !== void 0) {
|
|
1671
2067
|
wire.title = entry.title;
|
|
@@ -1753,65 +2149,6 @@ var PromptAmendedParams = z3.object({
|
|
|
1753
2149
|
originator: PromptOriginatorSchema,
|
|
1754
2150
|
amendedAt: z3.number()
|
|
1755
2151
|
});
|
|
1756
|
-
var StreamOpenParams = z3.object({
|
|
1757
|
-
sessionId: z3.string(),
|
|
1758
|
-
// 'memory' keeps the ring in RAM only — needed for the eventual MCP
|
|
1759
|
-
// tool surface. 'file' adds a temp file projection that the agent can
|
|
1760
|
-
// consume with shell tools (tail -f / head / grep) when MCP isn't
|
|
1761
|
-
// available. The temp file's path is returned in the response.
|
|
1762
|
-
mode: z3.enum(["memory", "file"]).optional(),
|
|
1763
|
-
// Ring capacity in bytes. Server clamps to a reasonable minimum and
|
|
1764
|
-
// its configured max; omitted falls back to the daemon default.
|
|
1765
|
-
capacityBytes: z3.number().int().positive().optional(),
|
|
1766
|
-
// File mode only. Soft cap in bytes; after this many bytes are
|
|
1767
|
-
// written to the file, further appends still land in the ring but
|
|
1768
|
-
// stop being mirrored to disk. The daemon emits one stream_truncated
|
|
1769
|
-
// session/update notification when the cap is first hit.
|
|
1770
|
-
fileCapBytes: z3.number().int().positive().optional()
|
|
1771
|
-
});
|
|
1772
|
-
var StreamOpenResult = z3.object({
|
|
1773
|
-
// Only present when mode === "file".
|
|
1774
|
-
filePath: z3.string().optional(),
|
|
1775
|
-
capacityBytes: z3.number().int().positive(),
|
|
1776
|
-
fileCapBytes: z3.number().int().positive().optional()
|
|
1777
|
-
});
|
|
1778
|
-
var StreamWriteParams = z3.object({
|
|
1779
|
-
sessionId: z3.string(),
|
|
1780
|
-
// Base64-encoded bytes. UTF-8 stdin gets re-encoded on the wire; the
|
|
1781
|
-
// ring is byte-exact so binary streams (audio, framed protocols) work
|
|
1782
|
-
// identically.
|
|
1783
|
-
chunk: z3.string(),
|
|
1784
|
-
// True on the final write. Pending long-poll reads / waits return with
|
|
1785
|
-
// eof:true once this is observed.
|
|
1786
|
-
eof: z3.boolean().optional()
|
|
1787
|
-
});
|
|
1788
|
-
var StreamWriteResult = z3.object({
|
|
1789
|
-
// Absolute writeCursor after this append landed.
|
|
1790
|
-
writeCursor: z3.number().int().nonnegative()
|
|
1791
|
-
});
|
|
1792
|
-
var StreamReadParams = z3.object({
|
|
1793
|
-
sessionId: z3.string(),
|
|
1794
|
-
cursor: z3.number().int().nonnegative(),
|
|
1795
|
-
// Cap on bytes returned. Server enforces a hard ceiling (STREAM_READ_MAX_BYTES,
|
|
1796
|
-
// currently 64 KiB) even when the caller asks for more.
|
|
1797
|
-
maxBytes: z3.number().int().positive().optional(),
|
|
1798
|
-
// Long-poll timeout in ms. 0 / omitted returns immediately with
|
|
1799
|
-
// whatever's available (possibly empty). Server cap 60s.
|
|
1800
|
-
waitMs: z3.number().int().nonnegative().optional()
|
|
1801
|
-
});
|
|
1802
|
-
var StreamReadResult = z3.object({
|
|
1803
|
-
// Base64-encoded bytes. Empty string when nothing new is available
|
|
1804
|
-
// and either waitMs was 0 or the long-poll expired without data.
|
|
1805
|
-
bytes: z3.string(),
|
|
1806
|
-
nextCursor: z3.number().int().nonnegative(),
|
|
1807
|
-
// Set when `cursor` pointed before the oldest still-resident byte —
|
|
1808
|
-
// value is the count of bytes that were evicted between the caller's
|
|
1809
|
-
// cursor and what we still have.
|
|
1810
|
-
gap: z3.number().int().nonnegative().optional(),
|
|
1811
|
-
// True when the producer has closed AND there are no more bytes
|
|
1812
|
-
// after nextCursor.
|
|
1813
|
-
eof: z3.boolean().optional()
|
|
1814
|
-
});
|
|
1815
2152
|
var AgentInstallProgressParams = z3.object({
|
|
1816
2153
|
agentId: z3.string(),
|
|
1817
2154
|
version: z3.string(),
|
|
@@ -1828,7 +2165,7 @@ var AgentInstallProgressParams = z3.object({
|
|
|
1828
2165
|
totalBytes: z3.number().optional(),
|
|
1829
2166
|
packageSpec: z3.string().optional()
|
|
1830
2167
|
});
|
|
1831
|
-
var AGENT_INSTALL_PROGRESS_METHOD = "hydra-acp/
|
|
2168
|
+
var AGENT_INSTALL_PROGRESS_METHOD = "hydra-acp/agents/install_progress";
|
|
1832
2169
|
|
|
1833
2170
|
// src/acp/framing.ts
|
|
1834
2171
|
function ndjsonStreamFromStdio(stdout, stdin) {
|
|
@@ -1931,6 +2268,13 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1931
2268
|
pending = /* @__PURE__ */ new Map();
|
|
1932
2269
|
closed = false;
|
|
1933
2270
|
closeHandlers = [];
|
|
2271
|
+
// Observers for error frames that arrive with no matching pending
|
|
2272
|
+
// request — e.g. an agent replying with an error to a notification
|
|
2273
|
+
// (which carries no id, so it can't be correlated the normal way).
|
|
2274
|
+
// session/cancel is the canonical case: an agent that doesn't support
|
|
2275
|
+
// it (current opencode) emits a MethodNotFound/UnsupportedOperation
|
|
2276
|
+
// error frame we'd otherwise silently drop. See handleResponse.
|
|
2277
|
+
orphanErrorHandlers = [];
|
|
1934
2278
|
onRequest(method, handler) {
|
|
1935
2279
|
this.requestHandlers.set(method, handler);
|
|
1936
2280
|
}
|
|
@@ -1966,6 +2310,10 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1966
2310
|
onClose(handler) {
|
|
1967
2311
|
this.closeHandlers.push(handler);
|
|
1968
2312
|
}
|
|
2313
|
+
// Subscribe to error frames that can't be matched to a pending request.
|
|
2314
|
+
onOrphanError(handler) {
|
|
2315
|
+
this.orphanErrorHandlers.push(handler);
|
|
2316
|
+
}
|
|
1969
2317
|
async request(method, params) {
|
|
1970
2318
|
return this.requestWithId(method, params).response;
|
|
1971
2319
|
}
|
|
@@ -2022,6 +2370,8 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
2022
2370
|
}
|
|
2023
2371
|
} else if ("id" in message) {
|
|
2024
2372
|
this.handleResponse(message);
|
|
2373
|
+
} else if ("error" in message) {
|
|
2374
|
+
this.handleResponse(message);
|
|
2025
2375
|
}
|
|
2026
2376
|
}
|
|
2027
2377
|
async handleRequest(req) {
|
|
@@ -2069,6 +2419,18 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
2069
2419
|
handleResponse(res) {
|
|
2070
2420
|
const pending = this.pending.get(res.id);
|
|
2071
2421
|
if (!pending) {
|
|
2422
|
+
if (res.error) {
|
|
2423
|
+
for (const handler of this.orphanErrorHandlers) {
|
|
2424
|
+
try {
|
|
2425
|
+
handler({
|
|
2426
|
+
code: res.error.code,
|
|
2427
|
+
message: res.error.message,
|
|
2428
|
+
data: res.error.data
|
|
2429
|
+
});
|
|
2430
|
+
} catch {
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2072
2434
|
return;
|
|
2073
2435
|
}
|
|
2074
2436
|
this.pending.delete(res.id);
|
|
@@ -2244,8 +2606,8 @@ stderr: ${tail}` : reason;
|
|
|
2244
2606
|
function openAgentLog(agentId) {
|
|
2245
2607
|
try {
|
|
2246
2608
|
const logPath = paths.agentLogFile(agentId);
|
|
2247
|
-
|
|
2248
|
-
const stream =
|
|
2609
|
+
fs7.mkdirSync(path5.dirname(logPath), { recursive: true });
|
|
2610
|
+
const stream = fs7.createWriteStream(logPath, { flags: "a" });
|
|
2249
2611
|
stream.on("error", () => void 0);
|
|
2250
2612
|
return stream;
|
|
2251
2613
|
} catch {
|
|
@@ -2254,7 +2616,7 @@ function openAgentLog(agentId) {
|
|
|
2254
2616
|
}
|
|
2255
2617
|
|
|
2256
2618
|
// src/core/session-manager.ts
|
|
2257
|
-
import * as
|
|
2619
|
+
import * as fs15 from "fs/promises";
|
|
2258
2620
|
import * as os2 from "os";
|
|
2259
2621
|
import * as path9 from "path";
|
|
2260
2622
|
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
@@ -2298,8 +2660,8 @@ var SessionStreamBuffer = class {
|
|
|
2298
2660
|
fileCapReached = false;
|
|
2299
2661
|
onFileCapReached;
|
|
2300
2662
|
logWriteError;
|
|
2301
|
-
// Single-flight chain for file appends so concurrent
|
|
2302
|
-
//
|
|
2663
|
+
// Single-flight chain for file appends so concurrent stdin writes
|
|
2664
|
+
// don't interleave their file writes.
|
|
2303
2665
|
fileWriteChain = Promise.resolve();
|
|
2304
2666
|
constructor(opts = {}) {
|
|
2305
2667
|
this.maxCapacityBytes = opts.capacityBytes ?? DEFAULT_CAPACITY_BYTES;
|
|
@@ -2728,6 +3090,30 @@ var HYDRA_COMMANDS = [
|
|
|
2728
3090
|
];
|
|
2729
3091
|
var VERB_INDEX = new Map(HYDRA_COMMANDS.map((c) => [c.verb, c]));
|
|
2730
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
|
+
|
|
2731
3117
|
// src/core/coalesce-replay.ts
|
|
2732
3118
|
function coalesceReplay(entries) {
|
|
2733
3119
|
if (entries.length === 0) {
|
|
@@ -2735,6 +3121,7 @@ function coalesceReplay(entries) {
|
|
|
2735
3121
|
}
|
|
2736
3122
|
const lastToolUpdateIndex = /* @__PURE__ */ new Map();
|
|
2737
3123
|
const mergedToolContent = /* @__PURE__ */ new Map();
|
|
3124
|
+
const carriedRawInput = /* @__PURE__ */ new Map();
|
|
2738
3125
|
for (let i = 0; i < entries.length; i++) {
|
|
2739
3126
|
const entry = entries[i];
|
|
2740
3127
|
if (entry === void 0) {
|
|
@@ -2749,6 +3136,9 @@ function coalesceReplay(entries) {
|
|
|
2749
3136
|
continue;
|
|
2750
3137
|
}
|
|
2751
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
|
+
}
|
|
2752
3142
|
if (Array.isArray(upd.content) && upd.content.length > 0) {
|
|
2753
3143
|
const buf = mergedToolContent.get(id);
|
|
2754
3144
|
if (buf) {
|
|
@@ -2788,11 +3178,11 @@ function coalesceReplay(entries) {
|
|
|
2788
3178
|
if (id !== void 0 && lastToolUpdateIndex.get(id) !== i) {
|
|
2789
3179
|
continue;
|
|
2790
3180
|
}
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
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));
|
|
2795
3184
|
}
|
|
3185
|
+
out.push(emitted);
|
|
2796
3186
|
continue;
|
|
2797
3187
|
}
|
|
2798
3188
|
if (kind === "plan") {
|
|
@@ -2863,24 +3253,40 @@ function withReplacedContent(entry, content) {
|
|
|
2863
3253
|
}
|
|
2864
3254
|
};
|
|
2865
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
|
+
}
|
|
2866
3272
|
|
|
2867
3273
|
// src/core/queue-store.ts
|
|
2868
|
-
import * as
|
|
3274
|
+
import * as fs8 from "fs/promises";
|
|
2869
3275
|
async function rewriteQueue(sessionId, entries) {
|
|
2870
3276
|
const file = paths.queueFile(sessionId);
|
|
2871
3277
|
if (entries.length === 0) {
|
|
2872
|
-
await
|
|
3278
|
+
await fs8.unlink(file).catch(() => void 0);
|
|
2873
3279
|
return;
|
|
2874
3280
|
}
|
|
2875
|
-
await
|
|
3281
|
+
await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
2876
3282
|
const body = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2877
|
-
await
|
|
3283
|
+
await fs8.writeFile(file, body, "utf8");
|
|
2878
3284
|
}
|
|
2879
3285
|
async function loadQueue(sessionId) {
|
|
2880
3286
|
const file = paths.queueFile(sessionId);
|
|
2881
3287
|
let text;
|
|
2882
3288
|
try {
|
|
2883
|
-
text = await
|
|
3289
|
+
text = await fs8.readFile(file, "utf8");
|
|
2884
3290
|
} catch (err) {
|
|
2885
3291
|
if (err.code === "ENOENT") {
|
|
2886
3292
|
return [];
|
|
@@ -2902,7 +3308,7 @@ async function loadQueue(sessionId) {
|
|
|
2902
3308
|
}
|
|
2903
3309
|
async function deleteQueue(sessionId) {
|
|
2904
3310
|
const file = paths.queueFile(sessionId);
|
|
2905
|
-
await
|
|
3311
|
+
await fs8.unlink(file).catch(() => void 0);
|
|
2906
3312
|
}
|
|
2907
3313
|
|
|
2908
3314
|
// src/core/session.ts
|
|
@@ -2920,7 +3326,7 @@ function stripHydraSessionPrefix(id) {
|
|
|
2920
3326
|
var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
2921
3327
|
var TRANSFORMER_CLAIM_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2922
3328
|
var RECENTLY_TERMINAL_LIMIT = 64;
|
|
2923
|
-
var Session = class {
|
|
3329
|
+
var Session = class _Session {
|
|
2924
3330
|
sessionId;
|
|
2925
3331
|
cwd;
|
|
2926
3332
|
// agent / agentId / upstreamSessionId are mutable so /hydra agent can
|
|
@@ -2994,6 +3400,16 @@ var Session = class {
|
|
|
2994
3400
|
// endpoint uses this to tail a live session's conversation stream
|
|
2995
3401
|
// without participating in turns or prompts.
|
|
2996
3402
|
broadcastHandlers = [];
|
|
3403
|
+
// Epoch ms of the most recent session/cancel we sent to the agent.
|
|
3404
|
+
// Used to attribute an id-less error frame from the agent to a cancel
|
|
3405
|
+
// (see wireAgent's orphan-error observer). Window kept short so an
|
|
3406
|
+
// unrelated later error isn't mislabeled.
|
|
3407
|
+
lastCancelAt = 0;
|
|
3408
|
+
static CANCEL_ERROR_WINDOW_MS = 2e3;
|
|
3409
|
+
// Set by forceCancel() so the in-flight turn's agent-kill rejection is
|
|
3410
|
+
// reported to the originator as a clean "cancelled" stopReason instead of
|
|
3411
|
+
// a raw "connection closed" error.
|
|
3412
|
+
forceCancelling = false;
|
|
2997
3413
|
// True once we've observed our first session/prompt; gates the
|
|
2998
3414
|
// first-prompt-seeded title so subsequent prompts don't churn it.
|
|
2999
3415
|
// Also read by SessionManager's onClose hook to decide whether to
|
|
@@ -3069,6 +3485,7 @@ var Session = class {
|
|
|
3069
3485
|
agentCommandsHandlers = [];
|
|
3070
3486
|
agentModesHandlers = [];
|
|
3071
3487
|
agentModelsHandlers = [];
|
|
3488
|
+
availableAgentsFn;
|
|
3072
3489
|
modelHandlers = [];
|
|
3073
3490
|
modeHandlers = [];
|
|
3074
3491
|
interactiveHandlers = [];
|
|
@@ -3141,6 +3558,7 @@ var Session = class {
|
|
|
3141
3558
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
3142
3559
|
this.idleEventTimeoutMs = init.idleEventTimeoutMs ?? 3e4;
|
|
3143
3560
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
3561
|
+
this.availableAgentsFn = init.availableAgents;
|
|
3144
3562
|
this.listSessions = init.listSessions;
|
|
3145
3563
|
this.logger = init.logger;
|
|
3146
3564
|
this.transformChain = init.transformChain ?? [];
|
|
@@ -3218,6 +3636,11 @@ var Session = class {
|
|
|
3218
3636
|
agent.connection.onRequest("session/request_permission", async (params) => {
|
|
3219
3637
|
return this.handlePermissionRequest(params);
|
|
3220
3638
|
});
|
|
3639
|
+
if (typeof agent.connection.onOrphanError === "function") {
|
|
3640
|
+
agent.connection.onOrphanError((error) => {
|
|
3641
|
+
this.handleOrphanError(error);
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3221
3644
|
agent.onExit(() => {
|
|
3222
3645
|
if (this.agent !== agent) {
|
|
3223
3646
|
return;
|
|
@@ -3225,6 +3648,31 @@ var Session = class {
|
|
|
3225
3648
|
this.markClosed({ deleteRecord: false });
|
|
3226
3649
|
});
|
|
3227
3650
|
}
|
|
3651
|
+
// An error frame arrived from the agent that couldn't be matched to a
|
|
3652
|
+
// pending request. The canonical case is a reply to our id-less
|
|
3653
|
+
// session/cancel notification: agents that don't support cancellation
|
|
3654
|
+
// (current opencode) answer with MethodNotFound (-32601) or an
|
|
3655
|
+
// UnsupportedOperation error. If one lands within the cancel window,
|
|
3656
|
+
// surface it to attached clients so the TUI can tell the user the
|
|
3657
|
+
// cancel didn't take rather than silently dropping it.
|
|
3658
|
+
handleOrphanError(error) {
|
|
3659
|
+
const sinceCancel = Date.now() - this.lastCancelAt;
|
|
3660
|
+
if (this.lastCancelAt === 0 || sinceCancel > _Session.CANCEL_ERROR_WINDOW_MS) {
|
|
3661
|
+
this.logger?.warn(
|
|
3662
|
+
`agent ${this.agentId} sent uncorrelated error frame code=${error.code} message=${error.message}`
|
|
3663
|
+
);
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
this.lastCancelAt = 0;
|
|
3667
|
+
this.logger?.warn(
|
|
3668
|
+
`agent ${this.agentId} rejected session/cancel code=${error.code} message=${error.message}`
|
|
3669
|
+
);
|
|
3670
|
+
this.broadcastQueueNotification("hydra-acp/cancel_failed", {
|
|
3671
|
+
sessionId: this.sessionId,
|
|
3672
|
+
code: error.code,
|
|
3673
|
+
message: error.message
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3228
3676
|
// Runs the response-side transformer chain, then the snapshot interceptors,
|
|
3229
3677
|
// then recordAndBroadcast. All state mutation happens after the chain exits.
|
|
3230
3678
|
// See forwardRequest for originatedBy / startIdx semantics.
|
|
@@ -3242,7 +3690,7 @@ var Session = class {
|
|
|
3242
3690
|
const token = `t_${generateChainToken()}`;
|
|
3243
3691
|
let result;
|
|
3244
3692
|
try {
|
|
3245
|
-
result = await t.connection.request("transformer/message", {
|
|
3693
|
+
result = await t.connection.request("hydra-acp/transformer/message", {
|
|
3246
3694
|
token,
|
|
3247
3695
|
phase: "response",
|
|
3248
3696
|
method: "session/update",
|
|
@@ -3268,7 +3716,7 @@ var Session = class {
|
|
|
3268
3716
|
const timer = setTimeout(() => {
|
|
3269
3717
|
if (this.pendingClaims.delete(token)) {
|
|
3270
3718
|
this.broadcastQueueNotification(
|
|
3271
|
-
"hydra-acp/
|
|
3719
|
+
"hydra-acp/transformer/abandoned_request",
|
|
3272
3720
|
{ sessionId: this.sessionId, token, transformerName: t.name }
|
|
3273
3721
|
);
|
|
3274
3722
|
void this.runResponseChain(
|
|
@@ -3315,7 +3763,10 @@ var Session = class {
|
|
|
3315
3763
|
return;
|
|
3316
3764
|
}
|
|
3317
3765
|
if (this.maybeApplyAgentConfigOption(envelope)) {
|
|
3318
|
-
this.recordAndBroadcast(
|
|
3766
|
+
this.recordAndBroadcast(
|
|
3767
|
+
"session/update",
|
|
3768
|
+
this.mergeAgentOptionIntoEnvelope(envelope)
|
|
3769
|
+
);
|
|
3319
3770
|
return;
|
|
3320
3771
|
}
|
|
3321
3772
|
if (this.maybeApplyAgentUsage(rawParams)) {
|
|
@@ -3373,11 +3824,11 @@ var Session = class {
|
|
|
3373
3824
|
// Read the persisted history from disk. Returns [] if no history
|
|
3374
3825
|
// file exists (fresh session, never prompted). Used by attach() and
|
|
3375
3826
|
// the HTTP /history endpoint.
|
|
3376
|
-
async getHistorySnapshot() {
|
|
3827
|
+
async getHistorySnapshot(tools = "inline") {
|
|
3377
3828
|
if (!this.historyStore) {
|
|
3378
3829
|
return [];
|
|
3379
3830
|
}
|
|
3380
|
-
return this.historyStore.load(this.sessionId).catch(() => []);
|
|
3831
|
+
return this.historyStore.load(this.sessionId, { tools }).catch(() => []);
|
|
3381
3832
|
}
|
|
3382
3833
|
// Subscribe to recordable broadcast entries — fires once per entry
|
|
3383
3834
|
// that lands in history (so snapshot-shaped session_info/model/mode/
|
|
@@ -3427,7 +3878,7 @@ var Session = class {
|
|
|
3427
3878
|
}
|
|
3428
3879
|
async loadReplay(historyPolicy, opts) {
|
|
3429
3880
|
const maybeCoalesce = (entries) => opts.raw ? entries : coalesceReplay(entries);
|
|
3430
|
-
const raw = await this.getHistorySnapshot();
|
|
3881
|
+
const raw = await this.getHistorySnapshot(opts.toolContent ?? "inline");
|
|
3431
3882
|
const state = this.buildStateSnapshotReplay();
|
|
3432
3883
|
if (historyPolicy === "after_message") {
|
|
3433
3884
|
const cutoff = opts.afterMessageId ? findMessageIdIndex(raw, opts.afterMessageId) : -1;
|
|
@@ -3650,7 +4101,7 @@ var Session = class {
|
|
|
3650
4101
|
// prompt_received a single, useful meaning ("the agent is now taking
|
|
3651
4102
|
// a turn on this prompt"), which is how attached clients (notably
|
|
3652
4103
|
// agent-shell) consume it. The accept-time signal that peers can use
|
|
3653
|
-
// for queue chip rendering is hydra-acp/
|
|
4104
|
+
// for queue chip rendering is hydra-acp/prompt_queue/added instead.
|
|
3654
4105
|
broadcastPromptReceived(entry) {
|
|
3655
4106
|
const sentBy = { clientId: entry.originator.clientId };
|
|
3656
4107
|
if (entry.originator.name) {
|
|
@@ -3741,7 +4192,7 @@ var Session = class {
|
|
|
3741
4192
|
this.recentlyTerminal.delete(oldest);
|
|
3742
4193
|
}
|
|
3743
4194
|
}
|
|
3744
|
-
// Fire hydra-acp/
|
|
4195
|
+
// Fire hydra-acp/prompt/amended for the M1→M2 linkage. The amendment's
|
|
3745
4196
|
// current content is read live from the queue entry so any update_prompt
|
|
3746
4197
|
// calls during the amend window are reflected. Best-effort: if M2 has
|
|
3747
4198
|
// already been cancelled out of the queue by the time we get here, we
|
|
@@ -3761,7 +4212,7 @@ var Session = class {
|
|
|
3761
4212
|
amendedAt: Date.now()
|
|
3762
4213
|
};
|
|
3763
4214
|
this.broadcastQueueNotification(
|
|
3764
|
-
"hydra-acp/
|
|
4215
|
+
"hydra-acp/prompt/amended",
|
|
3765
4216
|
params
|
|
3766
4217
|
);
|
|
3767
4218
|
}
|
|
@@ -3804,17 +4255,17 @@ var Session = class {
|
|
|
3804
4255
|
"hydra-acp": { amending: options.amending }
|
|
3805
4256
|
};
|
|
3806
4257
|
}
|
|
3807
|
-
this.broadcastQueueNotification("hydra-acp/
|
|
4258
|
+
this.broadcastQueueNotification("hydra-acp/prompt_queue/added", params);
|
|
3808
4259
|
}
|
|
3809
4260
|
broadcastQueueUpdated(messageId, prompt) {
|
|
3810
|
-
this.broadcastQueueNotification("hydra-acp/
|
|
4261
|
+
this.broadcastQueueNotification("hydra-acp/prompt_queue/updated", {
|
|
3811
4262
|
sessionId: this.sessionId,
|
|
3812
4263
|
messageId,
|
|
3813
4264
|
prompt
|
|
3814
4265
|
});
|
|
3815
4266
|
}
|
|
3816
4267
|
broadcastQueueRemoved(messageId, reason) {
|
|
3817
|
-
this.broadcastQueueNotification("hydra-acp/
|
|
4268
|
+
this.broadcastQueueNotification("hydra-acp/prompt_queue/removed", {
|
|
3818
4269
|
sessionId: this.sessionId,
|
|
3819
4270
|
messageId,
|
|
3820
4271
|
reason
|
|
@@ -4095,6 +4546,7 @@ var Session = class {
|
|
|
4095
4546
|
JsonRpcErrorCodes.SessionNotFound
|
|
4096
4547
|
);
|
|
4097
4548
|
}
|
|
4549
|
+
this.lastCancelAt = Date.now();
|
|
4098
4550
|
await this.agent.connection.notify("session/cancel", {
|
|
4099
4551
|
sessionId: this.upstreamSessionId
|
|
4100
4552
|
});
|
|
@@ -4110,7 +4562,7 @@ var Session = class {
|
|
|
4110
4562
|
this.transformChain.push(ref);
|
|
4111
4563
|
}
|
|
4112
4564
|
if (ref.intercepts.has("lifecycle:session.opened")) {
|
|
4113
|
-
void ref.connection.notify("transformer/session_event", {
|
|
4565
|
+
void ref.connection.notify("hydra-acp/transformer/session_event", {
|
|
4114
4566
|
event: "session.opened",
|
|
4115
4567
|
sessionId: this.sessionId
|
|
4116
4568
|
}).catch(() => void 0);
|
|
@@ -4134,7 +4586,7 @@ var Session = class {
|
|
|
4134
4586
|
const token = `t_${generateChainToken()}`;
|
|
4135
4587
|
let result;
|
|
4136
4588
|
try {
|
|
4137
|
-
result = await t.connection.request("transformer/message", {
|
|
4589
|
+
result = await t.connection.request("hydra-acp/transformer/message", {
|
|
4138
4590
|
token,
|
|
4139
4591
|
phase: "request",
|
|
4140
4592
|
method,
|
|
@@ -4160,7 +4612,7 @@ var Session = class {
|
|
|
4160
4612
|
const timer = setTimeout(() => {
|
|
4161
4613
|
if (this.pendingClaims.delete(token)) {
|
|
4162
4614
|
this.broadcastQueueNotification(
|
|
4163
|
-
"hydra-acp/
|
|
4615
|
+
"hydra-acp/transformer/abandoned_request",
|
|
4164
4616
|
{ sessionId: this.sessionId, token, transformerName: t.name }
|
|
4165
4617
|
);
|
|
4166
4618
|
void this.forwardRequest(
|
|
@@ -4202,7 +4654,7 @@ var Session = class {
|
|
|
4202
4654
|
claim.resolve(result);
|
|
4203
4655
|
return true;
|
|
4204
4656
|
}
|
|
4205
|
-
// Called by the WS handler on hydra-acp/keep_alive.
|
|
4657
|
+
// Called by the WS handler on hydra-acp/connection/keep_alive.
|
|
4206
4658
|
// Resets the abandonment timer for an outstanding processing claim.
|
|
4207
4659
|
keepAliveClaim(token, estimatedRemainingMs) {
|
|
4208
4660
|
const claim = this.pendingClaims.get(token);
|
|
@@ -4214,7 +4666,7 @@ var Session = class {
|
|
|
4214
4666
|
const timer = setTimeout(() => {
|
|
4215
4667
|
if (this.pendingClaims.delete(token)) {
|
|
4216
4668
|
this.broadcastQueueNotification(
|
|
4217
|
-
"hydra-acp/
|
|
4669
|
+
"hydra-acp/transformer/abandoned_request",
|
|
4218
4670
|
{ sessionId: this.sessionId, token, transformerName: claim.transformerName }
|
|
4219
4671
|
);
|
|
4220
4672
|
if (claim.side === "response") {
|
|
@@ -4379,18 +4831,20 @@ var Session = class {
|
|
|
4379
4831
|
} catch {
|
|
4380
4832
|
}
|
|
4381
4833
|
}
|
|
4834
|
+
this.broadcastConfigOptions();
|
|
4382
4835
|
return true;
|
|
4383
4836
|
}
|
|
4384
|
-
// Apply an
|
|
4385
|
-
// (not the spec-shaped current_model_update /
|
|
4386
|
-
// to carry
|
|
4387
|
-
// The payload is `configOptions: [{ id
|
|
4388
|
-
// [{ value, name }] }, ...]`. We harvest
|
|
4389
|
-
//
|
|
4390
|
-
//
|
|
4391
|
-
//
|
|
4392
|
-
//
|
|
4393
|
-
//
|
|
4837
|
+
// Apply an agent-emitted config_option_update. claude-acp and opencode
|
|
4838
|
+
// emit this (not the spec-shaped current_model_update /
|
|
4839
|
+
// available_modes_update) to carry the current value AND option list for
|
|
4840
|
+
// model and mode. The payload is `configOptions: [{ id, currentValue,
|
|
4841
|
+
// options: [{ value, name }] }, ...]`. We harvest the "model" and "mode"
|
|
4842
|
+
// entries — other ids (e.g. "effort") are agent-internal and ignored.
|
|
4843
|
+
// Harvesting the mode list here is what populates availableModes for
|
|
4844
|
+
// agents that never send available_modes_update (so the TUI's Shift+Tab
|
|
4845
|
+
// cycle has something to cycle through). Returns true when recognized so
|
|
4846
|
+
// the wireAgent loop stops trying further extractors (the original frame
|
|
4847
|
+
// still broadcasts; config-options-aware clients render it directly).
|
|
4394
4848
|
maybeApplyAgentConfigOption(params) {
|
|
4395
4849
|
const obj = params ?? {};
|
|
4396
4850
|
const update = obj.update ?? {};
|
|
@@ -4406,24 +4860,37 @@ var Session = class {
|
|
|
4406
4860
|
continue;
|
|
4407
4861
|
}
|
|
4408
4862
|
const opt = raw;
|
|
4409
|
-
if (opt.id
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4863
|
+
if (opt.id === "model") {
|
|
4864
|
+
const models = parseModelsList(opt.options);
|
|
4865
|
+
if (models.length > 0) {
|
|
4866
|
+
this.setAgentAdvertisedModels(models);
|
|
4867
|
+
}
|
|
4868
|
+
const cv = opt.currentValue;
|
|
4869
|
+
if (typeof cv === "string") {
|
|
4870
|
+
const trimmed = cv.trim();
|
|
4871
|
+
if (trimmed && trimmed !== this.currentModel) {
|
|
4872
|
+
this.logger?.info(
|
|
4873
|
+
`live config_option_update(model): sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4874
|
+
);
|
|
4875
|
+
this.applyModelChange(trimmed);
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
} else if (opt.id === "mode") {
|
|
4879
|
+
const modes = parseModesList(opt.options);
|
|
4880
|
+
if (modes.length > 0) {
|
|
4881
|
+
this.setAgentAdvertisedModes(modes);
|
|
4882
|
+
}
|
|
4883
|
+
const cv = opt.currentValue;
|
|
4884
|
+
if (typeof cv === "string") {
|
|
4885
|
+
const trimmed = cv.trim();
|
|
4886
|
+
if (trimmed && trimmed !== this.currentMode) {
|
|
4887
|
+
this.logger?.info(
|
|
4888
|
+
`live config_option_update(mode): sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4889
|
+
);
|
|
4890
|
+
this.applyModeChange(trimmed);
|
|
4891
|
+
}
|
|
4424
4892
|
}
|
|
4425
4893
|
}
|
|
4426
|
-
break;
|
|
4427
4894
|
}
|
|
4428
4895
|
return true;
|
|
4429
4896
|
}
|
|
@@ -4451,6 +4918,7 @@ var Session = class {
|
|
|
4451
4918
|
} catch {
|
|
4452
4919
|
}
|
|
4453
4920
|
}
|
|
4921
|
+
this.broadcastConfigOptions();
|
|
4454
4922
|
return true;
|
|
4455
4923
|
}
|
|
4456
4924
|
// usage_update carries any subset of {used, size, cost.amount,
|
|
@@ -4661,6 +5129,7 @@ var Session = class {
|
|
|
4661
5129
|
sessionId: this.upstreamSessionId,
|
|
4662
5130
|
update
|
|
4663
5131
|
});
|
|
5132
|
+
this.broadcastConfigOptions();
|
|
4664
5133
|
}
|
|
4665
5134
|
// Apply a mode change initiated by a client request (session/set_mode)
|
|
4666
5135
|
// when the agent doesn't emit a current_mode_update notification on its
|
|
@@ -4697,6 +5166,115 @@ var Session = class {
|
|
|
4697
5166
|
sessionId: this.upstreamSessionId,
|
|
4698
5167
|
update
|
|
4699
5168
|
});
|
|
5169
|
+
this.broadcastConfigOptions();
|
|
5170
|
+
}
|
|
5171
|
+
// Assemble the spec-shaped configOptions snapshot for this session.
|
|
5172
|
+
// Order reflects the agent's preferred prominence: model and mode (the
|
|
5173
|
+
// dimensions users toggle constantly) first, then the hydra-native
|
|
5174
|
+
// `agent` selector. model/mode are included only when hydra actually
|
|
5175
|
+
// knows that dimension for the connected agent; `agent` is always
|
|
5176
|
+
// present since hydra owns the swap concept regardless of the backend.
|
|
5177
|
+
buildConfigOptions() {
|
|
5178
|
+
const out = [];
|
|
5179
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
5180
|
+
const options = this.agentAdvertisedModels.map(
|
|
5181
|
+
(m) => ({
|
|
5182
|
+
value: m.modelId,
|
|
5183
|
+
name: m.name ?? m.modelId,
|
|
5184
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
5185
|
+
})
|
|
5186
|
+
);
|
|
5187
|
+
const currentValue = this.currentModel && options.some((o) => o.value === this.currentModel) ? this.currentModel : options[0].value;
|
|
5188
|
+
out.push({
|
|
5189
|
+
id: "model",
|
|
5190
|
+
name: "Model",
|
|
5191
|
+
category: "model",
|
|
5192
|
+
type: "select",
|
|
5193
|
+
currentValue,
|
|
5194
|
+
options
|
|
5195
|
+
});
|
|
5196
|
+
}
|
|
5197
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
5198
|
+
const options = this.agentAdvertisedModes.map(
|
|
5199
|
+
(m) => ({
|
|
5200
|
+
value: m.id,
|
|
5201
|
+
name: m.name ?? m.id,
|
|
5202
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
5203
|
+
})
|
|
5204
|
+
);
|
|
5205
|
+
const currentValue = this.currentMode && options.some((o) => o.value === this.currentMode) ? this.currentMode : options[0].value;
|
|
5206
|
+
out.push({
|
|
5207
|
+
id: "mode",
|
|
5208
|
+
name: "Session Mode",
|
|
5209
|
+
category: "mode",
|
|
5210
|
+
type: "select",
|
|
5211
|
+
currentValue,
|
|
5212
|
+
options
|
|
5213
|
+
});
|
|
5214
|
+
}
|
|
5215
|
+
const agents = this.availableAgentsFn?.() ?? [];
|
|
5216
|
+
const agentOptions = agents.map((a) => ({
|
|
5217
|
+
value: a.id,
|
|
5218
|
+
name: a.name ?? a.id,
|
|
5219
|
+
...a.description !== void 0 ? { description: a.description } : {}
|
|
5220
|
+
}));
|
|
5221
|
+
if (!agentOptions.some((o) => o.value === this.agentId)) {
|
|
5222
|
+
agentOptions.unshift({ value: this.agentId, name: this.agentId });
|
|
5223
|
+
}
|
|
5224
|
+
out.push({
|
|
5225
|
+
id: "agent",
|
|
5226
|
+
name: "Agent",
|
|
5227
|
+
category: "_hydra_agent",
|
|
5228
|
+
type: "select",
|
|
5229
|
+
currentValue: this.agentId,
|
|
5230
|
+
options: agentOptions
|
|
5231
|
+
});
|
|
5232
|
+
return out;
|
|
5233
|
+
}
|
|
5234
|
+
// Broadcast a config_option_update carrying the full snapshot. Fired
|
|
5235
|
+
// alongside the legacy current_mode_update / current_model_update and on
|
|
5236
|
+
// agent swaps so config-options-aware clients stay in sync via the
|
|
5237
|
+
// spec mechanism. config_option_update is a STATE_UPDATE_KIND, so this
|
|
5238
|
+
// broadcasts live but is not recorded to history.
|
|
5239
|
+
broadcastConfigOptions() {
|
|
5240
|
+
this.recordAndBroadcast("session/update", {
|
|
5241
|
+
sessionId: this.upstreamSessionId,
|
|
5242
|
+
update: {
|
|
5243
|
+
sessionUpdate: "config_option_update",
|
|
5244
|
+
configOptions: this.buildConfigOptions()
|
|
5245
|
+
}
|
|
5246
|
+
});
|
|
5247
|
+
}
|
|
5248
|
+
// Return a shallow clone of an agent-emitted config_option_update
|
|
5249
|
+
// envelope with the hydra-native `agent` option appended to its
|
|
5250
|
+
// configOptions (unless the agent already advertised one). Preserves
|
|
5251
|
+
// the agent's own options (mode/model/effort/…) and their order; the
|
|
5252
|
+
// `agent` selector rides last, matching its lower prominence. The
|
|
5253
|
+
// original envelope is not mutated.
|
|
5254
|
+
mergeAgentOptionIntoEnvelope(envelope) {
|
|
5255
|
+
if (!envelope || typeof envelope !== "object") {
|
|
5256
|
+
return envelope;
|
|
5257
|
+
}
|
|
5258
|
+
const env = envelope;
|
|
5259
|
+
if (!env.update || typeof env.update !== "object") {
|
|
5260
|
+
return envelope;
|
|
5261
|
+
}
|
|
5262
|
+
const update = env.update;
|
|
5263
|
+
const list = Array.isArray(update.configOptions) ? [...update.configOptions] : [];
|
|
5264
|
+
const hasAgent = list.some(
|
|
5265
|
+
(o) => o && typeof o === "object" && o.id === "agent"
|
|
5266
|
+
);
|
|
5267
|
+
if (hasAgent) {
|
|
5268
|
+
return envelope;
|
|
5269
|
+
}
|
|
5270
|
+
const agentOption = this.buildConfigOptions().find((o) => o.id === "agent");
|
|
5271
|
+
if (!agentOption) {
|
|
5272
|
+
return envelope;
|
|
5273
|
+
}
|
|
5274
|
+
return {
|
|
5275
|
+
...env,
|
|
5276
|
+
update: { ...update, configOptions: [...list, agentOption] }
|
|
5277
|
+
};
|
|
4700
5278
|
}
|
|
4701
5279
|
onUsageChange(handler) {
|
|
4702
5280
|
this.usageHandlers.push(handler);
|
|
@@ -4832,7 +5410,7 @@ var Session = class {
|
|
|
4832
5410
|
// "/hydra <name> <verb> [args]" — name matches a registered extension
|
|
4833
5411
|
// or transformer. We split the remainder into verb + args, validate the
|
|
4834
5412
|
// verb against what the process advertised, and forward as a
|
|
4835
|
-
// hydra-acp/
|
|
5413
|
+
// hydra-acp/commands/invoke request on the process's WS connection.
|
|
4836
5414
|
// The reply's text (if any) is broadcast as a synthetic
|
|
4837
5415
|
// agent_message_chunk so it appears in the conversation alongside the
|
|
4838
5416
|
// user's invocation.
|
|
@@ -4861,7 +5439,7 @@ var Session = class {
|
|
|
4861
5439
|
}
|
|
4862
5440
|
let reply;
|
|
4863
5441
|
try {
|
|
4864
|
-
reply = await entry.connection.request("hydra-acp/
|
|
5442
|
+
reply = await entry.connection.request("hydra-acp/commands/invoke", {
|
|
4865
5443
|
sessionId: this.sessionId,
|
|
4866
5444
|
verb,
|
|
4867
5445
|
args
|
|
@@ -4969,6 +5547,27 @@ ${text}
|
|
|
4969
5547
|
sessionUpdate: "agent_message_chunk",
|
|
4970
5548
|
content: { type: "text", text: `
|
|
4971
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}
|
|
4972
5571
|
` },
|
|
4973
5572
|
_meta: { "hydra-acp": { synthetic: true } }
|
|
4974
5573
|
}
|
|
@@ -4977,7 +5576,7 @@ ${body}
|
|
|
4977
5576
|
}
|
|
4978
5577
|
await this.forwardRequest("session/set_model", {
|
|
4979
5578
|
sessionId: this.sessionId,
|
|
4980
|
-
modelId
|
|
5579
|
+
modelId
|
|
4981
5580
|
});
|
|
4982
5581
|
return { stopReason: "end_turn" };
|
|
4983
5582
|
}
|
|
@@ -5074,6 +5673,14 @@ ${body}
|
|
|
5074
5673
|
// record. Spawns the new agent first so a failure leaves the old one
|
|
5075
5674
|
// intact; then injects a synthesized transcript so the new agent has
|
|
5076
5675
|
// context for the next turn.
|
|
5676
|
+
// Public entry for swapping the underlying agent from a client request
|
|
5677
|
+
// (session/set_config_option with configId "agent"), the protocol twin
|
|
5678
|
+
// of the `/hydra agent` text command. Delegates to the same swap
|
|
5679
|
+
// machinery so both paths share validation, transcript replay, and the
|
|
5680
|
+
// config_option_update broadcast.
|
|
5681
|
+
setAgent(newAgentId) {
|
|
5682
|
+
return this.runAgentCommand(newAgentId);
|
|
5683
|
+
}
|
|
5077
5684
|
runAgentCommand(newAgentId) {
|
|
5078
5685
|
if (!newAgentId) {
|
|
5079
5686
|
throw withCode(
|
|
@@ -5112,12 +5719,10 @@ ${body}
|
|
|
5112
5719
|
this.agentCapabilities = fresh.agentCapabilities;
|
|
5113
5720
|
this.agentAdvertisedCommands = [];
|
|
5114
5721
|
this.broadcastMergedCommands();
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
this.setAgentAdvertisedModes([]);
|
|
5120
|
-
}
|
|
5722
|
+
this.currentModel = fresh.initialModel;
|
|
5723
|
+
this.currentMode = fresh.initialMode;
|
|
5724
|
+
this.setAgentAdvertisedModels(fresh.initialModels ?? []);
|
|
5725
|
+
this.setAgentAdvertisedModes(fresh.initialModes ?? []);
|
|
5121
5726
|
await oldAgent.kill().catch(() => void 0);
|
|
5122
5727
|
if (transcript) {
|
|
5123
5728
|
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
@@ -5141,7 +5746,7 @@ ${body}
|
|
|
5141
5746
|
// down any in-flight request as a side effect. The record is kept
|
|
5142
5747
|
// (deleteRecord:false) so the session goes cold and can be resurrected.
|
|
5143
5748
|
// Returns end_turn so the prompt() caller's response resolves normally,
|
|
5144
|
-
// but every attached client has already received hydra-acp/
|
|
5749
|
+
// but every attached client has already received hydra-acp/session/closed
|
|
5145
5750
|
// by the time this returns.
|
|
5146
5751
|
async runKillCommand() {
|
|
5147
5752
|
await this.close({ deleteRecord: false });
|
|
@@ -5159,45 +5764,71 @@ ${body}
|
|
|
5159
5764
|
JsonRpcErrorCodes.InternalError
|
|
5160
5765
|
);
|
|
5161
5766
|
}
|
|
5162
|
-
const spawnAgent = this.spawnReplacementAgent;
|
|
5163
|
-
const agentId = this.agentId;
|
|
5164
5767
|
return this.enqueuePrompt(async () => {
|
|
5165
|
-
|
|
5166
|
-
const fresh = await spawnAgent({
|
|
5167
|
-
agentId,
|
|
5168
|
-
cwd: this.cwd,
|
|
5169
|
-
agentArgs: this.agentArgs
|
|
5170
|
-
});
|
|
5171
|
-
this.accumulateAndResetCost();
|
|
5172
|
-
this.wireAgent(fresh.agent);
|
|
5173
|
-
const oldAgent = this.agent;
|
|
5174
|
-
this.agent = fresh.agent;
|
|
5175
|
-
this.upstreamSessionId = fresh.upstreamSessionId;
|
|
5176
|
-
this.agentMeta = fresh.agentMeta;
|
|
5177
|
-
this.agentCapabilities = fresh.agentCapabilities;
|
|
5178
|
-
this.agentAdvertisedCommands = [];
|
|
5179
|
-
this.broadcastMergedCommands();
|
|
5180
|
-
if (this.agentAdvertisedModels.length > 0) {
|
|
5181
|
-
this.setAgentAdvertisedModels([]);
|
|
5182
|
-
}
|
|
5183
|
-
if (this.agentAdvertisedModes.length > 0) {
|
|
5184
|
-
this.setAgentAdvertisedModes([]);
|
|
5185
|
-
}
|
|
5186
|
-
await oldAgent.kill().catch(() => void 0);
|
|
5187
|
-
if (transcript) {
|
|
5188
|
-
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
5189
|
-
}
|
|
5190
|
-
this.broadcastAgentSwitch(agentId, agentId);
|
|
5191
|
-
const info = { agentId, upstreamSessionId: this.upstreamSessionId };
|
|
5192
|
-
for (const handler of this.agentChangeHandlers) {
|
|
5193
|
-
try {
|
|
5194
|
-
handler(info);
|
|
5195
|
-
} catch {
|
|
5196
|
-
}
|
|
5197
|
-
}
|
|
5768
|
+
await this.respawnAgent();
|
|
5198
5769
|
return { stopReason: "end_turn" };
|
|
5199
5770
|
});
|
|
5200
5771
|
}
|
|
5772
|
+
// Last-resort cancellation. When an agent ignores session/cancel (current
|
|
5773
|
+
// opencode returns UnsupportedOperation and keeps generating), the only
|
|
5774
|
+
// way to actually stop the turn is to tear the subprocess down. Rather
|
|
5775
|
+
// than respawn + replay the transcript (slow, and renders as an agent
|
|
5776
|
+
// "switch"), we close the session keeping its record: agent.kill() aborts
|
|
5777
|
+
// the in-flight turn, and the next prompt auto-resurrects via session/load
|
|
5778
|
+
// (cheap — the agent restores its own context). The forceCancelling flag
|
|
5779
|
+
// makes runQueueEntry render the aborted turn as "cancelled" rather than a
|
|
5780
|
+
// raw connection error. Runs OUTSIDE the prompt queue so a wedged turn
|
|
5781
|
+
// can't block it.
|
|
5782
|
+
async forceCancel() {
|
|
5783
|
+
if (this.closed || this.closing) {
|
|
5784
|
+
throw withCode(
|
|
5785
|
+
new Error("session is closing"),
|
|
5786
|
+
JsonRpcErrorCodes.SessionClosing
|
|
5787
|
+
);
|
|
5788
|
+
}
|
|
5789
|
+
this.lastCancelAt = 0;
|
|
5790
|
+
this.forceCancelling = true;
|
|
5791
|
+
await this.close({ deleteRecord: false });
|
|
5792
|
+
return { stopReason: "cancelled" };
|
|
5793
|
+
}
|
|
5794
|
+
// Shared kill-and-respawn used by /hydra restart (queued) and forceCancel
|
|
5795
|
+
// (immediate). Spawns a fresh agent, swaps it in, kills the old one, and
|
|
5796
|
+
// re-seeds the conversation transcript so the new process has context.
|
|
5797
|
+
async respawnAgent() {
|
|
5798
|
+
const spawnAgent = this.spawnReplacementAgent;
|
|
5799
|
+
const agentId = this.agentId;
|
|
5800
|
+
const transcript = await this.buildSwitchTranscript(agentId);
|
|
5801
|
+
const fresh = await spawnAgent({
|
|
5802
|
+
agentId,
|
|
5803
|
+
cwd: this.cwd,
|
|
5804
|
+
agentArgs: this.agentArgs
|
|
5805
|
+
});
|
|
5806
|
+
this.accumulateAndResetCost();
|
|
5807
|
+
this.wireAgent(fresh.agent);
|
|
5808
|
+
const oldAgent = this.agent;
|
|
5809
|
+
this.agent = fresh.agent;
|
|
5810
|
+
this.upstreamSessionId = fresh.upstreamSessionId;
|
|
5811
|
+
this.agentMeta = fresh.agentMeta;
|
|
5812
|
+
this.agentCapabilities = fresh.agentCapabilities;
|
|
5813
|
+
this.agentAdvertisedCommands = [];
|
|
5814
|
+
this.broadcastMergedCommands();
|
|
5815
|
+
this.currentModel = fresh.initialModel;
|
|
5816
|
+
this.currentMode = fresh.initialMode;
|
|
5817
|
+
this.setAgentAdvertisedModels(fresh.initialModels ?? []);
|
|
5818
|
+
this.setAgentAdvertisedModes(fresh.initialModes ?? []);
|
|
5819
|
+
await oldAgent.kill().catch(() => void 0);
|
|
5820
|
+
if (transcript) {
|
|
5821
|
+
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
5822
|
+
}
|
|
5823
|
+
this.broadcastAgentSwitch(agentId, agentId);
|
|
5824
|
+
const info = { agentId, upstreamSessionId: this.upstreamSessionId };
|
|
5825
|
+
for (const handler of this.agentChangeHandlers) {
|
|
5826
|
+
try {
|
|
5827
|
+
handler(info);
|
|
5828
|
+
} catch {
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5201
5832
|
// Walk the persisted history and produce a labeled transcript suitable
|
|
5202
5833
|
// for handing to a fresh agent. Includes user prompts, agent replies,
|
|
5203
5834
|
// and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
|
|
@@ -5298,15 +5929,13 @@ ${body}
|
|
|
5298
5929
|
return void 0;
|
|
5299
5930
|
});
|
|
5300
5931
|
}
|
|
5301
|
-
// Tell every attached client
|
|
5302
|
-
//
|
|
5932
|
+
// Tell every attached client the agent identity has changed via
|
|
5933
|
+
// session_info_update carrying agentId inside _meta["hydra-acp"] —
|
|
5303
5934
|
// the ACP schema for session_info_update is just title/updatedAt/_meta,
|
|
5304
5935
|
// so non-hydra clients harmlessly ignore the extension; hydra-aware
|
|
5305
|
-
// ones read it and relabel
|
|
5306
|
-
// transcript
|
|
5307
|
-
|
|
5308
|
-
// so a future /hydra agent's transcript builder filters them out.
|
|
5309
|
-
broadcastAgentSwitch(oldAgentId, newAgentId) {
|
|
5936
|
+
// ones read it and relabel. synthetic=true so a future /hydra agent's
|
|
5937
|
+
// transcript builder filters it out.
|
|
5938
|
+
broadcastAgentSwitch(_oldAgentId, newAgentId) {
|
|
5310
5939
|
this.recordAndBroadcast("session/update", {
|
|
5311
5940
|
sessionId: this.sessionId,
|
|
5312
5941
|
update: {
|
|
@@ -5314,19 +5943,7 @@ ${body}
|
|
|
5314
5943
|
_meta: { "hydra-acp": { synthetic: true, agentId: newAgentId } }
|
|
5315
5944
|
}
|
|
5316
5945
|
});
|
|
5317
|
-
this.
|
|
5318
|
-
sessionId: this.sessionId,
|
|
5319
|
-
update: {
|
|
5320
|
-
sessionUpdate: "agent_message_chunk",
|
|
5321
|
-
content: {
|
|
5322
|
-
type: "text",
|
|
5323
|
-
text: `
|
|
5324
|
-
_(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
5325
|
-
`
|
|
5326
|
-
},
|
|
5327
|
-
_meta: { "hydra-acp": { synthetic: true } }
|
|
5328
|
-
}
|
|
5329
|
-
});
|
|
5946
|
+
this.broadcastConfigOptions();
|
|
5330
5947
|
}
|
|
5331
5948
|
// stdin-stream lifecycle. Cat --stream calls openStream() once after
|
|
5332
5949
|
// session/new, then forwards stdin chunks via streamWrite(). The agent
|
|
@@ -5457,7 +6074,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5457
6074
|
requireStreamBuffer() {
|
|
5458
6075
|
if (this.streamBuffer === void 0) {
|
|
5459
6076
|
const err = new Error(
|
|
5460
|
-
`session ${this.sessionId} has no stream buffer;
|
|
6077
|
+
`session ${this.sessionId} has no stream buffer; POST /v1/sessions/:id/stdin/open first`
|
|
5461
6078
|
);
|
|
5462
6079
|
err.code = JsonRpcErrorCodes.StreamNotEnabled;
|
|
5463
6080
|
throw err;
|
|
@@ -5500,7 +6117,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5500
6117
|
const sessionId = this.sessionId;
|
|
5501
6118
|
this.queueWriteChain = this.queueWriteChain.catch(() => void 0).then(() => deleteQueue(sessionId).catch(() => void 0));
|
|
5502
6119
|
for (const client of this.clients.values()) {
|
|
5503
|
-
void client.connection.notify("hydra-acp/
|
|
6120
|
+
void client.connection.notify("hydra-acp/session/closed", { sessionId: this.sessionId }).catch(() => void 0);
|
|
5504
6121
|
}
|
|
5505
6122
|
this.clients.clear();
|
|
5506
6123
|
if (this.streamBuffer !== void 0) {
|
|
@@ -5607,7 +6224,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5607
6224
|
if (!t.intercepts.has(intercept)) {
|
|
5608
6225
|
continue;
|
|
5609
6226
|
}
|
|
5610
|
-
void t.connection.notify("transformer/session_event", {
|
|
6227
|
+
void t.connection.notify("hydra-acp/transformer/session_event", {
|
|
5611
6228
|
event,
|
|
5612
6229
|
sessionId: this.sessionId,
|
|
5613
6230
|
payload
|
|
@@ -5887,6 +6504,10 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5887
6504
|
}
|
|
5888
6505
|
);
|
|
5889
6506
|
} catch (err) {
|
|
6507
|
+
if (this.forceCancelling) {
|
|
6508
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
6509
|
+
return { stopReason: "cancelled" };
|
|
6510
|
+
}
|
|
5890
6511
|
if (!this.closed) {
|
|
5891
6512
|
this.broadcastTurnComplete(
|
|
5892
6513
|
entry.clientId,
|
|
@@ -6002,6 +6623,31 @@ function parseModelsList(list) {
|
|
|
6002
6623
|
}
|
|
6003
6624
|
return out;
|
|
6004
6625
|
}
|
|
6626
|
+
function parseModesList(list) {
|
|
6627
|
+
if (!Array.isArray(list)) {
|
|
6628
|
+
return [];
|
|
6629
|
+
}
|
|
6630
|
+
const out = [];
|
|
6631
|
+
for (const raw of list) {
|
|
6632
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
6633
|
+
continue;
|
|
6634
|
+
}
|
|
6635
|
+
const r = raw;
|
|
6636
|
+
const id = typeof r.value === "string" && r.value.trim() || typeof r.id === "string" && r.id.trim() || void 0;
|
|
6637
|
+
if (!id) {
|
|
6638
|
+
continue;
|
|
6639
|
+
}
|
|
6640
|
+
const mode = { id };
|
|
6641
|
+
if (typeof r.name === "string" && r.name.length > 0) {
|
|
6642
|
+
mode.name = r.name;
|
|
6643
|
+
}
|
|
6644
|
+
if (typeof r.description === "string" && r.description.length > 0) {
|
|
6645
|
+
mode.description = r.description;
|
|
6646
|
+
}
|
|
6647
|
+
out.push(mode);
|
|
6648
|
+
}
|
|
6649
|
+
return out;
|
|
6650
|
+
}
|
|
6005
6651
|
function extractAdvertisedModes(params) {
|
|
6006
6652
|
const obj = params ?? {};
|
|
6007
6653
|
const update = obj.update ?? {};
|
|
@@ -6193,7 +6839,7 @@ function firstLine(text, max) {
|
|
|
6193
6839
|
}
|
|
6194
6840
|
|
|
6195
6841
|
// src/core/session-store.ts
|
|
6196
|
-
import * as
|
|
6842
|
+
import * as fs9 from "fs/promises";
|
|
6197
6843
|
import * as path6 from "path";
|
|
6198
6844
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
6199
6845
|
import { z as z5 } from "zod";
|
|
@@ -6378,9 +7024,9 @@ var SessionRecord = z5.object({
|
|
|
6378
7024
|
// memory. Cleared after that first resurrect completes.
|
|
6379
7025
|
pendingHistorySync: z5.boolean().optional(),
|
|
6380
7026
|
// Set when this session was spawned as a child by a transformer via
|
|
6381
|
-
// hydra-acp/
|
|
7027
|
+
// hydra-acp/child_session/spawn. Points to the spawning session's id.
|
|
6382
7028
|
parentSessionId: z5.string().optional(),
|
|
6383
|
-
// Set when this session was created by hydra-acp/
|
|
7029
|
+
// Set when this session was created by hydra-acp/session/fork.
|
|
6384
7030
|
// forkedFromSessionId points to the local source session; forkedFromMessageId
|
|
6385
7031
|
// is the resolved forkAt — the messageId of the turn_complete the slice
|
|
6386
7032
|
// ended at. Kept so future UI can show "branched from turn N of session X".
|
|
@@ -6400,9 +7046,9 @@ var SessionRecord = z5.object({
|
|
|
6400
7046
|
createdAt: z5.string(),
|
|
6401
7047
|
updatedAt: z5.string()
|
|
6402
7048
|
});
|
|
6403
|
-
var
|
|
7049
|
+
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
6404
7050
|
function assertSafeId(id) {
|
|
6405
|
-
if (!
|
|
7051
|
+
if (!SESSION_ID_PATTERN2.test(id)) {
|
|
6406
7052
|
throw new Error(`unsafe session id: ${id}`);
|
|
6407
7053
|
}
|
|
6408
7054
|
}
|
|
@@ -6415,7 +7061,7 @@ var SessionStore = class {
|
|
|
6415
7061
|
});
|
|
6416
7062
|
}
|
|
6417
7063
|
async read(sessionId) {
|
|
6418
|
-
if (!
|
|
7064
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6419
7065
|
return void 0;
|
|
6420
7066
|
}
|
|
6421
7067
|
const parsed = await readJsonSafe(paths.sessionFile(sessionId));
|
|
@@ -6429,11 +7075,11 @@ var SessionStore = class {
|
|
|
6429
7075
|
}
|
|
6430
7076
|
}
|
|
6431
7077
|
async delete(sessionId) {
|
|
6432
|
-
if (!
|
|
7078
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6433
7079
|
return;
|
|
6434
7080
|
}
|
|
6435
7081
|
try {
|
|
6436
|
-
await
|
|
7082
|
+
await fs9.unlink(paths.sessionFile(sessionId));
|
|
6437
7083
|
} catch (err) {
|
|
6438
7084
|
const e = err;
|
|
6439
7085
|
if (e.code !== "ENOENT") {
|
|
@@ -6441,7 +7087,7 @@ var SessionStore = class {
|
|
|
6441
7087
|
}
|
|
6442
7088
|
}
|
|
6443
7089
|
try {
|
|
6444
|
-
await
|
|
7090
|
+
await fs9.rmdir(paths.sessionDir(sessionId));
|
|
6445
7091
|
} catch (err) {
|
|
6446
7092
|
const e = err;
|
|
6447
7093
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -6471,7 +7117,7 @@ var SessionStore = class {
|
|
|
6471
7117
|
async list() {
|
|
6472
7118
|
let entries;
|
|
6473
7119
|
try {
|
|
6474
|
-
entries = await
|
|
7120
|
+
entries = await fs9.readdir(paths.sessionsDir());
|
|
6475
7121
|
} catch (err) {
|
|
6476
7122
|
const e = err;
|
|
6477
7123
|
if (e.code === "ENOENT") {
|
|
@@ -6521,20 +7167,167 @@ function recordFromMemorySession(args) {
|
|
|
6521
7167
|
};
|
|
6522
7168
|
}
|
|
6523
7169
|
|
|
6524
|
-
// src/core/
|
|
7170
|
+
// src/core/tombstone-store.ts
|
|
6525
7171
|
import * as fs10 from "fs/promises";
|
|
7172
|
+
import { z as z6 } from "zod";
|
|
7173
|
+
var Tombstone = z6.object({
|
|
7174
|
+
version: z6.literal(1),
|
|
7175
|
+
agentId: z6.string(),
|
|
7176
|
+
upstreamSessionId: z6.string(),
|
|
7177
|
+
deletedAt: z6.string(),
|
|
7178
|
+
// Agent's last-reported updatedAt for this session at the moment we
|
|
7179
|
+
// deleted, snapshotted from SessionRecord.updatedAt. Compared against
|
|
7180
|
+
// the listing's updatedAt on subsequent syncs to detect that the
|
|
7181
|
+
// conversation has moved on (the agent / user revived it), in which
|
|
7182
|
+
// case the tombstone is dropped and the session re-imports. Absent
|
|
7183
|
+
// when the deleted record never carried a meaningful updatedAt.
|
|
7184
|
+
upstreamUpdatedAt: z6.string().optional(),
|
|
7185
|
+
cwd: z6.string().optional(),
|
|
7186
|
+
title: z6.string().optional(),
|
|
7187
|
+
reason: z6.enum(["user", "expired"]).optional()
|
|
7188
|
+
});
|
|
7189
|
+
var TombstoneStore = class {
|
|
7190
|
+
async add(t) {
|
|
7191
|
+
const full = { version: 1, ...t };
|
|
7192
|
+
await writeJsonAtomic(
|
|
7193
|
+
paths.tombstoneFile(t.agentId, t.upstreamSessionId),
|
|
7194
|
+
full,
|
|
7195
|
+
{ mode: 384 }
|
|
7196
|
+
);
|
|
7197
|
+
}
|
|
7198
|
+
async has(agentId, upstreamSessionId) {
|
|
7199
|
+
try {
|
|
7200
|
+
await fs10.access(paths.tombstoneFile(agentId, upstreamSessionId));
|
|
7201
|
+
return true;
|
|
7202
|
+
} catch {
|
|
7203
|
+
return false;
|
|
7204
|
+
}
|
|
7205
|
+
}
|
|
7206
|
+
// Returns the tombstone payload if the file exists. An unreadable or
|
|
7207
|
+
// unparseable file still counts as a tombstone — we synthesize a
|
|
7208
|
+
// bare record so the caller's "is this dead?" check stays correct,
|
|
7209
|
+
// but with no upstreamUpdatedAt the resurrection rule treats any
|
|
7210
|
+
// listed updatedAt as advancement (see SessionManager.syncFromAgent).
|
|
7211
|
+
async read(agentId, upstreamSessionId) {
|
|
7212
|
+
const file = paths.tombstoneFile(agentId, upstreamSessionId);
|
|
7213
|
+
const parsed = await readJsonSafe(file);
|
|
7214
|
+
if (parsed === void 0) {
|
|
7215
|
+
if (await this.has(agentId, upstreamSessionId)) {
|
|
7216
|
+
return {
|
|
7217
|
+
version: 1,
|
|
7218
|
+
agentId,
|
|
7219
|
+
upstreamSessionId,
|
|
7220
|
+
deletedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
7221
|
+
};
|
|
7222
|
+
}
|
|
7223
|
+
return void 0;
|
|
7224
|
+
}
|
|
7225
|
+
try {
|
|
7226
|
+
return Tombstone.parse(parsed);
|
|
7227
|
+
} catch {
|
|
7228
|
+
return {
|
|
7229
|
+
version: 1,
|
|
7230
|
+
agentId,
|
|
7231
|
+
upstreamSessionId,
|
|
7232
|
+
deletedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
7233
|
+
};
|
|
7234
|
+
}
|
|
7235
|
+
}
|
|
7236
|
+
async remove(agentId, upstreamSessionId) {
|
|
7237
|
+
try {
|
|
7238
|
+
await fs10.unlink(paths.tombstoneFile(agentId, upstreamSessionId));
|
|
7239
|
+
} catch (err) {
|
|
7240
|
+
const e = err;
|
|
7241
|
+
if (e.code !== "ENOENT") {
|
|
7242
|
+
throw err;
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
try {
|
|
7246
|
+
await fs10.rmdir(paths.tombstoneAgentDir(agentId));
|
|
7247
|
+
} catch (err) {
|
|
7248
|
+
const e = err;
|
|
7249
|
+
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
7250
|
+
throw err;
|
|
7251
|
+
}
|
|
7252
|
+
}
|
|
7253
|
+
}
|
|
7254
|
+
async list(agentId) {
|
|
7255
|
+
if (agentId !== void 0) {
|
|
7256
|
+
return this.listForAgent(agentId);
|
|
7257
|
+
}
|
|
7258
|
+
let agents;
|
|
7259
|
+
try {
|
|
7260
|
+
agents = await fs10.readdir(paths.tombstonesDir());
|
|
7261
|
+
} catch (err) {
|
|
7262
|
+
const e = err;
|
|
7263
|
+
if (e.code === "ENOENT") {
|
|
7264
|
+
return [];
|
|
7265
|
+
}
|
|
7266
|
+
throw err;
|
|
7267
|
+
}
|
|
7268
|
+
const out = [];
|
|
7269
|
+
for (const enc of agents) {
|
|
7270
|
+
let decoded;
|
|
7271
|
+
try {
|
|
7272
|
+
decoded = decodeURIComponent(enc);
|
|
7273
|
+
} catch {
|
|
7274
|
+
continue;
|
|
7275
|
+
}
|
|
7276
|
+
out.push(...await this.listForAgent(decoded));
|
|
7277
|
+
}
|
|
7278
|
+
return out;
|
|
7279
|
+
}
|
|
7280
|
+
async listForAgent(agentId) {
|
|
7281
|
+
let files;
|
|
7282
|
+
try {
|
|
7283
|
+
files = await fs10.readdir(paths.tombstoneAgentDir(agentId));
|
|
7284
|
+
} catch (err) {
|
|
7285
|
+
const e = err;
|
|
7286
|
+
if (e.code === "ENOENT") {
|
|
7287
|
+
return [];
|
|
7288
|
+
}
|
|
7289
|
+
throw err;
|
|
7290
|
+
}
|
|
7291
|
+
const out = [];
|
|
7292
|
+
for (const f of files) {
|
|
7293
|
+
let upstreamId;
|
|
7294
|
+
try {
|
|
7295
|
+
upstreamId = decodeURIComponent(f);
|
|
7296
|
+
} catch {
|
|
7297
|
+
continue;
|
|
7298
|
+
}
|
|
7299
|
+
const t = await this.read(agentId, upstreamId);
|
|
7300
|
+
if (t) {
|
|
7301
|
+
out.push(t);
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7304
|
+
return out;
|
|
7305
|
+
}
|
|
7306
|
+
};
|
|
7307
|
+
function shouldResurrectFromUpstream(tombstone, listingUpdatedAt) {
|
|
7308
|
+
if (listingUpdatedAt === void 0) {
|
|
7309
|
+
return false;
|
|
7310
|
+
}
|
|
7311
|
+
if (tombstone.upstreamUpdatedAt === void 0) {
|
|
7312
|
+
return true;
|
|
7313
|
+
}
|
|
7314
|
+
return listingUpdatedAt > tombstone.upstreamUpdatedAt;
|
|
7315
|
+
}
|
|
7316
|
+
|
|
7317
|
+
// src/core/synopsis-coordinator.ts
|
|
7318
|
+
import * as fs12 from "fs/promises";
|
|
6526
7319
|
|
|
6527
7320
|
// src/core/hydra-version.ts
|
|
6528
7321
|
import { fileURLToPath } from "url";
|
|
6529
7322
|
import * as path7 from "path";
|
|
6530
|
-
import * as
|
|
7323
|
+
import * as fs11 from "fs";
|
|
6531
7324
|
function resolveVersion() {
|
|
6532
7325
|
try {
|
|
6533
7326
|
let dir = path7.dirname(fileURLToPath(import.meta.url));
|
|
6534
7327
|
for (let i = 0; i < 8; i += 1) {
|
|
6535
7328
|
const candidate = path7.join(dir, "package.json");
|
|
6536
|
-
if (
|
|
6537
|
-
const pkg = JSON.parse(
|
|
7329
|
+
if (fs11.existsSync(candidate)) {
|
|
7330
|
+
const pkg = JSON.parse(fs11.readFileSync(candidate, "utf8"));
|
|
6538
7331
|
if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
|
|
6539
7332
|
return pkg.version;
|
|
6540
7333
|
}
|
|
@@ -7058,7 +7851,7 @@ var SynopsisCoordinator = class {
|
|
|
7058
7851
|
});
|
|
7059
7852
|
const modelId = this.opts.synopsisModel;
|
|
7060
7853
|
const synopsisCwd = paths.sessionDir(sessionId);
|
|
7061
|
-
await
|
|
7854
|
+
await fs12.mkdir(synopsisCwd, { recursive: true }).catch(() => void 0);
|
|
7062
7855
|
this.opts.logger?.info(
|
|
7063
7856
|
`synopsis: start sessionId=${sessionId} agentId=${synopsisAgentId} historyLen=${history.length} model=${JSON.stringify(modelId ?? "(default)")} cwd=${synopsisCwd}`
|
|
7064
7857
|
);
|
|
@@ -7161,8 +7954,217 @@ function describeFields(s) {
|
|
|
7161
7954
|
}
|
|
7162
7955
|
|
|
7163
7956
|
// src/core/history-store.ts
|
|
7164
|
-
import * as
|
|
7165
|
-
|
|
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_-]+$/;
|
|
7166
8168
|
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
7167
8169
|
var HistoryStore = class {
|
|
7168
8170
|
// Serialize writes per session id so appends and rewrites don't
|
|
@@ -7174,26 +8176,34 @@ var HistoryStore = class {
|
|
|
7174
8176
|
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
7175
8177
|
}
|
|
7176
8178
|
async append(sessionId, entry) {
|
|
7177
|
-
if (!
|
|
8179
|
+
if (!SESSION_ID_PATTERN3.test(sessionId)) {
|
|
7178
8180
|
return;
|
|
7179
8181
|
}
|
|
7180
8182
|
return this.enqueue(sessionId, async () => {
|
|
7181
|
-
await
|
|
7182
|
-
const
|
|
7183
|
-
|
|
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, {
|
|
7184
8190
|
encoding: "utf8",
|
|
7185
8191
|
mode: 384
|
|
7186
8192
|
});
|
|
7187
8193
|
});
|
|
7188
8194
|
}
|
|
7189
8195
|
async rewrite(sessionId, entries) {
|
|
7190
|
-
if (!
|
|
8196
|
+
if (!SESSION_ID_PATTERN3.test(sessionId)) {
|
|
7191
8197
|
return;
|
|
7192
8198
|
}
|
|
7193
8199
|
return this.enqueue(sessionId, async () => {
|
|
7194
|
-
await
|
|
7195
|
-
const
|
|
7196
|
-
|
|
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, {
|
|
7197
8207
|
encoding: "utf8",
|
|
7198
8208
|
mode: 384
|
|
7199
8209
|
});
|
|
@@ -7204,13 +8214,13 @@ var HistoryStore = class {
|
|
|
7204
8214
|
// it's safe to invoke alongside ongoing writes; a no-op if the file is
|
|
7205
8215
|
// already at or below the cap.
|
|
7206
8216
|
async compact(sessionId, maxEntries) {
|
|
7207
|
-
if (!
|
|
8217
|
+
if (!SESSION_ID_PATTERN3.test(sessionId)) {
|
|
7208
8218
|
return;
|
|
7209
8219
|
}
|
|
7210
8220
|
return this.enqueue(sessionId, async () => {
|
|
7211
8221
|
let raw;
|
|
7212
8222
|
try {
|
|
7213
|
-
raw = await
|
|
8223
|
+
raw = await fs13.readFile(paths.historyFile(sessionId), "utf8");
|
|
7214
8224
|
} catch (err) {
|
|
7215
8225
|
const e = err;
|
|
7216
8226
|
if (e.code === "ENOENT") {
|
|
@@ -7223,23 +8233,29 @@ var HistoryStore = class {
|
|
|
7223
8233
|
return;
|
|
7224
8234
|
}
|
|
7225
8235
|
const trimmed = lines.slice(-maxEntries);
|
|
7226
|
-
await
|
|
8236
|
+
await fs13.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
7227
8237
|
encoding: "utf8",
|
|
7228
8238
|
mode: 384
|
|
7229
8239
|
});
|
|
7230
8240
|
});
|
|
7231
8241
|
}
|
|
7232
|
-
|
|
7233
|
-
|
|
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)) {
|
|
7234
8249
|
return [];
|
|
7235
8250
|
}
|
|
8251
|
+
const expand = (opts.tools ?? "inline") === "inline";
|
|
7236
8252
|
const pending = this.writeQueues.get(sessionId);
|
|
7237
8253
|
if (pending) {
|
|
7238
8254
|
await pending;
|
|
7239
8255
|
}
|
|
7240
8256
|
let raw;
|
|
7241
8257
|
try {
|
|
7242
|
-
raw = await
|
|
8258
|
+
raw = await fs13.readFile(paths.historyFile(sessionId), "utf8");
|
|
7243
8259
|
} catch (err) {
|
|
7244
8260
|
const e = err;
|
|
7245
8261
|
if (e.code === "ENOENT") {
|
|
@@ -7274,10 +8290,25 @@ var HistoryStore = class {
|
|
|
7274
8290
|
recordedAt: obj.recordedAt
|
|
7275
8291
|
});
|
|
7276
8292
|
}
|
|
7277
|
-
|
|
7278
|
-
|
|
8293
|
+
const kept = out.length > this.maxEntries ? out.slice(-this.maxEntries) : out;
|
|
8294
|
+
if (!expand) {
|
|
8295
|
+
return kept;
|
|
7279
8296
|
}
|
|
7280
|
-
|
|
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;
|
|
7281
8312
|
}
|
|
7282
8313
|
// Wait for every pending append/rewrite/compact across all sessions to
|
|
7283
8314
|
// settle. Daemon shutdown calls this after closing sessions so the final
|
|
@@ -7293,20 +8324,21 @@ var HistoryStore = class {
|
|
|
7293
8324
|
await Promise.allSettled(pending);
|
|
7294
8325
|
}
|
|
7295
8326
|
async delete(sessionId) {
|
|
7296
|
-
if (!
|
|
8327
|
+
if (!SESSION_ID_PATTERN3.test(sessionId)) {
|
|
7297
8328
|
return;
|
|
7298
8329
|
}
|
|
7299
8330
|
return this.enqueue(sessionId, async () => {
|
|
7300
8331
|
try {
|
|
7301
|
-
await
|
|
8332
|
+
await fs13.unlink(paths.historyFile(sessionId));
|
|
7302
8333
|
} catch (err) {
|
|
7303
8334
|
const e = err;
|
|
7304
8335
|
if (e.code !== "ENOENT") {
|
|
7305
8336
|
throw err;
|
|
7306
8337
|
}
|
|
7307
8338
|
}
|
|
8339
|
+
await deleteToolBlobs(sessionId);
|
|
7308
8340
|
try {
|
|
7309
|
-
await
|
|
8341
|
+
await fs13.rmdir(paths.sessionDir(sessionId));
|
|
7310
8342
|
} catch (err) {
|
|
7311
8343
|
const e = err;
|
|
7312
8344
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -7330,74 +8362,79 @@ var HistoryStore = class {
|
|
|
7330
8362
|
};
|
|
7331
8363
|
|
|
7332
8364
|
// src/tui/history.ts
|
|
7333
|
-
import { promises as
|
|
8365
|
+
import { promises as fs14 } from "fs";
|
|
7334
8366
|
import * as path8 from "path";
|
|
7335
8367
|
async function saveHistory(file, history) {
|
|
7336
|
-
await
|
|
8368
|
+
await fs14.mkdir(path8.dirname(file), { recursive: true });
|
|
7337
8369
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
7338
|
-
await
|
|
8370
|
+
await fs14.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
7339
8371
|
}
|
|
7340
8372
|
|
|
7341
8373
|
// src/core/bundle.ts
|
|
7342
|
-
import { z as
|
|
7343
|
-
var HistoryEntrySchema =
|
|
7344
|
-
method:
|
|
7345
|
-
params:
|
|
7346
|
-
recordedAt:
|
|
8374
|
+
import { z as z7 } from "zod";
|
|
8375
|
+
var HistoryEntrySchema = z7.object({
|
|
8376
|
+
method: z7.string(),
|
|
8377
|
+
params: z7.unknown(),
|
|
8378
|
+
recordedAt: z7.number()
|
|
7347
8379
|
});
|
|
7348
|
-
var BundleSession =
|
|
8380
|
+
var BundleSession = z7.object({
|
|
7349
8381
|
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
7350
8382
|
// the local namespace; lineageId is what survives across hops).
|
|
7351
|
-
sessionId:
|
|
8383
|
+
sessionId: z7.string(),
|
|
7352
8384
|
// Required on bundles — the export path backfills if the source
|
|
7353
8385
|
// record was written before lineageId existed.
|
|
7354
|
-
lineageId:
|
|
8386
|
+
lineageId: z7.string(),
|
|
7355
8387
|
// The exporter's agent-side session id at export time. Carried so
|
|
7356
8388
|
// importers can persist it as a breadcrumb (and, eventually, as the
|
|
7357
8389
|
// handle a "connect back to origin" feature would need). Omitted on
|
|
7358
8390
|
// bundles whose source record never bound to an agent (e.g. a
|
|
7359
8391
|
// re-export of an imported, not-yet-attached session).
|
|
7360
|
-
upstreamSessionId:
|
|
7361
|
-
agentId:
|
|
7362
|
-
cwd:
|
|
7363
|
-
title:
|
|
8392
|
+
upstreamSessionId: z7.string().optional(),
|
|
8393
|
+
agentId: z7.string(),
|
|
8394
|
+
cwd: z7.string(),
|
|
8395
|
+
title: z7.string().optional(),
|
|
7364
8396
|
// Structured snapshot. Carried across export/import so a sync'd
|
|
7365
8397
|
// session on another machine surfaces the same synopsis in its
|
|
7366
8398
|
// picker / list / info views without re-asking the agent.
|
|
7367
8399
|
synopsis: SessionSynopsis.optional(),
|
|
7368
|
-
summarizedThroughEntry:
|
|
7369
|
-
currentModel:
|
|
7370
|
-
currentMode:
|
|
8400
|
+
summarizedThroughEntry: z7.number().int().nonnegative().optional(),
|
|
8401
|
+
currentModel: z7.string().optional(),
|
|
8402
|
+
currentMode: z7.string().optional(),
|
|
7371
8403
|
currentUsage: PersistedUsage.optional(),
|
|
7372
|
-
agentCommands:
|
|
7373
|
-
agentModes:
|
|
8404
|
+
agentCommands: z7.array(PersistedAgentCommand).optional(),
|
|
8405
|
+
agentModes: z7.array(PersistedAgentMode).optional(),
|
|
7374
8406
|
// Raw interactive tristate (NOT the resolved effectiveInteractive) so
|
|
7375
8407
|
// the value stays promotable on the destination: a cat/empty source
|
|
7376
8408
|
// arrives as undefined and a real turn there can still flip it to
|
|
7377
8409
|
// true. Carried alongside originatingClient so the importer's
|
|
7378
8410
|
// effectiveInteractive can re-apply the cat-name hint at read time
|
|
7379
8411
|
// without freezing a sticky `false` into the record.
|
|
7380
|
-
interactive:
|
|
8412
|
+
interactive: z7.boolean().optional(),
|
|
7381
8413
|
originatingClient: PersistedOriginatingClient.optional(),
|
|
7382
|
-
createdAt:
|
|
7383
|
-
updatedAt:
|
|
8414
|
+
createdAt: z7.string(),
|
|
8415
|
+
updatedAt: z7.string()
|
|
7384
8416
|
});
|
|
7385
|
-
var Bundle =
|
|
7386
|
-
version:
|
|
7387
|
-
exportedAt:
|
|
7388
|
-
exportedFrom:
|
|
7389
|
-
hydraVersion:
|
|
7390
|
-
machine:
|
|
8417
|
+
var Bundle = z7.object({
|
|
8418
|
+
version: z7.literal(1),
|
|
8419
|
+
exportedAt: z7.string(),
|
|
8420
|
+
exportedFrom: z7.object({
|
|
8421
|
+
hydraVersion: z7.string(),
|
|
8422
|
+
machine: z7.string(),
|
|
7391
8423
|
// Externally-reachable name (and optional ":port") for the exporting
|
|
7392
8424
|
// daemon, sourced from config.daemon.publicHost (or daemon.host when
|
|
7393
8425
|
// non-loopback). Carried so an importer can construct a hydra:// URL
|
|
7394
8426
|
// that dials back to the origin — e.g. over Tailscale. Omitted when
|
|
7395
8427
|
// the exporter has no routable address; never falls back to loopback.
|
|
7396
|
-
hydraHost:
|
|
8428
|
+
hydraHost: z7.string().optional()
|
|
7397
8429
|
}),
|
|
7398
8430
|
session: BundleSession,
|
|
7399
|
-
history:
|
|
7400
|
-
promptHistory:
|
|
8431
|
+
history: z7.array(HistoryEntrySchema),
|
|
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()
|
|
7401
8438
|
});
|
|
7402
8439
|
function encodeBundle(params) {
|
|
7403
8440
|
const bundle = {
|
|
@@ -7432,6 +8469,9 @@ function encodeBundle(params) {
|
|
|
7432
8469
|
if (params.promptHistory !== void 0) {
|
|
7433
8470
|
bundle.promptHistory = params.promptHistory;
|
|
7434
8471
|
}
|
|
8472
|
+
if (params.toolBlobs !== void 0 && Object.keys(params.toolBlobs).length > 0) {
|
|
8473
|
+
bundle.toolBlobs = params.toolBlobs;
|
|
8474
|
+
}
|
|
7435
8475
|
return bundle;
|
|
7436
8476
|
}
|
|
7437
8477
|
function decodeBundle(raw) {
|
|
@@ -7447,6 +8487,7 @@ var SessionManager = class {
|
|
|
7447
8487
|
this.registry = registry;
|
|
7448
8488
|
this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
|
|
7449
8489
|
this.store = store ?? new SessionStore();
|
|
8490
|
+
this.tombstones = options.tombstones ?? new TombstoneStore();
|
|
7450
8491
|
this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
|
|
7451
8492
|
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
7452
8493
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
@@ -7478,12 +8519,14 @@ var SessionManager = class {
|
|
|
7478
8519
|
logger: this.logger,
|
|
7479
8520
|
npmRegistry: this.npmRegistry
|
|
7480
8521
|
});
|
|
8522
|
+
void this.refreshAgentCatalog();
|
|
7481
8523
|
}
|
|
7482
8524
|
registry;
|
|
7483
8525
|
sessions = /* @__PURE__ */ new Map();
|
|
7484
8526
|
resurrectionInflight = /* @__PURE__ */ new Map();
|
|
7485
8527
|
spawner;
|
|
7486
8528
|
store;
|
|
8529
|
+
tombstones;
|
|
7487
8530
|
histories;
|
|
7488
8531
|
idleTimeoutMs;
|
|
7489
8532
|
defaultModels;
|
|
@@ -7505,6 +8548,25 @@ var SessionManager = class {
|
|
|
7505
8548
|
// out-of-band so session close is instant; persists synopsis/title
|
|
7506
8549
|
// via the same enqueueMetaWrite path the in-session handlers used.
|
|
7507
8550
|
synopsisCoordinator;
|
|
8551
|
+
// Cached agent catalog used to populate the `agent` config option's
|
|
8552
|
+
// value list. Refreshed lazily (fire-and-forget) since the underlying
|
|
8553
|
+
// registry load may hit the network; sessions read whatever snapshot is
|
|
8554
|
+
// current and always inject their own live agent if it's missing.
|
|
8555
|
+
agentCatalog = [];
|
|
8556
|
+
// Refresh the cached agent catalog from the registry. Fire-and-forget;
|
|
8557
|
+
// failures leave the prior snapshot in place. Called at construction and
|
|
8558
|
+
// after each session creation so the list tracks newly-installed agents.
|
|
8559
|
+
async refreshAgentCatalog() {
|
|
8560
|
+
try {
|
|
8561
|
+
const { agents } = await listAgents(this.registry);
|
|
8562
|
+
this.agentCatalog = agents.map((a) => ({
|
|
8563
|
+
id: a.id,
|
|
8564
|
+
name: a.name,
|
|
8565
|
+
...a.description !== void 0 ? { description: a.description } : {}
|
|
8566
|
+
}));
|
|
8567
|
+
} catch {
|
|
8568
|
+
}
|
|
8569
|
+
}
|
|
7508
8570
|
async create(params) {
|
|
7509
8571
|
const fresh = await this.bootstrapAgent({
|
|
7510
8572
|
agentId: params.agentId,
|
|
@@ -7521,7 +8583,7 @@ var SessionManager = class {
|
|
|
7521
8583
|
continue;
|
|
7522
8584
|
}
|
|
7523
8585
|
try {
|
|
7524
|
-
const result = await t.connection.request("transformer/message", {
|
|
8586
|
+
const result = await t.connection.request("hydra-acp/transformer/message", {
|
|
7525
8587
|
token: `t_${generateRawSessionId()}`,
|
|
7526
8588
|
phase: "response",
|
|
7527
8589
|
method: "initialize",
|
|
@@ -7551,6 +8613,7 @@ var SessionManager = class {
|
|
|
7551
8613
|
logger: this.logger,
|
|
7552
8614
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7553
8615
|
listSessions: () => this.list(),
|
|
8616
|
+
availableAgents: () => this.agentCatalog,
|
|
7554
8617
|
historyStore: this.histories,
|
|
7555
8618
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
7556
8619
|
currentModel: fresh.initialModel,
|
|
@@ -7718,6 +8781,7 @@ var SessionManager = class {
|
|
|
7718
8781
|
logger: this.logger,
|
|
7719
8782
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7720
8783
|
listSessions: () => this.list(),
|
|
8784
|
+
availableAgents: () => this.agentCatalog,
|
|
7721
8785
|
historyStore: this.histories,
|
|
7722
8786
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
7723
8787
|
currentModel: effectiveModel,
|
|
@@ -7798,6 +8862,7 @@ var SessionManager = class {
|
|
|
7798
8862
|
logger: this.logger,
|
|
7799
8863
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7800
8864
|
listSessions: () => this.list(),
|
|
8865
|
+
availableAgents: () => this.agentCatalog,
|
|
7801
8866
|
historyStore: this.histories,
|
|
7802
8867
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
7803
8868
|
currentModel: effectiveModel,
|
|
@@ -7821,7 +8886,7 @@ var SessionManager = class {
|
|
|
7821
8886
|
}
|
|
7822
8887
|
async dirExists(cwd) {
|
|
7823
8888
|
try {
|
|
7824
|
-
return (await
|
|
8889
|
+
return (await fs15.stat(cwd)).isDirectory();
|
|
7825
8890
|
} catch {
|
|
7826
8891
|
return false;
|
|
7827
8892
|
}
|
|
@@ -7927,7 +8992,7 @@ var SessionManager = class {
|
|
|
7927
8992
|
for (const rec of stored) {
|
|
7928
8993
|
existing.add(`${rec.agentId}::${rec.upstreamSessionId}`);
|
|
7929
8994
|
}
|
|
7930
|
-
const
|
|
8995
|
+
const synopsisSandboxDir = paths.sessionsDir();
|
|
7931
8996
|
const synced = [];
|
|
7932
8997
|
let skipped = 0;
|
|
7933
8998
|
for (const entry of entries) {
|
|
@@ -7936,10 +9001,21 @@ var SessionManager = class {
|
|
|
7936
9001
|
skipped += 1;
|
|
7937
9002
|
continue;
|
|
7938
9003
|
}
|
|
7939
|
-
if (
|
|
9004
|
+
if (isSynopsisSession(entry.cwd, synopsisSandboxDir)) {
|
|
7940
9005
|
skipped += 1;
|
|
7941
9006
|
continue;
|
|
7942
9007
|
}
|
|
9008
|
+
const tombstone = await this.tombstones.read(agentId, entry.sessionId).catch(() => void 0);
|
|
9009
|
+
if (tombstone) {
|
|
9010
|
+
if (!shouldResurrectFromUpstream(tombstone, entry.updatedAt)) {
|
|
9011
|
+
skipped += 1;
|
|
9012
|
+
continue;
|
|
9013
|
+
}
|
|
9014
|
+
await this.tombstones.remove(agentId, entry.sessionId).catch(() => void 0);
|
|
9015
|
+
this.logger?.info(
|
|
9016
|
+
`syncFromAgent: resurrecting tombstoned ${agentId}/${entry.sessionId} (upstream updatedAt advanced past ${tombstone.upstreamUpdatedAt ?? "<unset>"})`
|
|
9017
|
+
);
|
|
9018
|
+
}
|
|
7943
9019
|
existing.add(dedupeKey);
|
|
7944
9020
|
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
7945
9021
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -8000,6 +9076,25 @@ var SessionManager = class {
|
|
|
8000
9076
|
}
|
|
8001
9077
|
return out;
|
|
8002
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
|
+
}
|
|
8003
9098
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
8004
9099
|
// → session/new. Shared by create() and the /hydra agent path so both
|
|
8005
9100
|
// go through the same env / capabilities / error-handling.
|
|
@@ -8048,23 +9143,31 @@ var SessionManager = class {
|
|
|
8048
9143
|
const initialModels = extractInitialModels(newResult);
|
|
8049
9144
|
const desired = params.model ?? this.defaultModels[params.agentId];
|
|
8050
9145
|
if (desired && desired !== initialModel) {
|
|
8051
|
-
const
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
sessionId: sessionIdRaw,
|
|
8056
|
-
modelId: desired
|
|
8057
|
-
});
|
|
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)) {
|
|
8058
9150
|
initialModel = desired;
|
|
8059
|
-
} catch (err) {
|
|
8060
|
-
this.logger?.warn(
|
|
8061
|
-
`defaultModels[${params.agentId}]=${JSON.stringify(desired)} rejected by agent (${err.message}); session will use ${JSON.stringify(initialModel)}`
|
|
8062
|
-
);
|
|
8063
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
|
+
);
|
|
8064
9167
|
} else {
|
|
8065
9168
|
const known = initialModels.map((m) => m.modelId).join(", ");
|
|
8066
9169
|
this.logger?.warn(
|
|
8067
|
-
|
|
9170
|
+
`${where} not in agent's availableModels ([${known}]); skipping session/set_model, session will use ${JSON.stringify(initialModel)}`
|
|
8068
9171
|
);
|
|
8069
9172
|
}
|
|
8070
9173
|
}
|
|
@@ -8095,6 +9198,17 @@ var SessionManager = class {
|
|
|
8095
9198
|
session.onClose(({ deleteRecord }) => {
|
|
8096
9199
|
this.sessions.delete(session.sessionId);
|
|
8097
9200
|
if (deleteRecord) {
|
|
9201
|
+
if (session.upstreamSessionId) {
|
|
9202
|
+
void this.tombstones.add({
|
|
9203
|
+
agentId: session.agentId,
|
|
9204
|
+
upstreamSessionId: session.upstreamSessionId,
|
|
9205
|
+
deletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9206
|
+
upstreamUpdatedAt: new Date(session.updatedAt).toISOString(),
|
|
9207
|
+
cwd: session.cwd,
|
|
9208
|
+
title: session.title,
|
|
9209
|
+
reason: "user"
|
|
9210
|
+
}).catch(() => void 0);
|
|
9211
|
+
}
|
|
8098
9212
|
void this.store.delete(session.sessionId).catch(() => void 0);
|
|
8099
9213
|
void this.histories.delete(session.sessionId).catch(() => void 0);
|
|
8100
9214
|
return;
|
|
@@ -8186,6 +9300,12 @@ var SessionManager = class {
|
|
|
8186
9300
|
async loadHistory(sessionId) {
|
|
8187
9301
|
return this.histories.load(sessionId);
|
|
8188
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
|
+
}
|
|
8189
9309
|
async loadFromDisk(sessionId) {
|
|
8190
9310
|
const record = await this.store.read(sessionId);
|
|
8191
9311
|
if (!record) {
|
|
@@ -8304,6 +9424,35 @@ var SessionManager = class {
|
|
|
8304
9424
|
}
|
|
8305
9425
|
return session;
|
|
8306
9426
|
}
|
|
9427
|
+
// Synchronous SessionListEntry for a resident session. Mirrors the
|
|
9428
|
+
// live-session branch of list() but skips the async history probe:
|
|
9429
|
+
// callers on the attach/new hot path already hold the Session and
|
|
9430
|
+
// don't need the history-derived `interactive` inference (they pass
|
|
9431
|
+
// through the session's own tristate) or the history mtime (the
|
|
9432
|
+
// session's updatedAt is current). Used to build the reconciled
|
|
9433
|
+
// session/new + session/attach response `_meta["hydra-acp"]` from the
|
|
9434
|
+
// same shape session/list emits.
|
|
9435
|
+
liveListEntry(session) {
|
|
9436
|
+
return {
|
|
9437
|
+
sessionId: session.sessionId,
|
|
9438
|
+
upstreamSessionId: session.upstreamSessionId,
|
|
9439
|
+
cwd: session.cwd,
|
|
9440
|
+
title: session.title,
|
|
9441
|
+
agentId: session.agentId,
|
|
9442
|
+
currentModel: session.currentModel,
|
|
9443
|
+
currentUsage: session.currentUsage,
|
|
9444
|
+
parentSessionId: session.parentSessionId,
|
|
9445
|
+
forkedFromSessionId: session.forkedFromSessionId,
|
|
9446
|
+
forkedFromMessageId: session.forkedFromMessageId,
|
|
9447
|
+
originatingClient: session.originatingClient,
|
|
9448
|
+
interactive: session.interactive,
|
|
9449
|
+
updatedAt: new Date(session.updatedAt).toISOString(),
|
|
9450
|
+
attachedClients: session.attachedCount,
|
|
9451
|
+
status: "live",
|
|
9452
|
+
busy: session.turnStartedAt !== void 0,
|
|
9453
|
+
awaitingInput: session.awaitingInput
|
|
9454
|
+
};
|
|
9455
|
+
}
|
|
8307
9456
|
async list(filter = {}) {
|
|
8308
9457
|
const entries = [];
|
|
8309
9458
|
const liveIds = /* @__PURE__ */ new Set();
|
|
@@ -8394,7 +9543,7 @@ var SessionManager = class {
|
|
|
8394
9543
|
// disk. Backfills lineageId if the on-disk record pre-dates that
|
|
8395
9544
|
// field. Returns undefined if the session doesn't exist. Callers
|
|
8396
9545
|
// populate the bundle's exportedFrom metadata themselves.
|
|
8397
|
-
async exportBundle(sessionId) {
|
|
9546
|
+
async exportBundle(sessionId, opts = {}) {
|
|
8398
9547
|
const record = await this.store.read(sessionId);
|
|
8399
9548
|
if (!record) {
|
|
8400
9549
|
return void 0;
|
|
@@ -8417,9 +9566,20 @@ var SessionManager = class {
|
|
|
8417
9566
|
}).catch(() => void 0);
|
|
8418
9567
|
withLineage = backfilled;
|
|
8419
9568
|
}
|
|
8420
|
-
const
|
|
9569
|
+
const tools = opts.tools ?? "inline";
|
|
9570
|
+
const history = await this.histories.load(sessionId, tools === "references" ? { tools: "references" } : {}).catch(() => []);
|
|
8421
9571
|
const promptHistory = await loadPromptHistorySafely(sessionId);
|
|
8422
|
-
|
|
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 };
|
|
8423
9583
|
}
|
|
8424
9584
|
// Create a local session from an imported bundle. Without `replace`,
|
|
8425
9585
|
// a bundle with a lineageId we already have on disk throws
|
|
@@ -8580,9 +9740,18 @@ var SessionManager = class {
|
|
|
8580
9740
|
args.sessionId,
|
|
8581
9741
|
args.bundle.history
|
|
8582
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
|
+
}
|
|
8583
9752
|
const sourceMtime = new Date(args.bundle.session.updatedAt);
|
|
8584
9753
|
if (!Number.isNaN(sourceMtime.getTime())) {
|
|
8585
|
-
await
|
|
9754
|
+
await fs15.utimes(paths.historyFile(args.sessionId), sourceMtime, sourceMtime).catch(() => void 0);
|
|
8586
9755
|
}
|
|
8587
9756
|
if (args.bundle.promptHistory && args.bundle.promptHistory.length > 0) {
|
|
8588
9757
|
await saveHistory(
|
|
@@ -8637,6 +9806,17 @@ var SessionManager = class {
|
|
|
8637
9806
|
if (!record) {
|
|
8638
9807
|
return false;
|
|
8639
9808
|
}
|
|
9809
|
+
if (record.upstreamSessionId) {
|
|
9810
|
+
await this.tombstones.add({
|
|
9811
|
+
agentId: record.agentId,
|
|
9812
|
+
upstreamSessionId: record.upstreamSessionId,
|
|
9813
|
+
deletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9814
|
+
upstreamUpdatedAt: record.updatedAt,
|
|
9815
|
+
cwd: record.cwd,
|
|
9816
|
+
title: record.title,
|
|
9817
|
+
reason: "user"
|
|
9818
|
+
}).catch(() => void 0);
|
|
9819
|
+
}
|
|
8640
9820
|
await this.store.delete(sessionId).catch(() => void 0);
|
|
8641
9821
|
return true;
|
|
8642
9822
|
}
|
|
@@ -8846,13 +10026,13 @@ var SessionManager = class {
|
|
|
8846
10026
|
}
|
|
8847
10027
|
}
|
|
8848
10028
|
};
|
|
8849
|
-
function
|
|
10029
|
+
function isSynopsisSession(cwd, sandboxDir) {
|
|
8850
10030
|
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
8851
10031
|
return false;
|
|
8852
10032
|
}
|
|
8853
10033
|
const resolved = path9.resolve(cwd);
|
|
8854
|
-
const
|
|
8855
|
-
return resolved ===
|
|
10034
|
+
const base = path9.resolve(sandboxDir);
|
|
10035
|
+
return resolved === base || resolved.startsWith(base + path9.sep);
|
|
8856
10036
|
}
|
|
8857
10037
|
function mergeForPersistence(session, existing) {
|
|
8858
10038
|
const persistedCommands = session.mergedAvailableCommands().length > 0 ? session.agentOnlyAdvertisedCommands().map((c) => {
|
|
@@ -8977,6 +10157,13 @@ function extractInitialModel(result) {
|
|
|
8977
10157
|
}
|
|
8978
10158
|
}
|
|
8979
10159
|
}
|
|
10160
|
+
const fromConfig = findConfigOptionEntry(result, "model");
|
|
10161
|
+
if (fromConfig) {
|
|
10162
|
+
const cv = asString(fromConfig.currentValue);
|
|
10163
|
+
if (cv) {
|
|
10164
|
+
return cv;
|
|
10165
|
+
}
|
|
10166
|
+
}
|
|
8980
10167
|
return void 0;
|
|
8981
10168
|
}
|
|
8982
10169
|
function asString(value) {
|
|
@@ -8986,6 +10173,22 @@ function asString(value) {
|
|
|
8986
10173
|
const trimmed = value.trim();
|
|
8987
10174
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
8988
10175
|
}
|
|
10176
|
+
function findConfigOptionEntry(result, id) {
|
|
10177
|
+
const list = result.configOptions;
|
|
10178
|
+
if (!Array.isArray(list)) {
|
|
10179
|
+
return void 0;
|
|
10180
|
+
}
|
|
10181
|
+
for (const raw of list) {
|
|
10182
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
10183
|
+
continue;
|
|
10184
|
+
}
|
|
10185
|
+
const entry = raw;
|
|
10186
|
+
if (entry.id === id) {
|
|
10187
|
+
return entry;
|
|
10188
|
+
}
|
|
10189
|
+
}
|
|
10190
|
+
return void 0;
|
|
10191
|
+
}
|
|
8989
10192
|
function nonEmptyOrUndefined(arr) {
|
|
8990
10193
|
return arr.length > 0 ? arr : void 0;
|
|
8991
10194
|
}
|
|
@@ -9021,6 +10224,13 @@ function extractInitialModels(result) {
|
|
|
9021
10224
|
}
|
|
9022
10225
|
}
|
|
9023
10226
|
}
|
|
10227
|
+
const fromConfig = findConfigOptionEntry(result, "model");
|
|
10228
|
+
if (fromConfig) {
|
|
10229
|
+
const parsed = parseModelsList(fromConfig.options);
|
|
10230
|
+
if (parsed.length > 0) {
|
|
10231
|
+
return parsed;
|
|
10232
|
+
}
|
|
10233
|
+
}
|
|
9024
10234
|
return [];
|
|
9025
10235
|
}
|
|
9026
10236
|
function extractInitialModes(result) {
|
|
@@ -9055,6 +10265,13 @@ function extractInitialModes(result) {
|
|
|
9055
10265
|
}
|
|
9056
10266
|
}
|
|
9057
10267
|
}
|
|
10268
|
+
const fromConfig = findConfigOptionEntry(result, "mode");
|
|
10269
|
+
if (fromConfig) {
|
|
10270
|
+
const parsed = parseModesList(fromConfig.options);
|
|
10271
|
+
if (parsed.length > 0) {
|
|
10272
|
+
return parsed;
|
|
10273
|
+
}
|
|
10274
|
+
}
|
|
9058
10275
|
return [];
|
|
9059
10276
|
}
|
|
9060
10277
|
function extractInitialCurrentMode(result) {
|
|
@@ -9085,6 +10302,13 @@ function extractInitialCurrentMode(result) {
|
|
|
9085
10302
|
}
|
|
9086
10303
|
}
|
|
9087
10304
|
}
|
|
10305
|
+
const fromConfig = findConfigOptionEntry(result, "mode");
|
|
10306
|
+
if (fromConfig) {
|
|
10307
|
+
const cv = asString(fromConfig.currentValue);
|
|
10308
|
+
if (cv) {
|
|
10309
|
+
return cv;
|
|
10310
|
+
}
|
|
10311
|
+
}
|
|
9088
10312
|
return void 0;
|
|
9089
10313
|
}
|
|
9090
10314
|
async function restoreCurrentMode(opts) {
|
|
@@ -9146,41 +10370,14 @@ async function restoreCurrentModel(opts) {
|
|
|
9146
10370
|
});
|
|
9147
10371
|
logger?.info(
|
|
9148
10372
|
`resurrect: session/set_model accepted, effectiveModel=${JSON.stringify(persistedModel)}`
|
|
9149
|
-
);
|
|
9150
|
-
return persistedModel;
|
|
9151
|
-
} catch (err) {
|
|
9152
|
-
logger?.warn(
|
|
9153
|
-
`resurrect: session/set_model rejected by agent for modelId=${JSON.stringify(persistedModel)} (${err.message}); session will use ${JSON.stringify(agentReportedModel)}`
|
|
9154
|
-
);
|
|
9155
|
-
return agentReportedModel;
|
|
9156
|
-
}
|
|
9157
|
-
}
|
|
9158
|
-
function parseModesList(list) {
|
|
9159
|
-
if (!Array.isArray(list)) {
|
|
9160
|
-
return [];
|
|
9161
|
-
}
|
|
9162
|
-
const out = [];
|
|
9163
|
-
for (const raw of list) {
|
|
9164
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
9165
|
-
continue;
|
|
9166
|
-
}
|
|
9167
|
-
const r = raw;
|
|
9168
|
-
const id = asString(r.id) ?? asString(r.modeId);
|
|
9169
|
-
if (!id) {
|
|
9170
|
-
continue;
|
|
9171
|
-
}
|
|
9172
|
-
const mode = { id };
|
|
9173
|
-
const name = asString(r.name);
|
|
9174
|
-
if (name) {
|
|
9175
|
-
mode.name = name;
|
|
9176
|
-
}
|
|
9177
|
-
const description = asString(r.description);
|
|
9178
|
-
if (description) {
|
|
9179
|
-
mode.description = description;
|
|
9180
|
-
}
|
|
9181
|
-
out.push(mode);
|
|
10373
|
+
);
|
|
10374
|
+
return persistedModel;
|
|
10375
|
+
} catch (err) {
|
|
10376
|
+
logger?.warn(
|
|
10377
|
+
`resurrect: session/set_model rejected by agent for modelId=${JSON.stringify(persistedModel)} (${err.message}); session will use ${JSON.stringify(agentReportedModel)}`
|
|
10378
|
+
);
|
|
10379
|
+
return agentReportedModel;
|
|
9182
10380
|
}
|
|
9183
|
-
return out;
|
|
9184
10381
|
}
|
|
9185
10382
|
function findLastTurnComplete(history) {
|
|
9186
10383
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
@@ -9201,7 +10398,7 @@ function findLastTurnComplete(history) {
|
|
|
9201
10398
|
}
|
|
9202
10399
|
async function loadPromptHistorySafely(sessionId) {
|
|
9203
10400
|
try {
|
|
9204
|
-
const raw = await
|
|
10401
|
+
const raw = await fs15.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
9205
10402
|
const out = [];
|
|
9206
10403
|
for (const line of raw.split("\n")) {
|
|
9207
10404
|
if (line.length === 0) {
|
|
@@ -9222,7 +10419,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
9222
10419
|
}
|
|
9223
10420
|
async function historyStatus(sessionId) {
|
|
9224
10421
|
try {
|
|
9225
|
-
const st = await
|
|
10422
|
+
const st = await fs15.stat(paths.historyFile(sessionId));
|
|
9226
10423
|
return {
|
|
9227
10424
|
mtime: new Date(st.mtimeMs).toISOString(),
|
|
9228
10425
|
hasContent: st.size > 0
|
|
@@ -9243,7 +10440,7 @@ function effectiveInteractive(record, hasContent) {
|
|
|
9243
10440
|
|
|
9244
10441
|
// src/core/child-supervisor.ts
|
|
9245
10442
|
import { spawn as spawn4 } from "child_process";
|
|
9246
|
-
import * as
|
|
10443
|
+
import * as fs16 from "fs";
|
|
9247
10444
|
import * as fsp5 from "fs/promises";
|
|
9248
10445
|
import * as path10 from "path";
|
|
9249
10446
|
|
|
@@ -9616,7 +10813,7 @@ var ChildSupervisor = class {
|
|
|
9616
10813
|
}
|
|
9617
10814
|
const cfg = entry.config;
|
|
9618
10815
|
const command = cfg.command.length > 0 ? cfg.command : [cfg.name];
|
|
9619
|
-
const logStream =
|
|
10816
|
+
const logStream = fs16.createWriteStream(
|
|
9620
10817
|
this.adapter.paths.logFile(cfg.name),
|
|
9621
10818
|
{ flags: "a" }
|
|
9622
10819
|
);
|
|
@@ -9672,7 +10869,7 @@ var ChildSupervisor = class {
|
|
|
9672
10869
|
}
|
|
9673
10870
|
if (typeof child.pid === "number") {
|
|
9674
10871
|
try {
|
|
9675
|
-
|
|
10872
|
+
fs16.writeFileSync(
|
|
9676
10873
|
this.adapter.paths.pidFile(cfg.name),
|
|
9677
10874
|
`${child.pid}
|
|
9678
10875
|
`,
|
|
@@ -9698,7 +10895,7 @@ var ChildSupervisor = class {
|
|
|
9698
10895
|
});
|
|
9699
10896
|
child.on("exit", (code, signal) => {
|
|
9700
10897
|
try {
|
|
9701
|
-
|
|
10898
|
+
fs16.unlinkSync(this.adapter.paths.pidFile(cfg.name));
|
|
9702
10899
|
} catch {
|
|
9703
10900
|
}
|
|
9704
10901
|
logStream.write(
|
|
@@ -9808,13 +11005,13 @@ var TRANSFORMER_ADAPTER = {
|
|
|
9808
11005
|
}
|
|
9809
11006
|
};
|
|
9810
11007
|
var TransformerManager = class extends ChildSupervisor {
|
|
9811
|
-
// Transformers that have completed transformer/initialize and are ready to
|
|
11008
|
+
// Transformers that have completed hydra-acp/transformer/initialize and are ready to
|
|
9812
11009
|
// participate in chains. Keyed by transformer name.
|
|
9813
11010
|
connected = /* @__PURE__ */ new Map();
|
|
9814
11011
|
constructor(transformers, context, options = {}) {
|
|
9815
11012
|
super(transformers, TRANSFORMER_ADAPTER, context, options);
|
|
9816
11013
|
}
|
|
9817
|
-
// Called by the WS handler after transformer/initialize completes. The
|
|
11014
|
+
// Called by the WS handler after hydra-acp/transformer/initialize completes. The
|
|
9818
11015
|
// transformer is now eligible to participate in session chains.
|
|
9819
11016
|
registerConnection(name, connection, intercepts) {
|
|
9820
11017
|
this.connected.set(name, {
|
|
@@ -10038,7 +11235,7 @@ function startAgentSyncScheduler(opts) {
|
|
|
10038
11235
|
|
|
10039
11236
|
// src/core/session-tokens.ts
|
|
10040
11237
|
import * as path12 from "path";
|
|
10041
|
-
import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
11238
|
+
import { createHash as createHash2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
10042
11239
|
var TOKEN_PREFIX = "hydra_session_";
|
|
10043
11240
|
var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
|
|
10044
11241
|
var ID_LENGTH = 12;
|
|
@@ -10048,7 +11245,7 @@ function tokensFilePath() {
|
|
|
10048
11245
|
return path12.join(paths.home(), "session-tokens.json");
|
|
10049
11246
|
}
|
|
10050
11247
|
function sha256Hex(input) {
|
|
10051
|
-
return
|
|
11248
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
10052
11249
|
}
|
|
10053
11250
|
function randomHex(bytes) {
|
|
10054
11251
|
return randomBytes2(bytes).toString("hex");
|
|
@@ -10064,20 +11261,27 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
10064
11261
|
// keyed by hash
|
|
10065
11262
|
writeTimer = null;
|
|
10066
11263
|
writeInflight = null;
|
|
10067
|
-
|
|
11264
|
+
// Bound at construction so a debounced write that fires after the
|
|
11265
|
+
// process's HYDRA_ACP_HOME has changed (e.g. between tests) targets the
|
|
11266
|
+
// path this store was loaded from rather than re-resolving paths.home()
|
|
11267
|
+
// and clobbering an unrelated home's token file.
|
|
11268
|
+
filePath;
|
|
11269
|
+
constructor(records, filePath) {
|
|
11270
|
+
this.filePath = filePath;
|
|
10068
11271
|
for (const r of records) {
|
|
10069
11272
|
this.records.set(r.hash, r);
|
|
10070
11273
|
}
|
|
10071
11274
|
}
|
|
10072
11275
|
static async load() {
|
|
10073
11276
|
let records = [];
|
|
11277
|
+
const filePath = tokensFilePath();
|
|
10074
11278
|
const parsed = await readJsonSafe(
|
|
10075
|
-
|
|
11279
|
+
filePath
|
|
10076
11280
|
);
|
|
10077
11281
|
if (parsed && Array.isArray(parsed.records)) {
|
|
10078
11282
|
records = parsed.records.filter(isRecord);
|
|
10079
11283
|
}
|
|
10080
|
-
const store = new _SessionTokenStore(records);
|
|
11284
|
+
const store = new _SessionTokenStore(records, filePath);
|
|
10081
11285
|
const removed = store.sweepExpired(/* @__PURE__ */ new Date());
|
|
10082
11286
|
if (removed > 0) {
|
|
10083
11287
|
await store.flush();
|
|
@@ -10187,21 +11391,29 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
10187
11391
|
});
|
|
10188
11392
|
}, WRITE_DEBOUNCE_MS);
|
|
10189
11393
|
}
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
11394
|
+
// Serialize writes by chaining onto whatever write is in flight. Two
|
|
11395
|
+
// concurrent persists (e.g. a debounced scheduleWrite timer firing while
|
|
11396
|
+
// flush() awaits) would otherwise both write a temp file and race their
|
|
11397
|
+
// renames onto the same path — the older snapshot could win. Chaining
|
|
11398
|
+
// guarantees writes run one after another, each snapshotting records at
|
|
11399
|
+
// the moment it actually runs, so the final on-disk state reflects the
|
|
11400
|
+
// latest records. The returned promise resolves once THIS persist's
|
|
11401
|
+
// write has completed.
|
|
11402
|
+
persist() {
|
|
11403
|
+
const run2 = (this.writeInflight ?? Promise.resolve()).catch(() => void 0).then(
|
|
11404
|
+
() => writeJsonAtomic(
|
|
11405
|
+
this.filePath,
|
|
11406
|
+
{ records: Array.from(this.records.values()) },
|
|
11407
|
+
{ mode: 384 }
|
|
11408
|
+
)
|
|
10199
11409
|
);
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
11410
|
+
this.writeInflight = run2;
|
|
11411
|
+
run2.catch(() => void 0).finally(() => {
|
|
11412
|
+
if (this.writeInflight === run2) {
|
|
11413
|
+
this.writeInflight = null;
|
|
11414
|
+
}
|
|
11415
|
+
});
|
|
11416
|
+
return run2;
|
|
10205
11417
|
}
|
|
10206
11418
|
};
|
|
10207
11419
|
function isRecord(value) {
|
|
@@ -10363,6 +11575,7 @@ var AuthRateLimiter = class {
|
|
|
10363
11575
|
|
|
10364
11576
|
// src/daemon/routes/sessions.ts
|
|
10365
11577
|
import * as os3 from "os";
|
|
11578
|
+
import * as path13 from "path";
|
|
10366
11579
|
|
|
10367
11580
|
// src/core/render-update.ts
|
|
10368
11581
|
import stripAnsi from "strip-ansi";
|
|
@@ -10412,10 +11625,52 @@ function mapUpdate(update) {
|
|
|
10412
11625
|
return mapAvailableModes(u);
|
|
10413
11626
|
case "session_info_update":
|
|
10414
11627
|
return mapSessionInfo(u);
|
|
11628
|
+
case "config_option_update":
|
|
11629
|
+
return mapConfigOptions(u);
|
|
10415
11630
|
default:
|
|
10416
11631
|
return { kind: "unknown", sessionUpdate: tag, raw: update };
|
|
10417
11632
|
}
|
|
10418
11633
|
}
|
|
11634
|
+
function mapConfigOptions(u) {
|
|
11635
|
+
const list = u.configOptions;
|
|
11636
|
+
if (!Array.isArray(list)) {
|
|
11637
|
+
return null;
|
|
11638
|
+
}
|
|
11639
|
+
const options = [];
|
|
11640
|
+
for (const raw of list) {
|
|
11641
|
+
if (!raw || typeof raw !== "object") {
|
|
11642
|
+
continue;
|
|
11643
|
+
}
|
|
11644
|
+
const o = raw;
|
|
11645
|
+
if (typeof o.id !== "string" || typeof o.currentValue !== "string" || !Array.isArray(o.options)) {
|
|
11646
|
+
continue;
|
|
11647
|
+
}
|
|
11648
|
+
const values = [];
|
|
11649
|
+
for (const v of o.options) {
|
|
11650
|
+
if (!v || typeof v !== "object") {
|
|
11651
|
+
continue;
|
|
11652
|
+
}
|
|
11653
|
+
const vv = v;
|
|
11654
|
+
if (typeof vv.value !== "string") {
|
|
11655
|
+
continue;
|
|
11656
|
+
}
|
|
11657
|
+
values.push({
|
|
11658
|
+
value: vv.value,
|
|
11659
|
+
name: typeof vv.name === "string" ? vv.name : vv.value,
|
|
11660
|
+
...typeof vv.description === "string" ? { description: vv.description } : {}
|
|
11661
|
+
});
|
|
11662
|
+
}
|
|
11663
|
+
options.push({
|
|
11664
|
+
id: o.id,
|
|
11665
|
+
name: typeof o.name === "string" ? o.name : o.id,
|
|
11666
|
+
type: "select",
|
|
11667
|
+
currentValue: o.currentValue,
|
|
11668
|
+
options: values,
|
|
11669
|
+
...typeof o.category === "string" ? { category: o.category } : {}
|
|
11670
|
+
});
|
|
11671
|
+
}
|
|
11672
|
+
return { kind: "config-options", options };
|
|
11673
|
+
}
|
|
10419
11674
|
function mapSessionInfo(u) {
|
|
10420
11675
|
const rawTitle = readString(u, "title");
|
|
10421
11676
|
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
@@ -10557,6 +11812,23 @@ function isExitPlanModeTool(name) {
|
|
|
10557
11812
|
const normalised = name.toLowerCase().replace(/[_\s-]/g, "");
|
|
10558
11813
|
return normalised === "exitplanmode";
|
|
10559
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
|
+
}
|
|
10560
11832
|
function extractEditDiff(u) {
|
|
10561
11833
|
const content = u.content;
|
|
10562
11834
|
if (Array.isArray(content)) {
|
|
@@ -10568,16 +11840,18 @@ function extractEditDiff(u) {
|
|
|
10568
11840
|
if (b.type !== "diff") {
|
|
10569
11841
|
continue;
|
|
10570
11842
|
}
|
|
10571
|
-
const
|
|
10572
|
-
const
|
|
10573
|
-
if (
|
|
11843
|
+
const oldField = readDiffField(b.oldText);
|
|
11844
|
+
const newField = readDiffField(b.newText);
|
|
11845
|
+
if (oldField === void 0 && newField === void 0) {
|
|
10574
11846
|
continue;
|
|
10575
11847
|
}
|
|
10576
11848
|
const path15 = typeof b.path === "string" ? b.path : void 0;
|
|
10577
11849
|
return {
|
|
10578
11850
|
...path15 !== void 0 ? { path: path15 } : {},
|
|
10579
|
-
oldText:
|
|
10580
|
-
newText:
|
|
11851
|
+
oldText: oldField?.text ?? "",
|
|
11852
|
+
newText: newField?.text ?? "",
|
|
11853
|
+
...oldField?.ref ? { oldRef: oldField.ref } : {},
|
|
11854
|
+
...newField?.ref ? { newRef: newField.ref } : {}
|
|
10581
11855
|
};
|
|
10582
11856
|
}
|
|
10583
11857
|
}
|
|
@@ -10645,8 +11919,36 @@ function mapToolCall(u) {
|
|
|
10645
11919
|
if (diff !== null) {
|
|
10646
11920
|
event.editDiff = diff;
|
|
10647
11921
|
}
|
|
11922
|
+
const detail = extractToolDetail(u);
|
|
11923
|
+
if (detail !== void 0) {
|
|
11924
|
+
event.detail = detail;
|
|
11925
|
+
}
|
|
10648
11926
|
return event;
|
|
10649
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
|
+
}
|
|
10650
11952
|
function mapToolCallUpdate(u) {
|
|
10651
11953
|
const toolCallId = readString(u, "toolCallId") ?? readString(u, "id");
|
|
10652
11954
|
if (!toolCallId) {
|
|
@@ -10656,7 +11958,8 @@ function mapToolCallUpdate(u) {
|
|
|
10656
11958
|
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
10657
11959
|
const status = readString(u, "status");
|
|
10658
11960
|
const diff = extractEditDiff(u);
|
|
10659
|
-
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";
|
|
10660
11963
|
if (!meaningful) {
|
|
10661
11964
|
return null;
|
|
10662
11965
|
}
|
|
@@ -10676,6 +11979,9 @@ function mapToolCallUpdate(u) {
|
|
|
10676
11979
|
if (title !== void 0) {
|
|
10677
11980
|
event.title = title;
|
|
10678
11981
|
}
|
|
11982
|
+
if (detail !== void 0) {
|
|
11983
|
+
event.detail = detail;
|
|
11984
|
+
}
|
|
10679
11985
|
if (status !== void 0) {
|
|
10680
11986
|
event.status = status;
|
|
10681
11987
|
}
|
|
@@ -11518,6 +12824,53 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
11518
12824
|
}
|
|
11519
12825
|
reply.code(204).send();
|
|
11520
12826
|
});
|
|
12827
|
+
app.post("/v1/sessions/:id/stdin/open", async (request, reply) => {
|
|
12828
|
+
const raw = request.params.id;
|
|
12829
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
12830
|
+
const session = manager.get(id);
|
|
12831
|
+
if (!session) {
|
|
12832
|
+
reply.code(404).send({ error: "session not found" });
|
|
12833
|
+
return reply;
|
|
12834
|
+
}
|
|
12835
|
+
const body = request.body ?? {};
|
|
12836
|
+
const openOpts = {};
|
|
12837
|
+
if (body.mode === "memory" || body.mode === "file") {
|
|
12838
|
+
openOpts.mode = body.mode;
|
|
12839
|
+
}
|
|
12840
|
+
if (typeof body.capacityBytes === "number") {
|
|
12841
|
+
openOpts.capacityBytes = body.capacityBytes;
|
|
12842
|
+
}
|
|
12843
|
+
if (typeof body.fileCapBytes === "number") {
|
|
12844
|
+
openOpts.fileCapBytes = body.fileCapBytes;
|
|
12845
|
+
}
|
|
12846
|
+
if ((openOpts.mode ?? "memory") === "file") {
|
|
12847
|
+
openOpts.filePathFor = (sid) => path13.join(os3.tmpdir(), `hydra-acp-stdin-${sid}.log`);
|
|
12848
|
+
}
|
|
12849
|
+
try {
|
|
12850
|
+
return session.openStream(openOpts);
|
|
12851
|
+
} catch (err) {
|
|
12852
|
+
reply.code(409).send({ error: err.message });
|
|
12853
|
+
return reply;
|
|
12854
|
+
}
|
|
12855
|
+
});
|
|
12856
|
+
app.post("/v1/sessions/:id/stdin", async (request, reply) => {
|
|
12857
|
+
const raw = request.params.id;
|
|
12858
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
12859
|
+
const session = manager.get(id);
|
|
12860
|
+
if (!session) {
|
|
12861
|
+
reply.code(404).send({ error: "session not found" });
|
|
12862
|
+
return reply;
|
|
12863
|
+
}
|
|
12864
|
+
const body = request.body ?? {};
|
|
12865
|
+
const chunk = typeof body.chunk === "string" ? body.chunk : "";
|
|
12866
|
+
const eof = body.eof === true;
|
|
12867
|
+
try {
|
|
12868
|
+
return session.streamWrite(chunk, eof);
|
|
12869
|
+
} catch (err) {
|
|
12870
|
+
reply.code(409).send({ error: err.message });
|
|
12871
|
+
return reply;
|
|
12872
|
+
}
|
|
12873
|
+
});
|
|
11521
12874
|
app.patch("/v1/sessions/:id", async (request, reply) => {
|
|
11522
12875
|
const raw = request.params.id;
|
|
11523
12876
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -11562,15 +12915,21 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
11562
12915
|
app.get("/v1/sessions/:id/export", async (request, reply) => {
|
|
11563
12916
|
const raw = request.params.id;
|
|
11564
12917
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
11565
|
-
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
|
+
);
|
|
11566
12924
|
if (!exported) {
|
|
11567
12925
|
reply.code(404).send({ error: "session not found" });
|
|
11568
12926
|
return;
|
|
11569
12927
|
}
|
|
11570
12928
|
const bundle = encodeBundle({
|
|
11571
12929
|
record: exported.record,
|
|
11572
|
-
history: exported.history,
|
|
12930
|
+
history: toolMode === "summary" ? applyToolContentMode(exported.history, "summary") : exported.history,
|
|
11573
12931
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
12932
|
+
...exported.toolBlobs !== void 0 ? { toolBlobs: exported.toolBlobs } : {},
|
|
11574
12933
|
hydraVersion: HYDRA_VERSION,
|
|
11575
12934
|
machine: os3.hostname(),
|
|
11576
12935
|
hydraHost: resolveHydraHost(defaults)
|
|
@@ -11582,6 +12941,17 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
11582
12941
|
);
|
|
11583
12942
|
reply.code(200).send(bundle);
|
|
11584
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
|
+
});
|
|
11585
12955
|
app.get("/v1/sessions/:id/transcript", async (request, reply) => {
|
|
11586
12956
|
const raw = request.params.id;
|
|
11587
12957
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -11754,22 +13124,7 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
11754
13124
|
// src/daemon/routes/agents.ts
|
|
11755
13125
|
function registerAgentRoutes(app, registry, manager, opts = {}) {
|
|
11756
13126
|
app.get("/v1/agents", async () => {
|
|
11757
|
-
|
|
11758
|
-
const agents = await Promise.all(
|
|
11759
|
-
doc.agents.map(async (a) => ({
|
|
11760
|
-
id: a.id,
|
|
11761
|
-
name: a.name,
|
|
11762
|
-
version: a.version,
|
|
11763
|
-
description: a.description,
|
|
11764
|
-
distributions: Object.keys(a.distribution),
|
|
11765
|
-
installed: await agentInstallState(a)
|
|
11766
|
-
}))
|
|
11767
|
-
);
|
|
11768
|
-
return {
|
|
11769
|
-
version: doc.version,
|
|
11770
|
-
fetchedAt: registry.lastFetchedAt(),
|
|
11771
|
-
agents
|
|
11772
|
-
};
|
|
13127
|
+
return listAgents(registry);
|
|
11773
13128
|
});
|
|
11774
13129
|
app.get("/v1/registry", async () => {
|
|
11775
13130
|
return registry.load();
|
|
@@ -12078,22 +13433,22 @@ function registerConfigRoutes(app, snapshot) {
|
|
|
12078
13433
|
}
|
|
12079
13434
|
|
|
12080
13435
|
// src/daemon/routes/auth.ts
|
|
12081
|
-
import { z as
|
|
13436
|
+
import { z as z8 } from "zod";
|
|
12082
13437
|
|
|
12083
13438
|
// src/core/password.ts
|
|
12084
|
-
import * as
|
|
12085
|
-
import * as
|
|
13439
|
+
import * as fs17 from "fs/promises";
|
|
13440
|
+
import * as path14 from "path";
|
|
12086
13441
|
import { randomBytes as randomBytes3, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
12087
|
-
import { promisify } from "util";
|
|
12088
|
-
var scryptAsync =
|
|
13442
|
+
import { promisify as promisify2 } from "util";
|
|
13443
|
+
var scryptAsync = promisify2(scrypt);
|
|
12089
13444
|
function passwordHashPath() {
|
|
12090
|
-
return
|
|
13445
|
+
return path14.join(paths.home(), "password-hash");
|
|
12091
13446
|
}
|
|
12092
13447
|
var DEFAULT_N = 1 << 15;
|
|
12093
13448
|
var MAX_MEM = 128 * 1024 * 1024;
|
|
12094
13449
|
async function hasPassword() {
|
|
12095
13450
|
try {
|
|
12096
|
-
const text = await
|
|
13451
|
+
const text = await fs17.readFile(passwordHashPath(), "utf8");
|
|
12097
13452
|
return text.trim().length > 0;
|
|
12098
13453
|
} catch (err) {
|
|
12099
13454
|
const e = err;
|
|
@@ -12109,7 +13464,7 @@ async function verifyPassword(plaintext) {
|
|
|
12109
13464
|
}
|
|
12110
13465
|
let line;
|
|
12111
13466
|
try {
|
|
12112
|
-
line = (await
|
|
13467
|
+
line = (await fs17.readFile(passwordHashPath(), "utf8")).trim();
|
|
12113
13468
|
} catch (err) {
|
|
12114
13469
|
const e = err;
|
|
12115
13470
|
if (e.code === "ENOENT") {
|
|
@@ -12145,13 +13500,13 @@ async function verifyPassword(plaintext) {
|
|
|
12145
13500
|
}
|
|
12146
13501
|
|
|
12147
13502
|
// src/daemon/routes/auth.ts
|
|
12148
|
-
var LoginBody =
|
|
12149
|
-
password:
|
|
12150
|
-
label:
|
|
12151
|
-
ttlSec:
|
|
13503
|
+
var LoginBody = z8.object({
|
|
13504
|
+
password: z8.string().min(1),
|
|
13505
|
+
label: z8.string().min(1).max(256).optional(),
|
|
13506
|
+
ttlSec: z8.number().int().positive().optional()
|
|
12152
13507
|
});
|
|
12153
|
-
var LogoutBody =
|
|
12154
|
-
id:
|
|
13508
|
+
var LogoutBody = z8.object({
|
|
13509
|
+
id: z8.string().optional()
|
|
12155
13510
|
}).optional();
|
|
12156
13511
|
function registerAuthRoutes(app, deps) {
|
|
12157
13512
|
app.post(
|
|
@@ -12303,8 +13658,6 @@ function wsToMessageStream(ws) {
|
|
|
12303
13658
|
}
|
|
12304
13659
|
|
|
12305
13660
|
// src/daemon/acp-ws.ts
|
|
12306
|
-
import * as os4 from "os";
|
|
12307
|
-
import * as path14 from "path";
|
|
12308
13661
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
12309
13662
|
function registerAcpWsEndpoint(app, deps) {
|
|
12310
13663
|
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
@@ -12361,7 +13714,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12361
13714
|
});
|
|
12362
13715
|
if (processIdentity && deps.extensionCommands) {
|
|
12363
13716
|
const registry = deps.extensionCommands;
|
|
12364
|
-
connection.onRequest("hydra-acp/
|
|
13717
|
+
connection.onRequest("hydra-acp/commands/register", async (raw) => {
|
|
12365
13718
|
const params = raw ?? {};
|
|
12366
13719
|
const commands = Array.isArray(params.commands) ? params.commands.map((c) => {
|
|
12367
13720
|
if (!c || typeof c !== "object") {
|
|
@@ -12389,7 +13742,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12389
13742
|
}
|
|
12390
13743
|
if (processIdentity && deps.extensionMcp) {
|
|
12391
13744
|
const mcpRegistry = deps.extensionMcp;
|
|
12392
|
-
connection.onRequest("hydra-acp/
|
|
13745
|
+
connection.onRequest("hydra-acp/mcp_tools/register", async (raw) => {
|
|
12393
13746
|
const params = raw ?? {};
|
|
12394
13747
|
const instructions = typeof params.instructions === "string" ? params.instructions : void 0;
|
|
12395
13748
|
const tools = Array.isArray(params.tools) ? params.tools.map((t) => {
|
|
@@ -12432,7 +13785,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12432
13785
|
});
|
|
12433
13786
|
}
|
|
12434
13787
|
if (processIdentity?.kind === "transformer") {
|
|
12435
|
-
connection.onRequest("transformer/initialize", async (raw) => {
|
|
13788
|
+
connection.onRequest("hydra-acp/transformer/initialize", async (raw) => {
|
|
12436
13789
|
const params = raw ?? {};
|
|
12437
13790
|
const intercepts = Array.isArray(params.intercepts) ? params.intercepts.filter(
|
|
12438
13791
|
(v) => typeof v === "string"
|
|
@@ -12457,7 +13810,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12457
13810
|
connection.onClose(() => {
|
|
12458
13811
|
deps.transformers?.deregisterConnection(processIdentity.name);
|
|
12459
13812
|
});
|
|
12460
|
-
connection.onRequest("hydra-acp/
|
|
13813
|
+
connection.onRequest("hydra-acp/message/emit", async (raw) => {
|
|
12461
13814
|
const params = raw ?? {};
|
|
12462
13815
|
const sessionId = typeof params.sessionId === "string" ? params.sessionId : void 0;
|
|
12463
13816
|
const method = typeof params.method === "string" ? params.method : void 0;
|
|
@@ -12485,7 +13838,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12485
13838
|
}
|
|
12486
13839
|
throw Object.assign(new Error(`unsupported route: ${JSON.stringify(route)}`), { code: -32602 });
|
|
12487
13840
|
});
|
|
12488
|
-
connection.onRequest("hydra-acp/
|
|
13841
|
+
connection.onRequest("hydra-acp/child_session/spawn", async (raw) => {
|
|
12489
13842
|
const params = raw ?? {};
|
|
12490
13843
|
const agentId = typeof params.agentId === "string" ? params.agentId : deps.defaultAgent;
|
|
12491
13844
|
const cwd = typeof params.cwd === "string" ? params.cwd : void 0;
|
|
@@ -12502,7 +13855,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12502
13855
|
});
|
|
12503
13856
|
return { childSessionId: child.sessionId };
|
|
12504
13857
|
});
|
|
12505
|
-
connection.onRequest("hydra-acp/
|
|
13858
|
+
connection.onRequest("hydra-acp/session/fork", async (raw) => {
|
|
12506
13859
|
const params = raw ?? {};
|
|
12507
13860
|
if (typeof params.sessionId !== "string") {
|
|
12508
13861
|
throw Object.assign(
|
|
@@ -12519,7 +13872,30 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12519
13872
|
...agentId !== void 0 ? { agentId } : {}
|
|
12520
13873
|
});
|
|
12521
13874
|
});
|
|
12522
|
-
connection.onRequest("hydra-acp/
|
|
13875
|
+
connection.onRequest("hydra-acp/session/delete", async (raw) => {
|
|
13876
|
+
const params = raw ?? {};
|
|
13877
|
+
if (typeof params.sessionId !== "string") {
|
|
13878
|
+
throw Object.assign(
|
|
13879
|
+
new Error("hydra-acp/session/delete requires sessionId"),
|
|
13880
|
+
{ code: JsonRpcErrorCodes.InvalidParams }
|
|
13881
|
+
);
|
|
13882
|
+
}
|
|
13883
|
+
const id = await deps.manager.resolveCanonicalId(params.sessionId) ?? params.sessionId;
|
|
13884
|
+
const live = deps.manager.get(id);
|
|
13885
|
+
if (live) {
|
|
13886
|
+
await live.close({ deleteRecord: true });
|
|
13887
|
+
return { deleted: true, sessionId: id };
|
|
13888
|
+
}
|
|
13889
|
+
const removed = await deps.manager.deleteRecord(id);
|
|
13890
|
+
if (!removed) {
|
|
13891
|
+
throw Object.assign(
|
|
13892
|
+
new Error(`session ${id} not found`),
|
|
13893
|
+
{ code: JsonRpcErrorCodes.SessionNotFound }
|
|
13894
|
+
);
|
|
13895
|
+
}
|
|
13896
|
+
return { deleted: true, sessionId: id };
|
|
13897
|
+
});
|
|
13898
|
+
connection.onRequest("hydra-acp/child_session/await", async (raw) => {
|
|
12523
13899
|
const params = raw ?? {};
|
|
12524
13900
|
const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
|
|
12525
13901
|
const until = params.until === "idle" ? "idle" : "turn_complete";
|
|
@@ -12558,7 +13934,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12558
13934
|
child.onClose(() => finish());
|
|
12559
13935
|
});
|
|
12560
13936
|
});
|
|
12561
|
-
connection.onRequest("hydra-acp/
|
|
13937
|
+
connection.onRequest("hydra-acp/child_session/close", async (raw) => {
|
|
12562
13938
|
const params = raw ?? {};
|
|
12563
13939
|
const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
|
|
12564
13940
|
if (!childSessionId) {
|
|
@@ -12570,7 +13946,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12570
13946
|
}
|
|
12571
13947
|
return { ok: true };
|
|
12572
13948
|
});
|
|
12573
|
-
connection.onRequest("hydra-acp/keep_alive", async (raw) => {
|
|
13949
|
+
connection.onRequest("hydra-acp/connection/keep_alive", async (raw) => {
|
|
12574
13950
|
const params = raw ?? {};
|
|
12575
13951
|
const token2 = typeof params.token === "string" ? params.token : void 0;
|
|
12576
13952
|
const sessionId = typeof params.sessionId === "string" ? params.sessionId : void 0;
|
|
@@ -12582,6 +13958,24 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12582
13958
|
return { ok: true };
|
|
12583
13959
|
});
|
|
12584
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
|
+
});
|
|
12585
13979
|
connection.onRequest("session/new", async (raw) => {
|
|
12586
13980
|
const params = SessionNewParams.parse(raw);
|
|
12587
13981
|
const hydraMeta = extractHydraMeta(
|
|
@@ -12632,9 +14026,9 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12632
14026
|
try {
|
|
12633
14027
|
session = await deps.manager.create({
|
|
12634
14028
|
cwd: params.cwd,
|
|
12635
|
-
agentId:
|
|
14029
|
+
agentId: hydraMeta.agentId ?? deps.defaultAgent,
|
|
12636
14030
|
mcpServers: augmentedMcpServers,
|
|
12637
|
-
title: hydraMeta.
|
|
14031
|
+
title: hydraMeta.title,
|
|
12638
14032
|
agentArgs: hydraMeta.agentArgs,
|
|
12639
14033
|
model: hydraMeta.model,
|
|
12640
14034
|
onInstallProgress: makeInstallProgressForwarder(connection),
|
|
@@ -12685,14 +14079,16 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12685
14079
|
const modelsPayload = buildModelsPayload(session);
|
|
12686
14080
|
return {
|
|
12687
14081
|
sessionId: session.sessionId,
|
|
12688
|
-
// session/new is implicitly an attach; mirror session/attach's
|
|
12689
|
-
// shape by including the clientId so deferred-echo clients
|
|
12690
|
-
// (TUI's queue work) can recognize their own prompt_queue_added
|
|
12691
|
-
// events without an extra round-trip.
|
|
12692
|
-
clientId: client.clientId,
|
|
12693
14082
|
...modesPayload ? { modes: modesPayload } : {},
|
|
12694
14083
|
...modelsPayload ? { models: modelsPayload } : {},
|
|
12695
|
-
|
|
14084
|
+
configOptions: session.buildConfigOptions(),
|
|
14085
|
+
// session/new is a core ACP spec method, so the per-attachment
|
|
14086
|
+
// clientId rides under _meta["hydra-acp"] rather than top-level.
|
|
14087
|
+
// Deferred-echo clients (TUI's queue work) read it from there to
|
|
14088
|
+
// recognize their own prompt_queue_added events.
|
|
14089
|
+
_meta: buildResponseMeta(deps.manager, session, {
|
|
14090
|
+
clientId: client.clientId
|
|
14091
|
+
})
|
|
12696
14092
|
};
|
|
12697
14093
|
});
|
|
12698
14094
|
connection.onRequest("session/attach", async (raw) => {
|
|
@@ -12705,8 +14101,9 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12705
14101
|
deps.onTransformerVersion?.(processIdentity.name, attachVersion);
|
|
12706
14102
|
}
|
|
12707
14103
|
}
|
|
12708
|
-
const
|
|
12709
|
-
const
|
|
14104
|
+
const hydraAttach = extractHydraMeta(params._meta);
|
|
14105
|
+
const hydraHints = hydraAttach.resume;
|
|
14106
|
+
const readonly = hydraAttach.readonly === true;
|
|
12710
14107
|
app.log.info(
|
|
12711
14108
|
`session/attach sessionId=${params.sessionId} hasResumeHints=${!!hydraHints} readonly=${readonly}`
|
|
12712
14109
|
);
|
|
@@ -12781,11 +14178,17 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12781
14178
|
params.clientInfo,
|
|
12782
14179
|
params.clientId
|
|
12783
14180
|
);
|
|
12784
|
-
const drip =
|
|
14181
|
+
const drip = hydraAttach.replayMode === "drip";
|
|
12785
14182
|
const { entries: replay, appliedPolicy } = await session.attach(
|
|
12786
14183
|
client,
|
|
12787
14184
|
params.historyPolicy,
|
|
12788
|
-
{
|
|
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
|
+
}
|
|
12789
14192
|
);
|
|
12790
14193
|
state.attached.set(session.sessionId, {
|
|
12791
14194
|
sessionId: session.sessionId,
|
|
@@ -12796,7 +14199,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12796
14199
|
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length} readonly=${readonly}${drip ? " replayMode=drip" : ""}`
|
|
12797
14200
|
);
|
|
12798
14201
|
if (drip) {
|
|
12799
|
-
const speed =
|
|
14202
|
+
const speed = hydraAttach.dripSpeed && hydraAttach.dripSpeed > 0 ? hydraAttach.dripSpeed : 1;
|
|
12800
14203
|
const MAX_GAP_MS = 750;
|
|
12801
14204
|
void (async () => {
|
|
12802
14205
|
let prev = null;
|
|
@@ -12819,8 +14222,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12819
14222
|
}
|
|
12820
14223
|
})();
|
|
12821
14224
|
} else {
|
|
12822
|
-
|
|
12823
|
-
|
|
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
|
+
}
|
|
12824
14232
|
}
|
|
12825
14233
|
}
|
|
12826
14234
|
session.replayPendingPermissions(client);
|
|
@@ -12838,7 +14246,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12838
14246
|
replayed: replay.length,
|
|
12839
14247
|
...modesPayload ? { modes: modesPayload } : {},
|
|
12840
14248
|
...modelsPayload ? { models: modelsPayload } : {},
|
|
12841
|
-
|
|
14249
|
+
configOptions: session.buildConfigOptions(),
|
|
14250
|
+
_meta: buildResponseMeta(deps.manager, session)
|
|
12842
14251
|
};
|
|
12843
14252
|
});
|
|
12844
14253
|
connection.onRequest("session/detach", async (raw) => {
|
|
@@ -12855,7 +14264,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12855
14264
|
if (session) {
|
|
12856
14265
|
void deps.manager.reapIfOrphanedNonInteractive(params.sessionId);
|
|
12857
14266
|
}
|
|
12858
|
-
return {
|
|
14267
|
+
return {
|
|
14268
|
+
sessionId: params.sessionId,
|
|
14269
|
+
_meta: { [HYDRA_META_KEY]: { detachStatus: "detached" } }
|
|
14270
|
+
};
|
|
12859
14271
|
});
|
|
12860
14272
|
connection.onRequest("session/list", async (raw) => {
|
|
12861
14273
|
const params = SessionListParams.parse(raw ?? {});
|
|
@@ -12868,6 +14280,14 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12868
14280
|
};
|
|
12869
14281
|
return result;
|
|
12870
14282
|
});
|
|
14283
|
+
connection.onRequest("hydra-acp/agents/list", async () => {
|
|
14284
|
+
if (!deps.registry) {
|
|
14285
|
+
const err = new Error("agent registry unavailable");
|
|
14286
|
+
err.code = JsonRpcErrorCodes.InternalError;
|
|
14287
|
+
throw err;
|
|
14288
|
+
}
|
|
14289
|
+
return listAgents(deps.registry);
|
|
14290
|
+
});
|
|
12871
14291
|
connection.onRequest("session/prompt", async (raw) => {
|
|
12872
14292
|
const params = SessionPromptParams.parse(raw);
|
|
12873
14293
|
denyIfReadonly(params.sessionId, "session/prompt");
|
|
@@ -12943,9 +14363,9 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12943
14363
|
handleCancelParams(raw);
|
|
12944
14364
|
return null;
|
|
12945
14365
|
});
|
|
12946
|
-
connection.onRequest("hydra-acp/
|
|
14366
|
+
connection.onRequest("hydra-acp/prompt/cancel", async (raw) => {
|
|
12947
14367
|
const params = CancelPromptParams.parse(raw);
|
|
12948
|
-
denyIfReadonly(params.sessionId, "hydra-acp/
|
|
14368
|
+
denyIfReadonly(params.sessionId, "hydra-acp/prompt/cancel");
|
|
12949
14369
|
const session = deps.manager.get(params.sessionId);
|
|
12950
14370
|
if (!session) {
|
|
12951
14371
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
@@ -12954,78 +14374,44 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12954
14374
|
}
|
|
12955
14375
|
return session.cancelQueuedPrompt(params.messageId);
|
|
12956
14376
|
});
|
|
12957
|
-
connection.onRequest("hydra-acp/
|
|
12958
|
-
const params =
|
|
12959
|
-
denyIfReadonly(params.sessionId, "hydra-acp/
|
|
12960
|
-
const session = deps.manager.get(params.sessionId);
|
|
12961
|
-
if (!session) {
|
|
12962
|
-
const err = new Error(`session ${params.sessionId} not found`);
|
|
12963
|
-
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12964
|
-
throw err;
|
|
12965
|
-
}
|
|
12966
|
-
return session.updateQueuedPrompt(params.messageId, params.prompt);
|
|
12967
|
-
});
|
|
12968
|
-
connection.onRequest("hydra-acp/amend_prompt", async (raw) => {
|
|
12969
|
-
const params = AmendPromptParams.parse(raw);
|
|
12970
|
-
denyIfReadonly(params.sessionId, "hydra-acp/amend_prompt");
|
|
12971
|
-
const att = state.attached.get(params.sessionId);
|
|
12972
|
-
if (!att) {
|
|
12973
|
-
const err = new Error("not attached to session");
|
|
12974
|
-
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12975
|
-
throw err;
|
|
12976
|
-
}
|
|
14377
|
+
connection.onRequest("hydra-acp/session/force_cancel", async (raw) => {
|
|
14378
|
+
const params = SessionCancelParams.parse(raw);
|
|
14379
|
+
denyIfReadonly(params.sessionId, "hydra-acp/session/force_cancel");
|
|
12977
14380
|
const session = deps.manager.get(params.sessionId);
|
|
12978
14381
|
if (!session) {
|
|
12979
14382
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
12980
14383
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12981
14384
|
throw err;
|
|
12982
14385
|
}
|
|
12983
|
-
return session.
|
|
14386
|
+
return session.forceCancel();
|
|
12984
14387
|
});
|
|
12985
|
-
connection.onRequest("hydra-acp/
|
|
12986
|
-
const params =
|
|
12987
|
-
denyIfReadonly(params.sessionId, "hydra-acp/
|
|
14388
|
+
connection.onRequest("hydra-acp/prompt/update", async (raw) => {
|
|
14389
|
+
const params = UpdatePromptParams.parse(raw);
|
|
14390
|
+
denyIfReadonly(params.sessionId, "hydra-acp/prompt/update");
|
|
12988
14391
|
const session = deps.manager.get(params.sessionId);
|
|
12989
14392
|
if (!session) {
|
|
12990
14393
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
12991
14394
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12992
14395
|
throw err;
|
|
12993
14396
|
}
|
|
12994
|
-
|
|
12995
|
-
if (params.mode !== void 0) {
|
|
12996
|
-
openOpts.mode = params.mode;
|
|
12997
|
-
}
|
|
12998
|
-
if (params.capacityBytes !== void 0) {
|
|
12999
|
-
openOpts.capacityBytes = params.capacityBytes;
|
|
13000
|
-
}
|
|
13001
|
-
if (params.fileCapBytes !== void 0) {
|
|
13002
|
-
openOpts.fileCapBytes = params.fileCapBytes;
|
|
13003
|
-
}
|
|
13004
|
-
if ((params.mode ?? "memory") === "file") {
|
|
13005
|
-
openOpts.filePathFor = (sid) => path14.join(os4.tmpdir(), `hydra-acp-stdin-${sid}.log`);
|
|
13006
|
-
}
|
|
13007
|
-
return session.openStream(openOpts);
|
|
14397
|
+
return session.updateQueuedPrompt(params.messageId, params.prompt);
|
|
13008
14398
|
});
|
|
13009
|
-
connection.onRequest("hydra-acp/
|
|
13010
|
-
const params =
|
|
13011
|
-
denyIfReadonly(params.sessionId, "hydra-acp/
|
|
13012
|
-
const
|
|
13013
|
-
if (!
|
|
13014
|
-
const err = new Error(
|
|
14399
|
+
connection.onRequest("hydra-acp/prompt/amend", async (raw) => {
|
|
14400
|
+
const params = AmendPromptParams.parse(raw);
|
|
14401
|
+
denyIfReadonly(params.sessionId, "hydra-acp/prompt/amend");
|
|
14402
|
+
const att = state.attached.get(params.sessionId);
|
|
14403
|
+
if (!att) {
|
|
14404
|
+
const err = new Error("not attached to session");
|
|
13015
14405
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
13016
14406
|
throw err;
|
|
13017
14407
|
}
|
|
13018
|
-
return session.streamWrite(params.chunk, params.eof);
|
|
13019
|
-
});
|
|
13020
|
-
connection.onRequest("hydra-acp/stream_read", async (raw) => {
|
|
13021
|
-
const params = StreamReadParams.parse(raw);
|
|
13022
14408
|
const session = deps.manager.get(params.sessionId);
|
|
13023
14409
|
if (!session) {
|
|
13024
14410
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
13025
14411
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
13026
14412
|
throw err;
|
|
13027
14413
|
}
|
|
13028
|
-
return session.
|
|
14414
|
+
return session.amendPrompt(att.clientId, params);
|
|
13029
14415
|
});
|
|
13030
14416
|
connection.onRequest("session/load", async (raw) => {
|
|
13031
14417
|
const rawObj = raw ?? {};
|
|
@@ -13064,12 +14450,15 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
13064
14450
|
const modelsPayload = buildModelsPayload(session);
|
|
13065
14451
|
return {
|
|
13066
14452
|
sessionId: session.sessionId,
|
|
13067
|
-
// Same as session/new: include clientId so the deferred-echo
|
|
13068
|
-
// path in queue-aware clients can recognize own broadcasts.
|
|
13069
|
-
clientId: client.clientId,
|
|
13070
14453
|
...modesPayload ? { modes: modesPayload } : {},
|
|
13071
14454
|
...modelsPayload ? { models: modelsPayload } : {},
|
|
13072
|
-
|
|
14455
|
+
configOptions: session.buildConfigOptions(),
|
|
14456
|
+
// session/load is a core ACP spec method: clientId rides under
|
|
14457
|
+
// _meta["hydra-acp"] (not top-level), same as session/new. Lets
|
|
14458
|
+
// deferred-echo clients recognize their own broadcasts.
|
|
14459
|
+
_meta: buildResponseMeta(deps.manager, session, {
|
|
14460
|
+
clientId: client.clientId
|
|
14461
|
+
})
|
|
13073
14462
|
};
|
|
13074
14463
|
});
|
|
13075
14464
|
connection.onRequest("session/set_model", async (rawParams) => {
|
|
@@ -13096,8 +14485,11 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
13096
14485
|
return null;
|
|
13097
14486
|
}
|
|
13098
14487
|
app.log.info(decision.logMessage);
|
|
13099
|
-
const { modelId } =
|
|
13100
|
-
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
|
+
});
|
|
13101
14493
|
decision.session.applyModelChange(modelId);
|
|
13102
14494
|
return result;
|
|
13103
14495
|
});
|
|
@@ -13127,6 +14519,79 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
13127
14519
|
session.applyModeChange(params.modeId);
|
|
13128
14520
|
return result;
|
|
13129
14521
|
});
|
|
14522
|
+
connection.onRequest("session/set_config_option", async (rawParams) => {
|
|
14523
|
+
const params = rawParams;
|
|
14524
|
+
const invalid = (msg) => {
|
|
14525
|
+
const err = new Error(msg);
|
|
14526
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
14527
|
+
return err;
|
|
14528
|
+
};
|
|
14529
|
+
const sessionIdField = params?.sessionId;
|
|
14530
|
+
if (typeof sessionIdField === "string") {
|
|
14531
|
+
denyIfReadonly(sessionIdField, "session/set_config_option");
|
|
14532
|
+
}
|
|
14533
|
+
if (!params || typeof params.sessionId !== "string") {
|
|
14534
|
+
throw invalid("session/set_config_option requires string sessionId");
|
|
14535
|
+
}
|
|
14536
|
+
if (typeof params.configId !== "string") {
|
|
14537
|
+
throw invalid("session/set_config_option requires string configId");
|
|
14538
|
+
}
|
|
14539
|
+
if (typeof params.value !== "string") {
|
|
14540
|
+
throw invalid("session/set_config_option requires string value");
|
|
14541
|
+
}
|
|
14542
|
+
const session = deps.manager.get(params.sessionId);
|
|
14543
|
+
if (!session) {
|
|
14544
|
+
const err = new Error(
|
|
14545
|
+
`session ${params.sessionId} not found`
|
|
14546
|
+
);
|
|
14547
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
14548
|
+
throw err;
|
|
14549
|
+
}
|
|
14550
|
+
const option = session.buildConfigOptions().find((o) => o.id === params.configId);
|
|
14551
|
+
if (!option) {
|
|
14552
|
+
throw invalid(
|
|
14553
|
+
`unknown configId ${JSON.stringify(params.configId)} for this session`
|
|
14554
|
+
);
|
|
14555
|
+
}
|
|
14556
|
+
if (!option.options.some((v) => v.value === params.value)) {
|
|
14557
|
+
throw invalid(
|
|
14558
|
+
`value ${JSON.stringify(params.value)} is not valid for configId ${JSON.stringify(params.configId)}`
|
|
14559
|
+
);
|
|
14560
|
+
}
|
|
14561
|
+
switch (params.configId) {
|
|
14562
|
+
case "model": {
|
|
14563
|
+
if (params.value !== session.currentModel) {
|
|
14564
|
+
await session.forwardRequest("session/set_model", {
|
|
14565
|
+
sessionId: params.sessionId,
|
|
14566
|
+
modelId: params.value
|
|
14567
|
+
});
|
|
14568
|
+
}
|
|
14569
|
+
session.applyModelChange(params.value);
|
|
14570
|
+
break;
|
|
14571
|
+
}
|
|
14572
|
+
case "mode": {
|
|
14573
|
+
if (params.value !== session.currentMode) {
|
|
14574
|
+
await session.forwardRequest("session/set_mode", {
|
|
14575
|
+
sessionId: params.sessionId,
|
|
14576
|
+
modeId: params.value
|
|
14577
|
+
});
|
|
14578
|
+
}
|
|
14579
|
+
session.applyModeChange(params.value);
|
|
14580
|
+
break;
|
|
14581
|
+
}
|
|
14582
|
+
case "agent": {
|
|
14583
|
+
if (params.value !== session.agentId) {
|
|
14584
|
+
await session.setAgent(params.value);
|
|
14585
|
+
}
|
|
14586
|
+
break;
|
|
14587
|
+
}
|
|
14588
|
+
default:
|
|
14589
|
+
throw invalid(
|
|
14590
|
+
`configId ${JSON.stringify(params.configId)} is not settable`
|
|
14591
|
+
);
|
|
14592
|
+
}
|
|
14593
|
+
return { configOptions: session.buildConfigOptions() };
|
|
14594
|
+
});
|
|
13130
14595
|
connection.setDefaultHandler(async (rawParams, method) => {
|
|
13131
14596
|
if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
|
|
13132
14597
|
const err = new Error(`Method not found: ${method}`);
|
|
@@ -13246,114 +14711,101 @@ function decideSetModel(rawParams, manager) {
|
|
|
13246
14711
|
};
|
|
13247
14712
|
}
|
|
13248
14713
|
const advertised = session.availableModels();
|
|
13249
|
-
|
|
14714
|
+
const resolution = resolveModelId(params.modelId, advertised);
|
|
14715
|
+
if (resolution.kind === "none") {
|
|
13250
14716
|
return {
|
|
13251
14717
|
kind: "ok",
|
|
13252
14718
|
session,
|
|
14719
|
+
modelId: params.modelId,
|
|
13253
14720
|
logMessage: `session/set_model passthrough (no availableModels) sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
13254
14721
|
};
|
|
13255
14722
|
}
|
|
13256
|
-
|
|
13257
|
-
if (!match) {
|
|
13258
|
-
const known = advertised.map((m) => m.modelId).join(", ");
|
|
13259
|
-
if (session.currentModel !== void 0 && session.currentModel.length > 0) {
|
|
13260
|
-
return {
|
|
13261
|
-
kind: "no_op",
|
|
13262
|
-
session,
|
|
13263
|
-
sessionId: params.sessionId,
|
|
13264
|
-
currentModel: session.currentModel,
|
|
13265
|
-
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}]`
|
|
13266
|
-
};
|
|
13267
|
-
}
|
|
14723
|
+
if (resolution.kind === "exact") {
|
|
13268
14724
|
return {
|
|
13269
|
-
kind: "
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
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}]`
|
|
13273
14748
|
};
|
|
13274
14749
|
}
|
|
13275
14750
|
return {
|
|
13276
|
-
kind: "
|
|
13277
|
-
|
|
13278
|
-
|
|
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)`
|
|
13279
14755
|
};
|
|
13280
14756
|
}
|
|
13281
14757
|
function buildViewerResponseMeta(fromDisk) {
|
|
13282
|
-
const
|
|
14758
|
+
const entry = {
|
|
14759
|
+
sessionId: fromDisk.hydraSessionId,
|
|
13283
14760
|
upstreamSessionId: fromDisk.upstreamSessionId,
|
|
14761
|
+
cwd: fromDisk.cwd,
|
|
14762
|
+
title: fromDisk.title,
|
|
13284
14763
|
agentId: fromDisk.agentId,
|
|
13285
|
-
|
|
14764
|
+
currentModel: fromDisk.currentModel,
|
|
14765
|
+
currentUsage: fromDisk.currentUsage,
|
|
14766
|
+
forkedFromSessionId: fromDisk.forkedFromSessionId,
|
|
14767
|
+
forkedFromMessageId: fromDisk.forkedFromMessageId,
|
|
14768
|
+
originatingClient: fromDisk.originatingClient,
|
|
14769
|
+
interactive: fromDisk.interactive,
|
|
14770
|
+
updatedAt: fromDisk.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
14771
|
+
attachedClients: 0,
|
|
14772
|
+
status: "cold",
|
|
14773
|
+
busy: false,
|
|
14774
|
+
awaitingInput: false
|
|
13286
14775
|
};
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
}
|
|
13296
|
-
if (fromDisk.currentMode !== void 0) {
|
|
13297
|
-
ours.currentMode = fromDisk.currentMode;
|
|
13298
|
-
}
|
|
13299
|
-
if (fromDisk.currentUsage !== void 0) {
|
|
13300
|
-
ours.currentUsage = fromDisk.currentUsage;
|
|
13301
|
-
}
|
|
13302
|
-
if (fromDisk.agentCommands && fromDisk.agentCommands.length > 0) {
|
|
13303
|
-
ours.availableCommands = fromDisk.agentCommands;
|
|
13304
|
-
}
|
|
13305
|
-
if (fromDisk.agentModes && fromDisk.agentModes.length > 0) {
|
|
13306
|
-
ours.availableModes = fromDisk.agentModes;
|
|
13307
|
-
}
|
|
13308
|
-
if (fromDisk.agentModels && fromDisk.agentModels.length > 0) {
|
|
13309
|
-
ours.availableModels = fromDisk.agentModels;
|
|
13310
|
-
}
|
|
13311
|
-
return { [HYDRA_META_KEY]: ours };
|
|
14776
|
+
const extras = {
|
|
14777
|
+
currentMode: fromDisk.currentMode,
|
|
14778
|
+
agentArgs: fromDisk.agentArgs,
|
|
14779
|
+
availableCommands: fromDisk.agentCommands,
|
|
14780
|
+
availableModes: fromDisk.agentModes,
|
|
14781
|
+
availableModels: fromDisk.agentModels
|
|
14782
|
+
};
|
|
14783
|
+
return { [HYDRA_META_KEY]: buildHydraSessionMeta(entry, extras) };
|
|
13312
14784
|
}
|
|
13313
|
-
function buildResponseMeta(session) {
|
|
13314
|
-
const
|
|
13315
|
-
|
|
13316
|
-
|
|
13317
|
-
|
|
14785
|
+
function buildResponseMeta(manager, session, opts = {}) {
|
|
14786
|
+
const entry = manager.liveListEntry(session);
|
|
14787
|
+
const extras = {
|
|
14788
|
+
clientId: opts.clientId,
|
|
14789
|
+
currentMode: session.currentMode,
|
|
14790
|
+
agentArgs: session.agentArgs,
|
|
14791
|
+
availableCommands: session.mergedAvailableCommands(),
|
|
14792
|
+
availableModes: session.availableModes(),
|
|
14793
|
+
availableModels: session.availableModels(),
|
|
14794
|
+
// Mid-turn at attach time: hand the client the original prompt's
|
|
14795
|
+
// recordedAt so it can boot directly into "busy · Ns" instead of
|
|
14796
|
+
// sitting on "ready" until the next live notification.
|
|
14797
|
+
turnStartedAt: session.turnStartedAt,
|
|
14798
|
+
// The underlying agent's own initialize-time capability claim, captured
|
|
14799
|
+
// verbatim. Lets capability-aware clients (cat --stream) pick the right
|
|
14800
|
+
// consumption surface without re-probing the agent.
|
|
14801
|
+
agentCapabilities: session.agentCapabilities,
|
|
14802
|
+
// Snapshot of the daemon-owned prompt queue. Lets a late attacher
|
|
14803
|
+
// paint queue chips for entries that landed before it joined without
|
|
14804
|
+
// waiting for new prompt_queue_added notifications. Omitted entirely
|
|
14805
|
+
// when the queue is empty (the common case).
|
|
14806
|
+
queue: session.queueSnapshot()
|
|
13318
14807
|
};
|
|
13319
|
-
|
|
13320
|
-
ours.name = session.title;
|
|
13321
|
-
}
|
|
13322
|
-
if (session.agentArgs && session.agentArgs.length > 0) {
|
|
13323
|
-
ours.agentArgs = session.agentArgs;
|
|
13324
|
-
}
|
|
13325
|
-
if (session.currentModel !== void 0) {
|
|
13326
|
-
ours.currentModel = session.currentModel;
|
|
13327
|
-
}
|
|
13328
|
-
if (session.currentMode !== void 0) {
|
|
13329
|
-
ours.currentMode = session.currentMode;
|
|
13330
|
-
}
|
|
13331
|
-
if (session.currentUsage !== void 0) {
|
|
13332
|
-
ours.currentUsage = session.currentUsage;
|
|
13333
|
-
}
|
|
13334
|
-
const commands = session.mergedAvailableCommands();
|
|
13335
|
-
if (commands.length > 0) {
|
|
13336
|
-
ours.availableCommands = commands;
|
|
13337
|
-
}
|
|
13338
|
-
const modes = session.availableModes();
|
|
13339
|
-
if (modes.length > 0) {
|
|
13340
|
-
ours.availableModes = modes;
|
|
13341
|
-
}
|
|
13342
|
-
const models = session.availableModels();
|
|
13343
|
-
if (models.length > 0) {
|
|
13344
|
-
ours.availableModels = models;
|
|
13345
|
-
}
|
|
13346
|
-
if (session.turnStartedAt !== void 0) {
|
|
13347
|
-
ours.turnStartedAt = session.turnStartedAt;
|
|
13348
|
-
}
|
|
13349
|
-
if (session.agentCapabilities !== void 0) {
|
|
13350
|
-
ours.agentCapabilities = session.agentCapabilities;
|
|
13351
|
-
}
|
|
13352
|
-
const queue = session.queueSnapshot();
|
|
13353
|
-
if (queue.length > 0) {
|
|
13354
|
-
ours.queue = queue;
|
|
13355
|
-
}
|
|
13356
|
-
return mergeMeta(session.agentMeta, ours);
|
|
14808
|
+
return mergeMeta(session.agentMeta, buildHydraSessionMeta(entry, extras));
|
|
13357
14809
|
}
|
|
13358
14810
|
function buildInitializeResult() {
|
|
13359
14811
|
return {
|
|
@@ -13384,18 +14836,26 @@ function buildInitializeResult() {
|
|
|
13384
14836
|
description: "Bearer token presented at WS upgrade"
|
|
13385
14837
|
}
|
|
13386
14838
|
],
|
|
13387
|
-
// Advertise hydra-only capabilities via _meta["hydra-acp"]
|
|
13388
|
-
//
|
|
13389
|
-
//
|
|
13390
|
-
//
|
|
13391
|
-
// streaming-input probe lands
|
|
13392
|
-
//
|
|
14839
|
+
// Advertise hydra-only capabilities via _meta["hydra-acp"], grouped by
|
|
14840
|
+
// resource to mirror the hydra-acp/<resource>/<action> method
|
|
14841
|
+
// namespaces. Generic ACP clients ignore the field; capability-aware
|
|
14842
|
+
// clients probe here to gate UI before calling a method. `pipelining`
|
|
14843
|
+
// is false until the streaming-input probe lands; the rest are
|
|
14844
|
+
// unconditional method-availability flags. (Named `prompt`/`agents`,
|
|
14845
|
+
// not `promptCapabilities`/`agentCapabilities` — those are ACP spec
|
|
14846
|
+
// names with different meanings.)
|
|
13393
14847
|
_meta: mergeMeta(void 0, {
|
|
13394
|
-
|
|
13395
|
-
|
|
13396
|
-
|
|
13397
|
-
|
|
13398
|
-
|
|
14848
|
+
prompt: {
|
|
14849
|
+
queueing: true,
|
|
14850
|
+
cancelling: true,
|
|
14851
|
+
updating: true,
|
|
14852
|
+
amending: true,
|
|
14853
|
+
pipelining: false
|
|
14854
|
+
},
|
|
14855
|
+
agents: {
|
|
14856
|
+
list: true,
|
|
14857
|
+
installProgress: true
|
|
14858
|
+
}
|
|
13399
14859
|
})
|
|
13400
14860
|
};
|
|
13401
14861
|
}
|
|
@@ -13493,7 +14953,7 @@ var McpTokenRegistry = class {
|
|
|
13493
14953
|
import { randomUUID } from "crypto";
|
|
13494
14954
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13495
14955
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13496
|
-
import { z as
|
|
14956
|
+
import { z as z9 } from "zod";
|
|
13497
14957
|
|
|
13498
14958
|
// src/daemon/mcp/bearer.ts
|
|
13499
14959
|
var BEARER_PREFIX2 = "Bearer ";
|
|
@@ -13522,7 +14982,7 @@ function buildMcpServer(session) {
|
|
|
13522
14982
|
{
|
|
13523
14983
|
description: "Return the most recent `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means older bytes existed but have been evicted from the ring.",
|
|
13524
14984
|
inputSchema: {
|
|
13525
|
-
bytes:
|
|
14985
|
+
bytes: z9.number().int().min(1).describe("How many trailing bytes to return.")
|
|
13526
14986
|
}
|
|
13527
14987
|
},
|
|
13528
14988
|
async ({ bytes }) => {
|
|
@@ -13543,7 +15003,7 @@ function buildMcpServer(session) {
|
|
|
13543
15003
|
{
|
|
13544
15004
|
description: "Return the first `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means the head has already been evicted from the ring and the returned bytes start at the oldest still-resident cursor.",
|
|
13545
15005
|
inputSchema: {
|
|
13546
|
-
bytes:
|
|
15006
|
+
bytes: z9.number().int().min(1).describe("How many leading bytes to return.")
|
|
13547
15007
|
}
|
|
13548
15008
|
},
|
|
13549
15009
|
async ({ bytes }) => {
|
|
@@ -13564,13 +15024,13 @@ function buildMcpServer(session) {
|
|
|
13564
15024
|
{
|
|
13565
15025
|
description: "Read up to `max_bytes` bytes starting at absolute byte `cursor`. Returns `{bytes, nextCursor, gap?, eof?}` \u2014 `gap` is the number of bytes silently skipped because the ring had evicted them; `eof:true` means the producer closed and there is nothing left to read.",
|
|
13566
15026
|
inputSchema: {
|
|
13567
|
-
cursor:
|
|
15027
|
+
cursor: z9.number().int().min(0).describe(
|
|
13568
15028
|
"Absolute byte offset to start reading from. Use 0 to read from the very beginning (may produce a gap if old bytes have been evicted)."
|
|
13569
15029
|
),
|
|
13570
|
-
max_bytes:
|
|
15030
|
+
max_bytes: z9.number().int().min(1).optional().describe(
|
|
13571
15031
|
"Optional cap on how many bytes to return. Server caps at 64 KiB regardless."
|
|
13572
15032
|
),
|
|
13573
|
-
wait_ms:
|
|
15033
|
+
wait_ms: z9.number().int().min(0).optional().describe(
|
|
13574
15034
|
"If no bytes are available, block up to this many ms for more (capped server-side at 60_000)."
|
|
13575
15035
|
)
|
|
13576
15036
|
}
|
|
@@ -13593,8 +15053,8 @@ function buildMcpServer(session) {
|
|
|
13593
15053
|
{
|
|
13594
15054
|
description: "Block until bytes are available past `cursor`, the stream closes, or `timeout_ms` elapses. Returns one of {data, eof, timeout} plus the current `writeCursor`. Use this when you've consumed everything up to a cursor and want to wait for more without busy-polling.",
|
|
13595
15055
|
inputSchema: {
|
|
13596
|
-
cursor:
|
|
13597
|
-
timeout_ms:
|
|
15056
|
+
cursor: z9.number().int().min(0).describe("The cursor you've already consumed up to."),
|
|
15057
|
+
timeout_ms: z9.number().int().min(0).describe("Maximum ms to block (server caps at 60_000).")
|
|
13598
15058
|
}
|
|
13599
15059
|
},
|
|
13600
15060
|
async ({ cursor, timeout_ms }) => {
|
|
@@ -13617,17 +15077,17 @@ function buildMcpServer(session) {
|
|
|
13617
15077
|
{
|
|
13618
15078
|
description: "Scan piped stdin line-by-line and return lines matching `pattern`. Prefer this over `read` when the question is 'find lines that mention X' \u2014 it filters server-side so you don't pull and decode 64 KiB base64 windows. Returns `{matches: [{cursor, line, before?, after?}], truncated, nextCursor, gap?, scannedBytes, eof?}`. Lines come back as decoded UTF-8 strings (not base64). When `truncated:true`, re-call with `cursor: nextCursor` to resume.",
|
|
13619
15079
|
inputSchema: {
|
|
13620
|
-
pattern:
|
|
15080
|
+
pattern: z9.string().min(1).describe(
|
|
13621
15081
|
"Search pattern. Treated as a JavaScript regular expression by default (set `regex:false` for a literal substring match)."
|
|
13622
15082
|
),
|
|
13623
|
-
regex:
|
|
13624
|
-
case_insensitive:
|
|
13625
|
-
invert:
|
|
13626
|
-
max_matches:
|
|
13627
|
-
max_bytes:
|
|
13628
|
-
context_before:
|
|
13629
|
-
context_after:
|
|
13630
|
-
cursor:
|
|
15083
|
+
regex: z9.boolean().optional().describe("Default true. Pass false to treat `pattern` as a literal substring."),
|
|
15084
|
+
case_insensitive: z9.boolean().optional().describe("Default false. Pass true for case-insensitive matching."),
|
|
15085
|
+
invert: z9.boolean().optional().describe("Default false. Pass true to return lines that do NOT match the pattern."),
|
|
15086
|
+
max_matches: z9.number().int().min(1).optional().describe("Default 100. Capped server-side at 1000."),
|
|
15087
|
+
max_bytes: z9.number().int().min(1).optional().describe("Default 64 KiB output. Capped server-side at 256 KiB."),
|
|
15088
|
+
context_before: z9.number().int().min(0).optional().describe("Default 0. Number of lines before each match to include (capped at 20)."),
|
|
15089
|
+
context_after: z9.number().int().min(0).optional().describe("Default 0. Number of lines after each match to include (capped at 20)."),
|
|
15090
|
+
cursor: z9.number().int().min(0).optional().describe(
|
|
13631
15091
|
"Optional absolute byte offset to start scanning from. Omit to scan from the oldest still-resident byte. Pass the `nextCursor` from a previous truncated call to resume."
|
|
13632
15092
|
)
|
|
13633
15093
|
}
|
|
@@ -13884,7 +15344,7 @@ async function invokeWithTimeout(connection, server, tool, args, timeoutMs) {
|
|
|
13884
15344
|
});
|
|
13885
15345
|
try {
|
|
13886
15346
|
return await Promise.race([
|
|
13887
|
-
connection.request("hydra-acp/
|
|
15347
|
+
connection.request("hydra-acp/mcp_tools/invoke", {
|
|
13888
15348
|
server,
|
|
13889
15349
|
tool,
|
|
13890
15350
|
args
|
|
@@ -14091,6 +15551,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
14091
15551
|
stderrTailBytes: config.daemon.agentStderrTailBytes,
|
|
14092
15552
|
logger: agentLogger
|
|
14093
15553
|
});
|
|
15554
|
+
setToolBlobCompression(config.compressToolContent);
|
|
14094
15555
|
const extensionCommands = new ExtensionCommandRegistry();
|
|
14095
15556
|
const manager = new SessionManager(registry, spawner, void 0, {
|
|
14096
15557
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
@@ -14161,7 +15622,8 @@ async function startDaemon(config, serviceToken) {
|
|
|
14161
15622
|
extensionCommands,
|
|
14162
15623
|
mcpTokenRegistry,
|
|
14163
15624
|
extensionMcp,
|
|
14164
|
-
getDaemonOrigin
|
|
15625
|
+
getDaemonOrigin,
|
|
15626
|
+
registry
|
|
14165
15627
|
});
|
|
14166
15628
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
14167
15629
|
const address = app.server.address();
|
|
@@ -14221,7 +15683,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
14221
15683
|
setAgentPruneLogger(null);
|
|
14222
15684
|
await app.close();
|
|
14223
15685
|
try {
|
|
14224
|
-
|
|
15686
|
+
fs18.unlinkSync(paths.pidFile());
|
|
14225
15687
|
} catch {
|
|
14226
15688
|
}
|
|
14227
15689
|
try {
|