@hydra-acp/cli 0.1.4 → 0.1.5
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 +3 -3
- package/dist/cli.js +785 -237
- package/dist/index.d.ts +432 -31
- package/dist/index.js +570 -124
- 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";
|
|
@@ -481,12 +738,24 @@ var SessionListParams = z3.object({
|
|
|
481
738
|
cursor: z3.string().optional(),
|
|
482
739
|
limit: z3.number().int().positive().max(200).optional()
|
|
483
740
|
});
|
|
741
|
+
var SessionListUsage = z3.object({
|
|
742
|
+
used: z3.number().optional(),
|
|
743
|
+
size: z3.number().optional(),
|
|
744
|
+
costAmount: z3.number().optional(),
|
|
745
|
+
costCurrency: z3.string().optional()
|
|
746
|
+
});
|
|
484
747
|
var SessionListEntry = z3.object({
|
|
485
748
|
sessionId: z3.string(),
|
|
486
749
|
upstreamSessionId: z3.string().optional(),
|
|
487
750
|
cwd: z3.string(),
|
|
488
751
|
title: z3.string().optional(),
|
|
489
752
|
agentId: z3.string().optional(),
|
|
753
|
+
// Last-known model id, so list views can render `<agent>(<model>)`
|
|
754
|
+
// without resurrecting cold sessions to look it up.
|
|
755
|
+
currentModel: z3.string().optional(),
|
|
756
|
+
// Last-known usage snapshot so list views can show per-session cost
|
|
757
|
+
// (and tokens, in callers that care) without resurrecting cold sessions.
|
|
758
|
+
currentUsage: SessionListUsage.optional(),
|
|
490
759
|
updatedAt: z3.string(),
|
|
491
760
|
attachedClients: z3.number().int().nonnegative(),
|
|
492
761
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
@@ -568,13 +837,13 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
568
837
|
throw new Error("stream is closed");
|
|
569
838
|
}
|
|
570
839
|
const line = JSON.stringify(message) + "\n";
|
|
571
|
-
await new Promise((
|
|
840
|
+
await new Promise((resolve3, reject) => {
|
|
572
841
|
stdin.write(line, (err) => {
|
|
573
842
|
if (err) {
|
|
574
843
|
reject(err);
|
|
575
844
|
return;
|
|
576
845
|
}
|
|
577
|
-
|
|
846
|
+
resolve3();
|
|
578
847
|
});
|
|
579
848
|
});
|
|
580
849
|
},
|
|
@@ -633,9 +902,9 @@ var JsonRpcConnection = class {
|
|
|
633
902
|
}
|
|
634
903
|
const id = nanoid();
|
|
635
904
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
636
|
-
const response = new Promise((
|
|
905
|
+
const response = new Promise((resolve3, reject) => {
|
|
637
906
|
this.pending.set(id, {
|
|
638
|
-
resolve: (result) =>
|
|
907
|
+
resolve: (result) => resolve3(result),
|
|
639
908
|
reject
|
|
640
909
|
});
|
|
641
910
|
this.stream.send(message).catch((err) => {
|
|
@@ -773,7 +1042,7 @@ var AgentInstance = class _AgentInstance {
|
|
|
773
1042
|
...opts.plan.env,
|
|
774
1043
|
...opts.extraEnv ?? {}
|
|
775
1044
|
};
|
|
776
|
-
const child =
|
|
1045
|
+
const child = spawn2(opts.plan.command, opts.plan.args, {
|
|
777
1046
|
cwd: opts.cwd,
|
|
778
1047
|
env,
|
|
779
1048
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -806,8 +1075,8 @@ var HYDRA_COMMANDS = [
|
|
|
806
1075
|
description: "Regenerate the session title via the agent (or set manually with an arg)"
|
|
807
1076
|
},
|
|
808
1077
|
{
|
|
809
|
-
verb: "
|
|
810
|
-
name: "/hydra
|
|
1078
|
+
verb: "agent",
|
|
1079
|
+
name: "/hydra agent",
|
|
811
1080
|
argsHint: "<agent>",
|
|
812
1081
|
description: "Swap the agent backing this session, preserving context"
|
|
813
1082
|
}
|
|
@@ -829,7 +1098,7 @@ var COMPACT_EVERY = 200;
|
|
|
829
1098
|
var Session = class {
|
|
830
1099
|
sessionId;
|
|
831
1100
|
cwd;
|
|
832
|
-
// agent / agentId / upstreamSessionId are mutable so /hydra
|
|
1101
|
+
// agent / agentId / upstreamSessionId are mutable so /hydra agent can
|
|
833
1102
|
// replace the underlying agent process while keeping the same Session
|
|
834
1103
|
// record. agentMeta is the metadata returned by the agent at session/new
|
|
835
1104
|
// time; it gets refreshed on switch too.
|
|
@@ -844,6 +1113,7 @@ var Session = class {
|
|
|
844
1113
|
// stale-prone for snapshot-shaped events).
|
|
845
1114
|
currentModel;
|
|
846
1115
|
currentMode;
|
|
1116
|
+
currentUsage;
|
|
847
1117
|
updatedAt;
|
|
848
1118
|
createdAt;
|
|
849
1119
|
clients = /* @__PURE__ */ new Map();
|
|
@@ -905,6 +1175,7 @@ var Session = class {
|
|
|
905
1175
|
agentCommandsHandlers = [];
|
|
906
1176
|
modelHandlers = [];
|
|
907
1177
|
modeHandlers = [];
|
|
1178
|
+
usageHandlers = [];
|
|
908
1179
|
constructor(init) {
|
|
909
1180
|
this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
|
|
910
1181
|
this.cwd = init.cwd;
|
|
@@ -916,6 +1187,7 @@ var Session = class {
|
|
|
916
1187
|
this.title = init.title;
|
|
917
1188
|
this.currentModel = init.currentModel;
|
|
918
1189
|
this.currentMode = init.currentMode;
|
|
1190
|
+
this.currentUsage = init.currentUsage;
|
|
919
1191
|
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
920
1192
|
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
921
1193
|
}
|
|
@@ -945,7 +1217,7 @@ var Session = class {
|
|
|
945
1217
|
});
|
|
946
1218
|
}
|
|
947
1219
|
// Register session/update, session/request_permission, and onExit
|
|
948
|
-
// handlers on an agent connection. Re-run on every /hydra
|
|
1220
|
+
// handlers on an agent connection. Re-run on every /hydra agent so
|
|
949
1221
|
// the new agent is plumbed identically. The exit handler's identity
|
|
950
1222
|
// check is what makes switching safe: when the *old* agent exits as
|
|
951
1223
|
// part of a swap, this.agent has already been replaced, so we no-op
|
|
@@ -969,6 +1241,10 @@ var Session = class {
|
|
|
969
1241
|
this.recordAndBroadcast("session/update", params);
|
|
970
1242
|
return;
|
|
971
1243
|
}
|
|
1244
|
+
if (this.maybeApplyAgentUsage(params)) {
|
|
1245
|
+
this.recordAndBroadcast("session/update", params);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
972
1248
|
this.maybeApplyAgentSessionInfo(params);
|
|
973
1249
|
this.recordAndBroadcast("session/update", params);
|
|
974
1250
|
});
|
|
@@ -1286,6 +1562,49 @@ var Session = class {
|
|
|
1286
1562
|
}
|
|
1287
1563
|
return true;
|
|
1288
1564
|
}
|
|
1565
|
+
// usage_update carries any subset of {used, size, cost.amount,
|
|
1566
|
+
// cost.currency}. Merge non-undefined fields onto currentUsage so a
|
|
1567
|
+
// sparse update preserves prior values, and fire usage handlers only
|
|
1568
|
+
// if something actually changed.
|
|
1569
|
+
maybeApplyAgentUsage(params) {
|
|
1570
|
+
const obj = params ?? {};
|
|
1571
|
+
const update = obj.update ?? {};
|
|
1572
|
+
if (update.sessionUpdate !== "usage_update") {
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
const next = { ...this.currentUsage ?? {} };
|
|
1576
|
+
let changed = false;
|
|
1577
|
+
if (typeof update.used === "number" && next.used !== update.used) {
|
|
1578
|
+
next.used = update.used;
|
|
1579
|
+
changed = true;
|
|
1580
|
+
}
|
|
1581
|
+
if (typeof update.size === "number" && next.size !== update.size) {
|
|
1582
|
+
next.size = update.size;
|
|
1583
|
+
changed = true;
|
|
1584
|
+
}
|
|
1585
|
+
if (update.cost && typeof update.cost === "object") {
|
|
1586
|
+
const cost = update.cost;
|
|
1587
|
+
if (typeof cost.amount === "number" && next.costAmount !== cost.amount) {
|
|
1588
|
+
next.costAmount = cost.amount;
|
|
1589
|
+
changed = true;
|
|
1590
|
+
}
|
|
1591
|
+
if (typeof cost.currency === "string" && next.costCurrency !== cost.currency) {
|
|
1592
|
+
next.costCurrency = cost.currency;
|
|
1593
|
+
changed = true;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
if (!changed) {
|
|
1597
|
+
return true;
|
|
1598
|
+
}
|
|
1599
|
+
this.currentUsage = next;
|
|
1600
|
+
for (const handler of this.usageHandlers) {
|
|
1601
|
+
try {
|
|
1602
|
+
handler(next);
|
|
1603
|
+
} catch {
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return true;
|
|
1607
|
+
}
|
|
1289
1608
|
// Update the cached agent command list, fire persist handlers, and
|
|
1290
1609
|
// broadcast the merged list to attached clients. Idempotent on a
|
|
1291
1610
|
// structurally identical list so we don't churn meta.json on noisy
|
|
@@ -1316,6 +1635,9 @@ var Session = class {
|
|
|
1316
1635
|
onModeChange(handler) {
|
|
1317
1636
|
this.modeHandlers.push(handler);
|
|
1318
1637
|
}
|
|
1638
|
+
onUsageChange(handler) {
|
|
1639
|
+
this.usageHandlers.push(handler);
|
|
1640
|
+
}
|
|
1319
1641
|
// Returns a freshly merged command list (hydra ∪ agent) for callers
|
|
1320
1642
|
// that need a snapshot — notably acp-ws.ts's buildResponseMeta when
|
|
1321
1643
|
// assembling the attach response.
|
|
@@ -1378,8 +1700,8 @@ var Session = class {
|
|
|
1378
1700
|
switch (verb) {
|
|
1379
1701
|
case "title":
|
|
1380
1702
|
return this.runTitleCommand(arg);
|
|
1381
|
-
case "
|
|
1382
|
-
return this.
|
|
1703
|
+
case "agent":
|
|
1704
|
+
return this.runAgentCommand(arg);
|
|
1383
1705
|
default: {
|
|
1384
1706
|
const err = new Error(
|
|
1385
1707
|
`no dispatcher for /hydra verb ${verb}`
|
|
@@ -1415,7 +1737,7 @@ var Session = class {
|
|
|
1415
1737
|
}
|
|
1416
1738
|
// Send a prompt to the underlying agent and capture its reply chunks
|
|
1417
1739
|
// privately (no fan-out to clients, no recording into history). Used
|
|
1418
|
-
// by /hydra title's regen path and /hydra
|
|
1740
|
+
// by /hydra title's regen path and /hydra agent's transcript-injection
|
|
1419
1741
|
// path. Returns the joined agent_message_chunk text.
|
|
1420
1742
|
async runInternalPrompt(text) {
|
|
1421
1743
|
if (this.internalPromptCapture) {
|
|
@@ -1437,10 +1759,10 @@ var Session = class {
|
|
|
1437
1759
|
// record. Spawns the new agent first so a failure leaves the old one
|
|
1438
1760
|
// intact; then injects a synthesized transcript so the new agent has
|
|
1439
1761
|
// context for the next turn.
|
|
1440
|
-
|
|
1762
|
+
runAgentCommand(newAgentId) {
|
|
1441
1763
|
if (!newAgentId) {
|
|
1442
1764
|
throw withCode(
|
|
1443
|
-
new Error("/hydra
|
|
1765
|
+
new Error("/hydra agent requires an agent id"),
|
|
1444
1766
|
JsonRpcErrorCodes.InvalidParams
|
|
1445
1767
|
);
|
|
1446
1768
|
}
|
|
@@ -1574,7 +1896,7 @@ var Session = class {
|
|
|
1574
1896
|
// on the first wake-up of a session whose meta.json has an empty
|
|
1575
1897
|
// upstreamSessionId (the import marker). Wrapped in enqueuePrompt so
|
|
1576
1898
|
// any user prompts arriving mid-seed queue behind it (mirrors the
|
|
1577
|
-
// /hydra
|
|
1899
|
+
// /hydra agent path so the agent isn't asked to respond to a user
|
|
1578
1900
|
// turn before it has absorbed the imported transcript). Best-effort:
|
|
1579
1901
|
// if the agent fails to absorb the transcript we still leave the
|
|
1580
1902
|
// session usable — the user just continues without context.
|
|
@@ -1598,7 +1920,7 @@ var Session = class {
|
|
|
1598
1920
|
// ones read it and relabel) and (b) drop a visible banner into the
|
|
1599
1921
|
// transcript so users see the switch rather than just suddenly getting
|
|
1600
1922
|
// answers from a different agent. Both updates carry synthetic=true
|
|
1601
|
-
// so a future /hydra
|
|
1923
|
+
// so a future /hydra agent's transcript builder filters them out.
|
|
1602
1924
|
broadcastAgentSwitch(oldAgentId, newAgentId) {
|
|
1603
1925
|
this.recordAndBroadcast("session/update", {
|
|
1604
1926
|
sessionId: this.sessionId,
|
|
@@ -1745,7 +2067,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1745
2067
|
);
|
|
1746
2068
|
}
|
|
1747
2069
|
const clientParams = this.rewriteForClient(params);
|
|
1748
|
-
return new Promise((
|
|
2070
|
+
return new Promise((resolve3, reject) => {
|
|
1749
2071
|
let settled = false;
|
|
1750
2072
|
const outbound = [];
|
|
1751
2073
|
const entry = { addClient: sendTo };
|
|
@@ -1780,7 +2102,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1780
2102
|
result
|
|
1781
2103
|
}).catch(() => void 0);
|
|
1782
2104
|
}
|
|
1783
|
-
|
|
2105
|
+
resolve3(result);
|
|
1784
2106
|
});
|
|
1785
2107
|
}).catch((err) => {
|
|
1786
2108
|
settle(() => reject(err));
|
|
@@ -1792,16 +2114,16 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1792
2114
|
});
|
|
1793
2115
|
}
|
|
1794
2116
|
async enqueuePrompt(task) {
|
|
1795
|
-
return new Promise((
|
|
1796
|
-
const
|
|
2117
|
+
return new Promise((resolve3, reject) => {
|
|
2118
|
+
const run2 = async () => {
|
|
1797
2119
|
try {
|
|
1798
2120
|
const result = await task();
|
|
1799
|
-
|
|
2121
|
+
resolve3(result);
|
|
1800
2122
|
} catch (err) {
|
|
1801
2123
|
reject(err);
|
|
1802
2124
|
}
|
|
1803
2125
|
};
|
|
1804
|
-
this.promptQueue.push(
|
|
2126
|
+
this.promptQueue.push(run2);
|
|
1805
2127
|
void this.drainQueue();
|
|
1806
2128
|
});
|
|
1807
2129
|
}
|
|
@@ -1830,7 +2152,8 @@ var STATE_UPDATE_KINDS = /* @__PURE__ */ new Set([
|
|
|
1830
2152
|
"session_info_update",
|
|
1831
2153
|
"current_model_update",
|
|
1832
2154
|
"current_mode_update",
|
|
1833
|
-
"available_commands_update"
|
|
2155
|
+
"available_commands_update",
|
|
2156
|
+
"usage_update"
|
|
1834
2157
|
]);
|
|
1835
2158
|
function isStateUpdate(method, params) {
|
|
1836
2159
|
if (method !== "session/update") {
|
|
@@ -1915,8 +2238,8 @@ function firstLine(text, max) {
|
|
|
1915
2238
|
}
|
|
1916
2239
|
|
|
1917
2240
|
// src/core/session-store.ts
|
|
1918
|
-
import * as
|
|
1919
|
-
import * as
|
|
2241
|
+
import * as fs4 from "fs/promises";
|
|
2242
|
+
import * as path3 from "path";
|
|
1920
2243
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
1921
2244
|
import { z as z4 } from "zod";
|
|
1922
2245
|
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -1929,6 +2252,12 @@ var PersistedAgentCommand = z4.object({
|
|
|
1929
2252
|
name: z4.string(),
|
|
1930
2253
|
description: z4.string().optional()
|
|
1931
2254
|
});
|
|
2255
|
+
var PersistedUsage = z4.object({
|
|
2256
|
+
used: z4.number().optional(),
|
|
2257
|
+
size: z4.number().optional(),
|
|
2258
|
+
costAmount: z4.number().optional(),
|
|
2259
|
+
costCurrency: z4.string().optional()
|
|
2260
|
+
});
|
|
1932
2261
|
var SessionRecord = z4.object({
|
|
1933
2262
|
version: z4.literal(1),
|
|
1934
2263
|
sessionId: z4.string(),
|
|
@@ -1956,6 +2285,7 @@ var SessionRecord = z4.object({
|
|
|
1956
2285
|
// replay of a snapshot-shaped notification.
|
|
1957
2286
|
currentModel: z4.string().optional(),
|
|
1958
2287
|
currentMode: z4.string().optional(),
|
|
2288
|
+
currentUsage: PersistedUsage.optional(),
|
|
1959
2289
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
1960
2290
|
createdAt: z4.string(),
|
|
1961
2291
|
updatedAt: z4.string()
|
|
@@ -1969,9 +2299,9 @@ function assertSafeId(id) {
|
|
|
1969
2299
|
var SessionStore = class {
|
|
1970
2300
|
async write(record) {
|
|
1971
2301
|
assertSafeId(record.sessionId);
|
|
1972
|
-
await
|
|
2302
|
+
await fs4.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
1973
2303
|
const full = { version: 1, ...record };
|
|
1974
|
-
await
|
|
2304
|
+
await fs4.writeFile(
|
|
1975
2305
|
paths.sessionFile(record.sessionId),
|
|
1976
2306
|
JSON.stringify(full, null, 2) + "\n",
|
|
1977
2307
|
{ encoding: "utf8", mode: 384 }
|
|
@@ -1983,7 +2313,7 @@ var SessionStore = class {
|
|
|
1983
2313
|
}
|
|
1984
2314
|
let raw;
|
|
1985
2315
|
try {
|
|
1986
|
-
raw = await
|
|
2316
|
+
raw = await fs4.readFile(paths.sessionFile(sessionId), "utf8");
|
|
1987
2317
|
} catch (err) {
|
|
1988
2318
|
const e = err;
|
|
1989
2319
|
if (e.code === "ENOENT") {
|
|
@@ -2002,7 +2332,7 @@ var SessionStore = class {
|
|
|
2002
2332
|
return;
|
|
2003
2333
|
}
|
|
2004
2334
|
try {
|
|
2005
|
-
await
|
|
2335
|
+
await fs4.unlink(paths.sessionFile(sessionId));
|
|
2006
2336
|
} catch (err) {
|
|
2007
2337
|
const e = err;
|
|
2008
2338
|
if (e.code !== "ENOENT") {
|
|
@@ -2010,7 +2340,7 @@ var SessionStore = class {
|
|
|
2010
2340
|
}
|
|
2011
2341
|
}
|
|
2012
2342
|
try {
|
|
2013
|
-
await
|
|
2343
|
+
await fs4.rmdir(paths.sessionDir(sessionId));
|
|
2014
2344
|
} catch (err) {
|
|
2015
2345
|
const e = err;
|
|
2016
2346
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -2040,7 +2370,7 @@ var SessionStore = class {
|
|
|
2040
2370
|
async list() {
|
|
2041
2371
|
let entries;
|
|
2042
2372
|
try {
|
|
2043
|
-
entries = await
|
|
2373
|
+
entries = await fs4.readdir(paths.sessionsDir());
|
|
2044
2374
|
} catch (err) {
|
|
2045
2375
|
const e = err;
|
|
2046
2376
|
if (e.code === "ENOENT") {
|
|
@@ -2071,6 +2401,7 @@ function recordFromMemorySession(args) {
|
|
|
2071
2401
|
agentArgs: args.agentArgs,
|
|
2072
2402
|
currentModel: args.currentModel,
|
|
2073
2403
|
currentMode: args.currentMode,
|
|
2404
|
+
currentUsage: args.currentUsage,
|
|
2074
2405
|
agentCommands: args.agentCommands,
|
|
2075
2406
|
createdAt: args.createdAt ?? now,
|
|
2076
2407
|
updatedAt: args.updatedAt ?? now
|
|
@@ -2078,7 +2409,7 @@ function recordFromMemorySession(args) {
|
|
|
2078
2409
|
}
|
|
2079
2410
|
|
|
2080
2411
|
// src/core/history-store.ts
|
|
2081
|
-
import * as
|
|
2412
|
+
import * as fs5 from "fs/promises";
|
|
2082
2413
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
2083
2414
|
var MAX_ENTRIES = 1e3;
|
|
2084
2415
|
var HistoryStore = class {
|
|
@@ -2091,9 +2422,9 @@ var HistoryStore = class {
|
|
|
2091
2422
|
return;
|
|
2092
2423
|
}
|
|
2093
2424
|
return this.enqueue(sessionId, async () => {
|
|
2094
|
-
await
|
|
2425
|
+
await fs5.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
2095
2426
|
const line = JSON.stringify(entry) + "\n";
|
|
2096
|
-
await
|
|
2427
|
+
await fs5.appendFile(paths.historyFile(sessionId), line, {
|
|
2097
2428
|
encoding: "utf8",
|
|
2098
2429
|
mode: 384
|
|
2099
2430
|
});
|
|
@@ -2104,9 +2435,9 @@ var HistoryStore = class {
|
|
|
2104
2435
|
return;
|
|
2105
2436
|
}
|
|
2106
2437
|
return this.enqueue(sessionId, async () => {
|
|
2107
|
-
await
|
|
2438
|
+
await fs5.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
2108
2439
|
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2109
|
-
await
|
|
2440
|
+
await fs5.writeFile(paths.historyFile(sessionId), body, {
|
|
2110
2441
|
encoding: "utf8",
|
|
2111
2442
|
mode: 384
|
|
2112
2443
|
});
|
|
@@ -2123,7 +2454,7 @@ var HistoryStore = class {
|
|
|
2123
2454
|
return this.enqueue(sessionId, async () => {
|
|
2124
2455
|
let raw;
|
|
2125
2456
|
try {
|
|
2126
|
-
raw = await
|
|
2457
|
+
raw = await fs5.readFile(paths.historyFile(sessionId), "utf8");
|
|
2127
2458
|
} catch (err) {
|
|
2128
2459
|
const e = err;
|
|
2129
2460
|
if (e.code === "ENOENT") {
|
|
@@ -2136,7 +2467,7 @@ var HistoryStore = class {
|
|
|
2136
2467
|
return;
|
|
2137
2468
|
}
|
|
2138
2469
|
const trimmed = lines.slice(-maxEntries);
|
|
2139
|
-
await
|
|
2470
|
+
await fs5.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
2140
2471
|
encoding: "utf8",
|
|
2141
2472
|
mode: 384
|
|
2142
2473
|
});
|
|
@@ -2152,7 +2483,7 @@ var HistoryStore = class {
|
|
|
2152
2483
|
}
|
|
2153
2484
|
let raw;
|
|
2154
2485
|
try {
|
|
2155
|
-
raw = await
|
|
2486
|
+
raw = await fs5.readFile(paths.historyFile(sessionId), "utf8");
|
|
2156
2487
|
} catch (err) {
|
|
2157
2488
|
const e = err;
|
|
2158
2489
|
if (e.code === "ENOENT") {
|
|
@@ -2198,7 +2529,7 @@ var HistoryStore = class {
|
|
|
2198
2529
|
}
|
|
2199
2530
|
return this.enqueue(sessionId, async () => {
|
|
2200
2531
|
try {
|
|
2201
|
-
await
|
|
2532
|
+
await fs5.unlink(paths.historyFile(sessionId));
|
|
2202
2533
|
} catch (err) {
|
|
2203
2534
|
const e = err;
|
|
2204
2535
|
if (e.code !== "ENOENT") {
|
|
@@ -2206,7 +2537,7 @@ var HistoryStore = class {
|
|
|
2206
2537
|
}
|
|
2207
2538
|
}
|
|
2208
2539
|
try {
|
|
2209
|
-
await
|
|
2540
|
+
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
2210
2541
|
} catch (err) {
|
|
2211
2542
|
const e = err;
|
|
2212
2543
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -2230,12 +2561,12 @@ var HistoryStore = class {
|
|
|
2230
2561
|
};
|
|
2231
2562
|
|
|
2232
2563
|
// src/tui/history.ts
|
|
2233
|
-
import { promises as
|
|
2234
|
-
import * as
|
|
2564
|
+
import { promises as fs6 } from "fs";
|
|
2565
|
+
import * as path4 from "path";
|
|
2235
2566
|
async function saveHistory(file, history) {
|
|
2236
|
-
await
|
|
2567
|
+
await fs6.mkdir(path4.dirname(file), { recursive: true });
|
|
2237
2568
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
2238
|
-
await
|
|
2569
|
+
await fs6.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
2239
2570
|
}
|
|
2240
2571
|
|
|
2241
2572
|
// src/core/session-manager.ts
|
|
@@ -2248,6 +2579,7 @@ var SessionManager = class {
|
|
|
2248
2579
|
this.store = store ?? new SessionStore();
|
|
2249
2580
|
this.histories = new HistoryStore();
|
|
2250
2581
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
2582
|
+
this.defaultModels = options.defaultModels ?? {};
|
|
2251
2583
|
}
|
|
2252
2584
|
registry;
|
|
2253
2585
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -2256,6 +2588,7 @@ var SessionManager = class {
|
|
|
2256
2588
|
store;
|
|
2257
2589
|
histories;
|
|
2258
2590
|
idleTimeoutMs;
|
|
2591
|
+
defaultModels;
|
|
2259
2592
|
// Serialize meta.json read-modify-write operations per session id so
|
|
2260
2593
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
2261
2594
|
// back-to-back) don't lose writes via interleaved reads.
|
|
@@ -2277,7 +2610,8 @@ var SessionManager = class {
|
|
|
2277
2610
|
agentArgs: params.agentArgs,
|
|
2278
2611
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2279
2612
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2280
|
-
historyStore: this.histories
|
|
2613
|
+
historyStore: this.histories,
|
|
2614
|
+
currentModel: fresh.initialModel
|
|
2281
2615
|
});
|
|
2282
2616
|
await this.attachManagerHooks(session);
|
|
2283
2617
|
return session;
|
|
@@ -2322,7 +2656,7 @@ var SessionManager = class {
|
|
|
2322
2656
|
if (params.upstreamSessionId === "") {
|
|
2323
2657
|
return this.doResurrectFromImport(params);
|
|
2324
2658
|
}
|
|
2325
|
-
const plan = planSpawn(agentDef, params.agentArgs ?? []);
|
|
2659
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
2326
2660
|
const agent = this.spawner({
|
|
2327
2661
|
agentId: params.agentId,
|
|
2328
2662
|
cwd: params.cwd,
|
|
@@ -2335,11 +2669,14 @@ var SessionManager = class {
|
|
|
2335
2669
|
});
|
|
2336
2670
|
let loadResult;
|
|
2337
2671
|
try {
|
|
2338
|
-
loadResult = await agent.connection.request(
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2672
|
+
loadResult = await agent.connection.request(
|
|
2673
|
+
"session/load",
|
|
2674
|
+
{
|
|
2675
|
+
sessionId: params.upstreamSessionId,
|
|
2676
|
+
cwd: params.cwd,
|
|
2677
|
+
mcpServers: []
|
|
2678
|
+
}
|
|
2679
|
+
);
|
|
2343
2680
|
} catch (err) {
|
|
2344
2681
|
await agent.kill().catch(() => void 0);
|
|
2345
2682
|
throw new Error(
|
|
@@ -2358,8 +2695,13 @@ var SessionManager = class {
|
|
|
2358
2695
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2359
2696
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2360
2697
|
historyStore: this.histories,
|
|
2361
|
-
|
|
2698
|
+
// Prefer what we previously stored from a current_model_update; if
|
|
2699
|
+
// we never captured one (e.g. old opencode sessions on disk before
|
|
2700
|
+
// this fix), fall back to the model the agent ships in its
|
|
2701
|
+
// session/load response body.
|
|
2702
|
+
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
2362
2703
|
currentMode: params.currentMode,
|
|
2704
|
+
currentUsage: params.currentUsage,
|
|
2363
2705
|
agentCommands: params.agentCommands,
|
|
2364
2706
|
// Only gate the first-prompt title heuristic when we actually have
|
|
2365
2707
|
// a title to preserve. A title-less session (lost to a write race
|
|
@@ -2397,8 +2739,11 @@ var SessionManager = class {
|
|
|
2397
2739
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2398
2740
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2399
2741
|
historyStore: this.histories,
|
|
2400
|
-
|
|
2742
|
+
// Prefer the stored value (set by a previous current_model_update);
|
|
2743
|
+
// fall back to whatever the agent ships in its session/new response.
|
|
2744
|
+
currentModel: params.currentModel ?? fresh.initialModel,
|
|
2401
2745
|
currentMode: params.currentMode,
|
|
2746
|
+
currentUsage: params.currentUsage,
|
|
2402
2747
|
agentCommands: params.agentCommands,
|
|
2403
2748
|
firstPromptSeeded: !!params.title,
|
|
2404
2749
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
@@ -2408,7 +2753,7 @@ var SessionManager = class {
|
|
|
2408
2753
|
return session;
|
|
2409
2754
|
}
|
|
2410
2755
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
2411
|
-
// → session/new. Shared by create() and the /hydra
|
|
2756
|
+
// → session/new. Shared by create() and the /hydra agent path so both
|
|
2412
2757
|
// go through the same env / capabilities / error-handling.
|
|
2413
2758
|
async bootstrapAgent(params) {
|
|
2414
2759
|
const agentDef = await this.registry.getAgent(params.agentId);
|
|
@@ -2419,7 +2764,7 @@ var SessionManager = class {
|
|
|
2419
2764
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
2420
2765
|
throw err;
|
|
2421
2766
|
}
|
|
2422
|
-
const plan = planSpawn(agentDef, params.agentArgs ?? []);
|
|
2767
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? []);
|
|
2423
2768
|
const agent = this.spawner({
|
|
2424
2769
|
agentId: params.agentId,
|
|
2425
2770
|
cwd: params.cwd,
|
|
@@ -2431,14 +2776,36 @@ var SessionManager = class {
|
|
|
2431
2776
|
clientCapabilities: {},
|
|
2432
2777
|
clientInfo: { name: "hydra", version: "0.1.0" }
|
|
2433
2778
|
});
|
|
2434
|
-
const newResult = await agent.connection.request(
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2779
|
+
const newResult = await agent.connection.request(
|
|
2780
|
+
"session/new",
|
|
2781
|
+
{
|
|
2782
|
+
cwd: params.cwd,
|
|
2783
|
+
mcpServers: params.mcpServers ?? []
|
|
2784
|
+
}
|
|
2785
|
+
);
|
|
2786
|
+
const sessionIdRaw = newResult.sessionId;
|
|
2787
|
+
if (typeof sessionIdRaw !== "string") {
|
|
2788
|
+
throw new Error(
|
|
2789
|
+
`agent ${params.agentId} returned a non-string sessionId from session/new`
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
let initialModel = extractInitialModel(newResult);
|
|
2793
|
+
const desired = this.defaultModels[params.agentId];
|
|
2794
|
+
if (desired && desired !== initialModel) {
|
|
2795
|
+
try {
|
|
2796
|
+
await agent.connection.request("session/set_model", {
|
|
2797
|
+
sessionId: sessionIdRaw,
|
|
2798
|
+
modelId: desired
|
|
2799
|
+
});
|
|
2800
|
+
initialModel = desired;
|
|
2801
|
+
} catch {
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2438
2804
|
return {
|
|
2439
2805
|
agent,
|
|
2440
|
-
upstreamSessionId:
|
|
2441
|
-
agentMeta: newResult._meta
|
|
2806
|
+
upstreamSessionId: sessionIdRaw,
|
|
2807
|
+
agentMeta: newResult._meta,
|
|
2808
|
+
initialModel
|
|
2442
2809
|
};
|
|
2443
2810
|
} catch (err) {
|
|
2444
2811
|
await agent.kill().catch(() => void 0);
|
|
@@ -2449,7 +2816,7 @@ var SessionManager = class {
|
|
|
2449
2816
|
// bookkeeping. Called from both create() and resurrect() so the same
|
|
2450
2817
|
// session record + lifecycle handlers are wired regardless of origin.
|
|
2451
2818
|
// Returns once the initial disk record is written — callers should
|
|
2452
|
-
// await so a subsequent /hydra
|
|
2819
|
+
// await so a subsequent /hydra agent's persistAgentChange (which
|
|
2453
2820
|
// does read-then-write) finds the file in place.
|
|
2454
2821
|
async attachManagerHooks(session) {
|
|
2455
2822
|
session.onClose(({ deleteRecord }) => {
|
|
@@ -2477,6 +2844,11 @@ var SessionManager = class {
|
|
|
2477
2844
|
() => void 0
|
|
2478
2845
|
);
|
|
2479
2846
|
});
|
|
2847
|
+
session.onUsageChange((usage) => {
|
|
2848
|
+
void this.persistSnapshot(session.sessionId, {
|
|
2849
|
+
currentUsage: usageSnapshotToPersisted(usage)
|
|
2850
|
+
}).catch(() => void 0);
|
|
2851
|
+
});
|
|
2480
2852
|
session.onAgentCommandsChange((commands) => {
|
|
2481
2853
|
void this.persistSnapshot(session.sessionId, {
|
|
2482
2854
|
agentCommands: commands.map((c) => ({
|
|
@@ -2525,6 +2897,7 @@ var SessionManager = class {
|
|
|
2525
2897
|
agentArgs: record.agentArgs,
|
|
2526
2898
|
currentModel: record.currentModel,
|
|
2527
2899
|
currentMode: record.currentMode,
|
|
2900
|
+
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
2528
2901
|
agentCommands: record.agentCommands,
|
|
2529
2902
|
createdAt: record.createdAt
|
|
2530
2903
|
};
|
|
@@ -2593,6 +2966,8 @@ var SessionManager = class {
|
|
|
2593
2966
|
cwd: session.cwd,
|
|
2594
2967
|
title: session.title,
|
|
2595
2968
|
agentId: session.agentId,
|
|
2969
|
+
currentModel: session.currentModel,
|
|
2970
|
+
currentUsage: session.currentUsage,
|
|
2596
2971
|
updatedAt: used,
|
|
2597
2972
|
attachedClients: session.attachedCount,
|
|
2598
2973
|
status: "live"
|
|
@@ -2613,6 +2988,8 @@ var SessionManager = class {
|
|
|
2613
2988
|
cwd: r.cwd,
|
|
2614
2989
|
title: r.title,
|
|
2615
2990
|
agentId: r.agentId,
|
|
2991
|
+
currentModel: r.currentModel,
|
|
2992
|
+
currentUsage: r.currentUsage,
|
|
2616
2993
|
updatedAt: used,
|
|
2617
2994
|
attachedClients: 0,
|
|
2618
2995
|
status: "cold"
|
|
@@ -2720,6 +3097,7 @@ var SessionManager = class {
|
|
|
2720
3097
|
title: args.bundle.session.title,
|
|
2721
3098
|
currentModel: args.bundle.session.currentModel,
|
|
2722
3099
|
currentMode: args.bundle.session.currentMode,
|
|
3100
|
+
currentUsage: args.bundle.session.currentUsage,
|
|
2723
3101
|
agentCommands: args.bundle.session.agentCommands,
|
|
2724
3102
|
createdAt: args.preservedCreatedAt ?? now,
|
|
2725
3103
|
updatedAt: now
|
|
@@ -2755,7 +3133,7 @@ var SessionManager = class {
|
|
|
2755
3133
|
});
|
|
2756
3134
|
});
|
|
2757
3135
|
}
|
|
2758
|
-
// Persist an agent swap from /hydra
|
|
3136
|
+
// Persist an agent swap from /hydra agent. The on-disk record's
|
|
2759
3137
|
// agentId + upstreamSessionId both rotate so a daemon restart (and
|
|
2760
3138
|
// later resurrect) brings the session back up on the agent the user
|
|
2761
3139
|
// most recently switched to, not the one it was originally created on.
|
|
@@ -2787,6 +3165,7 @@ var SessionManager = class {
|
|
|
2787
3165
|
...record,
|
|
2788
3166
|
...update.currentModel !== void 0 ? { currentModel: update.currentModel } : {},
|
|
2789
3167
|
...update.currentMode !== void 0 ? { currentMode: update.currentMode } : {},
|
|
3168
|
+
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
2790
3169
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
2791
3170
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2792
3171
|
});
|
|
@@ -2841,13 +3220,73 @@ function mergeForPersistence(session, existing) {
|
|
|
2841
3220
|
agentArgs: session.agentArgs,
|
|
2842
3221
|
currentModel: session.currentModel ?? existing?.currentModel,
|
|
2843
3222
|
currentMode: session.currentMode ?? existing?.currentMode,
|
|
3223
|
+
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
2844
3224
|
agentCommands,
|
|
2845
3225
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
2846
3226
|
});
|
|
2847
3227
|
}
|
|
3228
|
+
function usageSnapshotToPersisted(usage) {
|
|
3229
|
+
if (!usage) {
|
|
3230
|
+
return void 0;
|
|
3231
|
+
}
|
|
3232
|
+
const out = {};
|
|
3233
|
+
if (usage.used !== void 0) {
|
|
3234
|
+
out.used = usage.used;
|
|
3235
|
+
}
|
|
3236
|
+
if (usage.size !== void 0) {
|
|
3237
|
+
out.size = usage.size;
|
|
3238
|
+
}
|
|
3239
|
+
if (usage.costAmount !== void 0) {
|
|
3240
|
+
out.costAmount = usage.costAmount;
|
|
3241
|
+
}
|
|
3242
|
+
if (usage.costCurrency !== void 0) {
|
|
3243
|
+
out.costCurrency = usage.costCurrency;
|
|
3244
|
+
}
|
|
3245
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
3246
|
+
}
|
|
3247
|
+
function persistedUsageToSnapshot(usage) {
|
|
3248
|
+
return usage ? { ...usage } : void 0;
|
|
3249
|
+
}
|
|
3250
|
+
function extractInitialModel(result) {
|
|
3251
|
+
const direct = asString(result.currentModelId) ?? asString(result.currentModel) ?? asString(result.modelId) ?? asString(result.model);
|
|
3252
|
+
if (direct) {
|
|
3253
|
+
return direct;
|
|
3254
|
+
}
|
|
3255
|
+
const models = result.models;
|
|
3256
|
+
if (models && typeof models === "object" && !Array.isArray(models)) {
|
|
3257
|
+
const m = asString(models.currentModelId) ?? asString(models.currentModel);
|
|
3258
|
+
if (m) {
|
|
3259
|
+
return m;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
const meta = result._meta;
|
|
3263
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
3264
|
+
for (const [key, value] of Object.entries(
|
|
3265
|
+
meta
|
|
3266
|
+
)) {
|
|
3267
|
+
if (key === "hydra-acp") {
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
3270
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
3271
|
+
const m = asString(value.modelId) ?? asString(value.model) ?? asString(value.currentModelId);
|
|
3272
|
+
if (m) {
|
|
3273
|
+
return m;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
return void 0;
|
|
3279
|
+
}
|
|
3280
|
+
function asString(value) {
|
|
3281
|
+
if (typeof value !== "string") {
|
|
3282
|
+
return void 0;
|
|
3283
|
+
}
|
|
3284
|
+
const trimmed = value.trim();
|
|
3285
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
3286
|
+
}
|
|
2848
3287
|
async function loadPromptHistorySafely(sessionId) {
|
|
2849
3288
|
try {
|
|
2850
|
-
const raw = await
|
|
3289
|
+
const raw = await fs7.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
2851
3290
|
const out = [];
|
|
2852
3291
|
for (const line of raw.split("\n")) {
|
|
2853
3292
|
if (line.length === 0) {
|
|
@@ -2868,7 +3307,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
2868
3307
|
}
|
|
2869
3308
|
async function historyMtimeIso(sessionId) {
|
|
2870
3309
|
try {
|
|
2871
|
-
const st = await
|
|
3310
|
+
const st = await fs7.stat(paths.historyFile(sessionId));
|
|
2872
3311
|
return new Date(st.mtimeMs).toISOString();
|
|
2873
3312
|
} catch {
|
|
2874
3313
|
return void 0;
|
|
@@ -2876,10 +3315,10 @@ async function historyMtimeIso(sessionId) {
|
|
|
2876
3315
|
}
|
|
2877
3316
|
|
|
2878
3317
|
// src/core/extensions.ts
|
|
2879
|
-
import { spawn as
|
|
2880
|
-
import * as
|
|
2881
|
-
import * as
|
|
2882
|
-
import * as
|
|
3318
|
+
import { spawn as spawn3 } from "child_process";
|
|
3319
|
+
import * as fs8 from "fs";
|
|
3320
|
+
import * as fsp2 from "fs/promises";
|
|
3321
|
+
import * as path5 from "path";
|
|
2883
3322
|
var RESTART_BASE_MS = 1e3;
|
|
2884
3323
|
var RESTART_CAP_MS = 6e4;
|
|
2885
3324
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -2900,7 +3339,7 @@ var ExtensionManager = class {
|
|
|
2900
3339
|
if (!this.context) {
|
|
2901
3340
|
throw new Error("ExtensionManager: setContext must be called before start");
|
|
2902
3341
|
}
|
|
2903
|
-
await
|
|
3342
|
+
await fsp2.mkdir(paths.extensionsDir(), { recursive: true });
|
|
2904
3343
|
await this.reapOrphans();
|
|
2905
3344
|
for (const entry of this.entries.values()) {
|
|
2906
3345
|
if (!entry.config.enabled) {
|
|
@@ -2926,9 +3365,9 @@ var ExtensionManager = class {
|
|
|
2926
3365
|
} catch {
|
|
2927
3366
|
}
|
|
2928
3367
|
tasks.push(
|
|
2929
|
-
new Promise((
|
|
3368
|
+
new Promise((resolve3) => {
|
|
2930
3369
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
2931
|
-
|
|
3370
|
+
resolve3();
|
|
2932
3371
|
return;
|
|
2933
3372
|
}
|
|
2934
3373
|
const timer = setTimeout(() => {
|
|
@@ -2936,11 +3375,11 @@ var ExtensionManager = class {
|
|
|
2936
3375
|
child.kill("SIGKILL");
|
|
2937
3376
|
} catch {
|
|
2938
3377
|
}
|
|
2939
|
-
|
|
3378
|
+
resolve3();
|
|
2940
3379
|
}, STOP_GRACE_MS);
|
|
2941
3380
|
child.on("exit", () => {
|
|
2942
3381
|
clearTimeout(timer);
|
|
2943
|
-
|
|
3382
|
+
resolve3();
|
|
2944
3383
|
});
|
|
2945
3384
|
})
|
|
2946
3385
|
);
|
|
@@ -3048,8 +3487,8 @@ var ExtensionManager = class {
|
|
|
3048
3487
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
3049
3488
|
return;
|
|
3050
3489
|
}
|
|
3051
|
-
const exited = new Promise((
|
|
3052
|
-
entry.exitWaiters.push(
|
|
3490
|
+
const exited = new Promise((resolve3) => {
|
|
3491
|
+
entry.exitWaiters.push(resolve3);
|
|
3053
3492
|
});
|
|
3054
3493
|
try {
|
|
3055
3494
|
child.kill("SIGTERM");
|
|
@@ -3109,7 +3548,7 @@ var ExtensionManager = class {
|
|
|
3109
3548
|
async reapOrphans() {
|
|
3110
3549
|
let entries;
|
|
3111
3550
|
try {
|
|
3112
|
-
entries = await
|
|
3551
|
+
entries = await fsp2.readdir(paths.extensionsDir());
|
|
3113
3552
|
} catch (err) {
|
|
3114
3553
|
const e = err;
|
|
3115
3554
|
if (e.code === "ENOENT") {
|
|
@@ -3121,10 +3560,10 @@ var ExtensionManager = class {
|
|
|
3121
3560
|
if (!entry.endsWith(".pid")) {
|
|
3122
3561
|
continue;
|
|
3123
3562
|
}
|
|
3124
|
-
const pidPath =
|
|
3563
|
+
const pidPath = path5.join(paths.extensionsDir(), entry);
|
|
3125
3564
|
let pid;
|
|
3126
3565
|
try {
|
|
3127
|
-
const raw = await
|
|
3566
|
+
const raw = await fsp2.readFile(pidPath, "utf8");
|
|
3128
3567
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
3129
3568
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
3130
3569
|
pid = parsed;
|
|
@@ -3147,7 +3586,7 @@ var ExtensionManager = class {
|
|
|
3147
3586
|
}
|
|
3148
3587
|
}
|
|
3149
3588
|
}
|
|
3150
|
-
await
|
|
3589
|
+
await fsp2.unlink(pidPath).catch(() => void 0);
|
|
3151
3590
|
}
|
|
3152
3591
|
}
|
|
3153
3592
|
spawn(entry, attempt) {
|
|
@@ -3160,7 +3599,7 @@ var ExtensionManager = class {
|
|
|
3160
3599
|
}
|
|
3161
3600
|
const ext = entry.config;
|
|
3162
3601
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
3163
|
-
const logStream =
|
|
3602
|
+
const logStream = fs8.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
3164
3603
|
flags: "a"
|
|
3165
3604
|
});
|
|
3166
3605
|
logStream.write(
|
|
@@ -3188,7 +3627,7 @@ var ExtensionManager = class {
|
|
|
3188
3627
|
const args = [...baseArgs, ...ext.args];
|
|
3189
3628
|
let child;
|
|
3190
3629
|
try {
|
|
3191
|
-
child =
|
|
3630
|
+
child = spawn3(cmd, args, {
|
|
3192
3631
|
env,
|
|
3193
3632
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3194
3633
|
detached: false
|
|
@@ -3210,7 +3649,7 @@ var ExtensionManager = class {
|
|
|
3210
3649
|
}
|
|
3211
3650
|
if (typeof child.pid === "number") {
|
|
3212
3651
|
try {
|
|
3213
|
-
|
|
3652
|
+
fs8.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
3214
3653
|
`, {
|
|
3215
3654
|
encoding: "utf8",
|
|
3216
3655
|
mode: 384
|
|
@@ -3235,7 +3674,7 @@ var ExtensionManager = class {
|
|
|
3235
3674
|
});
|
|
3236
3675
|
child.on("exit", (code, signal) => {
|
|
3237
3676
|
try {
|
|
3238
|
-
|
|
3677
|
+
fs8.unlinkSync(paths.extensionPidFile(ext.name));
|
|
3239
3678
|
} catch {
|
|
3240
3679
|
}
|
|
3241
3680
|
logStream.write(
|
|
@@ -3246,8 +3685,8 @@ var ExtensionManager = class {
|
|
|
3246
3685
|
entry.pid = void 0;
|
|
3247
3686
|
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
3248
3687
|
const waiters = entry.exitWaiters.splice(0);
|
|
3249
|
-
for (const
|
|
3250
|
-
|
|
3688
|
+
for (const resolve3 of waiters) {
|
|
3689
|
+
resolve3();
|
|
3251
3690
|
}
|
|
3252
3691
|
if (this.stopping || entry.manuallyStopped) {
|
|
3253
3692
|
try {
|
|
@@ -3365,6 +3804,7 @@ var BundleSession = z5.object({
|
|
|
3365
3804
|
title: z5.string().optional(),
|
|
3366
3805
|
currentModel: z5.string().optional(),
|
|
3367
3806
|
currentMode: z5.string().optional(),
|
|
3807
|
+
currentUsage: PersistedUsage.optional(),
|
|
3368
3808
|
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
3369
3809
|
createdAt: z5.string(),
|
|
3370
3810
|
updatedAt: z5.string()
|
|
@@ -3396,6 +3836,7 @@ function encodeBundle(params) {
|
|
|
3396
3836
|
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
3397
3837
|
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
3398
3838
|
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
3839
|
+
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
3399
3840
|
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
3400
3841
|
createdAt: params.record.createdAt,
|
|
3401
3842
|
updatedAt: params.record.updatedAt
|
|
@@ -3794,13 +4235,13 @@ function wsToMessageStream(ws) {
|
|
|
3794
4235
|
throw new Error("ws is closed");
|
|
3795
4236
|
}
|
|
3796
4237
|
const text = JSON.stringify(message);
|
|
3797
|
-
await new Promise((
|
|
4238
|
+
await new Promise((resolve3, reject) => {
|
|
3798
4239
|
ws.send(text, (err) => {
|
|
3799
4240
|
if (err) {
|
|
3800
4241
|
reject(err);
|
|
3801
4242
|
return;
|
|
3802
4243
|
}
|
|
3803
|
-
|
|
4244
|
+
resolve3();
|
|
3804
4245
|
});
|
|
3805
4246
|
});
|
|
3806
4247
|
},
|
|
@@ -4128,10 +4569,10 @@ var HYDRA_VERSION3 = "0.1.0";
|
|
|
4128
4569
|
async function startDaemon(config) {
|
|
4129
4570
|
ensureLoopbackOrTls(config);
|
|
4130
4571
|
const httpsOptions = config.daemon.tls ? {
|
|
4131
|
-
key: await
|
|
4132
|
-
cert: await
|
|
4572
|
+
key: await fsp3.readFile(config.daemon.tls.key),
|
|
4573
|
+
cert: await fsp3.readFile(config.daemon.tls.cert)
|
|
4133
4574
|
} : void 0;
|
|
4134
|
-
await
|
|
4575
|
+
await fsp3.mkdir(paths.home(), { recursive: true });
|
|
4135
4576
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
4136
4577
|
config.daemon.logLevel
|
|
4137
4578
|
);
|
|
@@ -4143,6 +4584,9 @@ async function startDaemon(config) {
|
|
|
4143
4584
|
https: httpsOptions ?? null
|
|
4144
4585
|
});
|
|
4145
4586
|
await app.register(websocketPlugin);
|
|
4587
|
+
setBinaryInstallLogger((msg) => {
|
|
4588
|
+
app.log.info(msg);
|
|
4589
|
+
});
|
|
4146
4590
|
const auth = bearerAuth({ config });
|
|
4147
4591
|
app.addHook("onRequest", async (request, reply) => {
|
|
4148
4592
|
if (request.routeOptions.config?.skipAuth) {
|
|
@@ -4155,7 +4599,8 @@ async function startDaemon(config) {
|
|
|
4155
4599
|
});
|
|
4156
4600
|
const registry = new Registry(config);
|
|
4157
4601
|
const manager = new SessionManager(registry, void 0, void 0, {
|
|
4158
|
-
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3
|
|
4602
|
+
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
4603
|
+
defaultModels: config.defaultModels
|
|
4159
4604
|
});
|
|
4160
4605
|
const extensions = new ExtensionManager(extensionList(config));
|
|
4161
4606
|
registerHealthRoutes(app, HYDRA_VERSION3);
|
|
@@ -4177,8 +4622,8 @@ async function startDaemon(config) {
|
|
|
4177
4622
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
4178
4623
|
const address = app.server.address();
|
|
4179
4624
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
4180
|
-
await
|
|
4181
|
-
await
|
|
4625
|
+
await fsp3.mkdir(paths.home(), { recursive: true });
|
|
4626
|
+
await fsp3.writeFile(
|
|
4182
4627
|
paths.pidFile(),
|
|
4183
4628
|
JSON.stringify({
|
|
4184
4629
|
pid: process.pid,
|
|
@@ -4203,9 +4648,10 @@ async function startDaemon(config) {
|
|
|
4203
4648
|
await extensions.stop();
|
|
4204
4649
|
await manager.closeAll();
|
|
4205
4650
|
await manager.flushMetaWrites();
|
|
4651
|
+
setBinaryInstallLogger(null);
|
|
4206
4652
|
await app.close();
|
|
4207
4653
|
try {
|
|
4208
|
-
|
|
4654
|
+
fs9.unlinkSync(paths.pidFile());
|
|
4209
4655
|
} catch {
|
|
4210
4656
|
}
|
|
4211
4657
|
try {
|