@hydra-acp/cli 0.1.7 → 0.1.9
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 +4 -4
- package/dist/cli.js +2089 -539
- package/dist/index.d.ts +82 -9
- package/dist/index.js +587 -105
- package/package.json +1 -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 fs10 from "fs";
|
|
3
|
+
import * as fsp4 from "fs/promises";
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import websocketPlugin from "@fastify/websocket";
|
|
6
6
|
import pino from "pino";
|
|
@@ -43,6 +43,18 @@ var paths = {
|
|
|
43
43
|
// machine's binaries cleanly separated. `ls agents/` immediately
|
|
44
44
|
// shows which platforms have ever installed anything.
|
|
45
45
|
agentInstallDir: (id, platformKey, version) => path.join(hydraHome(), "agents", platformKey, id, version),
|
|
46
|
+
// npm install cache for npx-distributed agents. The trailing
|
|
47
|
+
// node<ABI> segment keys on process.versions.modules so a Node
|
|
48
|
+
// major bump (different ABI → native modules incompatible) yields
|
|
49
|
+
// a fresh install rather than failing at require() time.
|
|
50
|
+
agentNpmInstallDir: (id, platformKey, version) => path.join(
|
|
51
|
+
hydraHome(),
|
|
52
|
+
"agents",
|
|
53
|
+
platformKey,
|
|
54
|
+
id,
|
|
55
|
+
version,
|
|
56
|
+
`node${process.versions.modules}`
|
|
57
|
+
),
|
|
46
58
|
sessionsDir: () => path.join(hydraHome(), "sessions"),
|
|
47
59
|
// One directory per session id under sessions/. Co-locates the
|
|
48
60
|
// session record, its transcript, and any future per-session state
|
|
@@ -69,7 +81,16 @@ var DaemonConfig = z.object({
|
|
|
69
81
|
authToken: z.string().min(16),
|
|
70
82
|
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
71
83
|
tls: TlsConfig.optional(),
|
|
72
|
-
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600)
|
|
84
|
+
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600),
|
|
85
|
+
// Cap on entries kept in a session's on-disk replay log (history.jsonl).
|
|
86
|
+
// Compaction trims to this many on a periodic basis; reads also slice
|
|
87
|
+
// to the tail at this length as a defensive measure against older
|
|
88
|
+
// daemons that may have written unbounded files.
|
|
89
|
+
sessionHistoryMaxEntries: z.number().int().positive().default(1e3),
|
|
90
|
+
// Bytes of trailing agent stderr buffered per AgentInstance so the
|
|
91
|
+
// daemon can include it in the diagnostic message when a spawn fails.
|
|
92
|
+
// Bump if your agents emit large tracebacks you want surfaced.
|
|
93
|
+
agentStderrTailBytes: z.number().int().positive().default(4096)
|
|
73
94
|
});
|
|
74
95
|
var RegistryConfig = z.object({
|
|
75
96
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
@@ -92,7 +113,14 @@ var TuiConfig = z.object({
|
|
|
92
113
|
// text selection requires shift+drag to bypass mouse reporting. Set
|
|
93
114
|
// false to disable capture — wheel scrollback stops working, but
|
|
94
115
|
// plain click-drag selects text via the terminal emulator.
|
|
95
|
-
mouse: z.boolean().default(true)
|
|
116
|
+
mouse: z.boolean().default(true),
|
|
117
|
+
// Size at which the TUI's session/update debug log (tui.log) rotates
|
|
118
|
+
// to tui.log.0 and resets. Bounds on-disk use at ~2x this value.
|
|
119
|
+
logMaxBytes: z.number().int().positive().default(5 * 1024 * 1024),
|
|
120
|
+
// Width cap on the cwd column in the `sessions list` output and the
|
|
121
|
+
// TUI picker. Set higher if you keep deeply-nested working directories
|
|
122
|
+
// and want them visible; the elastic title column shrinks to make room.
|
|
123
|
+
cwdColumnMaxWidth: z.number().int().positive().default(24)
|
|
96
124
|
});
|
|
97
125
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
98
126
|
var ExtensionBody = z.object({
|
|
@@ -128,9 +156,14 @@ var HydraConfig = z.object({
|
|
|
128
156
|
tui: TuiConfig.default({
|
|
129
157
|
repaintThrottleMs: 1e3,
|
|
130
158
|
maxScrollbackLines: 1e4,
|
|
131
|
-
mouse: true
|
|
159
|
+
mouse: true,
|
|
160
|
+
logMaxBytes: 5 * 1024 * 1024,
|
|
161
|
+
cwdColumnMaxWidth: 24
|
|
132
162
|
})
|
|
133
163
|
});
|
|
164
|
+
var HydraConfigReadOnly = HydraConfig.extend({
|
|
165
|
+
daemon: DaemonConfig.omit({ authToken: true })
|
|
166
|
+
});
|
|
134
167
|
function extensionList(config) {
|
|
135
168
|
return Object.entries(config.extensions).map(([name, body]) => ({
|
|
136
169
|
name,
|
|
@@ -472,9 +505,129 @@ async function fileExists(p) {
|
|
|
472
505
|
}
|
|
473
506
|
}
|
|
474
507
|
|
|
508
|
+
// src/core/npm-install.ts
|
|
509
|
+
import * as fsp2 from "fs/promises";
|
|
510
|
+
import * as path3 from "path";
|
|
511
|
+
import { spawn as spawn2 } from "child_process";
|
|
512
|
+
var logSink2 = (msg) => {
|
|
513
|
+
process.stderr.write(msg + "\n");
|
|
514
|
+
};
|
|
515
|
+
function setNpmInstallLogger(log) {
|
|
516
|
+
logSink2 = log ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
517
|
+
}
|
|
518
|
+
async function ensureNpmPackage(args) {
|
|
519
|
+
const platformKey = currentPlatformKey();
|
|
520
|
+
if (!platformKey) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
`Agent ${args.agentId}: cannot determine platform key for ${process.platform}/${process.arch}`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const installDir = paths.agentNpmInstallDir(
|
|
526
|
+
args.agentId,
|
|
527
|
+
platformKey,
|
|
528
|
+
args.version
|
|
529
|
+
);
|
|
530
|
+
const binPath = path3.join(installDir, "node_modules", ".bin", args.bin);
|
|
531
|
+
if (await fileExists2(binPath)) {
|
|
532
|
+
return binPath;
|
|
533
|
+
}
|
|
534
|
+
await installInto({
|
|
535
|
+
agentId: args.agentId,
|
|
536
|
+
packageSpec: args.packageSpec,
|
|
537
|
+
installDir
|
|
538
|
+
});
|
|
539
|
+
if (!await fileExists2(binPath)) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`Agent ${args.agentId}: npm install of ${args.packageSpec} did not produce bin ${args.bin} (looked in ${installDir}/node_modules/.bin/)`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return binPath;
|
|
545
|
+
}
|
|
546
|
+
async function installInto(args) {
|
|
547
|
+
await fsp2.mkdir(path3.dirname(args.installDir), { recursive: true });
|
|
548
|
+
const tempDir = await fsp2.mkdtemp(`${args.installDir}.partial-`);
|
|
549
|
+
try {
|
|
550
|
+
logSink2(
|
|
551
|
+
`hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
|
|
552
|
+
);
|
|
553
|
+
await runNpmInstall({
|
|
554
|
+
packageSpec: args.packageSpec,
|
|
555
|
+
cwd: tempDir
|
|
556
|
+
});
|
|
557
|
+
try {
|
|
558
|
+
await fsp2.rename(tempDir, args.installDir);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
const e = err;
|
|
561
|
+
if ((e.code === "EEXIST" || e.code === "ENOTEMPTY") && await fileExists2(args.installDir)) {
|
|
562
|
+
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
563
|
+
() => void 0
|
|
564
|
+
);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
throw err;
|
|
568
|
+
}
|
|
569
|
+
logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
572
|
+
() => void 0
|
|
573
|
+
);
|
|
574
|
+
throw err;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function runNpmInstall(args) {
|
|
578
|
+
return new Promise((resolve3, reject) => {
|
|
579
|
+
const child = spawn2(
|
|
580
|
+
"npm",
|
|
581
|
+
["install", "--no-audit", "--no-fund", "--silent", args.packageSpec],
|
|
582
|
+
{
|
|
583
|
+
cwd: args.cwd,
|
|
584
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
let stderrTail = "";
|
|
588
|
+
child.stdout?.on("data", (chunk) => {
|
|
589
|
+
void chunk;
|
|
590
|
+
});
|
|
591
|
+
child.stderr?.setEncoding("utf8");
|
|
592
|
+
child.stderr?.on("data", (chunk) => {
|
|
593
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
594
|
+
});
|
|
595
|
+
child.on("error", (err) => {
|
|
596
|
+
const msg = err.code === "ENOENT" ? `npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)` : err.message;
|
|
597
|
+
reject(new Error(msg));
|
|
598
|
+
});
|
|
599
|
+
child.on("exit", (code, signal) => {
|
|
600
|
+
if (code === 0) {
|
|
601
|
+
resolve3();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
605
|
+
const tail = stderrTail.trim();
|
|
606
|
+
reject(
|
|
607
|
+
new Error(
|
|
608
|
+
tail ? `npm install ${args.packageSpec} failed (${reason})
|
|
609
|
+
stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
|
|
610
|
+
)
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async function fileExists2(p) {
|
|
616
|
+
try {
|
|
617
|
+
await fsp2.access(p);
|
|
618
|
+
return true;
|
|
619
|
+
} catch {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
475
624
|
// src/core/registry.ts
|
|
476
625
|
var NpxDistribution = z2.object({
|
|
477
626
|
package: z2.string(),
|
|
627
|
+
// The bin to invoke after install. Defaults to the package basename
|
|
628
|
+
// (e.g. "claude-code" for "@anthropic-ai/claude-code"). Required when
|
|
629
|
+
// the package exposes a bin name that differs from its basename.
|
|
630
|
+
bin: z2.string().optional(),
|
|
478
631
|
args: z2.array(z2.string()).optional(),
|
|
479
632
|
env: z2.record(z2.string()).optional()
|
|
480
633
|
});
|
|
@@ -638,9 +791,23 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
638
791
|
if (agent.distribution.npx) {
|
|
639
792
|
const npx = agent.distribution.npx;
|
|
640
793
|
const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
|
|
794
|
+
if (process.env.HYDRA_ACP_SKIP_NPM_PREFETCH) {
|
|
795
|
+
return {
|
|
796
|
+
command: "npx",
|
|
797
|
+
args: ["-y", npx.package, ...tail],
|
|
798
|
+
env: npx.env ?? {}
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
802
|
+
const binPath = await ensureNpmPackage({
|
|
803
|
+
agentId: agent.id,
|
|
804
|
+
version: agent.version ?? "current",
|
|
805
|
+
packageSpec: npx.package,
|
|
806
|
+
bin
|
|
807
|
+
});
|
|
641
808
|
return {
|
|
642
|
-
command:
|
|
643
|
-
args:
|
|
809
|
+
command: binPath,
|
|
810
|
+
args: tail,
|
|
644
811
|
env: npx.env ?? {}
|
|
645
812
|
};
|
|
646
813
|
}
|
|
@@ -675,26 +842,34 @@ async function planSpawn(agent, callerArgs = []) {
|
|
|
675
842
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
676
843
|
}
|
|
677
844
|
|
|
678
|
-
// src/core/session-manager.ts
|
|
679
|
-
import * as fs7 from "fs/promises";
|
|
680
|
-
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
681
|
-
|
|
682
845
|
// src/core/agent-instance.ts
|
|
683
|
-
import { spawn as
|
|
846
|
+
import { spawn as spawn3 } from "child_process";
|
|
684
847
|
|
|
685
848
|
// src/acp/types.ts
|
|
686
849
|
import { z as z3 } from "zod";
|
|
850
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
687
851
|
var JsonRpcErrorCodes = {
|
|
688
852
|
ParseError: -32700,
|
|
689
853
|
InvalidRequest: -32600,
|
|
690
854
|
MethodNotFound: -32601,
|
|
691
855
|
InvalidParams: -32602,
|
|
692
856
|
InternalError: -32603,
|
|
857
|
+
// -32001…-32003 reserved for RFD #533 attach semantics:
|
|
858
|
+
// -32001 Session not found
|
|
859
|
+
// -32002 Not authorised to attach
|
|
860
|
+
// -32003 Session does not support multi-client attach
|
|
861
|
+
// We emit -32001 (matching); the other two are reserved for spec
|
|
862
|
+
// alignment even though we don't currently emit them (we bearer-auth
|
|
863
|
+
// at WS upgrade time and always support multi-client attach).
|
|
693
864
|
SessionNotFound: -32001,
|
|
694
|
-
|
|
695
|
-
|
|
865
|
+
NotAuthorisedToAttach: -32002,
|
|
866
|
+
MultiClientNotSupported: -32003,
|
|
696
867
|
AgentNotInstalled: -32005,
|
|
697
|
-
|
|
868
|
+
// Hydra-internal codes — outside the RFD's reserved range so they
|
|
869
|
+
// can't collide with future spec assignments.
|
|
870
|
+
BundleAlreadyImported: -32010,
|
|
871
|
+
PermissionDenied: -32011,
|
|
872
|
+
AlreadyAttached: -32012
|
|
698
873
|
};
|
|
699
874
|
var InitializeParams = z3.object({
|
|
700
875
|
protocolVersion: z3.number().optional(),
|
|
@@ -704,7 +879,12 @@ var InitializeParams = z3.object({
|
|
|
704
879
|
version: z3.string().optional()
|
|
705
880
|
}).optional()
|
|
706
881
|
});
|
|
707
|
-
var HistoryPolicy = z3.enum([
|
|
882
|
+
var HistoryPolicy = z3.enum([
|
|
883
|
+
"full",
|
|
884
|
+
"pending_only",
|
|
885
|
+
"none",
|
|
886
|
+
"after_message"
|
|
887
|
+
]);
|
|
708
888
|
var SessionNewParams = z3.object({
|
|
709
889
|
cwd: z3.string(),
|
|
710
890
|
agentId: z3.string().optional(),
|
|
@@ -720,6 +900,18 @@ var SessionResumeHints = z3.object({
|
|
|
720
900
|
var SessionAttachParams = z3.object({
|
|
721
901
|
sessionId: z3.string(),
|
|
722
902
|
historyPolicy: HistoryPolicy.default("full"),
|
|
903
|
+
// Required when historyPolicy is "after_message"; ignored otherwise.
|
|
904
|
+
// The proxy replays history entries strictly after the entry whose
|
|
905
|
+
// messageId matches this value. If the id isn't found in the buffer,
|
|
906
|
+
// the response.historyPolicy field surfaces "full" so the caller
|
|
907
|
+
// knows we fell back. Per RFD #533.
|
|
908
|
+
afterMessageId: z3.string().optional(),
|
|
909
|
+
// Caller-assigned opaque id (e.g. a UUID). When provided, the proxy
|
|
910
|
+
// echoes it in resolvedBy/sentBy and lifecycle events so other
|
|
911
|
+
// clients can disambiguate multiple instances of the same
|
|
912
|
+
// clientInfo.name. When omitted, the proxy assigns one and returns
|
|
913
|
+
// it in the response. Per RFD #533.
|
|
914
|
+
clientId: z3.string().optional(),
|
|
723
915
|
clientInfo: z3.object({
|
|
724
916
|
name: z3.string(),
|
|
725
917
|
version: z3.string().optional()
|
|
@@ -1013,6 +1205,13 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1013
1205
|
}
|
|
1014
1206
|
await this.stream.close();
|
|
1015
1207
|
}
|
|
1208
|
+
// Force-close with an error. Rejects all pending requests and fires
|
|
1209
|
+
// close handlers carrying `err`. Used by transports that detect a
|
|
1210
|
+
// failure (e.g. child process crash, spawn ENOENT) the stream itself
|
|
1211
|
+
// can't surface as a stdout/stdin error.
|
|
1212
|
+
fail(err) {
|
|
1213
|
+
this.handleClose(err);
|
|
1214
|
+
}
|
|
1016
1215
|
handleIncoming(message) {
|
|
1017
1216
|
if ("method" in message) {
|
|
1018
1217
|
if ("id" in message && message.id !== void 0) {
|
|
@@ -1105,17 +1304,22 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1105
1304
|
};
|
|
1106
1305
|
|
|
1107
1306
|
// src/core/agent-instance.ts
|
|
1307
|
+
var DEFAULT_STDERR_TAIL_BYTES = 4096;
|
|
1108
1308
|
var AgentInstance = class _AgentInstance {
|
|
1109
1309
|
agentId;
|
|
1110
1310
|
cwd;
|
|
1111
1311
|
connection;
|
|
1112
1312
|
child;
|
|
1113
1313
|
exited = false;
|
|
1314
|
+
killed = false;
|
|
1315
|
+
stderrTail = "";
|
|
1316
|
+
stderrTailBytes;
|
|
1114
1317
|
exitHandlers = [];
|
|
1115
1318
|
constructor(opts, child) {
|
|
1116
1319
|
this.agentId = opts.agentId;
|
|
1117
1320
|
this.cwd = opts.cwd;
|
|
1118
1321
|
this.child = child;
|
|
1322
|
+
this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
|
|
1119
1323
|
if (!child.stdout || !child.stdin) {
|
|
1120
1324
|
throw new Error("agent subprocess missing stdio");
|
|
1121
1325
|
}
|
|
@@ -1123,22 +1327,36 @@ var AgentInstance = class _AgentInstance {
|
|
|
1123
1327
|
this.connection = new JsonRpcConnection(stream);
|
|
1124
1328
|
child.stderr?.setEncoding("utf8");
|
|
1125
1329
|
child.stderr?.on("data", (chunk) => {
|
|
1330
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
|
|
1126
1331
|
process.stderr.write(`[${opts.agentId}] ${chunk}`);
|
|
1127
1332
|
});
|
|
1333
|
+
child.on("error", (err) => {
|
|
1334
|
+
const msg = this.formatFailure(err.message);
|
|
1335
|
+
this.connection.fail(new Error(msg));
|
|
1336
|
+
});
|
|
1128
1337
|
child.on("exit", (code, signal) => {
|
|
1129
1338
|
this.exited = true;
|
|
1339
|
+
if (!this.killed) {
|
|
1340
|
+
const reason = `agent ${opts.agentId} exited before responding (code=${code} signal=${signal})`;
|
|
1341
|
+
this.connection.fail(new Error(this.formatFailure(reason)));
|
|
1342
|
+
}
|
|
1130
1343
|
for (const handler of this.exitHandlers) {
|
|
1131
1344
|
handler(code, signal);
|
|
1132
1345
|
}
|
|
1133
1346
|
});
|
|
1134
1347
|
}
|
|
1348
|
+
formatFailure(reason) {
|
|
1349
|
+
const tail = this.stderrTail.trim();
|
|
1350
|
+
return tail ? `${reason}
|
|
1351
|
+
stderr: ${tail}` : reason;
|
|
1352
|
+
}
|
|
1135
1353
|
static spawn(opts) {
|
|
1136
1354
|
const env = {
|
|
1137
1355
|
...process.env,
|
|
1138
1356
|
...opts.plan.env,
|
|
1139
1357
|
...opts.extraEnv ?? {}
|
|
1140
1358
|
};
|
|
1141
|
-
const child =
|
|
1359
|
+
const child = spawn3(opts.plan.command, opts.plan.args, {
|
|
1142
1360
|
cwd: opts.cwd,
|
|
1143
1361
|
env,
|
|
1144
1362
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1155,11 +1373,17 @@ var AgentInstance = class _AgentInstance {
|
|
|
1155
1373
|
if (this.exited) {
|
|
1156
1374
|
return;
|
|
1157
1375
|
}
|
|
1376
|
+
this.killed = true;
|
|
1158
1377
|
await this.connection.close().catch(() => void 0);
|
|
1159
1378
|
this.child.kill(signal);
|
|
1160
1379
|
}
|
|
1161
1380
|
};
|
|
1162
1381
|
|
|
1382
|
+
// src/core/session-manager.ts
|
|
1383
|
+
import * as fs8 from "fs/promises";
|
|
1384
|
+
import * as os2 from "os";
|
|
1385
|
+
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
1386
|
+
|
|
1163
1387
|
// src/core/session.ts
|
|
1164
1388
|
import { customAlphabet } from "nanoid";
|
|
1165
1389
|
|
|
@@ -1189,8 +1413,10 @@ function hydraCommandsAsAdvertised() {
|
|
|
1189
1413
|
var HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
1190
1414
|
var generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
1191
1415
|
var HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
1192
|
-
|
|
1193
|
-
|
|
1416
|
+
function generateMessageId() {
|
|
1417
|
+
return `m_${generateHydraId()}`;
|
|
1418
|
+
}
|
|
1419
|
+
var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
1194
1420
|
var Session = class {
|
|
1195
1421
|
sessionId;
|
|
1196
1422
|
cwd;
|
|
@@ -1232,11 +1458,13 @@ var Session = class {
|
|
|
1232
1458
|
// Bumped by broadcastPromptReceived, cleared by broadcastTurnComplete.
|
|
1233
1459
|
// Drives the mid-turn elapsed counter delivered to fresh attachers.
|
|
1234
1460
|
promptStartedAt;
|
|
1235
|
-
// Counts appends since the last compaction. When it hits
|
|
1461
|
+
// Counts appends since the last compaction. When it hits compactEvery
|
|
1236
1462
|
// we ask the history store to trim the file to the most recent
|
|
1237
|
-
//
|
|
1463
|
+
// historyMaxEntries. Keeps file growth bounded without per-append
|
|
1238
1464
|
// file-size checks.
|
|
1239
1465
|
appendCount = 0;
|
|
1466
|
+
historyMaxEntries;
|
|
1467
|
+
compactEvery;
|
|
1240
1468
|
// Permission requests that have been broadcast to one or more
|
|
1241
1469
|
// clients but have not yet resolved. Replayed to clients that
|
|
1242
1470
|
// attach mid-flight so a late joiner sees the prompt instead of an
|
|
@@ -1293,6 +1521,8 @@ var Session = class {
|
|
|
1293
1521
|
this.firstPromptSeeded = true;
|
|
1294
1522
|
}
|
|
1295
1523
|
this.historyStore = init.historyStore;
|
|
1524
|
+
this.historyMaxEntries = init.historyMaxEntries ?? DEFAULT_HISTORY_MAX_ENTRIES;
|
|
1525
|
+
this.compactEvery = Math.max(1, Math.floor(this.historyMaxEntries * 0.2));
|
|
1296
1526
|
this.updatedAt = Date.now();
|
|
1297
1527
|
this.createdAt = init.createdAt ?? this.updatedAt;
|
|
1298
1528
|
this.lastRecordedAt = this.updatedAt;
|
|
@@ -1360,6 +1590,30 @@ var Session = class {
|
|
|
1360
1590
|
get attachedCount() {
|
|
1361
1591
|
return this.clients.size;
|
|
1362
1592
|
}
|
|
1593
|
+
// Roster of currently-attached clients, optionally excluding one
|
|
1594
|
+
// clientId. Used by the daemon to populate connectedClients on the
|
|
1595
|
+
// session/attach response (per RFD #533) — the freshly-attaching
|
|
1596
|
+
// client wants to see who else is on the session but not itself in
|
|
1597
|
+
// the list.
|
|
1598
|
+
connectedClients(excludeClientId) {
|
|
1599
|
+
const out = [];
|
|
1600
|
+
for (const client of this.clients.values()) {
|
|
1601
|
+
if (excludeClientId && client.clientId === excludeClientId) {
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
const entry = {
|
|
1605
|
+
clientId: client.clientId
|
|
1606
|
+
};
|
|
1607
|
+
if (client.clientInfo?.name) {
|
|
1608
|
+
entry.name = client.clientInfo.name;
|
|
1609
|
+
}
|
|
1610
|
+
if (client.clientInfo?.version) {
|
|
1611
|
+
entry.version = client.clientInfo.version;
|
|
1612
|
+
}
|
|
1613
|
+
out.push(entry);
|
|
1614
|
+
}
|
|
1615
|
+
return out;
|
|
1616
|
+
}
|
|
1363
1617
|
// Wall-clock when the in-flight agent turn began, or undefined when
|
|
1364
1618
|
// idle. Tracked in-memory by broadcastPromptReceived/broadcastTurnComplete
|
|
1365
1619
|
// so the daemon can hand a fresh attacher mid-turn the right elapsed
|
|
@@ -1391,10 +1645,12 @@ var Session = class {
|
|
|
1391
1645
|
};
|
|
1392
1646
|
}
|
|
1393
1647
|
// Register a client and (asynchronously) load the replay slice it
|
|
1394
|
-
// should receive.
|
|
1395
|
-
//
|
|
1396
|
-
//
|
|
1397
|
-
|
|
1648
|
+
// should receive. Returns both the slice to replay and the actual
|
|
1649
|
+
// historyPolicy applied (which may differ from the requested one
|
|
1650
|
+
// when after_message falls back to full). Validation errors throw
|
|
1651
|
+
// synchronously so callers can rely on either the registration being
|
|
1652
|
+
// in effect or having thrown; the disk-load is the only async work.
|
|
1653
|
+
attach(client, historyPolicy, opts = {}) {
|
|
1398
1654
|
if (this.closed) {
|
|
1399
1655
|
throw withCode(
|
|
1400
1656
|
new Error("session is closed"),
|
|
@@ -1410,9 +1666,20 @@ var Session = class {
|
|
|
1410
1666
|
this.clients.set(client.clientId, client);
|
|
1411
1667
|
this.updatedAt = Date.now();
|
|
1412
1668
|
if (historyPolicy === "none" || historyPolicy === "pending_only") {
|
|
1413
|
-
return Promise.resolve([]);
|
|
1669
|
+
return Promise.resolve({ entries: [], appliedPolicy: historyPolicy });
|
|
1414
1670
|
}
|
|
1415
|
-
return this.
|
|
1671
|
+
return this.loadReplay(historyPolicy, opts);
|
|
1672
|
+
}
|
|
1673
|
+
async loadReplay(historyPolicy, opts) {
|
|
1674
|
+
const all = await this.getHistorySnapshot();
|
|
1675
|
+
if (historyPolicy === "after_message") {
|
|
1676
|
+
const cutoff = opts.afterMessageId ? findMessageIdIndex(all, opts.afterMessageId) : -1;
|
|
1677
|
+
if (cutoff < 0) {
|
|
1678
|
+
return { entries: all, appliedPolicy: "full" };
|
|
1679
|
+
}
|
|
1680
|
+
return { entries: all.slice(cutoff + 1), appliedPolicy: "after_message" };
|
|
1681
|
+
}
|
|
1682
|
+
return { entries: all, appliedPolicy: "full" };
|
|
1416
1683
|
}
|
|
1417
1684
|
// Dispatch in-flight permission requests to a freshly-attached
|
|
1418
1685
|
// client. Called by the daemon's WS handler *after* it finishes
|
|
@@ -1424,8 +1691,39 @@ var Session = class {
|
|
|
1424
1691
|
}
|
|
1425
1692
|
}
|
|
1426
1693
|
detach(clientId) {
|
|
1427
|
-
|
|
1428
|
-
|
|
1694
|
+
const leaving = this.clients.get(clientId);
|
|
1695
|
+
if (!leaving) {
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
this.clients.delete(clientId);
|
|
1699
|
+
this.updatedAt = Date.now();
|
|
1700
|
+
this.broadcastClientDisconnected(leaving);
|
|
1701
|
+
}
|
|
1702
|
+
// Notify remaining attached clients that a peer just left, per
|
|
1703
|
+
// RFD #533. Fires for both explicit session/detach and ws-close
|
|
1704
|
+
// teardown (acp-ws calls Session.detach() in both paths). The
|
|
1705
|
+
// notification is broadcast (not recorded) — peer presence is
|
|
1706
|
+
// transient, not part of conversation history.
|
|
1707
|
+
broadcastClientDisconnected(client) {
|
|
1708
|
+
const info = {
|
|
1709
|
+
clientId: client.clientId
|
|
1710
|
+
};
|
|
1711
|
+
if (client.clientInfo?.name) {
|
|
1712
|
+
info.name = client.clientInfo.name;
|
|
1713
|
+
}
|
|
1714
|
+
if (client.clientInfo?.version) {
|
|
1715
|
+
info.version = client.clientInfo.version;
|
|
1716
|
+
}
|
|
1717
|
+
const update = {
|
|
1718
|
+
sessionUpdate: "client_disconnected",
|
|
1719
|
+
client: info,
|
|
1720
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1721
|
+
};
|
|
1722
|
+
for (const peer of this.clients.values()) {
|
|
1723
|
+
void peer.connection.notify("session/update", {
|
|
1724
|
+
sessionId: this.sessionId,
|
|
1725
|
+
update
|
|
1726
|
+
}).catch(() => void 0);
|
|
1429
1727
|
}
|
|
1430
1728
|
}
|
|
1431
1729
|
async prompt(clientId, params) {
|
|
@@ -1478,6 +1776,7 @@ var Session = class {
|
|
|
1478
1776
|
sessionId: this.sessionId,
|
|
1479
1777
|
update: {
|
|
1480
1778
|
sessionUpdate: "prompt_received",
|
|
1779
|
+
messageId: generateMessageId(),
|
|
1481
1780
|
prompt: promptParams.prompt,
|
|
1482
1781
|
sentBy
|
|
1483
1782
|
}
|
|
@@ -1503,7 +1802,8 @@ var Session = class {
|
|
|
1503
1802
|
broadcastTurnComplete(originatorClientId, response) {
|
|
1504
1803
|
const stopReason = response && typeof response === "object" && "stopReason" in response && typeof response.stopReason === "string" ? response.stopReason : void 0;
|
|
1505
1804
|
const update = {
|
|
1506
|
-
sessionUpdate: "turn_complete"
|
|
1805
|
+
sessionUpdate: "turn_complete",
|
|
1806
|
+
messageId: generateMessageId()
|
|
1507
1807
|
};
|
|
1508
1808
|
if (stopReason !== void 0) {
|
|
1509
1809
|
update.stopReason = stopReason;
|
|
@@ -2120,10 +2420,11 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2120
2420
|
recordAndBroadcast(method, params, excludeClientId) {
|
|
2121
2421
|
const rewritten = this.rewriteForClient(params);
|
|
2122
2422
|
const recordable = !isStateUpdate(method, rewritten);
|
|
2423
|
+
const broadcast = recordable ? ensureMessageIdOnUpdate(method, rewritten) : rewritten;
|
|
2123
2424
|
if (recordable) {
|
|
2124
2425
|
const entry = {
|
|
2125
2426
|
method,
|
|
2126
|
-
params:
|
|
2427
|
+
params: broadcast,
|
|
2127
2428
|
recordedAt: Date.now()
|
|
2128
2429
|
};
|
|
2129
2430
|
this.lastRecordedAt = entry.recordedAt;
|
|
@@ -2131,9 +2432,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2131
2432
|
if (this.historyStore) {
|
|
2132
2433
|
const store = this.historyStore;
|
|
2133
2434
|
void store.append(this.sessionId, entry).catch(() => void 0);
|
|
2134
|
-
if (this.appendCount >=
|
|
2435
|
+
if (this.appendCount >= this.compactEvery) {
|
|
2135
2436
|
this.appendCount = 0;
|
|
2136
|
-
void store.compact(this.sessionId,
|
|
2437
|
+
void store.compact(this.sessionId, this.historyMaxEntries).catch(
|
|
2137
2438
|
() => void 0
|
|
2138
2439
|
);
|
|
2139
2440
|
}
|
|
@@ -2151,7 +2452,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2151
2452
|
if (excludeClientId && client.clientId === excludeClientId) {
|
|
2152
2453
|
continue;
|
|
2153
2454
|
}
|
|
2154
|
-
void client.connection.notify(method,
|
|
2455
|
+
void client.connection.notify(method, broadcast).catch(() => void 0);
|
|
2155
2456
|
}
|
|
2156
2457
|
}
|
|
2157
2458
|
async handlePermissionRequest(params) {
|
|
@@ -2163,11 +2464,13 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2163
2464
|
);
|
|
2164
2465
|
}
|
|
2165
2466
|
const clientParams = this.rewriteForClient(params);
|
|
2467
|
+
const toolCallId = extractToolCallId(clientParams);
|
|
2166
2468
|
return new Promise((resolve3, reject) => {
|
|
2167
2469
|
let settled = false;
|
|
2168
2470
|
const outbound = [];
|
|
2169
2471
|
const entry = { addClient: sendTo };
|
|
2170
2472
|
this.inFlightPermissions.add(entry);
|
|
2473
|
+
const sessionId = this.sessionId;
|
|
2171
2474
|
const settle = (fn) => {
|
|
2172
2475
|
if (settled) {
|
|
2173
2476
|
return;
|
|
@@ -2180,22 +2483,25 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
2180
2483
|
if (settled) {
|
|
2181
2484
|
return;
|
|
2182
2485
|
}
|
|
2183
|
-
const
|
|
2486
|
+
const response = client.connection.request(
|
|
2184
2487
|
"session/request_permission",
|
|
2185
2488
|
clientParams
|
|
2186
2489
|
);
|
|
2187
|
-
outbound.push({ client
|
|
2490
|
+
outbound.push({ client });
|
|
2188
2491
|
void response.then((result) => {
|
|
2189
2492
|
settle(() => {
|
|
2493
|
+
const update = buildPermissionResolvedUpdate({
|
|
2494
|
+
toolCallId,
|
|
2495
|
+
result,
|
|
2496
|
+
resolver: client
|
|
2497
|
+
});
|
|
2190
2498
|
for (const o of outbound) {
|
|
2191
2499
|
if (o.client.clientId === client.clientId) {
|
|
2192
2500
|
continue;
|
|
2193
2501
|
}
|
|
2194
|
-
void o.client.connection.notify("session/
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
resolvedBy: client.clientId,
|
|
2198
|
-
result
|
|
2502
|
+
void o.client.connection.notify("session/update", {
|
|
2503
|
+
sessionId,
|
|
2504
|
+
update
|
|
2199
2505
|
}).catch(() => void 0);
|
|
2200
2506
|
}
|
|
2201
2507
|
resolve3(result);
|
|
@@ -2308,6 +2614,97 @@ function extractAdvertisedCommands(params) {
|
|
|
2308
2614
|
}
|
|
2309
2615
|
return out;
|
|
2310
2616
|
}
|
|
2617
|
+
function ensureMessageIdOnUpdate(method, params) {
|
|
2618
|
+
if (method !== "session/update" || !params || typeof params !== "object") {
|
|
2619
|
+
return params;
|
|
2620
|
+
}
|
|
2621
|
+
const p = params;
|
|
2622
|
+
if (!p.update || typeof p.update !== "object" || Array.isArray(p.update)) {
|
|
2623
|
+
return params;
|
|
2624
|
+
}
|
|
2625
|
+
const u = p.update;
|
|
2626
|
+
if (typeof u.messageId === "string") {
|
|
2627
|
+
return params;
|
|
2628
|
+
}
|
|
2629
|
+
return {
|
|
2630
|
+
...params,
|
|
2631
|
+
update: { ...p.update, messageId: generateMessageId() }
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
function findMessageIdIndex(history, target) {
|
|
2635
|
+
for (let i = 0; i < history.length; i++) {
|
|
2636
|
+
const entry = history[i];
|
|
2637
|
+
if (!entry || entry.method !== "session/update") {
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2640
|
+
const params = entry.params;
|
|
2641
|
+
if (params?.update?.messageId === target) {
|
|
2642
|
+
return i;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
return -1;
|
|
2646
|
+
}
|
|
2647
|
+
function extractToolCallId(params) {
|
|
2648
|
+
if (!params || typeof params !== "object") {
|
|
2649
|
+
return void 0;
|
|
2650
|
+
}
|
|
2651
|
+
const toolCall = params.toolCall;
|
|
2652
|
+
if (!toolCall || typeof toolCall !== "object") {
|
|
2653
|
+
return void 0;
|
|
2654
|
+
}
|
|
2655
|
+
const id = toolCall.toolCallId;
|
|
2656
|
+
return typeof id === "string" ? id : void 0;
|
|
2657
|
+
}
|
|
2658
|
+
function buildPermissionResolvedUpdate(args) {
|
|
2659
|
+
const outcome = extractOutcome(args.result);
|
|
2660
|
+
const update = {
|
|
2661
|
+
sessionUpdate: "permission_resolved"
|
|
2662
|
+
};
|
|
2663
|
+
if (args.toolCallId !== void 0) {
|
|
2664
|
+
update.toolCallId = args.toolCallId;
|
|
2665
|
+
}
|
|
2666
|
+
if (outcome) {
|
|
2667
|
+
update.outcome = outcome;
|
|
2668
|
+
if (outcome.kind === "selected" && typeof outcome.optionId === "string") {
|
|
2669
|
+
update.chosenOptionId = outcome.optionId;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
update.resolvedBy = buildResolvedBy(args.resolver);
|
|
2673
|
+
return update;
|
|
2674
|
+
}
|
|
2675
|
+
function extractOutcome(result) {
|
|
2676
|
+
if (!result || typeof result !== "object") {
|
|
2677
|
+
return void 0;
|
|
2678
|
+
}
|
|
2679
|
+
const raw = result.outcome;
|
|
2680
|
+
if (!raw || typeof raw !== "object") {
|
|
2681
|
+
return void 0;
|
|
2682
|
+
}
|
|
2683
|
+
const kind = raw.kind;
|
|
2684
|
+
if (typeof kind !== "string") {
|
|
2685
|
+
return void 0;
|
|
2686
|
+
}
|
|
2687
|
+
const out = { kind };
|
|
2688
|
+
const optionId = raw.optionId;
|
|
2689
|
+
if (typeof optionId === "string") {
|
|
2690
|
+
out.optionId = optionId;
|
|
2691
|
+
}
|
|
2692
|
+
const reason = raw.reason;
|
|
2693
|
+
if (typeof reason === "string") {
|
|
2694
|
+
out.reason = reason;
|
|
2695
|
+
}
|
|
2696
|
+
return out;
|
|
2697
|
+
}
|
|
2698
|
+
function buildResolvedBy(client) {
|
|
2699
|
+
const out = { clientId: client.clientId };
|
|
2700
|
+
if (client.clientInfo?.name) {
|
|
2701
|
+
out.name = client.clientInfo.name;
|
|
2702
|
+
}
|
|
2703
|
+
if (client.clientInfo?.version) {
|
|
2704
|
+
out.version = client.clientInfo.version;
|
|
2705
|
+
}
|
|
2706
|
+
return out;
|
|
2707
|
+
}
|
|
2311
2708
|
function extractPromptText(prompt) {
|
|
2312
2709
|
if (typeof prompt === "string") {
|
|
2313
2710
|
return prompt;
|
|
@@ -2335,7 +2732,7 @@ function firstLine(text, max) {
|
|
|
2335
2732
|
|
|
2336
2733
|
// src/core/session-store.ts
|
|
2337
2734
|
import * as fs4 from "fs/promises";
|
|
2338
|
-
import * as
|
|
2735
|
+
import * as path4 from "path";
|
|
2339
2736
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
2340
2737
|
import { z as z4 } from "zod";
|
|
2341
2738
|
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -2507,12 +2904,16 @@ function recordFromMemorySession(args) {
|
|
|
2507
2904
|
// src/core/history-store.ts
|
|
2508
2905
|
import * as fs5 from "fs/promises";
|
|
2509
2906
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
2510
|
-
var
|
|
2907
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
2511
2908
|
var HistoryStore = class {
|
|
2512
2909
|
// Serialize writes per session id so appends and rewrites don't
|
|
2513
2910
|
// interleave JSONL lines on disk. The chain swallows errors so one
|
|
2514
2911
|
// failed append doesn't poison every subsequent write.
|
|
2515
2912
|
writeQueues = /* @__PURE__ */ new Map();
|
|
2913
|
+
maxEntries;
|
|
2914
|
+
constructor(options = {}) {
|
|
2915
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2916
|
+
}
|
|
2516
2917
|
async append(sessionId, entry) {
|
|
2517
2918
|
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
2518
2919
|
return;
|
|
@@ -2614,8 +3015,8 @@ var HistoryStore = class {
|
|
|
2614
3015
|
recordedAt: obj.recordedAt
|
|
2615
3016
|
});
|
|
2616
3017
|
}
|
|
2617
|
-
if (out.length >
|
|
2618
|
-
return out.slice(-
|
|
3018
|
+
if (out.length > this.maxEntries) {
|
|
3019
|
+
return out.slice(-this.maxEntries);
|
|
2619
3020
|
}
|
|
2620
3021
|
return out;
|
|
2621
3022
|
}
|
|
@@ -2658,13 +3059,40 @@ var HistoryStore = class {
|
|
|
2658
3059
|
|
|
2659
3060
|
// src/tui/history.ts
|
|
2660
3061
|
import { promises as fs6 } from "fs";
|
|
2661
|
-
import * as
|
|
3062
|
+
import * as path5 from "path";
|
|
2662
3063
|
async function saveHistory(file, history) {
|
|
2663
|
-
await fs6.mkdir(
|
|
3064
|
+
await fs6.mkdir(path5.dirname(file), { recursive: true });
|
|
2664
3065
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
2665
3066
|
await fs6.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
2666
3067
|
}
|
|
2667
3068
|
|
|
3069
|
+
// src/core/hydra-version.ts
|
|
3070
|
+
import { fileURLToPath } from "url";
|
|
3071
|
+
import * as path6 from "path";
|
|
3072
|
+
import * as fs7 from "fs";
|
|
3073
|
+
function resolveVersion() {
|
|
3074
|
+
try {
|
|
3075
|
+
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
3076
|
+
for (let i = 0; i < 8; i += 1) {
|
|
3077
|
+
const candidate = path6.join(dir, "package.json");
|
|
3078
|
+
if (fs7.existsSync(candidate)) {
|
|
3079
|
+
const pkg = JSON.parse(fs7.readFileSync(candidate, "utf8"));
|
|
3080
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
|
|
3081
|
+
return pkg.version;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
const parent = path6.dirname(dir);
|
|
3085
|
+
if (parent === dir) {
|
|
3086
|
+
break;
|
|
3087
|
+
}
|
|
3088
|
+
dir = parent;
|
|
3089
|
+
}
|
|
3090
|
+
} catch {
|
|
3091
|
+
}
|
|
3092
|
+
return "0.0.0";
|
|
3093
|
+
}
|
|
3094
|
+
var HYDRA_VERSION = resolveVersion();
|
|
3095
|
+
|
|
2668
3096
|
// src/core/session-manager.ts
|
|
2669
3097
|
var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
2670
3098
|
var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
|
|
@@ -2673,7 +3101,8 @@ var SessionManager = class {
|
|
|
2673
3101
|
this.registry = registry;
|
|
2674
3102
|
this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
|
|
2675
3103
|
this.store = store ?? new SessionStore();
|
|
2676
|
-
this.
|
|
3104
|
+
this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
|
|
3105
|
+
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
2677
3106
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
2678
3107
|
this.defaultModels = options.defaultModels ?? {};
|
|
2679
3108
|
}
|
|
@@ -2685,6 +3114,7 @@ var SessionManager = class {
|
|
|
2685
3114
|
histories;
|
|
2686
3115
|
idleTimeoutMs;
|
|
2687
3116
|
defaultModels;
|
|
3117
|
+
sessionHistoryMaxEntries;
|
|
2688
3118
|
// Serialize meta.json read-modify-write operations per session id so
|
|
2689
3119
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
2690
3120
|
// back-to-back) don't lose writes via interleaved reads.
|
|
@@ -2708,6 +3138,7 @@ var SessionManager = class {
|
|
|
2708
3138
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2709
3139
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2710
3140
|
historyStore: this.histories,
|
|
3141
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
2711
3142
|
currentModel: fresh.initialModel
|
|
2712
3143
|
});
|
|
2713
3144
|
await this.attachManagerHooks(session);
|
|
@@ -2759,11 +3190,16 @@ var SessionManager = class {
|
|
|
2759
3190
|
cwd: params.cwd,
|
|
2760
3191
|
plan
|
|
2761
3192
|
});
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
3193
|
+
try {
|
|
3194
|
+
await agent.connection.request("initialize", {
|
|
3195
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
3196
|
+
clientCapabilities: {},
|
|
3197
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
3198
|
+
});
|
|
3199
|
+
} catch (err) {
|
|
3200
|
+
await agent.kill().catch(() => void 0);
|
|
3201
|
+
throw err;
|
|
3202
|
+
}
|
|
2767
3203
|
let loadResult;
|
|
2768
3204
|
try {
|
|
2769
3205
|
loadResult = await agent.connection.request(
|
|
@@ -2775,10 +3211,12 @@ var SessionManager = class {
|
|
|
2775
3211
|
}
|
|
2776
3212
|
);
|
|
2777
3213
|
} catch (err) {
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
3214
|
+
process.stderr.write(
|
|
3215
|
+
`session/load failed for upstream ${params.upstreamSessionId} on ${params.agentId} (${err.message}); recovering via import-reseed
|
|
3216
|
+
`
|
|
2781
3217
|
);
|
|
3218
|
+
await agent.kill().catch(() => void 0);
|
|
3219
|
+
return this.doResurrectFromImport(params);
|
|
2782
3220
|
}
|
|
2783
3221
|
const session = new Session({
|
|
2784
3222
|
sessionId: params.hydraSessionId,
|
|
@@ -2792,6 +3230,7 @@ var SessionManager = class {
|
|
|
2792
3230
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2793
3231
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2794
3232
|
historyStore: this.histories,
|
|
3233
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
2795
3234
|
// Prefer what we previously stored from a current_model_update; if
|
|
2796
3235
|
// we never captured one (e.g. old opencode sessions on disk before
|
|
2797
3236
|
// this fix), fall back to the model the agent ships in its
|
|
@@ -2818,15 +3257,16 @@ var SessionManager = class {
|
|
|
2818
3257
|
// so subsequent resurrects of this session use the normal session/load
|
|
2819
3258
|
// path.
|
|
2820
3259
|
async doResurrectFromImport(params) {
|
|
3260
|
+
const cwd = await this.resolveImportCwd(params.cwd);
|
|
2821
3261
|
const fresh = await this.bootstrapAgent({
|
|
2822
3262
|
agentId: params.agentId,
|
|
2823
|
-
cwd
|
|
3263
|
+
cwd,
|
|
2824
3264
|
agentArgs: params.agentArgs,
|
|
2825
3265
|
mcpServers: []
|
|
2826
3266
|
});
|
|
2827
3267
|
const session = new Session({
|
|
2828
3268
|
sessionId: params.hydraSessionId,
|
|
2829
|
-
cwd
|
|
3269
|
+
cwd,
|
|
2830
3270
|
agentId: params.agentId,
|
|
2831
3271
|
agent: fresh.agent,
|
|
2832
3272
|
upstreamSessionId: fresh.upstreamSessionId,
|
|
@@ -2836,6 +3276,7 @@ var SessionManager = class {
|
|
|
2836
3276
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
2837
3277
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
2838
3278
|
historyStore: this.histories,
|
|
3279
|
+
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
2839
3280
|
// Prefer the stored value (set by a previous current_model_update);
|
|
2840
3281
|
// fall back to whatever the agent ships in its session/new response.
|
|
2841
3282
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
@@ -2849,6 +3290,16 @@ var SessionManager = class {
|
|
|
2849
3290
|
void session.seedFromImport().catch(() => void 0);
|
|
2850
3291
|
return session;
|
|
2851
3292
|
}
|
|
3293
|
+
async resolveImportCwd(cwd) {
|
|
3294
|
+
try {
|
|
3295
|
+
const stat2 = await fs8.stat(cwd);
|
|
3296
|
+
if (stat2.isDirectory()) {
|
|
3297
|
+
return cwd;
|
|
3298
|
+
}
|
|
3299
|
+
} catch {
|
|
3300
|
+
}
|
|
3301
|
+
return os2.homedir();
|
|
3302
|
+
}
|
|
2852
3303
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
2853
3304
|
// → session/new. Shared by create() and the /hydra agent path so both
|
|
2854
3305
|
// go through the same env / capabilities / error-handling.
|
|
@@ -2869,9 +3320,9 @@ var SessionManager = class {
|
|
|
2869
3320
|
});
|
|
2870
3321
|
try {
|
|
2871
3322
|
await agent.connection.request("initialize", {
|
|
2872
|
-
protocolVersion:
|
|
3323
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2873
3324
|
clientCapabilities: {},
|
|
2874
|
-
clientInfo: { name: "hydra", version:
|
|
3325
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
2875
3326
|
});
|
|
2876
3327
|
const newResult = await agent.connection.request(
|
|
2877
3328
|
"session/new",
|
|
@@ -3151,7 +3602,8 @@ var SessionManager = class {
|
|
|
3151
3602
|
await this.writeImportedRecord({
|
|
3152
3603
|
sessionId: existing.sessionId,
|
|
3153
3604
|
bundle,
|
|
3154
|
-
preservedCreatedAt: existing.createdAt
|
|
3605
|
+
preservedCreatedAt: existing.createdAt,
|
|
3606
|
+
cwd: opts.cwd
|
|
3155
3607
|
});
|
|
3156
3608
|
return {
|
|
3157
3609
|
sessionId: existing.sessionId,
|
|
@@ -3160,7 +3612,11 @@ var SessionManager = class {
|
|
|
3160
3612
|
};
|
|
3161
3613
|
}
|
|
3162
3614
|
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
3163
|
-
await this.writeImportedRecord({
|
|
3615
|
+
await this.writeImportedRecord({
|
|
3616
|
+
sessionId: newId,
|
|
3617
|
+
bundle,
|
|
3618
|
+
cwd: opts.cwd
|
|
3619
|
+
});
|
|
3164
3620
|
return {
|
|
3165
3621
|
sessionId: newId,
|
|
3166
3622
|
importedFromSessionId: bundle.session.sessionId,
|
|
@@ -3190,7 +3646,7 @@ var SessionManager = class {
|
|
|
3190
3646
|
upstreamSessionId: "",
|
|
3191
3647
|
importedFromSessionId: args.bundle.session.sessionId,
|
|
3192
3648
|
agentId: args.bundle.session.agentId,
|
|
3193
|
-
cwd: args.bundle.session.cwd,
|
|
3649
|
+
cwd: args.cwd ?? args.bundle.session.cwd,
|
|
3194
3650
|
title: args.bundle.session.title,
|
|
3195
3651
|
currentModel: args.bundle.session.currentModel,
|
|
3196
3652
|
currentMode: args.bundle.session.currentMode,
|
|
@@ -3383,7 +3839,7 @@ function asString(value) {
|
|
|
3383
3839
|
}
|
|
3384
3840
|
async function loadPromptHistorySafely(sessionId) {
|
|
3385
3841
|
try {
|
|
3386
|
-
const raw = await
|
|
3842
|
+
const raw = await fs8.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
3387
3843
|
const out = [];
|
|
3388
3844
|
for (const line of raw.split("\n")) {
|
|
3389
3845
|
if (line.length === 0) {
|
|
@@ -3404,7 +3860,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
3404
3860
|
}
|
|
3405
3861
|
async function historyMtimeIso(sessionId) {
|
|
3406
3862
|
try {
|
|
3407
|
-
const st = await
|
|
3863
|
+
const st = await fs8.stat(paths.historyFile(sessionId));
|
|
3408
3864
|
return new Date(st.mtimeMs).toISOString();
|
|
3409
3865
|
} catch {
|
|
3410
3866
|
return void 0;
|
|
@@ -3412,10 +3868,10 @@ async function historyMtimeIso(sessionId) {
|
|
|
3412
3868
|
}
|
|
3413
3869
|
|
|
3414
3870
|
// src/core/extensions.ts
|
|
3415
|
-
import { spawn as
|
|
3416
|
-
import * as
|
|
3417
|
-
import * as
|
|
3418
|
-
import * as
|
|
3871
|
+
import { spawn as spawn4 } from "child_process";
|
|
3872
|
+
import * as fs9 from "fs";
|
|
3873
|
+
import * as fsp3 from "fs/promises";
|
|
3874
|
+
import * as path7 from "path";
|
|
3419
3875
|
var RESTART_BASE_MS = 1e3;
|
|
3420
3876
|
var RESTART_CAP_MS = 6e4;
|
|
3421
3877
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -3436,7 +3892,7 @@ var ExtensionManager = class {
|
|
|
3436
3892
|
if (!this.context) {
|
|
3437
3893
|
throw new Error("ExtensionManager: setContext must be called before start");
|
|
3438
3894
|
}
|
|
3439
|
-
await
|
|
3895
|
+
await fsp3.mkdir(paths.extensionsDir(), { recursive: true });
|
|
3440
3896
|
await this.reapOrphans();
|
|
3441
3897
|
for (const entry of this.entries.values()) {
|
|
3442
3898
|
if (!entry.config.enabled) {
|
|
@@ -3645,7 +4101,7 @@ var ExtensionManager = class {
|
|
|
3645
4101
|
async reapOrphans() {
|
|
3646
4102
|
let entries;
|
|
3647
4103
|
try {
|
|
3648
|
-
entries = await
|
|
4104
|
+
entries = await fsp3.readdir(paths.extensionsDir());
|
|
3649
4105
|
} catch (err) {
|
|
3650
4106
|
const e = err;
|
|
3651
4107
|
if (e.code === "ENOENT") {
|
|
@@ -3657,10 +4113,10 @@ var ExtensionManager = class {
|
|
|
3657
4113
|
if (!entry.endsWith(".pid")) {
|
|
3658
4114
|
continue;
|
|
3659
4115
|
}
|
|
3660
|
-
const pidPath =
|
|
4116
|
+
const pidPath = path7.join(paths.extensionsDir(), entry);
|
|
3661
4117
|
let pid;
|
|
3662
4118
|
try {
|
|
3663
|
-
const raw = await
|
|
4119
|
+
const raw = await fsp3.readFile(pidPath, "utf8");
|
|
3664
4120
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
3665
4121
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
3666
4122
|
pid = parsed;
|
|
@@ -3683,7 +4139,7 @@ var ExtensionManager = class {
|
|
|
3683
4139
|
}
|
|
3684
4140
|
}
|
|
3685
4141
|
}
|
|
3686
|
-
await
|
|
4142
|
+
await fsp3.unlink(pidPath).catch(() => void 0);
|
|
3687
4143
|
}
|
|
3688
4144
|
}
|
|
3689
4145
|
spawn(entry, attempt) {
|
|
@@ -3696,7 +4152,7 @@ var ExtensionManager = class {
|
|
|
3696
4152
|
}
|
|
3697
4153
|
const ext = entry.config;
|
|
3698
4154
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
3699
|
-
const logStream =
|
|
4155
|
+
const logStream = fs9.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
3700
4156
|
flags: "a"
|
|
3701
4157
|
});
|
|
3702
4158
|
logStream.write(
|
|
@@ -3724,7 +4180,7 @@ var ExtensionManager = class {
|
|
|
3724
4180
|
const args = [...baseArgs, ...ext.args];
|
|
3725
4181
|
let child;
|
|
3726
4182
|
try {
|
|
3727
|
-
child =
|
|
4183
|
+
child = spawn4(cmd, args, {
|
|
3728
4184
|
env,
|
|
3729
4185
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3730
4186
|
detached: false
|
|
@@ -3746,7 +4202,7 @@ var ExtensionManager = class {
|
|
|
3746
4202
|
}
|
|
3747
4203
|
if (typeof child.pid === "number") {
|
|
3748
4204
|
try {
|
|
3749
|
-
|
|
4205
|
+
fs9.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
3750
4206
|
`, {
|
|
3751
4207
|
encoding: "utf8",
|
|
3752
4208
|
mode: 384
|
|
@@ -3771,7 +4227,7 @@ var ExtensionManager = class {
|
|
|
3771
4227
|
});
|
|
3772
4228
|
child.on("exit", (code, signal) => {
|
|
3773
4229
|
try {
|
|
3774
|
-
|
|
4230
|
+
fs9.unlinkSync(paths.extensionPidFile(ext.name));
|
|
3775
4231
|
} catch {
|
|
3776
4232
|
}
|
|
3777
4233
|
logStream.write(
|
|
@@ -3880,7 +4336,7 @@ function constantTimeEqual(a, b) {
|
|
|
3880
4336
|
}
|
|
3881
4337
|
|
|
3882
4338
|
// src/daemon/routes/sessions.ts
|
|
3883
|
-
import * as
|
|
4339
|
+
import * as os3 from "os";
|
|
3884
4340
|
|
|
3885
4341
|
// src/core/bundle.ts
|
|
3886
4342
|
import { z as z5 } from "zod";
|
|
@@ -3950,7 +4406,6 @@ function decodeBundle(raw) {
|
|
|
3950
4406
|
}
|
|
3951
4407
|
|
|
3952
4408
|
// src/daemon/routes/sessions.ts
|
|
3953
|
-
var HYDRA_VERSION = "0.1.0";
|
|
3954
4409
|
function registerSessionRoutes(app, manager, defaults) {
|
|
3955
4410
|
app.get("/v1/sessions", async (request) => {
|
|
3956
4411
|
const query = request.query;
|
|
@@ -4021,12 +4476,12 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
4021
4476
|
history: exported.history,
|
|
4022
4477
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
4023
4478
|
hydraVersion: HYDRA_VERSION,
|
|
4024
|
-
machine:
|
|
4479
|
+
machine: os3.hostname()
|
|
4025
4480
|
});
|
|
4026
4481
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4027
4482
|
reply.header(
|
|
4028
4483
|
"Content-Disposition",
|
|
4029
|
-
`attachment; filename="
|
|
4484
|
+
`attachment; filename="${id}-${stamp}.hydra"`
|
|
4030
4485
|
);
|
|
4031
4486
|
reply.code(200).send(bundle);
|
|
4032
4487
|
});
|
|
@@ -4036,6 +4491,14 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
4036
4491
|
reply.code(400).send({ error: "missing bundle" });
|
|
4037
4492
|
return;
|
|
4038
4493
|
}
|
|
4494
|
+
let cwdOverride;
|
|
4495
|
+
if (body.cwd !== void 0) {
|
|
4496
|
+
if (typeof body.cwd !== "string" || body.cwd.length === 0) {
|
|
4497
|
+
reply.code(400).send({ error: "cwd must be a non-empty string" });
|
|
4498
|
+
return;
|
|
4499
|
+
}
|
|
4500
|
+
cwdOverride = body.cwd;
|
|
4501
|
+
}
|
|
4039
4502
|
let bundle;
|
|
4040
4503
|
try {
|
|
4041
4504
|
bundle = decodeBundle(body.bundle);
|
|
@@ -4048,7 +4511,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
4048
4511
|
}
|
|
4049
4512
|
try {
|
|
4050
4513
|
const result = await manager.importBundle(bundle, {
|
|
4051
|
-
replace: body.replace === true
|
|
4514
|
+
replace: body.replace === true,
|
|
4515
|
+
...cwdOverride !== void 0 ? { cwd: cwdOverride } : {}
|
|
4052
4516
|
});
|
|
4053
4517
|
reply.code(201).send(result);
|
|
4054
4518
|
} catch (err) {
|
|
@@ -4359,8 +4823,6 @@ function wsToMessageStream(ws) {
|
|
|
4359
4823
|
}
|
|
4360
4824
|
|
|
4361
4825
|
// src/daemon/acp-ws.ts
|
|
4362
|
-
var HYDRA_VERSION2 = "0.1.0";
|
|
4363
|
-
var HYDRA_PROTOCOL_VERSION = 1;
|
|
4364
4826
|
function registerAcpWsEndpoint(app, deps) {
|
|
4365
4827
|
app.get("/acp", { websocket: true }, (socket, request) => {
|
|
4366
4828
|
const token = tokenFromUpgradeRequest({
|
|
@@ -4454,15 +4916,20 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
4454
4916
|
connection,
|
|
4455
4917
|
session,
|
|
4456
4918
|
state,
|
|
4457
|
-
params.clientInfo
|
|
4919
|
+
params.clientInfo,
|
|
4920
|
+
params.clientId
|
|
4921
|
+
);
|
|
4922
|
+
const { entries: replay, appliedPolicy } = await session.attach(
|
|
4923
|
+
client,
|
|
4924
|
+
params.historyPolicy,
|
|
4925
|
+
{ afterMessageId: params.afterMessageId }
|
|
4458
4926
|
);
|
|
4459
|
-
const replay = await session.attach(client, params.historyPolicy);
|
|
4460
4927
|
state.attached.set(session.sessionId, {
|
|
4461
4928
|
sessionId: session.sessionId,
|
|
4462
4929
|
clientId: client.clientId
|
|
4463
4930
|
});
|
|
4464
4931
|
app.log.info(
|
|
4465
|
-
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} replayed=${replay.length}`
|
|
4932
|
+
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length}`
|
|
4466
4933
|
);
|
|
4467
4934
|
for (const note of replay) {
|
|
4468
4935
|
await connection.notify(note.method, note.params);
|
|
@@ -4470,6 +4937,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
4470
4937
|
session.replayPendingPermissions(client);
|
|
4471
4938
|
return {
|
|
4472
4939
|
sessionId: session.sessionId,
|
|
4940
|
+
clientId: client.clientId,
|
|
4941
|
+
connectedClients: session.connectedClients(client.clientId),
|
|
4942
|
+
// appliedPolicy surfaces whether after_message fell back to full
|
|
4943
|
+
// (because afterMessageId wasn't found in history) — RFD #533
|
|
4944
|
+
// says the response.historyPolicy should reflect what actually
|
|
4945
|
+
// ran, not what was asked for.
|
|
4946
|
+
historyPolicy: appliedPolicy,
|
|
4473
4947
|
replayed: replay.length,
|
|
4474
4948
|
_meta: buildResponseMeta(session)
|
|
4475
4949
|
};
|
|
@@ -4485,7 +4959,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
4485
4959
|
const session = deps.manager.get(params.sessionId);
|
|
4486
4960
|
session?.detach(att.clientId);
|
|
4487
4961
|
state.attached.delete(params.sessionId);
|
|
4488
|
-
return {
|
|
4962
|
+
return { sessionId: params.sessionId, status: "detached" };
|
|
4489
4963
|
});
|
|
4490
4964
|
connection.onRequest("session/list", async (raw) => {
|
|
4491
4965
|
const params = SessionListParams.parse(raw ?? {});
|
|
@@ -4558,7 +5032,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
4558
5032
|
session = await deps.manager.resurrect(fromDisk);
|
|
4559
5033
|
}
|
|
4560
5034
|
const client = bindClientToSession(connection, session, state);
|
|
4561
|
-
const replay = await session.attach(client, "pending_only");
|
|
5035
|
+
const { entries: replay } = await session.attach(client, "pending_only");
|
|
4562
5036
|
state.attached.set(session.sessionId, {
|
|
4563
5037
|
sessionId: session.sessionId,
|
|
4564
5038
|
clientId: client.clientId
|
|
@@ -4623,8 +5097,8 @@ function buildResponseMeta(session) {
|
|
|
4623
5097
|
}
|
|
4624
5098
|
function buildInitializeResult() {
|
|
4625
5099
|
return {
|
|
4626
|
-
protocolVersion:
|
|
4627
|
-
agentInfo: { name: "hydra", version:
|
|
5100
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
5101
|
+
agentInfo: { name: "hydra", version: HYDRA_VERSION },
|
|
4628
5102
|
agentCapabilities: {
|
|
4629
5103
|
// hydra is a transparent proxy: prompt blocks and MCP server configs are
|
|
4630
5104
|
// forwarded to the underlying agent unchanged. We claim the union of
|
|
@@ -4652,25 +5126,24 @@ function buildInitializeResult() {
|
|
|
4652
5126
|
]
|
|
4653
5127
|
};
|
|
4654
5128
|
}
|
|
4655
|
-
function bindClientToSession(connection, session, state, clientInfo) {
|
|
5129
|
+
function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
|
|
4656
5130
|
void state;
|
|
4657
5131
|
void session;
|
|
4658
5132
|
return {
|
|
4659
|
-
clientId: `cli_${nanoid2(8)}`,
|
|
5133
|
+
clientId: callerClientId ?? `cli_${nanoid2(8)}`,
|
|
4660
5134
|
connection,
|
|
4661
5135
|
clientInfo
|
|
4662
5136
|
};
|
|
4663
5137
|
}
|
|
4664
5138
|
|
|
4665
5139
|
// src/daemon/server.ts
|
|
4666
|
-
var HYDRA_VERSION3 = "0.1.0";
|
|
4667
5140
|
async function startDaemon(config) {
|
|
4668
5141
|
ensureLoopbackOrTls(config);
|
|
4669
5142
|
const httpsOptions = config.daemon.tls ? {
|
|
4670
|
-
key: await
|
|
4671
|
-
cert: await
|
|
5143
|
+
key: await fsp4.readFile(config.daemon.tls.key),
|
|
5144
|
+
cert: await fsp4.readFile(config.daemon.tls.cert)
|
|
4672
5145
|
} : void 0;
|
|
4673
|
-
await
|
|
5146
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
4674
5147
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
4675
5148
|
config.daemon.logLevel
|
|
4676
5149
|
);
|
|
@@ -4679,12 +5152,18 @@ async function startDaemon(config) {
|
|
|
4679
5152
|
level: config.daemon.logLevel,
|
|
4680
5153
|
stream: logStream
|
|
4681
5154
|
},
|
|
4682
|
-
https: httpsOptions ?? null
|
|
5155
|
+
https: httpsOptions ?? null,
|
|
5156
|
+
// Session bundles can be large (full history + tool output);
|
|
5157
|
+
// the 1MB Fastify default rejects ordinary imports.
|
|
5158
|
+
bodyLimit: 256 * 1024 * 1024
|
|
4683
5159
|
});
|
|
4684
5160
|
await app.register(websocketPlugin);
|
|
4685
5161
|
setBinaryInstallLogger((msg) => {
|
|
4686
5162
|
app.log.info(msg);
|
|
4687
5163
|
});
|
|
5164
|
+
setNpmInstallLogger((msg) => {
|
|
5165
|
+
app.log.info(msg);
|
|
5166
|
+
});
|
|
4688
5167
|
const auth = bearerAuth({ config });
|
|
4689
5168
|
app.addHook("onRequest", async (request, reply) => {
|
|
4690
5169
|
if (request.routeOptions.config?.skipAuth) {
|
|
@@ -4696,12 +5175,14 @@ async function startDaemon(config) {
|
|
|
4696
5175
|
await auth(request, reply);
|
|
4697
5176
|
});
|
|
4698
5177
|
const registry = new Registry(config);
|
|
4699
|
-
const
|
|
5178
|
+
const spawner = (opts) => AgentInstance.spawn({ ...opts, stderrTailBytes: config.daemon.agentStderrTailBytes });
|
|
5179
|
+
const manager = new SessionManager(registry, spawner, void 0, {
|
|
4700
5180
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
4701
|
-
defaultModels: config.defaultModels
|
|
5181
|
+
defaultModels: config.defaultModels,
|
|
5182
|
+
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
|
|
4702
5183
|
});
|
|
4703
5184
|
const extensions = new ExtensionManager(extensionList(config));
|
|
4704
|
-
registerHealthRoutes(app,
|
|
5185
|
+
registerHealthRoutes(app, HYDRA_VERSION);
|
|
4705
5186
|
registerSessionRoutes(app, manager, {
|
|
4706
5187
|
agentId: config.defaultAgent,
|
|
4707
5188
|
cwd: config.defaultCwd
|
|
@@ -4720,8 +5201,8 @@ async function startDaemon(config) {
|
|
|
4720
5201
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
4721
5202
|
const address = app.server.address();
|
|
4722
5203
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
4723
|
-
await
|
|
4724
|
-
await
|
|
5204
|
+
await fsp4.mkdir(paths.home(), { recursive: true });
|
|
5205
|
+
await fsp4.writeFile(
|
|
4725
5206
|
paths.pidFile(),
|
|
4726
5207
|
JSON.stringify({
|
|
4727
5208
|
pid: process.pid,
|
|
@@ -4747,9 +5228,10 @@ async function startDaemon(config) {
|
|
|
4747
5228
|
await manager.closeAll();
|
|
4748
5229
|
await manager.flushMetaWrites();
|
|
4749
5230
|
setBinaryInstallLogger(null);
|
|
5231
|
+
setNpmInstallLogger(null);
|
|
4750
5232
|
await app.close();
|
|
4751
5233
|
try {
|
|
4752
|
-
|
|
5234
|
+
fs10.unlinkSync(paths.pidFile());
|
|
4753
5235
|
} catch {
|
|
4754
5236
|
}
|
|
4755
5237
|
try {
|