@hydra-acp/cli 0.1.4 → 0.1.6
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 +16 -13
- package/dist/cli.js +834 -254
- package/dist/index.d.ts +433 -31
- package/dist/index.js +577 -126
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/daemon/server.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
2
|
+
import * as fs9 from "fs";
|
|
3
|
+
import * as fsp3 from "fs/promises";
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import websocketPlugin from "@fastify/websocket";
|
|
6
6
|
import pino from "pino";
|
|
@@ -35,7 +35,11 @@ var paths = {
|
|
|
35
35
|
currentLogFile: () => path.join(hydraHome(), "current.log"),
|
|
36
36
|
registryCache: () => path.join(hydraHome(), "registry.json"),
|
|
37
37
|
agentsDir: () => path.join(hydraHome(), "agents"),
|
|
38
|
-
|
|
38
|
+
// <platformKey>/<agentId>/<version>/ — platform at the top so a Hydra
|
|
39
|
+
// home shared between machines (NFS, rsync'd dotfiles) keeps each
|
|
40
|
+
// machine's binaries cleanly separated. `ls agents/` immediately
|
|
41
|
+
// shows which platforms have ever installed anything.
|
|
42
|
+
agentInstallDir: (id, platformKey, version) => path.join(hydraHome(), "agents", platformKey, id, version),
|
|
39
43
|
sessionsDir: () => path.join(hydraHome(), "sessions"),
|
|
40
44
|
// One directory per session id under sessions/. Co-locates the
|
|
41
45
|
// session record, its transcript, and any future per-session state
|
|
@@ -95,6 +99,14 @@ var HydraConfig = z.object({
|
|
|
95
99
|
daemon: DaemonConfig,
|
|
96
100
|
registry: RegistryConfig.default({ url: REGISTRY_URL_DEFAULT, ttlHours: 24 }),
|
|
97
101
|
defaultAgent: z.string().default("claude-acp"),
|
|
102
|
+
// Optional per-agent default model id. When a brand-new agent process
|
|
103
|
+
// is spawned (session/new path), hydra issues session/set_model with
|
|
104
|
+
// the matching entry so the user lands on their preferred model from
|
|
105
|
+
// the first prompt. Not applied on resurrect — those sessions keep
|
|
106
|
+
// whatever the user last selected. Keys are agent ids; values are the
|
|
107
|
+
// raw model id strings the agent expects (claude-acp: "claude-opus-4-7",
|
|
108
|
+
// opencode: "openai/gpt-5-codex" or "ncp-anthropic/claude-opus-4-7", …).
|
|
109
|
+
defaultModels: z.record(z.string(), z.string()).default({}),
|
|
98
110
|
// Where new sessions land when POST /v1/sessions omits cwd. Stored as
|
|
99
111
|
// a literal string ("~", "~/dev", "$HOME/work") so the config file is
|
|
100
112
|
// portable across machines; expanded via expandHome at use time.
|
|
@@ -193,8 +205,213 @@ function expandHome(p) {
|
|
|
193
205
|
}
|
|
194
206
|
|
|
195
207
|
// src/core/registry.ts
|
|
196
|
-
import * as
|
|
208
|
+
import * as fs3 from "fs/promises";
|
|
197
209
|
import { z as z2 } from "zod";
|
|
210
|
+
|
|
211
|
+
// src/core/binary-install.ts
|
|
212
|
+
import * as fs2 from "fs";
|
|
213
|
+
import * as fsp from "fs/promises";
|
|
214
|
+
import * as path2 from "path";
|
|
215
|
+
import { spawn } from "child_process";
|
|
216
|
+
import { Readable } from "stream";
|
|
217
|
+
function currentPlatformKey() {
|
|
218
|
+
const osPart = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "windows" : void 0;
|
|
219
|
+
const archPart = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : void 0;
|
|
220
|
+
if (!osPart || !archPart) {
|
|
221
|
+
return void 0;
|
|
222
|
+
}
|
|
223
|
+
return `${osPart}-${archPart}`;
|
|
224
|
+
}
|
|
225
|
+
function pickBinaryTarget(distribution, platformKey = currentPlatformKey()) {
|
|
226
|
+
if (!platformKey) {
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
return distribution[platformKey];
|
|
230
|
+
}
|
|
231
|
+
var logSink = (msg) => {
|
|
232
|
+
process.stderr.write(msg + "\n");
|
|
233
|
+
};
|
|
234
|
+
function setBinaryInstallLogger(log) {
|
|
235
|
+
logSink = log ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
236
|
+
}
|
|
237
|
+
async function ensureBinary(args) {
|
|
238
|
+
if (!args.target.archive) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Agent ${args.agentId} has no archive URL for ${currentPlatformKey() ?? "this platform"}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (!args.target.cmd) {
|
|
244
|
+
throw new Error(`Agent ${args.agentId} has no cmd in its binary target`);
|
|
245
|
+
}
|
|
246
|
+
const platformKey = currentPlatformKey();
|
|
247
|
+
if (!platformKey) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Agent ${args.agentId}: cannot determine platform key for ${process.platform}/${process.arch}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const installDir = paths.agentInstallDir(
|
|
253
|
+
args.agentId,
|
|
254
|
+
platformKey,
|
|
255
|
+
args.version
|
|
256
|
+
);
|
|
257
|
+
const cmdPath = path2.resolve(installDir, args.target.cmd);
|
|
258
|
+
if (await fileExists(cmdPath)) {
|
|
259
|
+
return cmdPath;
|
|
260
|
+
}
|
|
261
|
+
await downloadAndExtract({
|
|
262
|
+
agentId: args.agentId,
|
|
263
|
+
archiveUrl: args.target.archive,
|
|
264
|
+
installDir
|
|
265
|
+
});
|
|
266
|
+
if (!await fileExists(cmdPath)) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Agent ${args.agentId}: extracted archive did not contain ${args.target.cmd} (looked in ${installDir})`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (process.platform !== "win32") {
|
|
272
|
+
await fsp.chmod(cmdPath, 493).catch(() => void 0);
|
|
273
|
+
}
|
|
274
|
+
return cmdPath;
|
|
275
|
+
}
|
|
276
|
+
async function downloadAndExtract(args) {
|
|
277
|
+
await fsp.mkdir(path2.dirname(args.installDir), { recursive: true });
|
|
278
|
+
const tempDir = await fsp.mkdtemp(`${args.installDir}.partial-`);
|
|
279
|
+
try {
|
|
280
|
+
logSink(`hydra-acp: downloading ${args.agentId} from ${args.archiveUrl}`);
|
|
281
|
+
const archivePath = await downloadTo({
|
|
282
|
+
url: args.archiveUrl,
|
|
283
|
+
dir: tempDir,
|
|
284
|
+
agentId: args.agentId
|
|
285
|
+
});
|
|
286
|
+
logSink(`hydra-acp: extracting ${args.agentId}`);
|
|
287
|
+
await extract(archivePath, tempDir);
|
|
288
|
+
await fsp.unlink(archivePath).catch(() => void 0);
|
|
289
|
+
try {
|
|
290
|
+
await fsp.rename(tempDir, args.installDir);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
const e = err;
|
|
293
|
+
if ((e.code === "EEXIST" || e.code === "ENOTEMPTY") && await fileExists(args.installDir)) {
|
|
294
|
+
await fsp.rm(tempDir, { recursive: true, force: true }).catch(
|
|
295
|
+
() => void 0
|
|
296
|
+
);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
301
|
+
logSink(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function downloadTo(args) {
|
|
308
|
+
const filename = inferArchiveName(args.url);
|
|
309
|
+
const dest = path2.join(args.dir, filename);
|
|
310
|
+
const response = await fetch(args.url, { redirect: "follow" });
|
|
311
|
+
if (!response.ok || !response.body) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Failed to download ${args.url}: HTTP ${response.status} ${response.statusText}`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const total = Number(response.headers.get("content-length") ?? "0");
|
|
317
|
+
const out = fs2.createWriteStream(dest);
|
|
318
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
319
|
+
let received = 0;
|
|
320
|
+
let lastEmit = Date.now();
|
|
321
|
+
const EMIT_INTERVAL_MS = 2e3;
|
|
322
|
+
nodeStream.on("data", (chunk) => {
|
|
323
|
+
received += chunk.length;
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
if (now - lastEmit < EMIT_INTERVAL_MS) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
lastEmit = now;
|
|
329
|
+
logSink(formatProgress(args.agentId, received, total));
|
|
330
|
+
});
|
|
331
|
+
await new Promise((resolve3, reject) => {
|
|
332
|
+
nodeStream.on("error", reject);
|
|
333
|
+
out.on("error", reject);
|
|
334
|
+
out.on("finish", () => resolve3());
|
|
335
|
+
nodeStream.pipe(out);
|
|
336
|
+
});
|
|
337
|
+
logSink(formatProgress(
|
|
338
|
+
args.agentId,
|
|
339
|
+
received,
|
|
340
|
+
total,
|
|
341
|
+
/* done */
|
|
342
|
+
true
|
|
343
|
+
));
|
|
344
|
+
return dest;
|
|
345
|
+
}
|
|
346
|
+
function formatProgress(agentId, received, total, done = false) {
|
|
347
|
+
const rxMb = (received / 1e6).toFixed(1);
|
|
348
|
+
if (total > 0) {
|
|
349
|
+
const totalMb = (total / 1e6).toFixed(1);
|
|
350
|
+
const pct = Math.min(100, Math.floor(received / total * 100));
|
|
351
|
+
const tag2 = done ? "downloaded" : "downloading";
|
|
352
|
+
return `hydra-acp: ${tag2} ${agentId} ${rxMb}/${totalMb} MB (${pct}%)`;
|
|
353
|
+
}
|
|
354
|
+
const tag = done ? "downloaded" : "downloading";
|
|
355
|
+
return `hydra-acp: ${tag} ${agentId} ${rxMb} MB`;
|
|
356
|
+
}
|
|
357
|
+
function inferArchiveName(url) {
|
|
358
|
+
const u = new URL(url);
|
|
359
|
+
const base = path2.posix.basename(u.pathname);
|
|
360
|
+
return base || "archive";
|
|
361
|
+
}
|
|
362
|
+
async function extract(archivePath, dest) {
|
|
363
|
+
const lower = archivePath.toLowerCase();
|
|
364
|
+
if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz") || lower.endsWith(".tar")) {
|
|
365
|
+
await run("tar", ["-xf", archivePath, "-C", dest]);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (lower.endsWith(".zip")) {
|
|
369
|
+
if (await hasCommand("unzip")) {
|
|
370
|
+
await run("unzip", ["-q", archivePath, "-d", dest]);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
await run("tar", ["-xf", archivePath, "-C", dest]);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
throw new Error(`Unsupported archive format: ${archivePath}`);
|
|
377
|
+
}
|
|
378
|
+
function run(cmd, args) {
|
|
379
|
+
return new Promise((resolve3, reject) => {
|
|
380
|
+
const child = spawn(cmd, args, {
|
|
381
|
+
stdio: ["ignore", "ignore", "inherit"]
|
|
382
|
+
});
|
|
383
|
+
child.on("error", reject);
|
|
384
|
+
child.on("exit", (code, signal) => {
|
|
385
|
+
if (code === 0) {
|
|
386
|
+
resolve3();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
reject(
|
|
390
|
+
new Error(
|
|
391
|
+
`${cmd} ${args.join(" ")} exited with ${code !== null ? `code ${code}` : `signal ${signal}`}`
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async function hasCommand(name) {
|
|
398
|
+
return new Promise((resolve3) => {
|
|
399
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
400
|
+
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
401
|
+
child.on("error", () => resolve3(false));
|
|
402
|
+
child.on("exit", (code) => resolve3(code === 0));
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
async function fileExists(p) {
|
|
406
|
+
try {
|
|
407
|
+
await fsp.access(p);
|
|
408
|
+
return true;
|
|
409
|
+
} catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/core/registry.ts
|
|
198
415
|
var NpxDistribution = z2.object({
|
|
199
416
|
package: z2.string(),
|
|
200
417
|
args: z2.array(z2.string()).optional(),
|
|
@@ -202,7 +419,9 @@ var NpxDistribution = z2.object({
|
|
|
202
419
|
});
|
|
203
420
|
var BinaryTarget = z2.object({
|
|
204
421
|
archive: z2.string().url().optional(),
|
|
205
|
-
cmd: z2.string().optional()
|
|
422
|
+
cmd: z2.string().optional(),
|
|
423
|
+
args: z2.array(z2.string()).optional(),
|
|
424
|
+
env: z2.record(z2.string()).optional()
|
|
206
425
|
});
|
|
207
426
|
var BinaryDistribution = z2.object({
|
|
208
427
|
"darwin-aarch64": BinaryTarget.optional(),
|
|
@@ -291,34 +510,59 @@ var Registry = class {
|
|
|
291
510
|
if (!response.ok) {
|
|
292
511
|
throw new Error(`Registry fetch failed: HTTP ${response.status}`);
|
|
293
512
|
}
|
|
294
|
-
const
|
|
295
|
-
const data = RegistryDocument.parse(
|
|
296
|
-
return { fetchedAt: Date.now(), data };
|
|
513
|
+
const raw = await response.json();
|
|
514
|
+
const data = RegistryDocument.parse(raw);
|
|
515
|
+
return { fetchedAt: Date.now(), raw, data };
|
|
297
516
|
}
|
|
298
517
|
async readDiskCache() {
|
|
518
|
+
let text;
|
|
299
519
|
try {
|
|
300
|
-
|
|
301
|
-
const parsed = JSON.parse(raw);
|
|
302
|
-
if (typeof parsed.fetchedAt === "number" && parsed.data && Array.isArray(parsed.data.agents)) {
|
|
303
|
-
return parsed;
|
|
304
|
-
}
|
|
520
|
+
text = await fs3.readFile(paths.registryCache(), "utf8");
|
|
305
521
|
} catch (err) {
|
|
306
522
|
const e = err;
|
|
307
|
-
if (e.code
|
|
308
|
-
|
|
523
|
+
if (e.code === "ENOENT") {
|
|
524
|
+
return void 0;
|
|
309
525
|
}
|
|
526
|
+
throw err;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
const parsed = JSON.parse(text);
|
|
530
|
+
if (typeof parsed.fetchedAt !== "number" || parsed.data === void 0) {
|
|
531
|
+
return void 0;
|
|
532
|
+
}
|
|
533
|
+
const data = RegistryDocument.parse(parsed.data);
|
|
534
|
+
return { fetchedAt: parsed.fetchedAt, raw: parsed.data, data };
|
|
535
|
+
} catch {
|
|
536
|
+
return void 0;
|
|
310
537
|
}
|
|
311
|
-
return void 0;
|
|
312
538
|
}
|
|
539
|
+
// Atomic write: dump to a sibling temp path, then rename onto the
|
|
540
|
+
// target. POSIX rename is atomic within a filesystem, so readers
|
|
541
|
+
// either see the old file or the fully-written new file — never a
|
|
542
|
+
// truncated middle. This also makes simultaneous writers safe
|
|
543
|
+
// without a lock file: the loser of the rename race just gets its
|
|
544
|
+
// version replaced by the winner's.
|
|
313
545
|
async writeDiskCache(cache) {
|
|
314
|
-
await
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
546
|
+
await fs3.mkdir(paths.home(), { recursive: true });
|
|
547
|
+
const final = paths.registryCache();
|
|
548
|
+
const tmp = `${final}.tmp-${process.pid}-${randSuffix()}`;
|
|
549
|
+
const body = JSON.stringify(
|
|
550
|
+
{ fetchedAt: cache.fetchedAt, data: cache.raw },
|
|
551
|
+
null,
|
|
552
|
+
2
|
|
553
|
+
) + "\n";
|
|
554
|
+
try {
|
|
555
|
+
await fs3.writeFile(tmp, body, "utf8");
|
|
556
|
+
await fs3.rename(tmp, final);
|
|
557
|
+
} catch (err) {
|
|
558
|
+
await fs3.unlink(tmp).catch(() => void 0);
|
|
559
|
+
throw err;
|
|
560
|
+
}
|
|
320
561
|
}
|
|
321
562
|
};
|
|
563
|
+
function randSuffix() {
|
|
564
|
+
return Math.random().toString(36).slice(2, 10);
|
|
565
|
+
}
|
|
322
566
|
function npxPackageBasename(agent) {
|
|
323
567
|
const pkg = agent.distribution.npx?.package;
|
|
324
568
|
if (!pkg) {
|
|
@@ -329,7 +573,7 @@ function npxPackageBasename(agent) {
|
|
|
329
573
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
330
574
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
331
575
|
}
|
|
332
|
-
function planSpawn(agent, extraArgs = []) {
|
|
576
|
+
async function planSpawn(agent, extraArgs = []) {
|
|
333
577
|
if (agent.distribution.npx) {
|
|
334
578
|
const npx = agent.distribution.npx;
|
|
335
579
|
const args = ["-y", npx.package, ...npx.args ?? [], ...extraArgs];
|
|
@@ -340,9 +584,22 @@ function planSpawn(agent, extraArgs = []) {
|
|
|
340
584
|
};
|
|
341
585
|
}
|
|
342
586
|
if (agent.distribution.binary) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
587
|
+
const target = pickBinaryTarget(agent.distribution.binary);
|
|
588
|
+
if (!target) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Agent ${agent.id} has no binary distribution for ${currentPlatformKey() ?? "this platform"}.`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
const cmdPath = await ensureBinary({
|
|
594
|
+
agentId: agent.id,
|
|
595
|
+
version: agent.version ?? "current",
|
|
596
|
+
target
|
|
597
|
+
});
|
|
598
|
+
return {
|
|
599
|
+
command: cmdPath,
|
|
600
|
+
args: [...target.args ?? [], ...extraArgs],
|
|
601
|
+
env: target.env ?? {}
|
|
602
|
+
};
|
|
346
603
|
}
|
|
347
604
|
if (agent.distribution.uvx) {
|
|
348
605
|
const uvx = agent.distribution.uvx;
|
|
@@ -357,11 +614,11 @@ function planSpawn(agent, extraArgs = []) {
|
|
|
357
614
|
}
|
|
358
615
|
|
|
359
616
|
// src/core/session-manager.ts
|
|
360
|
-
import * as
|
|
617
|
+
import * as fs7 from "fs/promises";
|
|
361
618
|
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
362
619
|
|
|
363
620
|
// src/core/agent-instance.ts
|
|
364
|
-
import { spawn } from "child_process";
|
|
621
|
+
import { spawn as spawn2 } from "child_process";
|
|
365
622
|
|
|
366
623
|
// src/acp/types.ts
|
|
367
624
|
import { z as z3 } from "zod";
|
|
@@ -439,6 +696,9 @@ function extractHydraMeta(meta) {
|
|
|
439
696
|
out.resume = parsed.data;
|
|
440
697
|
}
|
|
441
698
|
}
|
|
699
|
+
if (typeof obj.model === "string") {
|
|
700
|
+
out.model = obj.model;
|
|
701
|
+
}
|
|
442
702
|
if (typeof obj.currentModel === "string") {
|
|
443
703
|
out.currentModel = obj.currentModel;
|
|
444
704
|
}
|
|
@@ -481,12 +741,24 @@ var SessionListParams = z3.object({
|
|
|
481
741
|
cursor: z3.string().optional(),
|
|
482
742
|
limit: z3.number().int().positive().max(200).optional()
|
|
483
743
|
});
|
|
744
|
+
var SessionListUsage = z3.object({
|
|
745
|
+
used: z3.number().optional(),
|
|
746
|
+
size: z3.number().optional(),
|
|
747
|
+
costAmount: z3.number().optional(),
|
|
748
|
+
costCurrency: z3.string().optional()
|
|
749
|
+
});
|
|
484
750
|
var SessionListEntry = z3.object({
|
|
485
751
|
sessionId: z3.string(),
|
|
486
752
|
upstreamSessionId: z3.string().optional(),
|
|
487
753
|
cwd: z3.string(),
|
|
488
754
|
title: z3.string().optional(),
|
|
489
755
|
agentId: z3.string().optional(),
|
|
756
|
+
// Last-known model id, so list views can render `<agent>(<model>)`
|
|
757
|
+
// without resurrecting cold sessions to look it up.
|
|
758
|
+
currentModel: z3.string().optional(),
|
|
759
|
+
// Last-known usage snapshot so list views can show per-session cost
|
|
760
|
+
// (and tokens, in callers that care) without resurrecting cold sessions.
|
|
761
|
+
currentUsage: SessionListUsage.optional(),
|
|
490
762
|
updatedAt: z3.string(),
|
|
491
763
|
attachedClients: z3.number().int().nonnegative(),
|
|
492
764
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
@@ -568,13 +840,13 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
568
840
|
throw new Error("stream is closed");
|
|
569
841
|
}
|
|
570
842
|
const line = JSON.stringify(message) + "\n";
|
|
571
|
-
await new Promise((
|
|
843
|
+
await new Promise((resolve3, reject) => {
|
|
572
844
|
stdin.write(line, (err) => {
|
|
573
845
|
if (err) {
|
|
574
846
|
reject(err);
|
|
575
847
|
return;
|
|
576
848
|
}
|
|
577
|
-
|
|
849
|
+
resolve3();
|
|
578
850
|
});
|
|
579
851
|
});
|
|
580
852
|
},
|
|
@@ -633,9 +905,9 @@ var JsonRpcConnection = class {
|
|
|
633
905
|
}
|
|
634
906
|
const id = nanoid();
|
|
635
907
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
636
|
-
const response = new Promise((
|
|
908
|
+
const response = new Promise((resolve3, reject) => {
|
|
637
909
|
this.pending.set(id, {
|
|
638
|
-
resolve: (result) =>
|
|
910
|
+
resolve: (result) => resolve3(result),
|
|
639
911
|
reject
|
|
640
912
|
});
|
|
641
913
|
this.stream.send(message).catch((err) => {
|
|
@@ -773,7 +1045,7 @@ var AgentInstance = class _AgentInstance {
|
|
|
773
1045
|
...opts.plan.env,
|
|
774
1046
|
...opts.extraEnv ?? {}
|
|
775
1047
|
};
|
|
776
|
-
const child =
|
|
1048
|
+
const child = spawn2(opts.plan.command, opts.plan.args, {
|
|
777
1049
|
cwd: opts.cwd,
|
|
778
1050
|
env,
|
|
779
1051
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -806,8 +1078,8 @@ var HYDRA_COMMANDS = [
|
|
|
806
1078
|
description: "Regenerate the session title via the agent (or set manually with an arg)"
|
|
807
1079
|
},
|
|
808
1080
|
{
|
|
809
|
-
verb: "
|
|
810
|
-
name: "/hydra
|
|
1081
|
+
verb: "agent",
|
|
1082
|
+
name: "/hydra agent",
|
|
811
1083
|
argsHint: "<agent>",
|
|
812
1084
|
description: "Swap the agent backing this session, preserving context"
|
|
813
1085
|
}
|
|
@@ -829,7 +1101,7 @@ var COMPACT_EVERY = 200;
|
|
|
829
1101
|
var Session = class {
|
|
830
1102
|
sessionId;
|
|
831
1103
|
cwd;
|
|
832
|
-
// agent / agentId / upstreamSessionId are mutable so /hydra
|
|
1104
|
+
// agent / agentId / upstreamSessionId are mutable so /hydra agent can
|
|
833
1105
|
// replace the underlying agent process while keeping the same Session
|
|
834
1106
|
// record. agentMeta is the metadata returned by the agent at session/new
|
|
835
1107
|
// time; it gets refreshed on switch too.
|
|
@@ -844,6 +1116,7 @@ var Session = class {
|
|
|
844
1116
|
// stale-prone for snapshot-shaped events).
|
|
845
1117
|
currentModel;
|
|
846
1118
|
currentMode;
|
|
1119
|
+
currentUsage;
|
|
847
1120
|
updatedAt;
|
|
848
1121
|
createdAt;
|
|
849
1122
|
clients = /* @__PURE__ */ new Map();
|
|
@@ -905,6 +1178,7 @@ var Session = class {
|
|
|
905
1178
|
agentCommandsHandlers = [];
|
|
906
1179
|
modelHandlers = [];
|
|
907
1180
|
modeHandlers = [];
|
|
1181
|
+
usageHandlers = [];
|
|
908
1182
|
constructor(init) {
|
|
909
1183
|
this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
|
|
910
1184
|
this.cwd = init.cwd;
|
|
@@ -916,6 +1190,7 @@ var Session = class {
|
|
|
916
1190
|
this.title = init.title;
|
|
917
1191
|
this.currentModel = init.currentModel;
|
|
918
1192
|
this.currentMode = init.currentMode;
|
|
1193
|
+
this.currentUsage = init.currentUsage;
|
|
919
1194
|
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
920
1195
|
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
921
1196
|
}
|
|
@@ -945,7 +1220,7 @@ var Session = class {
|
|
|
945
1220
|
});
|
|
946
1221
|
}
|
|
947
1222
|
// Register session/update, session/request_permission, and onExit
|
|
948
|
-
// handlers on an agent connection. Re-run on every /hydra
|
|
1223
|
+
// handlers on an agent connection. Re-run on every /hydra agent so
|
|
949
1224
|
// the new agent is plumbed identically. The exit handler's identity
|
|
950
1225
|
// check is what makes switching safe: when the *old* agent exits as
|
|
951
1226
|
// part of a swap, this.agent has already been replaced, so we no-op
|
|
@@ -969,6 +1244,10 @@ var Session = class {
|
|
|
969
1244
|
this.recordAndBroadcast("session/update", params);
|
|
970
1245
|
return;
|
|
971
1246
|
}
|
|
1247
|
+
if (this.maybeApplyAgentUsage(params)) {
|
|
1248
|
+
this.recordAndBroadcast("session/update", params);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
972
1251
|
this.maybeApplyAgentSessionInfo(params);
|
|
973
1252
|
this.recordAndBroadcast("session/update", params);
|
|
974
1253
|
});
|
|
@@ -1286,6 +1565,49 @@ var Session = class {
|
|
|
1286
1565
|
}
|
|
1287
1566
|
return true;
|
|
1288
1567
|
}
|
|
1568
|
+
// usage_update carries any subset of {used, size, cost.amount,
|
|
1569
|
+
// cost.currency}. Merge non-undefined fields onto currentUsage so a
|
|
1570
|
+
// sparse update preserves prior values, and fire usage handlers only
|
|
1571
|
+
// if something actually changed.
|
|
1572
|
+
maybeApplyAgentUsage(params) {
|
|
1573
|
+
const obj = params ?? {};
|
|
1574
|
+
const update = obj.update ?? {};
|
|
1575
|
+
if (update.sessionUpdate !== "usage_update") {
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
const next = { ...this.currentUsage ?? {} };
|
|
1579
|
+
let changed = false;
|
|
1580
|
+
if (typeof update.used === "number" && next.used !== update.used) {
|
|
1581
|
+
next.used = update.used;
|
|
1582
|
+
changed = true;
|
|
1583
|
+
}
|
|
1584
|
+
if (typeof update.size === "number" && next.size !== update.size) {
|
|
1585
|
+
next.size = update.size;
|
|
1586
|
+
changed = true;
|
|
1587
|
+
}
|
|
1588
|
+
if (update.cost && typeof update.cost === "object") {
|
|
1589
|
+
const cost = update.cost;
|
|
1590
|
+
if (typeof cost.amount === "number" && next.costAmount !== cost.amount) {
|
|
1591
|
+
next.costAmount = cost.amount;
|
|
1592
|
+
changed = true;
|
|
1593
|
+
}
|
|
1594
|
+
if (typeof cost.currency === "string" && next.costCurrency !== cost.currency) {
|
|
1595
|
+
next.costCurrency = cost.currency;
|
|
1596
|
+
changed = true;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (!changed) {
|
|
1600
|
+
return true;
|
|
1601
|
+
}
|
|
1602
|
+
this.currentUsage = next;
|
|
1603
|
+
for (const handler of this.usageHandlers) {
|
|
1604
|
+
try {
|
|
1605
|
+
handler(next);
|
|
1606
|
+
} catch {
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return true;
|
|
1610
|
+
}
|
|
1289
1611
|
// Update the cached agent command list, fire persist handlers, and
|
|
1290
1612
|
// broadcast the merged list to attached clients. Idempotent on a
|
|
1291
1613
|
// structurally identical list so we don't churn meta.json on noisy
|
|
@@ -1316,6 +1638,9 @@ var Session = class {
|
|
|
1316
1638
|
onModeChange(handler) {
|
|
1317
1639
|
this.modeHandlers.push(handler);
|
|
1318
1640
|
}
|
|
1641
|
+
onUsageChange(handler) {
|
|
1642
|
+
this.usageHandlers.push(handler);
|
|
1643
|
+
}
|
|
1319
1644
|
// Returns a freshly merged command list (hydra ∪ agent) for callers
|
|
1320
1645
|
// that need a snapshot — notably acp-ws.ts's buildResponseMeta when
|
|
1321
1646
|
// assembling the attach response.
|
|
@@ -1378,8 +1703,8 @@ var Session = class {
|
|
|
1378
1703
|
switch (verb) {
|
|
1379
1704
|
case "title":
|
|
1380
1705
|
return this.runTitleCommand(arg);
|
|
1381
|
-
case "
|
|
1382
|
-
return this.
|
|
1706
|
+
case "agent":
|
|
1707
|
+
return this.runAgentCommand(arg);
|
|
1383
1708
|
default: {
|
|
1384
1709
|
const err = new Error(
|
|
1385
1710
|
`no dispatcher for /hydra verb ${verb}`
|
|
@@ -1415,7 +1740,7 @@ var Session = class {
|
|
|
1415
1740
|
}
|
|
1416
1741
|
// Send a prompt to the underlying agent and capture its reply chunks
|
|
1417
1742
|
// privately (no fan-out to clients, no recording into history). Used
|
|
1418
|
-
// by /hydra title's regen path and /hydra
|
|
1743
|
+
// by /hydra title's regen path and /hydra agent's transcript-injection
|
|
1419
1744
|
// path. Returns the joined agent_message_chunk text.
|
|
1420
1745
|
async runInternalPrompt(text) {
|
|
1421
1746
|
if (this.internalPromptCapture) {
|
|
@@ -1437,10 +1762,10 @@ var Session = class {
|
|
|
1437
1762
|
// record. Spawns the new agent first so a failure leaves the old one
|
|
1438
1763
|
// intact; then injects a synthesized transcript so the new agent has
|
|
1439
1764
|
// context for the next turn.
|
|
1440
|
-
|
|
1765
|
+
runAgentCommand(newAgentId) {
|
|
1441
1766
|
if (!newAgentId) {
|
|
1442
1767
|
throw withCode(
|
|
1443
|
-
new Error("/hydra
|
|
1768
|
+
new Error("/hydra agent requires an agent id"),
|
|
1444
1769
|
JsonRpcErrorCodes.InvalidParams
|
|
1445
1770
|
);
|
|
1446
1771
|
}
|
|
@@ -1574,7 +1899,7 @@ var Session = class {
|
|
|
1574
1899
|
// on the first wake-up of a session whose meta.json has an empty
|
|
1575
1900
|
// upstreamSessionId (the import marker). Wrapped in enqueuePrompt so
|
|
1576
1901
|
// any user prompts arriving mid-seed queue behind it (mirrors the
|
|
1577
|
-
// /hydra
|
|
1902
|
+
// /hydra agent path so the agent isn't asked to respond to a user
|
|
1578
1903
|
// turn before it has absorbed the imported transcript). Best-effort:
|
|
1579
1904
|
// if the agent fails to absorb the transcript we still leave the
|
|
1580
1905
|
// session usable — the user just continues without context.
|
|
@@ -1598,7 +1923,7 @@ var Session = class {
|
|
|
1598
1923
|
// ones read it and relabel) and (b) drop a visible banner into the
|
|
1599
1924
|
// transcript so users see the switch rather than just suddenly getting
|
|
1600
1925
|
// answers from a different agent. Both updates carry synthetic=true
|
|
1601
|
-
// so a future /hydra
|
|
1926
|
+
// so a future /hydra agent's transcript builder filters them out.
|
|
1602
1927
|
broadcastAgentSwitch(oldAgentId, newAgentId) {
|
|
1603
1928
|
this.recordAndBroadcast("session/update", {
|
|
1604
1929
|
sessionId: this.sessionId,
|
|
@@ -1745,7 +2070,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1745
2070
|
);
|
|
1746
2071
|
}
|
|
1747
2072
|
const clientParams = this.rewriteForClient(params);
|
|
1748
|
-
return new Promise((
|
|
2073
|
+
return new Promise((resolve3, reject) => {
|
|
1749
2074
|
let settled = false;
|
|
1750
2075
|
const outbound = [];
|
|
1751
2076
|
const entry = { addClient: sendTo };
|
|
@@ -1780,7 +2105,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1780
2105
|
result
|
|
1781
2106
|
}).catch(() => void 0);
|
|
1782
2107
|
}
|
|
1783
|
-
|
|
2108
|
+
resolve3(result);
|
|
1784
2109
|
});
|
|
1785
2110
|
}).catch((err) => {
|
|
1786
2111
|
settle(() => reject(err));
|
|
@@ -1792,16 +2117,16 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1792
2117
|
});
|
|
1793
2118
|
}
|
|
1794
2119
|
async enqueuePrompt(task) {
|
|
1795
|
-
return new Promise((
|
|
1796
|
-
const
|
|
2120
|
+
return new Promise((resolve3, reject) => {
|
|
2121
|
+
const run2 = async () => {
|
|
1797
2122
|
try {
|
|
1798
2123
|
const result = await task();
|
|
1799
|
-
|
|
2124
|
+
resolve3(result);
|
|
1800
2125
|
} catch (err) {
|
|
1801
2126
|
reject(err);
|
|
1802
2127
|
}
|
|
1803
2128
|
};
|
|
1804
|
-
this.promptQueue.push(
|
|
2129
|
+
this.promptQueue.push(run2);
|
|
1805
2130
|
void this.drainQueue();
|
|
1806
2131
|
});
|
|
1807
2132
|
}
|
|
@@ -1830,7 +2155,8 @@ var STATE_UPDATE_KINDS = /* @__PURE__ */ new Set([
|
|
|
1830
2155
|
"session_info_update",
|
|
1831
2156
|
"current_model_update",
|
|
1832
2157
|
"current_mode_update",
|
|
1833
|
-
"available_commands_update"
|
|
2158
|
+
"available_commands_update",
|
|
2159
|
+
"usage_update"
|
|
1834
2160
|
]);
|
|
1835
2161
|
function isStateUpdate(method, params) {
|
|
1836
2162
|
if (method !== "session/update") {
|
|
@@ -1915,8 +2241,8 @@ function firstLine(text, max) {
|
|
|
1915
2241
|
}
|
|
1916
2242
|
|
|
1917
2243
|
// src/core/session-store.ts
|
|
1918
|
-
import * as
|
|
1919
|
-
import * as
|
|
2244
|
+
import * as fs4 from "fs/promises";
|
|
2245
|
+
import * as path3 from "path";
|
|
1920
2246
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
1921
2247
|
import { z as z4 } from "zod";
|
|
1922
2248
|
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -1929,6 +2255,12 @@ var PersistedAgentCommand = z4.object({
|
|
|
1929
2255
|
name: z4.string(),
|
|
1930
2256
|
description: z4.string().optional()
|
|
1931
2257
|
});
|
|
2258
|
+
var PersistedUsage = z4.object({
|
|
2259
|
+
used: z4.number().optional(),
|
|
2260
|
+
size: z4.number().optional(),
|
|
2261
|
+
costAmount: z4.number().optional(),
|
|
2262
|
+
costCurrency: z4.string().optional()
|
|
2263
|
+
});
|
|
1932
2264
|
var SessionRecord = z4.object({
|
|
1933
2265
|
version: z4.literal(1),
|
|
1934
2266
|
sessionId: z4.string(),
|
|
@@ -1956,6 +2288,7 @@ var SessionRecord = z4.object({
|
|
|
1956
2288
|
// replay of a snapshot-shaped notification.
|
|
1957
2289
|
currentModel: z4.string().optional(),
|
|
1958
2290
|
currentMode: z4.string().optional(),
|
|
2291
|
+
currentUsage: PersistedUsage.optional(),
|
|
1959
2292
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
1960
2293
|
createdAt: z4.string(),
|
|
1961
2294
|
updatedAt: z4.string()
|
|
@@ -1969,9 +2302,9 @@ function assertSafeId(id) {
|
|
|
1969
2302
|
var SessionStore = class {
|
|
1970
2303
|
async write(record) {
|
|
1971
2304
|
assertSafeId(record.sessionId);
|
|
1972
|
-
await
|
|
2305
|
+
await fs4.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
1973
2306
|
const full = { version: 1, ...record };
|
|
1974
|
-
await
|
|
2307
|
+
await fs4.writeFile(
|
|
1975
2308
|
paths.sessionFile(record.sessionId),
|
|
1976
2309
|
JSON.stringify(full, null, 2) + "\n",
|
|
1977
2310
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -1983,7 +2316,7 @@ var SessionStore = class {
|
|
|
1983
2316
|
}
|
|
1984
2317
|
let raw;
|
|
1985
2318
|
try {
|
|
1986
|
-
raw = await
|
|
2319
|
+
raw = await fs4.readFile(paths.sessionFile(sessionId), "utf8");
|
|
1987
2320
|
} catch (err) {
|
|
1988
2321
|
const e = err;
|
|
1989
2322
|
if (e.code === "ENOENT") {
|
|
@@ -2002,7 +2335,7 @@ var SessionStore = class {
|
|
|
2002
2335
|
return;
|
|
2003
2336
|
}
|
|
2004
2337
|
try {
|
|
2005
|
-
await
|
|
2338
|
+
await fs4.unlink(paths.sessionFile(sessionId));
|
|
2006
2339
|
} catch (err) {
|
|
2007
2340
|
const e = err;
|
|
2008
2341
|
if (e.code !== "ENOENT") {
|
|
@@ -2010,7 +2343,7 @@ var SessionStore = class {
|
|
|
2010
2343
|
}
|
|
2011
2344
|
}
|
|
2012
2345
|
try {
|
|
2013
|
-
await
|
|
2346
|
+
await fs4.rmdir(paths.sessionDir(sessionId));
|
|
2014
2347
|
} catch (err) {
|
|
2015
2348
|
const e = err;
|
|
2016
2349
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -2040,7 +2373,7 @@ var SessionStore = class {
|
|
|
2040
2373
|
async list() {
|
|
2041
2374
|
let entries;
|
|
2042
2375
|
try {
|
|
2043
|
-
entries = await
|
|
2376
|
+
entries = await fs4.readdir(paths.sessionsDir());
|
|
2044
2377
|
} catch (err) {
|
|
2045
2378
|
const e = err;
|
|
2046
2379
|
if (e.code === "ENOENT") {
|
|
@@ -2071,6 +2404,7 @@ function recordFromMemorySession(args) {
|
|
|
2071
2404
|
agentArgs: args.agentArgs,
|
|
2072
2405
|
currentModel: args.currentModel,
|
|
2073
2406
|
currentMode: args.currentMode,
|
|
2407
|
+
currentUsage: args.currentUsage,
|
|
2074
2408
|
agentCommands: args.agentCommands,
|
|
2075
2409
|
createdAt: args.createdAt ?? now,
|
|
2076
2410
|
updatedAt: args.updatedAt ?? now
|
|
@@ -2078,7 +2412,7 @@ function recordFromMemorySession(args) {
|
|
|
2078
2412
|
}
|
|
2079
2413
|
|
|
2080
2414
|
// src/core/history-store.ts
|
|
2081
|
-
import * as
|
|
2415
|
+
import * as fs5 from "fs/promises";
|
|
2082
2416
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
2083
2417
|
var MAX_ENTRIES = 1e3;
|
|
2084
2418
|
var HistoryStore = class {
|
|
@@ -2091,9 +2425,9 @@ var HistoryStore = class {
|
|
|
2091
2425
|
return;
|
|
2092
2426
|
}
|
|
2093
2427
|
return this.enqueue(sessionId, async () => {
|
|
2094
|
-
await
|
|
2428
|
+
await fs5.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
2095
2429
|
const line = JSON.stringify(entry) + "\n";
|
|
2096
|
-
await
|
|
2430
|
+
await fs5.appendFile(paths.historyFile(sessionId), line, {
|
|
2097
2431
|
encoding: "utf8",
|
|
2098
2432
|
mode: 384
|
|
2099
2433
|
});
|
|
@@ -2104,9 +2438,9 @@ var HistoryStore = class {
|
|
|
2104
2438
|
return;
|
|
2105
2439
|
}
|
|
2106
2440
|
return this.enqueue(sessionId, async () => {
|
|
2107
|
-
await
|
|
2441
|
+
await fs5.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
2108
2442
|
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2109
|
-
await
|
|
2443
|
+
await fs5.writeFile(paths.historyFile(sessionId), body, {
|
|
2110
2444
|
encoding: "utf8",
|
|
2111
2445
|
mode: 384
|
|
2112
2446
|
});
|
|
@@ -2123,7 +2457,7 @@ var HistoryStore = class {
|
|
|
2123
2457
|
return this.enqueue(sessionId, async () => {
|
|
2124
2458
|
let raw;
|
|
2125
2459
|
try {
|
|
2126
|
-
raw = await
|
|
2460
|
+
raw = await fs5.readFile(paths.historyFile(sessionId), "utf8");
|
|
2127
2461
|
} catch (err) {
|
|
2128
2462
|
const e = err;
|
|
2129
2463
|
if (e.code === "ENOENT") {
|
|
@@ -2136,7 +2470,7 @@ var HistoryStore = class {
|
|
|
2136
2470
|
return;
|
|
2137
2471
|
}
|
|
2138
2472
|
const trimmed = lines.slice(-maxEntries);
|
|
2139
|
-
await
|
|
2473
|
+
await fs5.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
2140
2474
|
encoding: "utf8",
|
|
2141
2475
|
mode: 384
|
|
2142
2476
|
});
|
|
@@ -2152,7 +2486,7 @@ var HistoryStore = class {
|
|
|
2152
2486
|
}
|
|
2153
2487
|
let raw;
|
|
2154
2488
|
try {
|
|
2155
|
-
raw = await
|
|
2489
|
+
raw = await fs5.readFile(paths.historyFile(sessionId), "utf8");
|
|
2156
2490
|
} catch (err) {
|
|
2157
2491
|
const e = err;
|
|
2158
2492
|
if (e.code === "ENOENT") {
|
|
@@ -2198,7 +2532,7 @@ var HistoryStore = class {
|
|
|
2198
2532
|
}
|
|
2199
2533
|
return this.enqueue(sessionId, async () => {
|
|
2200
2534
|
try {
|
|
2201
|
-
await
|
|
2535
|
+
await fs5.unlink(paths.historyFile(sessionId));
|
|
2202
2536
|
} catch (err) {
|
|
2203
2537
|
const e = err;
|
|
2204
2538
|
if (e.code !== "ENOENT") {
|
|
@@ -2206,7 +2540,7 @@ var HistoryStore = class {
|
|
|
2206
2540
|
}
|
|
2207
2541
|
}
|
|
2208
2542
|
try {
|
|
2209
|
-
await
|
|
2543
|
+
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
2210
2544
|
} catch (err) {
|
|
2211
2545
|
const e = err;
|
|
2212
2546
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -2230,12 +2564,12 @@ var HistoryStore = class {
|
|
|
2230
2564
|
};
|
|
2231
2565
|
|
|
2232
2566
|
// src/tui/history.ts
|
|
2233
|
-
import { promises as
|
|
2234
|
-
import * as
|
|
2567
|
+
import { promises as fs6 } from "fs";
|
|
2568
|
+
import * as path4 from "path";
|
|
2235
2569
|
async function saveHistory(file, history) {
|
|
2236
|
-
await
|
|
2570
|
+
await fs6.mkdir(path4.dirname(file), { recursive: true });
|
|
2237
2571
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
2238
|
-
await
|
|
2572
|
+
await fs6.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
2239
2573
|
}
|
|
2240
2574
|
|
|
2241
2575
|
// src/core/session-manager.ts
|
|
@@ -2248,6 +2582,7 @@ var SessionManager = class {
|
|
|
2248
2582
|
this.store = store ?? new SessionStore();
|
|
2249
2583
|
this.histories = new HistoryStore();
|
|
2250
2584
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
2585
|
+
this.defaultModels = options.defaultModels ?? {};
|
|
2251
2586
|
}
|
|
2252
2587
|
registry;
|
|
2253
2588
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -2256,6 +2591,7 @@ var SessionManager = class {
|
|
|
2256
2591
|
store;
|
|
2257
2592
|
histories;
|
|
2258
2593
|
idleTimeoutMs;
|
|
2594
|
+
defaultModels;
|
|
2259
2595
|
// Serialize meta.json read-modify-write operations per session id so
|
|
2260
2596
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
2261
2597
|
// back-to-back) don't lose writes via interleaved reads.
|
|
@@ -2265,7 +2601,8 @@ var SessionManager = class {
|
|
|
2265
2601
|
agentId: params.agentId,
|
|
2266
2602
|
cwd: params.cwd,
|
|
2267
2603
|
agentArgs: params.agentArgs,
|
|
2268
|
-
mcpServers: params.mcpServers
|
|
2604
|
+
mcpServers: params.mcpServers,
|
|
2605
|
+
model: params.model
|
|
2269
2606
|
});
|
|
2270
2607
|
const session = new Session({
|
|
2271
2608
|
cwd: params.cwd,
|
|
@@ -2277,7 +2614,8 @@ var SessionManager = class {
|
|
|
2277
2614
|
agentArgs: params.agentArgs,
|
|
2278
2615
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2279
2616
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2280
|
-
historyStore: this.histories
|
|
2617
|
+
historyStore: this.histories,
|
|
2618
|
+
currentModel: fresh.initialModel
|
|
2281
2619
|
});
|
|
2282
2620
|
await this.attachManagerHooks(session);
|
|
2283
2621
|
return session;
|
|
@@ -2322,7 +2660,7 @@ var SessionManager = class {
|
|
|
2322
2660
|
if (params.upstreamSessionId === "") {
|
|
2323
2661
|
return this.doResurrectFromImport(params);
|
|
2324
2662
|
}
|
|
2325
|
-
const plan = planSpawn(agentDef, params.agentArgs ?? []);
|
|
2663
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
2326
2664
|
const agent = this.spawner({
|
|
2327
2665
|
agentId: params.agentId,
|
|
2328
2666
|
cwd: params.cwd,
|
|
@@ -2335,11 +2673,14 @@ var SessionManager = class {
|
|
|
2335
2673
|
});
|
|
2336
2674
|
let loadResult;
|
|
2337
2675
|
try {
|
|
2338
|
-
loadResult = await agent.connection.request(
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2676
|
+
loadResult = await agent.connection.request(
|
|
2677
|
+
"session/load",
|
|
2678
|
+
{
|
|
2679
|
+
sessionId: params.upstreamSessionId,
|
|
2680
|
+
cwd: params.cwd,
|
|
2681
|
+
mcpServers: []
|
|
2682
|
+
}
|
|
2683
|
+
);
|
|
2343
2684
|
} catch (err) {
|
|
2344
2685
|
await agent.kill().catch(() => void 0);
|
|
2345
2686
|
throw new Error(
|
|
@@ -2358,8 +2699,13 @@ var SessionManager = class {
|
|
|
2358
2699
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2359
2700
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2360
2701
|
historyStore: this.histories,
|
|
2361
|
-
|
|
2702
|
+
// Prefer what we previously stored from a current_model_update; if
|
|
2703
|
+
// we never captured one (e.g. old opencode sessions on disk before
|
|
2704
|
+
// this fix), fall back to the model the agent ships in its
|
|
2705
|
+
// session/load response body.
|
|
2706
|
+
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
2362
2707
|
currentMode: params.currentMode,
|
|
2708
|
+
currentUsage: params.currentUsage,
|
|
2363
2709
|
agentCommands: params.agentCommands,
|
|
2364
2710
|
// Only gate the first-prompt title heuristic when we actually have
|
|
2365
2711
|
// a title to preserve. A title-less session (lost to a write race
|
|
@@ -2397,8 +2743,11 @@ var SessionManager = class {
|
|
|
2397
2743
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2398
2744
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2399
2745
|
historyStore: this.histories,
|
|
2400
|
-
|
|
2746
|
+
// Prefer the stored value (set by a previous current_model_update);
|
|
2747
|
+
// fall back to whatever the agent ships in its session/new response.
|
|
2748
|
+
currentModel: params.currentModel ?? fresh.initialModel,
|
|
2401
2749
|
currentMode: params.currentMode,
|
|
2750
|
+
currentUsage: params.currentUsage,
|
|
2402
2751
|
agentCommands: params.agentCommands,
|
|
2403
2752
|
firstPromptSeeded: !!params.title,
|
|
2404
2753
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
@@ -2408,7 +2757,7 @@ var SessionManager = class {
|
|
|
2408
2757
|
return session;
|
|
2409
2758
|
}
|
|
2410
2759
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
2411
|
-
// → session/new. Shared by create() and the /hydra
|
|
2760
|
+
// → session/new. Shared by create() and the /hydra agent path so both
|
|
2412
2761
|
// go through the same env / capabilities / error-handling.
|
|
2413
2762
|
async bootstrapAgent(params) {
|
|
2414
2763
|
const agentDef = await this.registry.getAgent(params.agentId);
|
|
@@ -2419,7 +2768,7 @@ var SessionManager = class {
|
|
|
2419
2768
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
2420
2769
|
throw err;
|
|
2421
2770
|
}
|
|
2422
|
-
const plan = planSpawn(agentDef, params.agentArgs ?? []);
|
|
2771
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
2423
2772
|
const agent = this.spawner({
|
|
2424
2773
|
agentId: params.agentId,
|
|
2425
2774
|
cwd: params.cwd,
|
|
@@ -2431,14 +2780,36 @@ var SessionManager = class {
|
|
|
2431
2780
|
clientCapabilities: {},
|
|
2432
2781
|
clientInfo: { name: "hydra", version: "0.1.0" }
|
|
2433
2782
|
});
|
|
2434
|
-
const newResult = await agent.connection.request(
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2783
|
+
const newResult = await agent.connection.request(
|
|
2784
|
+
"session/new",
|
|
2785
|
+
{
|
|
2786
|
+
cwd: params.cwd,
|
|
2787
|
+
mcpServers: params.mcpServers ?? []
|
|
2788
|
+
}
|
|
2789
|
+
);
|
|
2790
|
+
const sessionIdRaw = newResult.sessionId;
|
|
2791
|
+
if (typeof sessionIdRaw !== "string") {
|
|
2792
|
+
throw new Error(
|
|
2793
|
+
`agent ${params.agentId} returned a non-string sessionId from session/new`
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
let initialModel = extractInitialModel(newResult);
|
|
2797
|
+
const desired = params.model ?? this.defaultModels[params.agentId];
|
|
2798
|
+
if (desired && desired !== initialModel) {
|
|
2799
|
+
try {
|
|
2800
|
+
await agent.connection.request("session/set_model", {
|
|
2801
|
+
sessionId: sessionIdRaw,
|
|
2802
|
+
modelId: desired
|
|
2803
|
+
});
|
|
2804
|
+
initialModel = desired;
|
|
2805
|
+
} catch {
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2438
2808
|
return {
|
|
2439
2809
|
agent,
|
|
2440
|
-
upstreamSessionId:
|
|
2441
|
-
agentMeta: newResult._meta
|
|
2810
|
+
upstreamSessionId: sessionIdRaw,
|
|
2811
|
+
agentMeta: newResult._meta,
|
|
2812
|
+
initialModel
|
|
2442
2813
|
};
|
|
2443
2814
|
} catch (err) {
|
|
2444
2815
|
await agent.kill().catch(() => void 0);
|
|
@@ -2449,7 +2820,7 @@ var SessionManager = class {
|
|
|
2449
2820
|
// bookkeeping. Called from both create() and resurrect() so the same
|
|
2450
2821
|
// session record + lifecycle handlers are wired regardless of origin.
|
|
2451
2822
|
// Returns once the initial disk record is written — callers should
|
|
2452
|
-
// await so a subsequent /hydra
|
|
2823
|
+
// await so a subsequent /hydra agent's persistAgentChange (which
|
|
2453
2824
|
// does read-then-write) finds the file in place.
|
|
2454
2825
|
async attachManagerHooks(session) {
|
|
2455
2826
|
session.onClose(({ deleteRecord }) => {
|
|
@@ -2477,6 +2848,11 @@ var SessionManager = class {
|
|
|
2477
2848
|
() => void 0
|
|
2478
2849
|
);
|
|
2479
2850
|
});
|
|
2851
|
+
session.onUsageChange((usage) => {
|
|
2852
|
+
void this.persistSnapshot(session.sessionId, {
|
|
2853
|
+
currentUsage: usageSnapshotToPersisted(usage)
|
|
2854
|
+
}).catch(() => void 0);
|
|
2855
|
+
});
|
|
2480
2856
|
session.onAgentCommandsChange((commands) => {
|
|
2481
2857
|
void this.persistSnapshot(session.sessionId, {
|
|
2482
2858
|
agentCommands: commands.map((c) => ({
|
|
@@ -2525,6 +2901,7 @@ var SessionManager = class {
|
|
|
2525
2901
|
agentArgs: record.agentArgs,
|
|
2526
2902
|
currentModel: record.currentModel,
|
|
2527
2903
|
currentMode: record.currentMode,
|
|
2904
|
+
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
2528
2905
|
agentCommands: record.agentCommands,
|
|
2529
2906
|
createdAt: record.createdAt
|
|
2530
2907
|
};
|
|
@@ -2593,6 +2970,8 @@ var SessionManager = class {
|
|
|
2593
2970
|
cwd: session.cwd,
|
|
2594
2971
|
title: session.title,
|
|
2595
2972
|
agentId: session.agentId,
|
|
2973
|
+
currentModel: session.currentModel,
|
|
2974
|
+
currentUsage: session.currentUsage,
|
|
2596
2975
|
updatedAt: used,
|
|
2597
2976
|
attachedClients: session.attachedCount,
|
|
2598
2977
|
status: "live"
|
|
@@ -2613,6 +2992,8 @@ var SessionManager = class {
|
|
|
2613
2992
|
cwd: r.cwd,
|
|
2614
2993
|
title: r.title,
|
|
2615
2994
|
agentId: r.agentId,
|
|
2995
|
+
currentModel: r.currentModel,
|
|
2996
|
+
currentUsage: r.currentUsage,
|
|
2616
2997
|
updatedAt: used,
|
|
2617
2998
|
attachedClients: 0,
|
|
2618
2999
|
status: "cold"
|
|
@@ -2720,6 +3101,7 @@ var SessionManager = class {
|
|
|
2720
3101
|
title: args.bundle.session.title,
|
|
2721
3102
|
currentModel: args.bundle.session.currentModel,
|
|
2722
3103
|
currentMode: args.bundle.session.currentMode,
|
|
3104
|
+
currentUsage: args.bundle.session.currentUsage,
|
|
2723
3105
|
agentCommands: args.bundle.session.agentCommands,
|
|
2724
3106
|
createdAt: args.preservedCreatedAt ?? now,
|
|
2725
3107
|
updatedAt: now
|
|
@@ -2755,7 +3137,7 @@ var SessionManager = class {
|
|
|
2755
3137
|
});
|
|
2756
3138
|
});
|
|
2757
3139
|
}
|
|
2758
|
-
// Persist an agent swap from /hydra
|
|
3140
|
+
// Persist an agent swap from /hydra agent. The on-disk record's
|
|
2759
3141
|
// agentId + upstreamSessionId both rotate so a daemon restart (and
|
|
2760
3142
|
// later resurrect) brings the session back up on the agent the user
|
|
2761
3143
|
// most recently switched to, not the one it was originally created on.
|
|
@@ -2787,6 +3169,7 @@ var SessionManager = class {
|
|
|
2787
3169
|
...record,
|
|
2788
3170
|
...update.currentModel !== void 0 ? { currentModel: update.currentModel } : {},
|
|
2789
3171
|
...update.currentMode !== void 0 ? { currentMode: update.currentMode } : {},
|
|
3172
|
+
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
2790
3173
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
2791
3174
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2792
3175
|
});
|
|
@@ -2841,13 +3224,73 @@ function mergeForPersistence(session, existing) {
|
|
|
2841
3224
|
agentArgs: session.agentArgs,
|
|
2842
3225
|
currentModel: session.currentModel ?? existing?.currentModel,
|
|
2843
3226
|
currentMode: session.currentMode ?? existing?.currentMode,
|
|
3227
|
+
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
2844
3228
|
agentCommands,
|
|
2845
3229
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
2846
3230
|
});
|
|
2847
3231
|
}
|
|
3232
|
+
function usageSnapshotToPersisted(usage) {
|
|
3233
|
+
if (!usage) {
|
|
3234
|
+
return void 0;
|
|
3235
|
+
}
|
|
3236
|
+
const out = {};
|
|
3237
|
+
if (usage.used !== void 0) {
|
|
3238
|
+
out.used = usage.used;
|
|
3239
|
+
}
|
|
3240
|
+
if (usage.size !== void 0) {
|
|
3241
|
+
out.size = usage.size;
|
|
3242
|
+
}
|
|
3243
|
+
if (usage.costAmount !== void 0) {
|
|
3244
|
+
out.costAmount = usage.costAmount;
|
|
3245
|
+
}
|
|
3246
|
+
if (usage.costCurrency !== void 0) {
|
|
3247
|
+
out.costCurrency = usage.costCurrency;
|
|
3248
|
+
}
|
|
3249
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
3250
|
+
}
|
|
3251
|
+
function persistedUsageToSnapshot(usage) {
|
|
3252
|
+
return usage ? { ...usage } : void 0;
|
|
3253
|
+
}
|
|
3254
|
+
function extractInitialModel(result) {
|
|
3255
|
+
const direct = asString(result.currentModelId) ?? asString(result.currentModel) ?? asString(result.modelId) ?? asString(result.model);
|
|
3256
|
+
if (direct) {
|
|
3257
|
+
return direct;
|
|
3258
|
+
}
|
|
3259
|
+
const models = result.models;
|
|
3260
|
+
if (models && typeof models === "object" && !Array.isArray(models)) {
|
|
3261
|
+
const m = asString(models.currentModelId) ?? asString(models.currentModel);
|
|
3262
|
+
if (m) {
|
|
3263
|
+
return m;
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
const meta = result._meta;
|
|
3267
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
3268
|
+
for (const [key, value] of Object.entries(
|
|
3269
|
+
meta
|
|
3270
|
+
)) {
|
|
3271
|
+
if (key === "hydra-acp") {
|
|
3272
|
+
continue;
|
|
3273
|
+
}
|
|
3274
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
3275
|
+
const m = asString(value.modelId) ?? asString(value.model) ?? asString(value.currentModelId);
|
|
3276
|
+
if (m) {
|
|
3277
|
+
return m;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
return void 0;
|
|
3283
|
+
}
|
|
3284
|
+
function asString(value) {
|
|
3285
|
+
if (typeof value !== "string") {
|
|
3286
|
+
return void 0;
|
|
3287
|
+
}
|
|
3288
|
+
const trimmed = value.trim();
|
|
3289
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
3290
|
+
}
|
|
2848
3291
|
async function loadPromptHistorySafely(sessionId) {
|
|
2849
3292
|
try {
|
|
2850
|
-
const raw = await
|
|
3293
|
+
const raw = await fs7.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
2851
3294
|
const out = [];
|
|
2852
3295
|
for (const line of raw.split("\n")) {
|
|
2853
3296
|
if (line.length === 0) {
|
|
@@ -2868,7 +3311,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
2868
3311
|
}
|
|
2869
3312
|
async function historyMtimeIso(sessionId) {
|
|
2870
3313
|
try {
|
|
2871
|
-
const st = await
|
|
3314
|
+
const st = await fs7.stat(paths.historyFile(sessionId));
|
|
2872
3315
|
return new Date(st.mtimeMs).toISOString();
|
|
2873
3316
|
} catch {
|
|
2874
3317
|
return void 0;
|
|
@@ -2876,10 +3319,10 @@ async function historyMtimeIso(sessionId) {
|
|
|
2876
3319
|
}
|
|
2877
3320
|
|
|
2878
3321
|
// src/core/extensions.ts
|
|
2879
|
-
import { spawn as
|
|
2880
|
-
import * as
|
|
2881
|
-
import * as
|
|
2882
|
-
import * as
|
|
3322
|
+
import { spawn as spawn3 } from "child_process";
|
|
3323
|
+
import * as fs8 from "fs";
|
|
3324
|
+
import * as fsp2 from "fs/promises";
|
|
3325
|
+
import * as path5 from "path";
|
|
2883
3326
|
var RESTART_BASE_MS = 1e3;
|
|
2884
3327
|
var RESTART_CAP_MS = 6e4;
|
|
2885
3328
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -2900,7 +3343,7 @@ var ExtensionManager = class {
|
|
|
2900
3343
|
if (!this.context) {
|
|
2901
3344
|
throw new Error("ExtensionManager: setContext must be called before start");
|
|
2902
3345
|
}
|
|
2903
|
-
await
|
|
3346
|
+
await fsp2.mkdir(paths.extensionsDir(), { recursive: true });
|
|
2904
3347
|
await this.reapOrphans();
|
|
2905
3348
|
for (const entry of this.entries.values()) {
|
|
2906
3349
|
if (!entry.config.enabled) {
|
|
@@ -2926,9 +3369,9 @@ var ExtensionManager = class {
|
|
|
2926
3369
|
} catch {
|
|
2927
3370
|
}
|
|
2928
3371
|
tasks.push(
|
|
2929
|
-
new Promise((
|
|
3372
|
+
new Promise((resolve3) => {
|
|
2930
3373
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
2931
|
-
|
|
3374
|
+
resolve3();
|
|
2932
3375
|
return;
|
|
2933
3376
|
}
|
|
2934
3377
|
const timer = setTimeout(() => {
|
|
@@ -2936,11 +3379,11 @@ var ExtensionManager = class {
|
|
|
2936
3379
|
child.kill("SIGKILL");
|
|
2937
3380
|
} catch {
|
|
2938
3381
|
}
|
|
2939
|
-
|
|
3382
|
+
resolve3();
|
|
2940
3383
|
}, STOP_GRACE_MS);
|
|
2941
3384
|
child.on("exit", () => {
|
|
2942
3385
|
clearTimeout(timer);
|
|
2943
|
-
|
|
3386
|
+
resolve3();
|
|
2944
3387
|
});
|
|
2945
3388
|
})
|
|
2946
3389
|
);
|
|
@@ -3048,8 +3491,8 @@ var ExtensionManager = class {
|
|
|
3048
3491
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
3049
3492
|
return;
|
|
3050
3493
|
}
|
|
3051
|
-
const exited = new Promise((
|
|
3052
|
-
entry.exitWaiters.push(
|
|
3494
|
+
const exited = new Promise((resolve3) => {
|
|
3495
|
+
entry.exitWaiters.push(resolve3);
|
|
3053
3496
|
});
|
|
3054
3497
|
try {
|
|
3055
3498
|
child.kill("SIGTERM");
|
|
@@ -3109,7 +3552,7 @@ var ExtensionManager = class {
|
|
|
3109
3552
|
async reapOrphans() {
|
|
3110
3553
|
let entries;
|
|
3111
3554
|
try {
|
|
3112
|
-
entries = await
|
|
3555
|
+
entries = await fsp2.readdir(paths.extensionsDir());
|
|
3113
3556
|
} catch (err) {
|
|
3114
3557
|
const e = err;
|
|
3115
3558
|
if (e.code === "ENOENT") {
|
|
@@ -3121,10 +3564,10 @@ var ExtensionManager = class {
|
|
|
3121
3564
|
if (!entry.endsWith(".pid")) {
|
|
3122
3565
|
continue;
|
|
3123
3566
|
}
|
|
3124
|
-
const pidPath =
|
|
3567
|
+
const pidPath = path5.join(paths.extensionsDir(), entry);
|
|
3125
3568
|
let pid;
|
|
3126
3569
|
try {
|
|
3127
|
-
const raw = await
|
|
3570
|
+
const raw = await fsp2.readFile(pidPath, "utf8");
|
|
3128
3571
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
3129
3572
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
3130
3573
|
pid = parsed;
|
|
@@ -3147,7 +3590,7 @@ var ExtensionManager = class {
|
|
|
3147
3590
|
}
|
|
3148
3591
|
}
|
|
3149
3592
|
}
|
|
3150
|
-
await
|
|
3593
|
+
await fsp2.unlink(pidPath).catch(() => void 0);
|
|
3151
3594
|
}
|
|
3152
3595
|
}
|
|
3153
3596
|
spawn(entry, attempt) {
|
|
@@ -3160,7 +3603,7 @@ var ExtensionManager = class {
|
|
|
3160
3603
|
}
|
|
3161
3604
|
const ext = entry.config;
|
|
3162
3605
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
3163
|
-
const logStream =
|
|
3606
|
+
const logStream = fs8.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
3164
3607
|
flags: "a"
|
|
3165
3608
|
});
|
|
3166
3609
|
logStream.write(
|
|
@@ -3188,7 +3631,7 @@ var ExtensionManager = class {
|
|
|
3188
3631
|
const args = [...baseArgs, ...ext.args];
|
|
3189
3632
|
let child;
|
|
3190
3633
|
try {
|
|
3191
|
-
child =
|
|
3634
|
+
child = spawn3(cmd, args, {
|
|
3192
3635
|
env,
|
|
3193
3636
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3194
3637
|
detached: false
|
|
@@ -3210,7 +3653,7 @@ var ExtensionManager = class {
|
|
|
3210
3653
|
}
|
|
3211
3654
|
if (typeof child.pid === "number") {
|
|
3212
3655
|
try {
|
|
3213
|
-
|
|
3656
|
+
fs8.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
3214
3657
|
`, {
|
|
3215
3658
|
encoding: "utf8",
|
|
3216
3659
|
mode: 384
|
|
@@ -3235,7 +3678,7 @@ var ExtensionManager = class {
|
|
|
3235
3678
|
});
|
|
3236
3679
|
child.on("exit", (code, signal) => {
|
|
3237
3680
|
try {
|
|
3238
|
-
|
|
3681
|
+
fs8.unlinkSync(paths.extensionPidFile(ext.name));
|
|
3239
3682
|
} catch {
|
|
3240
3683
|
}
|
|
3241
3684
|
logStream.write(
|
|
@@ -3246,8 +3689,8 @@ var ExtensionManager = class {
|
|
|
3246
3689
|
entry.pid = void 0;
|
|
3247
3690
|
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
3248
3691
|
const waiters = entry.exitWaiters.splice(0);
|
|
3249
|
-
for (const
|
|
3250
|
-
|
|
3692
|
+
for (const resolve3 of waiters) {
|
|
3693
|
+
resolve3();
|
|
3251
3694
|
}
|
|
3252
3695
|
if (this.stopping || entry.manuallyStopped) {
|
|
3253
3696
|
try {
|
|
@@ -3365,6 +3808,7 @@ var BundleSession = z5.object({
|
|
|
3365
3808
|
title: z5.string().optional(),
|
|
3366
3809
|
currentModel: z5.string().optional(),
|
|
3367
3810
|
currentMode: z5.string().optional(),
|
|
3811
|
+
currentUsage: PersistedUsage.optional(),
|
|
3368
3812
|
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
3369
3813
|
createdAt: z5.string(),
|
|
3370
3814
|
updatedAt: z5.string()
|
|
@@ -3396,6 +3840,7 @@ function encodeBundle(params) {
|
|
|
3396
3840
|
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
3397
3841
|
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
3398
3842
|
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
3843
|
+
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
3399
3844
|
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
3400
3845
|
createdAt: params.record.createdAt,
|
|
3401
3846
|
updatedAt: params.record.updatedAt
|
|
@@ -3794,13 +4239,13 @@ function wsToMessageStream(ws) {
|
|
|
3794
4239
|
throw new Error("ws is closed");
|
|
3795
4240
|
}
|
|
3796
4241
|
const text = JSON.stringify(message);
|
|
3797
|
-
await new Promise((
|
|
4242
|
+
await new Promise((resolve3, reject) => {
|
|
3798
4243
|
ws.send(text, (err) => {
|
|
3799
4244
|
if (err) {
|
|
3800
4245
|
reject(err);
|
|
3801
4246
|
return;
|
|
3802
4247
|
}
|
|
3803
|
-
|
|
4248
|
+
resolve3();
|
|
3804
4249
|
});
|
|
3805
4250
|
});
|
|
3806
4251
|
},
|
|
@@ -3864,7 +4309,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
3864
4309
|
agentId: params.agentId ?? deps.defaultAgent,
|
|
3865
4310
|
mcpServers: params.mcpServers,
|
|
3866
4311
|
title: hydraMeta.name,
|
|
3867
|
-
agentArgs: hydraMeta.agentArgs
|
|
4312
|
+
agentArgs: hydraMeta.agentArgs,
|
|
4313
|
+
model: hydraMeta.model
|
|
3868
4314
|
});
|
|
3869
4315
|
const client = bindClientToSession(connection, session, state);
|
|
3870
4316
|
await session.attach(client, "full");
|
|
@@ -4128,10 +4574,10 @@ var HYDRA_VERSION3 = "0.1.0";
|
|
|
4128
4574
|
async function startDaemon(config) {
|
|
4129
4575
|
ensureLoopbackOrTls(config);
|
|
4130
4576
|
const httpsOptions = config.daemon.tls ? {
|
|
4131
|
-
key: await
|
|
4132
|
-
cert: await
|
|
4577
|
+
key: await fsp3.readFile(config.daemon.tls.key),
|
|
4578
|
+
cert: await fsp3.readFile(config.daemon.tls.cert)
|
|
4133
4579
|
} : void 0;
|
|
4134
|
-
await
|
|
4580
|
+
await fsp3.mkdir(paths.home(), { recursive: true });
|
|
4135
4581
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
4136
4582
|
config.daemon.logLevel
|
|
4137
4583
|
);
|
|
@@ -4143,6 +4589,9 @@ async function startDaemon(config) {
|
|
|
4143
4589
|
https: httpsOptions ?? null
|
|
4144
4590
|
});
|
|
4145
4591
|
await app.register(websocketPlugin);
|
|
4592
|
+
setBinaryInstallLogger((msg) => {
|
|
4593
|
+
app.log.info(msg);
|
|
4594
|
+
});
|
|
4146
4595
|
const auth = bearerAuth({ config });
|
|
4147
4596
|
app.addHook("onRequest", async (request, reply) => {
|
|
4148
4597
|
if (request.routeOptions.config?.skipAuth) {
|
|
@@ -4155,7 +4604,8 @@ async function startDaemon(config) {
|
|
|
4155
4604
|
});
|
|
4156
4605
|
const registry = new Registry(config);
|
|
4157
4606
|
const manager = new SessionManager(registry, void 0, void 0, {
|
|
4158
|
-
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3
|
|
4607
|
+
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
4608
|
+
defaultModels: config.defaultModels
|
|
4159
4609
|
});
|
|
4160
4610
|
const extensions = new ExtensionManager(extensionList(config));
|
|
4161
4611
|
registerHealthRoutes(app, HYDRA_VERSION3);
|
|
@@ -4177,8 +4627,8 @@ async function startDaemon(config) {
|
|
|
4177
4627
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
4178
4628
|
const address = app.server.address();
|
|
4179
4629
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
4180
|
-
await
|
|
4181
|
-
await
|
|
4630
|
+
await fsp3.mkdir(paths.home(), { recursive: true });
|
|
4631
|
+
await fsp3.writeFile(
|
|
4182
4632
|
paths.pidFile(),
|
|
4183
4633
|
JSON.stringify({
|
|
4184
4634
|
pid: process.pid,
|
|
@@ -4203,9 +4653,10 @@ async function startDaemon(config) {
|
|
|
4203
4653
|
await extensions.stop();
|
|
4204
4654
|
await manager.closeAll();
|
|
4205
4655
|
await manager.flushMetaWrites();
|
|
4656
|
+
setBinaryInstallLogger(null);
|
|
4206
4657
|
await app.close();
|
|
4207
4658
|
try {
|
|
4208
|
-
|
|
4659
|
+
fs9.unlinkSync(paths.pidFile());
|
|
4209
4660
|
} catch {
|
|
4210
4661
|
}
|
|
4211
4662
|
try {
|