@hydra-acp/cli 0.1.51 → 0.1.53
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 +50 -52
- package/dist/cli.js +3977 -2269
- package/dist/index.d.ts +88 -2
- package/dist/index.js +1223 -408
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import pino from "pino";
|
|
|
18
18
|
import createPinoRoll from "pino-roll";
|
|
19
19
|
|
|
20
20
|
// src/core/config.ts
|
|
21
|
-
import * as
|
|
21
|
+
import * as fs3 from "fs/promises";
|
|
22
22
|
import { homedir as homedir2 } from "os";
|
|
23
23
|
import { z } from "zod";
|
|
24
24
|
|
|
@@ -156,6 +156,70 @@ async function ensureServiceToken() {
|
|
|
156
156
|
return token;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// src/core/json-store.ts
|
|
160
|
+
import * as fs2 from "fs/promises";
|
|
161
|
+
import * as fsSync from "fs";
|
|
162
|
+
import { randomBytes } from "crypto";
|
|
163
|
+
async function writeJsonAtomic(filePath, data, opts = {}) {
|
|
164
|
+
const pretty = opts.pretty ?? true;
|
|
165
|
+
const body = (pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data)) + "\n";
|
|
166
|
+
await writeFileAtomic(filePath, body, opts);
|
|
167
|
+
}
|
|
168
|
+
async function writeFileAtomic(filePath, body, opts = {}) {
|
|
169
|
+
const dir = dirname(filePath);
|
|
170
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
171
|
+
const tmp = `${filePath}.tmp-${process.pid}-${randSuffix()}`;
|
|
172
|
+
try {
|
|
173
|
+
const writeOpts = {
|
|
174
|
+
encoding: "utf8"
|
|
175
|
+
};
|
|
176
|
+
if (opts.mode !== void 0) {
|
|
177
|
+
writeOpts.mode = opts.mode;
|
|
178
|
+
}
|
|
179
|
+
await fs2.writeFile(tmp, body, writeOpts);
|
|
180
|
+
await fs2.rename(tmp, filePath);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
await fs2.unlink(tmp).catch(() => void 0);
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
if (opts.mode !== void 0) {
|
|
186
|
+
try {
|
|
187
|
+
fsSync.chmodSync(filePath, opts.mode);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function readJsonSafe(filePath) {
|
|
193
|
+
let raw;
|
|
194
|
+
try {
|
|
195
|
+
raw = await fs2.readFile(filePath, "utf8");
|
|
196
|
+
} catch (err) {
|
|
197
|
+
const e = err;
|
|
198
|
+
if (e.code === "ENOENT") {
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
if (raw.trim().length === 0) {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(raw);
|
|
208
|
+
} catch {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function dirname(p) {
|
|
213
|
+
const slash = p.lastIndexOf("/");
|
|
214
|
+
if (slash <= 0) {
|
|
215
|
+
return ".";
|
|
216
|
+
}
|
|
217
|
+
return p.slice(0, slash);
|
|
218
|
+
}
|
|
219
|
+
function randSuffix() {
|
|
220
|
+
return randomBytes(4).toString("hex");
|
|
221
|
+
}
|
|
222
|
+
|
|
159
223
|
// src/core/config.ts
|
|
160
224
|
var REGISTRY_URL_DEFAULT = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
161
225
|
var TlsConfig = z.object({
|
|
@@ -184,7 +248,13 @@ var DaemonConfig = z.object({
|
|
|
184
248
|
// tunnel (ngrok) or VPN (Tailscale) under a different name. The
|
|
185
249
|
// `--host` flag on `share` overrides this; omitting both falls
|
|
186
250
|
// back to `daemon.host`, then to "127.0.0.1" with a stderr warning.
|
|
187
|
-
publicHost: z.string().optional()
|
|
251
|
+
publicHost: z.string().optional(),
|
|
252
|
+
// How often (minutes) the daemon runs `agent sync` against every
|
|
253
|
+
// installed (non-uvx) agent in the background, picking up sessions
|
|
254
|
+
// created outside hydra so the picker can resurrect them. Spawns
|
|
255
|
+
// are staggered across the window — N agents on a 60-minute interval
|
|
256
|
+
// mean one agent spawn every 60/N minutes. Set 0 to disable entirely.
|
|
257
|
+
agentSyncIntervalMinutes: z.number().nonnegative().default(60)
|
|
188
258
|
});
|
|
189
259
|
var RegistryConfig = z.object({
|
|
190
260
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
@@ -239,7 +309,22 @@ var TuiConfig = z.object({
|
|
|
239
309
|
// shared across all sessions; it's append-only on disk, so long-lived
|
|
240
310
|
// installs can grow past this — it's enforced at load time and per
|
|
241
311
|
// append in memory.
|
|
242
|
-
promptHistoryMaxEntries: z.number().int().positive().default(2e3)
|
|
312
|
+
promptHistoryMaxEntries: z.number().int().positive().default(2e3),
|
|
313
|
+
// How edit-style tool calls (Edit, Write, str_replace) render in
|
|
314
|
+
// scrollback, *in addition to* the normal tool row inside the tools
|
|
315
|
+
// block.
|
|
316
|
+
// "none" — nothing extra; the collapsed tool row is the only signal.
|
|
317
|
+
// "edit" (default) — a one-line scrollback mark naming the file
|
|
318
|
+
// that was touched, so the user can scroll back and see which
|
|
319
|
+
// files moved without expanding the tools block. Suppressed on
|
|
320
|
+
// tool-only turns (no agent prose) since the marks would only
|
|
321
|
+
// duplicate the still-visible tool rows.
|
|
322
|
+
// "diff" — same mark plus a syntax-highlighted unified diff body,
|
|
323
|
+
// Claude Code's Update(file) look.
|
|
324
|
+
// The diff payload is extracted from the ACP wire (content[]
|
|
325
|
+
// type:"diff" entries, falling back to rawInput shapes), so any agent
|
|
326
|
+
// that emits one of those shapes gets the treatment.
|
|
327
|
+
showFileUpdates: z.enum(["none", "edit", "diff"]).default("edit")
|
|
243
328
|
});
|
|
244
329
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
245
330
|
var ExtensionBody = z.object({
|
|
@@ -294,7 +379,8 @@ var HydraConfig = z.object({
|
|
|
294
379
|
progressIndicator: true,
|
|
295
380
|
defaultEnterAction: "amend",
|
|
296
381
|
showThoughts: true,
|
|
297
|
-
promptHistoryMaxEntries: 2e3
|
|
382
|
+
promptHistoryMaxEntries: 2e3,
|
|
383
|
+
showFileUpdates: "edit"
|
|
298
384
|
})
|
|
299
385
|
});
|
|
300
386
|
function extensionList(config) {
|
|
@@ -310,17 +396,8 @@ function transformerList(config) {
|
|
|
310
396
|
}));
|
|
311
397
|
}
|
|
312
398
|
async function readConfigFile() {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
raw = await fs2.readFile(paths.config(), "utf8");
|
|
316
|
-
} catch (err) {
|
|
317
|
-
const e = err;
|
|
318
|
-
if (e.code === "ENOENT") {
|
|
319
|
-
return {};
|
|
320
|
-
}
|
|
321
|
-
throw err;
|
|
322
|
-
}
|
|
323
|
-
return JSON.parse(raw);
|
|
399
|
+
const parsed = await readJsonSafe(paths.config());
|
|
400
|
+
return parsed ?? {};
|
|
324
401
|
}
|
|
325
402
|
async function migrateLegacyAuthToken() {
|
|
326
403
|
const raw = await readConfigFile();
|
|
@@ -331,7 +408,7 @@ async function migrateLegacyAuthToken() {
|
|
|
331
408
|
}
|
|
332
409
|
let tokenFileExists = false;
|
|
333
410
|
try {
|
|
334
|
-
await
|
|
411
|
+
await fs3.access(paths.authToken());
|
|
335
412
|
tokenFileExists = true;
|
|
336
413
|
} catch (err) {
|
|
337
414
|
const e = err;
|
|
@@ -349,10 +426,7 @@ async function migrateLegacyAuthToken() {
|
|
|
349
426
|
if (Object.keys(daemon).length === 0) {
|
|
350
427
|
delete raw.daemon;
|
|
351
428
|
}
|
|
352
|
-
await
|
|
353
|
-
encoding: "utf8",
|
|
354
|
-
mode: 384
|
|
355
|
-
});
|
|
429
|
+
await writeJsonAtomic(paths.config(), raw, { mode: 384 });
|
|
356
430
|
process.stderr.write(
|
|
357
431
|
`hydra-acp: migrated auth token from ${paths.config()} to ${paths.authToken()}.
|
|
358
432
|
`
|
|
@@ -363,11 +437,7 @@ async function loadConfig() {
|
|
|
363
437
|
return HydraConfig.parse(await readConfigFile());
|
|
364
438
|
}
|
|
365
439
|
async function writeConfig(config) {
|
|
366
|
-
await
|
|
367
|
-
await fs2.writeFile(paths.config(), JSON.stringify(config, null, 2) + "\n", {
|
|
368
|
-
encoding: "utf8",
|
|
369
|
-
mode: 384
|
|
370
|
-
});
|
|
440
|
+
await writeJsonAtomic(paths.config(), config, { mode: 384 });
|
|
371
441
|
}
|
|
372
442
|
function defaultConfig() {
|
|
373
443
|
return HydraConfig.parse({});
|
|
@@ -386,11 +456,12 @@ function expandHome(p) {
|
|
|
386
456
|
}
|
|
387
457
|
|
|
388
458
|
// src/core/registry.ts
|
|
389
|
-
import * as
|
|
459
|
+
import * as fs5 from "fs/promises";
|
|
460
|
+
import * as path4 from "path";
|
|
390
461
|
import { z as z2 } from "zod";
|
|
391
462
|
|
|
392
463
|
// src/core/binary-install.ts
|
|
393
|
-
import * as
|
|
464
|
+
import * as fs4 from "fs";
|
|
394
465
|
import * as fsp from "fs/promises";
|
|
395
466
|
import * as path2 from "path";
|
|
396
467
|
import { spawn } from "child_process";
|
|
@@ -523,7 +594,7 @@ async function downloadTo(args) {
|
|
|
523
594
|
);
|
|
524
595
|
}
|
|
525
596
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
526
|
-
const out =
|
|
597
|
+
const out = fs4.createWriteStream(dest);
|
|
527
598
|
const nodeStream = Readable.fromWeb(response.body);
|
|
528
599
|
safeEmit(args.onProgress, {
|
|
529
600
|
phase: "download_start",
|
|
@@ -554,10 +625,10 @@ async function downloadTo(args) {
|
|
|
554
625
|
logSink(formatProgress(args.agentId, received, total));
|
|
555
626
|
}
|
|
556
627
|
});
|
|
557
|
-
await new Promise((
|
|
628
|
+
await new Promise((resolve4, reject) => {
|
|
558
629
|
nodeStream.on("error", reject);
|
|
559
630
|
out.on("error", reject);
|
|
560
|
-
out.on("finish", () =>
|
|
631
|
+
out.on("finish", () => resolve4());
|
|
561
632
|
nodeStream.pipe(out);
|
|
562
633
|
});
|
|
563
634
|
logSink(formatProgress(
|
|
@@ -609,14 +680,14 @@ async function extract(archivePath, dest) {
|
|
|
609
680
|
throw new Error(`Unsupported archive format: ${archivePath}`);
|
|
610
681
|
}
|
|
611
682
|
function run(cmd, args) {
|
|
612
|
-
return new Promise((
|
|
683
|
+
return new Promise((resolve4, reject) => {
|
|
613
684
|
const child = spawn(cmd, args, {
|
|
614
685
|
stdio: ["ignore", "ignore", "inherit"]
|
|
615
686
|
});
|
|
616
687
|
child.on("error", reject);
|
|
617
688
|
child.on("exit", (code, signal) => {
|
|
618
689
|
if (code === 0) {
|
|
619
|
-
|
|
690
|
+
resolve4();
|
|
620
691
|
return;
|
|
621
692
|
}
|
|
622
693
|
reject(
|
|
@@ -628,11 +699,11 @@ function run(cmd, args) {
|
|
|
628
699
|
});
|
|
629
700
|
}
|
|
630
701
|
async function hasCommand(name) {
|
|
631
|
-
return new Promise((
|
|
702
|
+
return new Promise((resolve4) => {
|
|
632
703
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
633
704
|
const child = spawn(finder, [name], { stdio: "ignore" });
|
|
634
|
-
child.on("error", () =>
|
|
635
|
-
child.on("exit", (code) =>
|
|
705
|
+
child.on("error", () => resolve4(false));
|
|
706
|
+
child.on("exit", (code) => resolve4(code === 0));
|
|
636
707
|
});
|
|
637
708
|
}
|
|
638
709
|
async function fileExists(p) {
|
|
@@ -759,7 +830,7 @@ function runNpmInstall(args) {
|
|
|
759
830
|
}
|
|
760
831
|
async function runNpmInstallOnce(args, attempt) {
|
|
761
832
|
try {
|
|
762
|
-
await new Promise((
|
|
833
|
+
await new Promise((resolve4, reject) => {
|
|
763
834
|
const registryArgs = args.registry ? ["--registry", args.registry] : [];
|
|
764
835
|
let child;
|
|
765
836
|
try {
|
|
@@ -801,7 +872,7 @@ async function runNpmInstallOnce(args, attempt) {
|
|
|
801
872
|
});
|
|
802
873
|
child.on("exit", (code, signal) => {
|
|
803
874
|
if (code === 0) {
|
|
804
|
-
|
|
875
|
+
resolve4();
|
|
805
876
|
return;
|
|
806
877
|
}
|
|
807
878
|
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
@@ -988,6 +1059,13 @@ var Registry = class {
|
|
|
988
1059
|
await this.writeDiskCache(fresh);
|
|
989
1060
|
return fresh.data;
|
|
990
1061
|
}
|
|
1062
|
+
// Epoch ms of the last successful registry fetch (in-memory or
|
|
1063
|
+
// disk). Returns undefined before load()/refresh() has populated the
|
|
1064
|
+
// cache. Used by `/v1/agents` to surface "synced N minutes ago" in
|
|
1065
|
+
// the CLI without exposing the full cache shape.
|
|
1066
|
+
lastFetchedAt() {
|
|
1067
|
+
return this.cache?.fetchedAt;
|
|
1068
|
+
}
|
|
991
1069
|
async getAgent(id) {
|
|
992
1070
|
const doc = await this.load();
|
|
993
1071
|
const exact = doc.agents.find((a) => a.id === id);
|
|
@@ -1016,54 +1094,26 @@ var Registry = class {
|
|
|
1016
1094
|
return cached;
|
|
1017
1095
|
}
|
|
1018
1096
|
async readDiskCache() {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (e.code === "ENOENT") {
|
|
1025
|
-
return void 0;
|
|
1026
|
-
}
|
|
1027
|
-
throw err;
|
|
1097
|
+
const parsed = await readJsonSafe(
|
|
1098
|
+
paths.registryCache()
|
|
1099
|
+
);
|
|
1100
|
+
if (!parsed || typeof parsed.fetchedAt !== "number" || parsed.data === void 0) {
|
|
1101
|
+
return void 0;
|
|
1028
1102
|
}
|
|
1029
1103
|
try {
|
|
1030
|
-
const parsed = JSON.parse(text);
|
|
1031
|
-
if (typeof parsed.fetchedAt !== "number" || parsed.data === void 0) {
|
|
1032
|
-
return void 0;
|
|
1033
|
-
}
|
|
1034
1104
|
const data = RegistryDocument.parse(parsed.data);
|
|
1035
1105
|
return { fetchedAt: parsed.fetchedAt, raw: parsed.data, data };
|
|
1036
1106
|
} catch {
|
|
1037
1107
|
return void 0;
|
|
1038
1108
|
}
|
|
1039
1109
|
}
|
|
1040
|
-
// Atomic write: dump to a sibling temp path, then rename onto the
|
|
1041
|
-
// target. POSIX rename is atomic within a filesystem, so readers
|
|
1042
|
-
// either see the old file or the fully-written new file — never a
|
|
1043
|
-
// truncated middle. This also makes simultaneous writers safe
|
|
1044
|
-
// without a lock file: the loser of the rename race just gets its
|
|
1045
|
-
// version replaced by the winner's.
|
|
1046
1110
|
async writeDiskCache(cache) {
|
|
1047
|
-
await
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
{ fetchedAt: cache.fetchedAt, data: cache.raw },
|
|
1052
|
-
null,
|
|
1053
|
-
2
|
|
1054
|
-
) + "\n";
|
|
1055
|
-
try {
|
|
1056
|
-
await fs4.writeFile(tmp, body, "utf8");
|
|
1057
|
-
await fs4.rename(tmp, final);
|
|
1058
|
-
} catch (err) {
|
|
1059
|
-
await fs4.unlink(tmp).catch(() => void 0);
|
|
1060
|
-
throw err;
|
|
1061
|
-
}
|
|
1111
|
+
await writeJsonAtomic(paths.registryCache(), {
|
|
1112
|
+
fetchedAt: cache.fetchedAt,
|
|
1113
|
+
data: cache.raw
|
|
1114
|
+
});
|
|
1062
1115
|
}
|
|
1063
1116
|
};
|
|
1064
|
-
function randSuffix() {
|
|
1065
|
-
return Math.random().toString(36).slice(2, 10);
|
|
1066
|
-
}
|
|
1067
1117
|
function npxPackageBasename(agent) {
|
|
1068
1118
|
const pkg = agent.distribution.npx?.package;
|
|
1069
1119
|
if (!pkg) {
|
|
@@ -1074,6 +1124,46 @@ function npxPackageBasename(agent) {
|
|
|
1074
1124
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
1075
1125
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
1076
1126
|
}
|
|
1127
|
+
async function agentInstallState(agent) {
|
|
1128
|
+
const platformKey = currentPlatformKey();
|
|
1129
|
+
if (!platformKey) {
|
|
1130
|
+
return "no";
|
|
1131
|
+
}
|
|
1132
|
+
const version = agent.version ?? "current";
|
|
1133
|
+
if (agent.distribution.binary) {
|
|
1134
|
+
const target = pickBinaryTarget(agent.distribution.binary, platformKey);
|
|
1135
|
+
if (target?.cmd) {
|
|
1136
|
+
const cmdPath = path4.resolve(
|
|
1137
|
+
paths.agentInstallDir(agent.id, platformKey, version),
|
|
1138
|
+
target.cmd
|
|
1139
|
+
);
|
|
1140
|
+
if (await fileExists3(cmdPath)) {
|
|
1141
|
+
return "yes";
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (agent.distribution.npx) {
|
|
1146
|
+
const npx = agent.distribution.npx;
|
|
1147
|
+
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
1148
|
+
const installDir = paths.agentNpmInstallDir(agent.id, platformKey, version);
|
|
1149
|
+
const binPath = path4.join(installDir, "node_modules", ".bin", bin);
|
|
1150
|
+
if (await fileExists3(binPath)) {
|
|
1151
|
+
return "yes";
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (!agent.distribution.npx && !agent.distribution.binary && agent.distribution.uvx) {
|
|
1155
|
+
return "lazy";
|
|
1156
|
+
}
|
|
1157
|
+
return "no";
|
|
1158
|
+
}
|
|
1159
|
+
async function fileExists3(p) {
|
|
1160
|
+
try {
|
|
1161
|
+
await fs5.access(p);
|
|
1162
|
+
return true;
|
|
1163
|
+
} catch {
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1077
1167
|
async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
1078
1168
|
const version = agent.version ?? "current";
|
|
1079
1169
|
if (agent.distribution.npx) {
|
|
@@ -1422,6 +1512,11 @@ var SessionListEntry = z3.object({
|
|
|
1422
1512
|
importedFromUpstreamSessionId: z3.string().optional(),
|
|
1423
1513
|
// Set when this session was spawned as a child by a transformer.
|
|
1424
1514
|
parentSessionId: z3.string().optional(),
|
|
1515
|
+
// Local-fork breadcrumbs set by hydra-acp/fork_session. Distinct from
|
|
1516
|
+
// the imported* family above: a fork is a local branch off another
|
|
1517
|
+
// local session, an import is a cross-machine takeover.
|
|
1518
|
+
forkedFromSessionId: z3.string().optional(),
|
|
1519
|
+
forkedFromMessageId: z3.string().optional(),
|
|
1425
1520
|
// clientInfo from the process that issued session/new. Lets list views
|
|
1426
1521
|
// hide cat-style ancillary sessions by default while letting an
|
|
1427
1522
|
// override flag surface them.
|
|
@@ -1689,13 +1784,13 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
1689
1784
|
throw new Error("stream is closed");
|
|
1690
1785
|
}
|
|
1691
1786
|
const line = JSON.stringify(message) + "\n";
|
|
1692
|
-
await new Promise((
|
|
1787
|
+
await new Promise((resolve4, reject) => {
|
|
1693
1788
|
stdin.write(line, (err) => {
|
|
1694
1789
|
if (err) {
|
|
1695
1790
|
reject(err);
|
|
1696
1791
|
return;
|
|
1697
1792
|
}
|
|
1698
|
-
|
|
1793
|
+
resolve4();
|
|
1699
1794
|
});
|
|
1700
1795
|
});
|
|
1701
1796
|
},
|
|
@@ -1787,9 +1882,9 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1787
1882
|
}
|
|
1788
1883
|
const id = nanoid();
|
|
1789
1884
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
1790
|
-
const response = new Promise((
|
|
1885
|
+
const response = new Promise((resolve4, reject) => {
|
|
1791
1886
|
this.pending.set(id, {
|
|
1792
|
-
resolve: (result) =>
|
|
1887
|
+
resolve: (result) => resolve4(result),
|
|
1793
1888
|
reject
|
|
1794
1889
|
});
|
|
1795
1890
|
this.stream.send(message).catch((err) => {
|
|
@@ -2019,7 +2114,7 @@ stderr: ${tail}` : reason;
|
|
|
2019
2114
|
};
|
|
2020
2115
|
|
|
2021
2116
|
// src/core/session-manager.ts
|
|
2022
|
-
import * as
|
|
2117
|
+
import * as fs11 from "fs/promises";
|
|
2023
2118
|
import * as os2 from "os";
|
|
2024
2119
|
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
2025
2120
|
|
|
@@ -2216,14 +2311,14 @@ var SessionStreamBuffer = class {
|
|
|
2216
2311
|
if (cap === 0) {
|
|
2217
2312
|
return Promise.resolve("timeout");
|
|
2218
2313
|
}
|
|
2219
|
-
return new Promise((
|
|
2314
|
+
return new Promise((resolve4) => {
|
|
2220
2315
|
const waiter = {
|
|
2221
2316
|
resolve: (outcome) => {
|
|
2222
2317
|
if (waiter.timer !== void 0) {
|
|
2223
2318
|
clearTimeout(waiter.timer);
|
|
2224
2319
|
waiter.timer = void 0;
|
|
2225
2320
|
}
|
|
2226
|
-
|
|
2321
|
+
resolve4(outcome);
|
|
2227
2322
|
},
|
|
2228
2323
|
timer: setTimeout(() => {
|
|
2229
2324
|
const idx = this.waiters.indexOf(waiter);
|
|
@@ -2231,7 +2326,7 @@ var SessionStreamBuffer = class {
|
|
|
2231
2326
|
this.waiters.splice(idx, 1);
|
|
2232
2327
|
}
|
|
2233
2328
|
waiter.timer = void 0;
|
|
2234
|
-
|
|
2329
|
+
resolve4("timeout");
|
|
2235
2330
|
}, cap)
|
|
2236
2331
|
};
|
|
2237
2332
|
this.waiters.push(waiter);
|
|
@@ -2434,8 +2529,8 @@ var SessionStreamBuffer = class {
|
|
|
2434
2529
|
return out;
|
|
2435
2530
|
}
|
|
2436
2531
|
scheduleFileWrite(chunk) {
|
|
2437
|
-
const
|
|
2438
|
-
if (
|
|
2532
|
+
const path14 = this.filePath;
|
|
2533
|
+
if (path14 === void 0) {
|
|
2439
2534
|
return;
|
|
2440
2535
|
}
|
|
2441
2536
|
if (this.fileCapReached) {
|
|
@@ -2450,7 +2545,7 @@ var SessionStreamBuffer = class {
|
|
|
2450
2545
|
const slice = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining);
|
|
2451
2546
|
this.fileBytesWritten += slice.length;
|
|
2452
2547
|
const willHitCap = this.fileBytesWritten >= this.fileCapBytes;
|
|
2453
|
-
this.fileWriteChain = this.fileWriteChain.then(() => fsp3.appendFile(
|
|
2548
|
+
this.fileWriteChain = this.fileWriteChain.then(() => fsp3.appendFile(path14, slice)).catch((err) => {
|
|
2454
2549
|
this.logWriteError?.(err);
|
|
2455
2550
|
});
|
|
2456
2551
|
if (willHitCap && !this.fileCapReached) {
|
|
@@ -2499,22 +2594,22 @@ function hydraCommandsAsAdvertised() {
|
|
|
2499
2594
|
}
|
|
2500
2595
|
|
|
2501
2596
|
// src/core/queue-store.ts
|
|
2502
|
-
import * as
|
|
2597
|
+
import * as fs6 from "fs/promises";
|
|
2503
2598
|
async function rewriteQueue(sessionId, entries) {
|
|
2504
2599
|
const file = paths.queueFile(sessionId);
|
|
2505
2600
|
if (entries.length === 0) {
|
|
2506
|
-
await
|
|
2601
|
+
await fs6.unlink(file).catch(() => void 0);
|
|
2507
2602
|
return;
|
|
2508
2603
|
}
|
|
2509
|
-
await
|
|
2604
|
+
await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
2510
2605
|
const body = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2511
|
-
await
|
|
2606
|
+
await fs6.writeFile(file, body, "utf8");
|
|
2512
2607
|
}
|
|
2513
2608
|
async function loadQueue(sessionId) {
|
|
2514
2609
|
const file = paths.queueFile(sessionId);
|
|
2515
2610
|
let text;
|
|
2516
2611
|
try {
|
|
2517
|
-
text = await
|
|
2612
|
+
text = await fs6.readFile(file, "utf8");
|
|
2518
2613
|
} catch (err) {
|
|
2519
2614
|
if (err.code === "ENOENT") {
|
|
2520
2615
|
return [];
|
|
@@ -2536,7 +2631,7 @@ async function loadQueue(sessionId) {
|
|
|
2536
2631
|
}
|
|
2537
2632
|
async function deleteQueue(sessionId) {
|
|
2538
2633
|
const file = paths.queueFile(sessionId);
|
|
2539
|
-
await
|
|
2634
|
+
await fs6.unlink(file).catch(() => void 0);
|
|
2540
2635
|
}
|
|
2541
2636
|
|
|
2542
2637
|
// src/core/session.ts
|
|
@@ -2568,6 +2663,8 @@ var Session = class {
|
|
|
2568
2663
|
agentCapabilities;
|
|
2569
2664
|
agentArgs;
|
|
2570
2665
|
parentSessionId;
|
|
2666
|
+
forkedFromSessionId;
|
|
2667
|
+
forkedFromMessageId;
|
|
2571
2668
|
originatingClient;
|
|
2572
2669
|
title;
|
|
2573
2670
|
// Snapshot state delivered to attaching clients via the attach
|
|
@@ -2596,6 +2693,13 @@ var Session = class {
|
|
|
2596
2693
|
// enqueue) and leave the file out of sync with in-memory state.
|
|
2597
2694
|
queueWriteChain = Promise.resolve();
|
|
2598
2695
|
closed = false;
|
|
2696
|
+
// Set true at the start of close() / markClosed before any await yields.
|
|
2697
|
+
// drainQueue checks this between iterations and bails out, so a queued
|
|
2698
|
+
// entry can't be promoted to currentEntry (with its prompt_received and
|
|
2699
|
+
// synthesized turn_complete(interrupted)) while the session is tearing
|
|
2700
|
+
// down. markClosed sweeps the remaining queue with the normal abandoned
|
|
2701
|
+
// / cancelled handling.
|
|
2702
|
+
closing = false;
|
|
2599
2703
|
closeHandlers = [];
|
|
2600
2704
|
titleHandlers = [];
|
|
2601
2705
|
// Subscribers notified after every entry that's actually persisted to
|
|
@@ -2723,6 +2827,8 @@ var Session = class {
|
|
|
2723
2827
|
this.agentCapabilities = init.agentCapabilities;
|
|
2724
2828
|
this.agentArgs = init.agentArgs;
|
|
2725
2829
|
this.parentSessionId = init.parentSessionId;
|
|
2830
|
+
this.forkedFromSessionId = init.forkedFromSessionId;
|
|
2831
|
+
this.forkedFromMessageId = init.forkedFromMessageId;
|
|
2726
2832
|
this.originatingClient = init.originatingClient;
|
|
2727
2833
|
this.title = init.title;
|
|
2728
2834
|
this.currentModel = init.currentModel;
|
|
@@ -2863,7 +2969,7 @@ var Session = class {
|
|
|
2863
2969
|
const claimIdx = i;
|
|
2864
2970
|
const claimEnvelope = envelope;
|
|
2865
2971
|
const claimOriginatedBy = new Set(originatedBy);
|
|
2866
|
-
await new Promise((
|
|
2972
|
+
await new Promise((resolve4) => {
|
|
2867
2973
|
const timer = setTimeout(() => {
|
|
2868
2974
|
if (this.pendingClaims.delete(token)) {
|
|
2869
2975
|
this.broadcastQueueNotification(
|
|
@@ -2874,14 +2980,14 @@ var Session = class {
|
|
|
2874
2980
|
claimEnvelope,
|
|
2875
2981
|
/* @__PURE__ */ new Set([...claimOriginatedBy, t.name]),
|
|
2876
2982
|
claimIdx + 1
|
|
2877
|
-
).then(
|
|
2983
|
+
).then(resolve4);
|
|
2878
2984
|
}
|
|
2879
2985
|
}, TRANSFORMER_CLAIM_TIMEOUT_MS);
|
|
2880
2986
|
if (typeof timer.unref === "function") {
|
|
2881
2987
|
timer.unref();
|
|
2882
2988
|
}
|
|
2883
2989
|
this.pendingClaims.set(token, {
|
|
2884
|
-
resolve: () =>
|
|
2990
|
+
resolve: () => resolve4(),
|
|
2885
2991
|
timer,
|
|
2886
2992
|
transformerName: t.name,
|
|
2887
2993
|
method: "session/update",
|
|
@@ -3721,7 +3827,7 @@ var Session = class {
|
|
|
3721
3827
|
const claimIdx = i;
|
|
3722
3828
|
const claimEnvelope = envelope;
|
|
3723
3829
|
const claimOriginatedBy = new Set(originatedBy);
|
|
3724
|
-
return new Promise((
|
|
3830
|
+
return new Promise((resolve4) => {
|
|
3725
3831
|
const timer = setTimeout(() => {
|
|
3726
3832
|
if (this.pendingClaims.delete(token)) {
|
|
3727
3833
|
this.broadcastQueueNotification(
|
|
@@ -3733,14 +3839,14 @@ var Session = class {
|
|
|
3733
3839
|
claimEnvelope,
|
|
3734
3840
|
/* @__PURE__ */ new Set([...claimOriginatedBy, t.name]),
|
|
3735
3841
|
claimIdx + 1
|
|
3736
|
-
).then(
|
|
3842
|
+
).then(resolve4).catch(() => resolve4(defaultStopPayload(method)));
|
|
3737
3843
|
}
|
|
3738
3844
|
}, TRANSFORMER_CLAIM_TIMEOUT_MS);
|
|
3739
3845
|
if (typeof timer.unref === "function") {
|
|
3740
3846
|
timer.unref();
|
|
3741
3847
|
}
|
|
3742
3848
|
this.pendingClaims.set(token, {
|
|
3743
|
-
resolve:
|
|
3849
|
+
resolve: resolve4,
|
|
3744
3850
|
timer,
|
|
3745
3851
|
transformerName: t.name,
|
|
3746
3852
|
method,
|
|
@@ -3831,6 +3937,7 @@ var Session = class {
|
|
|
3831
3937
|
if (this.closed) {
|
|
3832
3938
|
return;
|
|
3833
3939
|
}
|
|
3940
|
+
this.closing = true;
|
|
3834
3941
|
this.logger?.info(
|
|
3835
3942
|
`session ${this.sessionId} closing deleteRecord=${opts.deleteRecord ?? false} regenTitle=${opts.regenTitle ?? false}`
|
|
3836
3943
|
);
|
|
@@ -4493,12 +4600,12 @@ ${text}
|
|
|
4493
4600
|
} else {
|
|
4494
4601
|
const inList = current ? models.some((m) => m.modelId === current) : true;
|
|
4495
4602
|
const lines = models.map((m) => {
|
|
4496
|
-
const marker = m.modelId === current ? "
|
|
4603
|
+
const marker = m.modelId === current ? "\u25B6 " : " ";
|
|
4497
4604
|
const desc = m.name && m.name !== m.modelId ? ` ${m.name}` : "";
|
|
4498
|
-
return `${m.modelId}${
|
|
4605
|
+
return `${marker}${m.modelId}${desc}`;
|
|
4499
4606
|
});
|
|
4500
4607
|
if (!inList && current) {
|
|
4501
|
-
lines.unshift(
|
|
4608
|
+
lines.unshift(`\u25B6 ${current}`);
|
|
4502
4609
|
}
|
|
4503
4610
|
body = lines.join("\n");
|
|
4504
4611
|
}
|
|
@@ -4962,21 +5069,22 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
4962
5069
|
if (this.closed) {
|
|
4963
5070
|
return;
|
|
4964
5071
|
}
|
|
5072
|
+
this.closing = true;
|
|
4965
5073
|
this.closed = true;
|
|
4966
5074
|
this.cancelIdleTimer();
|
|
4967
5075
|
if (this.extensionCommandsUnsub) {
|
|
4968
5076
|
this.extensionCommandsUnsub();
|
|
4969
5077
|
this.extensionCommandsUnsub = void 0;
|
|
4970
5078
|
}
|
|
4971
|
-
if (this.currentEntry?.kind === "user") {
|
|
5079
|
+
if (this.currentEntry?.kind === "user" && !this.recentlyTerminal.has(this.currentEntry.messageId)) {
|
|
4972
5080
|
this.broadcastTurnComplete(
|
|
4973
5081
|
this.currentEntry.clientId,
|
|
4974
5082
|
{ stopReason: "interrupted" },
|
|
4975
5083
|
this.currentEntry.messageId,
|
|
4976
5084
|
this.currentEntry.wasAmend
|
|
4977
5085
|
);
|
|
4978
|
-
this.currentEntry = void 0;
|
|
4979
5086
|
}
|
|
5087
|
+
this.currentEntry = void 0;
|
|
4980
5088
|
const stranded = this.promptQueue;
|
|
4981
5089
|
this.promptQueue = [];
|
|
4982
5090
|
for (const entry of stranded) {
|
|
@@ -4998,12 +5106,12 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
4998
5106
|
this.clients.clear();
|
|
4999
5107
|
if (this.streamBuffer !== void 0) {
|
|
5000
5108
|
const buf = this.streamBuffer;
|
|
5001
|
-
const
|
|
5109
|
+
const path14 = this.streamFilePath;
|
|
5002
5110
|
this.streamBuffer = void 0;
|
|
5003
5111
|
this.streamFilePath = void 0;
|
|
5004
5112
|
buf.close();
|
|
5005
|
-
if (
|
|
5006
|
-
void buf.drainFileWrites().then(() => fsp4.unlink(
|
|
5113
|
+
if (path14 !== void 0) {
|
|
5114
|
+
void buf.drainFileWrites().then(() => fsp4.unlink(path14).catch(() => void 0));
|
|
5007
5115
|
}
|
|
5008
5116
|
}
|
|
5009
5117
|
for (const handler of this.closeHandlers) {
|
|
@@ -5165,7 +5273,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5165
5273
|
}
|
|
5166
5274
|
const clientParams = this.rewriteForClient(params);
|
|
5167
5275
|
const toolCallId = extractToolCallId(clientParams);
|
|
5168
|
-
return new Promise((
|
|
5276
|
+
return new Promise((resolve4, reject) => {
|
|
5169
5277
|
let settled = false;
|
|
5170
5278
|
const outbound = [];
|
|
5171
5279
|
const entry = { addClient: sendTo };
|
|
@@ -5204,7 +5312,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5204
5312
|
update
|
|
5205
5313
|
}).catch(() => void 0);
|
|
5206
5314
|
}
|
|
5207
|
-
|
|
5315
|
+
resolve4(result);
|
|
5208
5316
|
});
|
|
5209
5317
|
}).catch((err) => {
|
|
5210
5318
|
settle(() => reject(err));
|
|
@@ -5220,14 +5328,14 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5220
5328
|
// in flight, but doesn't emit prompt_queue_* broadcasts — clients
|
|
5221
5329
|
// shouldn't see hydra's housekeeping in their chip list.
|
|
5222
5330
|
async enqueuePrompt(task) {
|
|
5223
|
-
return new Promise((
|
|
5331
|
+
return new Promise((resolve4, reject) => {
|
|
5224
5332
|
const entry = {
|
|
5225
5333
|
kind: "internal",
|
|
5226
5334
|
messageId: generateMessageId(),
|
|
5227
5335
|
enqueuedAt: Date.now(),
|
|
5228
5336
|
cancelled: false,
|
|
5229
5337
|
task,
|
|
5230
|
-
resolve:
|
|
5338
|
+
resolve: resolve4,
|
|
5231
5339
|
reject
|
|
5232
5340
|
};
|
|
5233
5341
|
this.promptQueue.push(entry);
|
|
@@ -5246,7 +5354,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5246
5354
|
if (client.clientInfo?.name) originator.name = client.clientInfo.name;
|
|
5247
5355
|
if (client.clientInfo?.version)
|
|
5248
5356
|
originator.version = client.clientInfo.version;
|
|
5249
|
-
return new Promise((
|
|
5357
|
+
return new Promise((resolve4, reject) => {
|
|
5250
5358
|
const entry = {
|
|
5251
5359
|
kind: "user",
|
|
5252
5360
|
messageId,
|
|
@@ -5255,7 +5363,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5255
5363
|
prompt: promptArray,
|
|
5256
5364
|
enqueuedAt: Date.now(),
|
|
5257
5365
|
cancelled: false,
|
|
5258
|
-
resolve:
|
|
5366
|
+
resolve: resolve4,
|
|
5259
5367
|
reject
|
|
5260
5368
|
};
|
|
5261
5369
|
this.promptQueue.push(entry);
|
|
@@ -5305,6 +5413,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5305
5413
|
await new Promise((r) => setImmediate(r));
|
|
5306
5414
|
try {
|
|
5307
5415
|
while (this.promptQueue.length > 0) {
|
|
5416
|
+
if (this.closing) {
|
|
5417
|
+
break;
|
|
5418
|
+
}
|
|
5308
5419
|
const next = this.promptQueue.shift();
|
|
5309
5420
|
if (!next) {
|
|
5310
5421
|
break;
|
|
@@ -5676,8 +5787,8 @@ function firstLine(text, max) {
|
|
|
5676
5787
|
}
|
|
5677
5788
|
|
|
5678
5789
|
// src/core/session-store.ts
|
|
5679
|
-
import * as
|
|
5680
|
-
import * as
|
|
5790
|
+
import * as fs7 from "fs/promises";
|
|
5791
|
+
import * as path5 from "path";
|
|
5681
5792
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
5682
5793
|
import { z as z4 } from "zod";
|
|
5683
5794
|
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -5761,6 +5872,12 @@ var SessionRecord = z4.object({
|
|
|
5761
5872
|
// Set when this session was spawned as a child by a transformer via
|
|
5762
5873
|
// hydra-acp/spawn_child_session. Points to the spawning session's id.
|
|
5763
5874
|
parentSessionId: z4.string().optional(),
|
|
5875
|
+
// Set when this session was created by hydra-acp/fork_session.
|
|
5876
|
+
// forkedFromSessionId points to the local source session; forkedFromMessageId
|
|
5877
|
+
// is the resolved forkAt — the messageId of the turn_complete the slice
|
|
5878
|
+
// ended at. Kept so future UI can show "branched from turn N of session X".
|
|
5879
|
+
forkedFromSessionId: z4.string().optional(),
|
|
5880
|
+
forkedFromMessageId: z4.string().optional(),
|
|
5764
5881
|
// clientInfo from the process that issued session/new. Picker and
|
|
5765
5882
|
// `sessions list` use this to hide cat-style ancillary sessions by
|
|
5766
5883
|
// default; carried in meta.json so cold sessions filter the same way.
|
|
@@ -5777,30 +5894,21 @@ function assertSafeId(id) {
|
|
|
5777
5894
|
var SessionStore = class {
|
|
5778
5895
|
async write(record) {
|
|
5779
5896
|
assertSafeId(record.sessionId);
|
|
5780
|
-
await fs6.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
5781
5897
|
const full = { version: 1, ...record };
|
|
5782
|
-
await
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
{ encoding: "utf8", mode: 384 }
|
|
5786
|
-
);
|
|
5898
|
+
await writeJsonAtomic(paths.sessionFile(record.sessionId), full, {
|
|
5899
|
+
mode: 384
|
|
5900
|
+
});
|
|
5787
5901
|
}
|
|
5788
5902
|
async read(sessionId) {
|
|
5789
5903
|
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
5790
5904
|
return void 0;
|
|
5791
5905
|
}
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
} catch (err) {
|
|
5796
|
-
const e = err;
|
|
5797
|
-
if (e.code === "ENOENT") {
|
|
5798
|
-
return void 0;
|
|
5799
|
-
}
|
|
5800
|
-
throw err;
|
|
5906
|
+
const parsed = await readJsonSafe(paths.sessionFile(sessionId));
|
|
5907
|
+
if (parsed === void 0) {
|
|
5908
|
+
return void 0;
|
|
5801
5909
|
}
|
|
5802
5910
|
try {
|
|
5803
|
-
return SessionRecord.parse(
|
|
5911
|
+
return SessionRecord.parse(parsed);
|
|
5804
5912
|
} catch {
|
|
5805
5913
|
return void 0;
|
|
5806
5914
|
}
|
|
@@ -5810,7 +5918,7 @@ var SessionStore = class {
|
|
|
5810
5918
|
return;
|
|
5811
5919
|
}
|
|
5812
5920
|
try {
|
|
5813
|
-
await
|
|
5921
|
+
await fs7.unlink(paths.sessionFile(sessionId));
|
|
5814
5922
|
} catch (err) {
|
|
5815
5923
|
const e = err;
|
|
5816
5924
|
if (e.code !== "ENOENT") {
|
|
@@ -5818,7 +5926,7 @@ var SessionStore = class {
|
|
|
5818
5926
|
}
|
|
5819
5927
|
}
|
|
5820
5928
|
try {
|
|
5821
|
-
await
|
|
5929
|
+
await fs7.rmdir(paths.sessionDir(sessionId));
|
|
5822
5930
|
} catch (err) {
|
|
5823
5931
|
const e = err;
|
|
5824
5932
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -5848,7 +5956,7 @@ var SessionStore = class {
|
|
|
5848
5956
|
async list() {
|
|
5849
5957
|
let entries;
|
|
5850
5958
|
try {
|
|
5851
|
-
entries = await
|
|
5959
|
+
entries = await fs7.readdir(paths.sessionsDir());
|
|
5852
5960
|
} catch (err) {
|
|
5853
5961
|
const e = err;
|
|
5854
5962
|
if (e.code === "ENOENT") {
|
|
@@ -5887,6 +5995,8 @@ function recordFromMemorySession(args) {
|
|
|
5887
5995
|
agentModels: args.agentModels,
|
|
5888
5996
|
pendingHistorySync: args.pendingHistorySync,
|
|
5889
5997
|
parentSessionId: args.parentSessionId,
|
|
5998
|
+
forkedFromSessionId: args.forkedFromSessionId,
|
|
5999
|
+
forkedFromMessageId: args.forkedFromMessageId,
|
|
5890
6000
|
originatingClient: args.originatingClient,
|
|
5891
6001
|
createdAt: args.createdAt ?? now,
|
|
5892
6002
|
updatedAt: args.updatedAt ?? now
|
|
@@ -5894,7 +6004,7 @@ function recordFromMemorySession(args) {
|
|
|
5894
6004
|
}
|
|
5895
6005
|
|
|
5896
6006
|
// src/core/history-store.ts
|
|
5897
|
-
import * as
|
|
6007
|
+
import * as fs8 from "fs/promises";
|
|
5898
6008
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
5899
6009
|
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
5900
6010
|
var HistoryStore = class {
|
|
@@ -5911,9 +6021,9 @@ var HistoryStore = class {
|
|
|
5911
6021
|
return;
|
|
5912
6022
|
}
|
|
5913
6023
|
return this.enqueue(sessionId, async () => {
|
|
5914
|
-
await
|
|
6024
|
+
await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
5915
6025
|
const line = JSON.stringify(entry) + "\n";
|
|
5916
|
-
await
|
|
6026
|
+
await fs8.appendFile(paths.historyFile(sessionId), line, {
|
|
5917
6027
|
encoding: "utf8",
|
|
5918
6028
|
mode: 384
|
|
5919
6029
|
});
|
|
@@ -5924,9 +6034,9 @@ var HistoryStore = class {
|
|
|
5924
6034
|
return;
|
|
5925
6035
|
}
|
|
5926
6036
|
return this.enqueue(sessionId, async () => {
|
|
5927
|
-
await
|
|
6037
|
+
await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
5928
6038
|
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
5929
|
-
await
|
|
6039
|
+
await fs8.writeFile(paths.historyFile(sessionId), body, {
|
|
5930
6040
|
encoding: "utf8",
|
|
5931
6041
|
mode: 384
|
|
5932
6042
|
});
|
|
@@ -5943,7 +6053,7 @@ var HistoryStore = class {
|
|
|
5943
6053
|
return this.enqueue(sessionId, async () => {
|
|
5944
6054
|
let raw;
|
|
5945
6055
|
try {
|
|
5946
|
-
raw = await
|
|
6056
|
+
raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
|
|
5947
6057
|
} catch (err) {
|
|
5948
6058
|
const e = err;
|
|
5949
6059
|
if (e.code === "ENOENT") {
|
|
@@ -5956,7 +6066,7 @@ var HistoryStore = class {
|
|
|
5956
6066
|
return;
|
|
5957
6067
|
}
|
|
5958
6068
|
const trimmed = lines.slice(-maxEntries);
|
|
5959
|
-
await
|
|
6069
|
+
await fs8.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
5960
6070
|
encoding: "utf8",
|
|
5961
6071
|
mode: 384
|
|
5962
6072
|
});
|
|
@@ -5972,7 +6082,7 @@ var HistoryStore = class {
|
|
|
5972
6082
|
}
|
|
5973
6083
|
let raw;
|
|
5974
6084
|
try {
|
|
5975
|
-
raw = await
|
|
6085
|
+
raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
|
|
5976
6086
|
} catch (err) {
|
|
5977
6087
|
const e = err;
|
|
5978
6088
|
if (e.code === "ENOENT") {
|
|
@@ -6012,13 +6122,26 @@ var HistoryStore = class {
|
|
|
6012
6122
|
}
|
|
6013
6123
|
return out;
|
|
6014
6124
|
}
|
|
6125
|
+
// Wait for every pending append/rewrite/compact across all sessions to
|
|
6126
|
+
// settle. Daemon shutdown calls this after closing sessions so the final
|
|
6127
|
+
// turn_complete(interrupted) emitted by markClosed reaches disk before
|
|
6128
|
+
// the process exits — without this, history-replay attaches after a
|
|
6129
|
+
// restart see an unmatched prompt_received and leak pendingTurns on
|
|
6130
|
+
// every client.
|
|
6131
|
+
async flushAll() {
|
|
6132
|
+
const pending = [...this.writeQueues.values()];
|
|
6133
|
+
if (pending.length === 0) {
|
|
6134
|
+
return;
|
|
6135
|
+
}
|
|
6136
|
+
await Promise.allSettled(pending);
|
|
6137
|
+
}
|
|
6015
6138
|
async delete(sessionId) {
|
|
6016
6139
|
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6017
6140
|
return;
|
|
6018
6141
|
}
|
|
6019
6142
|
return this.enqueue(sessionId, async () => {
|
|
6020
6143
|
try {
|
|
6021
|
-
await
|
|
6144
|
+
await fs8.unlink(paths.historyFile(sessionId));
|
|
6022
6145
|
} catch (err) {
|
|
6023
6146
|
const e = err;
|
|
6024
6147
|
if (e.code !== "ENOENT") {
|
|
@@ -6026,7 +6149,7 @@ var HistoryStore = class {
|
|
|
6026
6149
|
}
|
|
6027
6150
|
}
|
|
6028
6151
|
try {
|
|
6029
|
-
await
|
|
6152
|
+
await fs8.rmdir(paths.sessionDir(sessionId));
|
|
6030
6153
|
} catch (err) {
|
|
6031
6154
|
const e = err;
|
|
6032
6155
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -6050,30 +6173,113 @@ var HistoryStore = class {
|
|
|
6050
6173
|
};
|
|
6051
6174
|
|
|
6052
6175
|
// src/tui/history.ts
|
|
6053
|
-
import { promises as
|
|
6054
|
-
import * as
|
|
6176
|
+
import { promises as fs9 } from "fs";
|
|
6177
|
+
import * as path6 from "path";
|
|
6055
6178
|
async function saveHistory(file, history) {
|
|
6056
|
-
await
|
|
6179
|
+
await fs9.mkdir(path6.dirname(file), { recursive: true });
|
|
6057
6180
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
6058
|
-
await
|
|
6181
|
+
await fs9.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
6182
|
+
}
|
|
6183
|
+
|
|
6184
|
+
// src/core/bundle.ts
|
|
6185
|
+
import { z as z5 } from "zod";
|
|
6186
|
+
var HistoryEntrySchema = z5.object({
|
|
6187
|
+
method: z5.string(),
|
|
6188
|
+
params: z5.unknown(),
|
|
6189
|
+
recordedAt: z5.number()
|
|
6190
|
+
});
|
|
6191
|
+
var BundleSession = z5.object({
|
|
6192
|
+
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
6193
|
+
// the local namespace; lineageId is what survives across hops).
|
|
6194
|
+
sessionId: z5.string(),
|
|
6195
|
+
// Required on bundles — the export path backfills if the source
|
|
6196
|
+
// record was written before lineageId existed.
|
|
6197
|
+
lineageId: z5.string(),
|
|
6198
|
+
// The exporter's agent-side session id at export time. Carried so
|
|
6199
|
+
// importers can persist it as a breadcrumb (and, eventually, as the
|
|
6200
|
+
// handle a "connect back to origin" feature would need). Omitted on
|
|
6201
|
+
// bundles whose source record never bound to an agent (e.g. a
|
|
6202
|
+
// re-export of an imported, not-yet-attached session).
|
|
6203
|
+
upstreamSessionId: z5.string().optional(),
|
|
6204
|
+
agentId: z5.string(),
|
|
6205
|
+
cwd: z5.string(),
|
|
6206
|
+
title: z5.string().optional(),
|
|
6207
|
+
currentModel: z5.string().optional(),
|
|
6208
|
+
currentMode: z5.string().optional(),
|
|
6209
|
+
currentUsage: PersistedUsage.optional(),
|
|
6210
|
+
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
6211
|
+
agentModes: z5.array(PersistedAgentMode).optional(),
|
|
6212
|
+
createdAt: z5.string(),
|
|
6213
|
+
updatedAt: z5.string()
|
|
6214
|
+
});
|
|
6215
|
+
var Bundle = z5.object({
|
|
6216
|
+
version: z5.literal(1),
|
|
6217
|
+
exportedAt: z5.string(),
|
|
6218
|
+
exportedFrom: z5.object({
|
|
6219
|
+
hydraVersion: z5.string(),
|
|
6220
|
+
machine: z5.string(),
|
|
6221
|
+
// Externally-reachable name (and optional ":port") for the exporting
|
|
6222
|
+
// daemon, sourced from config.daemon.publicHost (or daemon.host when
|
|
6223
|
+
// non-loopback). Carried so an importer can construct a hydra:// URL
|
|
6224
|
+
// that dials back to the origin — e.g. over Tailscale. Omitted when
|
|
6225
|
+
// the exporter has no routable address; never falls back to loopback.
|
|
6226
|
+
hydraHost: z5.string().optional()
|
|
6227
|
+
}),
|
|
6228
|
+
session: BundleSession,
|
|
6229
|
+
history: z5.array(HistoryEntrySchema),
|
|
6230
|
+
promptHistory: z5.array(z5.string()).optional()
|
|
6231
|
+
});
|
|
6232
|
+
function encodeBundle(params) {
|
|
6233
|
+
const bundle = {
|
|
6234
|
+
version: 1,
|
|
6235
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6236
|
+
exportedFrom: {
|
|
6237
|
+
hydraVersion: params.hydraVersion,
|
|
6238
|
+
machine: params.machine,
|
|
6239
|
+
...params.hydraHost !== void 0 && params.hydraHost.length > 0 ? { hydraHost: params.hydraHost } : {}
|
|
6240
|
+
},
|
|
6241
|
+
session: {
|
|
6242
|
+
sessionId: params.record.sessionId,
|
|
6243
|
+
lineageId: params.record.lineageId,
|
|
6244
|
+
...params.record.upstreamSessionId ? { upstreamSessionId: params.record.upstreamSessionId } : {},
|
|
6245
|
+
agentId: params.record.agentId,
|
|
6246
|
+
cwd: params.record.cwd,
|
|
6247
|
+
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
6248
|
+
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
6249
|
+
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
6250
|
+
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
6251
|
+
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
6252
|
+
...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
|
|
6253
|
+
createdAt: params.record.createdAt,
|
|
6254
|
+
updatedAt: params.record.updatedAt
|
|
6255
|
+
},
|
|
6256
|
+
history: params.history
|
|
6257
|
+
};
|
|
6258
|
+
if (params.promptHistory !== void 0) {
|
|
6259
|
+
bundle.promptHistory = params.promptHistory;
|
|
6260
|
+
}
|
|
6261
|
+
return bundle;
|
|
6262
|
+
}
|
|
6263
|
+
function decodeBundle(raw) {
|
|
6264
|
+
return Bundle.parse(raw);
|
|
6059
6265
|
}
|
|
6060
6266
|
|
|
6061
6267
|
// src/core/hydra-version.ts
|
|
6062
6268
|
import { fileURLToPath } from "url";
|
|
6063
|
-
import * as
|
|
6064
|
-
import * as
|
|
6269
|
+
import * as path7 from "path";
|
|
6270
|
+
import * as fs10 from "fs";
|
|
6065
6271
|
function resolveVersion() {
|
|
6066
6272
|
try {
|
|
6067
|
-
let dir =
|
|
6273
|
+
let dir = path7.dirname(fileURLToPath(import.meta.url));
|
|
6068
6274
|
for (let i = 0; i < 8; i += 1) {
|
|
6069
|
-
const candidate =
|
|
6070
|
-
if (
|
|
6071
|
-
const pkg = JSON.parse(
|
|
6275
|
+
const candidate = path7.join(dir, "package.json");
|
|
6276
|
+
if (fs10.existsSync(candidate)) {
|
|
6277
|
+
const pkg = JSON.parse(fs10.readFileSync(candidate, "utf8"));
|
|
6072
6278
|
if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
|
|
6073
6279
|
return pkg.version;
|
|
6074
6280
|
}
|
|
6075
6281
|
}
|
|
6076
|
-
const parent =
|
|
6282
|
+
const parent = path7.dirname(dir);
|
|
6077
6283
|
if (parent === dir) {
|
|
6078
6284
|
break;
|
|
6079
6285
|
}
|
|
@@ -6349,6 +6555,8 @@ var SessionManager = class {
|
|
|
6349
6555
|
firstPromptSeeded: !!params.title,
|
|
6350
6556
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
6351
6557
|
originatingClient: params.originatingClient,
|
|
6558
|
+
forkedFromSessionId: params.forkedFromSessionId,
|
|
6559
|
+
forkedFromMessageId: params.forkedFromMessageId,
|
|
6352
6560
|
extensionCommands: this.extensionCommands
|
|
6353
6561
|
});
|
|
6354
6562
|
await this.attachManagerHooks(session);
|
|
@@ -6417,6 +6625,8 @@ var SessionManager = class {
|
|
|
6417
6625
|
firstPromptSeeded: !!params.title,
|
|
6418
6626
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
6419
6627
|
originatingClient: params.originatingClient,
|
|
6628
|
+
forkedFromSessionId: params.forkedFromSessionId,
|
|
6629
|
+
forkedFromMessageId: params.forkedFromMessageId,
|
|
6420
6630
|
extensionCommands: this.extensionCommands
|
|
6421
6631
|
});
|
|
6422
6632
|
await this.attachManagerHooks(session);
|
|
@@ -6425,7 +6635,7 @@ var SessionManager = class {
|
|
|
6425
6635
|
}
|
|
6426
6636
|
async resolveImportCwd(cwd) {
|
|
6427
6637
|
try {
|
|
6428
|
-
const stat2 = await
|
|
6638
|
+
const stat2 = await fs11.stat(cwd);
|
|
6429
6639
|
if (stat2.isDirectory()) {
|
|
6430
6640
|
return cwd;
|
|
6431
6641
|
}
|
|
@@ -6767,7 +6977,9 @@ var SessionManager = class {
|
|
|
6767
6977
|
agentModels: record.agentModels,
|
|
6768
6978
|
createdAt: record.createdAt,
|
|
6769
6979
|
pendingHistorySync: record.pendingHistorySync,
|
|
6770
|
-
originatingClient: record.originatingClient
|
|
6980
|
+
originatingClient: record.originatingClient,
|
|
6981
|
+
forkedFromSessionId: record.forkedFromSessionId,
|
|
6982
|
+
forkedFromMessageId: record.forkedFromMessageId
|
|
6771
6983
|
};
|
|
6772
6984
|
}
|
|
6773
6985
|
async clearPendingHistorySync(sessionId) {
|
|
@@ -6868,6 +7080,8 @@ var SessionManager = class {
|
|
|
6868
7080
|
currentModel: session.currentModel,
|
|
6869
7081
|
currentUsage: session.currentUsage,
|
|
6870
7082
|
parentSessionId: session.parentSessionId,
|
|
7083
|
+
forkedFromSessionId: session.forkedFromSessionId,
|
|
7084
|
+
forkedFromMessageId: session.forkedFromMessageId,
|
|
6871
7085
|
originatingClient: session.originatingClient,
|
|
6872
7086
|
updatedAt: used,
|
|
6873
7087
|
attachedClients: session.attachedCount,
|
|
@@ -6898,6 +7112,8 @@ var SessionManager = class {
|
|
|
6898
7112
|
importedFromMachine: r.importedFromMachine,
|
|
6899
7113
|
importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
|
|
6900
7114
|
parentSessionId: r.parentSessionId,
|
|
7115
|
+
forkedFromSessionId: r.forkedFromSessionId,
|
|
7116
|
+
forkedFromMessageId: r.forkedFromMessageId,
|
|
6901
7117
|
originatingClient: r.originatingClient,
|
|
6902
7118
|
updatedAt: used,
|
|
6903
7119
|
attachedClients: 0,
|
|
@@ -6985,10 +7201,114 @@ var SessionManager = class {
|
|
|
6985
7201
|
replaced: false
|
|
6986
7202
|
};
|
|
6987
7203
|
}
|
|
6988
|
-
//
|
|
6989
|
-
//
|
|
7204
|
+
// Branch an existing local session into a new one that shares context
|
|
7205
|
+
// up to the chosen turn boundary and diverges from there. Composes the
|
|
7206
|
+
// import pipeline: synthesizes a Bundle from the source's record and
|
|
7207
|
+
// sliced history, mints a fresh lineageId, then writes the new record
|
|
7208
|
+
// via writeImportedRecord with forked* breadcrumbs instead of
|
|
7209
|
+
// imported*. The fork carries upstreamSessionId="" so the first attach
|
|
7210
|
+
// triggers seedFromImport — same wire shape as an imported session.
|
|
7211
|
+
//
|
|
7212
|
+
// forkAt defaults to the messageId of the source's most recent
|
|
7213
|
+
// turn_complete; explicit forkAt must reference a session/update
|
|
7214
|
+
// entry that's present in the source's history.jsonl. Cutting at a
|
|
7215
|
+
// completed turn excludes any in-flight prompt by construction
|
|
7216
|
+
// (history.jsonl is appended serially per session), so no locking
|
|
7217
|
+
// against the live source is needed.
|
|
7218
|
+
//
|
|
7219
|
+
// agentId defaults to the source's agent. Overriding to a different
|
|
7220
|
+
// agent scrubs agent-specific state from the fork (model, mode,
|
|
7221
|
+
// usage, agent-emitted commands/modes/models) so the new agent boots
|
|
7222
|
+
// clean — title and conversation transcript are agent-agnostic and
|
|
7223
|
+
// are kept.
|
|
7224
|
+
async forkSession(sourceSessionId, opts = {}) {
|
|
7225
|
+
const sourceRecord = await this.store.read(sourceSessionId);
|
|
7226
|
+
if (!sourceRecord) {
|
|
7227
|
+
const err = new Error(`source session not found: ${sourceSessionId}`);
|
|
7228
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
7229
|
+
throw err;
|
|
7230
|
+
}
|
|
7231
|
+
const targetAgentId = opts.agentId ?? sourceRecord.agentId;
|
|
7232
|
+
const crossAgent = targetAgentId !== sourceRecord.agentId;
|
|
7233
|
+
if (crossAgent) {
|
|
7234
|
+
const def = await this.registry.getAgent(targetAgentId);
|
|
7235
|
+
if (!def) {
|
|
7236
|
+
const err = new Error(
|
|
7237
|
+
`agent ${targetAgentId} not found in registry`
|
|
7238
|
+
);
|
|
7239
|
+
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
7240
|
+
throw err;
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7243
|
+
const sourceHistory = await this.histories.load(sourceSessionId).catch(() => []);
|
|
7244
|
+
let cutoffIndex;
|
|
7245
|
+
let forkedAt;
|
|
7246
|
+
if (opts.forkAt !== void 0) {
|
|
7247
|
+
cutoffIndex = findMessageIdIndex(sourceHistory, opts.forkAt);
|
|
7248
|
+
if (cutoffIndex < 0) {
|
|
7249
|
+
const err = new Error(
|
|
7250
|
+
`forkAt messageId not found in source history: ${opts.forkAt}`
|
|
7251
|
+
);
|
|
7252
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
7253
|
+
throw err;
|
|
7254
|
+
}
|
|
7255
|
+
forkedAt = opts.forkAt;
|
|
7256
|
+
} else {
|
|
7257
|
+
const found = findLastTurnComplete(sourceHistory);
|
|
7258
|
+
if (!found) {
|
|
7259
|
+
const err = new Error(
|
|
7260
|
+
`source session ${sourceSessionId} has no completed turns to fork from`
|
|
7261
|
+
);
|
|
7262
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
7263
|
+
throw err;
|
|
7264
|
+
}
|
|
7265
|
+
cutoffIndex = found.index;
|
|
7266
|
+
forkedAt = found.messageId;
|
|
7267
|
+
}
|
|
7268
|
+
const slicedHistory = sourceHistory.slice(0, cutoffIndex + 1);
|
|
7269
|
+
const promptHistory = await loadPromptHistorySafely(sourceSessionId);
|
|
7270
|
+
const recordForBundle = {
|
|
7271
|
+
...sourceRecord,
|
|
7272
|
+
lineageId: generateLineageId(),
|
|
7273
|
+
agentId: targetAgentId,
|
|
7274
|
+
...crossAgent ? {
|
|
7275
|
+
currentModel: void 0,
|
|
7276
|
+
currentMode: void 0,
|
|
7277
|
+
currentUsage: void 0,
|
|
7278
|
+
agentCommands: void 0,
|
|
7279
|
+
agentModes: void 0,
|
|
7280
|
+
agentModels: void 0
|
|
7281
|
+
} : {}
|
|
7282
|
+
};
|
|
7283
|
+
const bundle = encodeBundle({
|
|
7284
|
+
record: recordForBundle,
|
|
7285
|
+
history: slicedHistory,
|
|
7286
|
+
promptHistory: promptHistory.length > 0 ? promptHistory : void 0,
|
|
7287
|
+
hydraVersion: HYDRA_VERSION,
|
|
7288
|
+
machine: os2.hostname()
|
|
7289
|
+
});
|
|
7290
|
+
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
7291
|
+
await this.writeImportedRecord({
|
|
7292
|
+
sessionId: newId,
|
|
7293
|
+
bundle,
|
|
7294
|
+
cwd: opts.cwd,
|
|
7295
|
+
forkedFromSessionId: sourceSessionId,
|
|
7296
|
+
forkedFromMessageId: forkedAt
|
|
7297
|
+
});
|
|
7298
|
+
return {
|
|
7299
|
+
sessionId: newId,
|
|
7300
|
+
forkedFromSessionId: sourceSessionId,
|
|
7301
|
+
forkedAt
|
|
7302
|
+
};
|
|
7303
|
+
}
|
|
7304
|
+
// Write the imported (or forked) bundle's history.jsonl, prompt-history
|
|
7305
|
+
// (if present), and meta.json. upstreamSessionId is left empty as the
|
|
6990
7306
|
// marker that the first attach should bootstrap a fresh agent and
|
|
6991
|
-
// run seedFromImport rather than calling session/load.
|
|
7307
|
+
// run seedFromImport rather than calling session/load. When
|
|
7308
|
+
// forkedFromSessionId is set, the record is marked as a local fork
|
|
7309
|
+
// (forked* fields populated) instead of a cross-machine import
|
|
7310
|
+
// (imported* fields populated) — both share the seed-on-first-attach
|
|
7311
|
+
// wire shape but trace differently in list views.
|
|
6992
7312
|
async writeImportedRecord(args) {
|
|
6993
7313
|
await this.histories.rewrite(
|
|
6994
7314
|
args.sessionId,
|
|
@@ -6996,7 +7316,7 @@ var SessionManager = class {
|
|
|
6996
7316
|
);
|
|
6997
7317
|
const sourceMtime = new Date(args.bundle.session.updatedAt);
|
|
6998
7318
|
if (!Number.isNaN(sourceMtime.getTime())) {
|
|
6999
|
-
await
|
|
7319
|
+
await fs11.utimes(paths.historyFile(args.sessionId), sourceMtime, sourceMtime).catch(() => void 0);
|
|
7000
7320
|
}
|
|
7001
7321
|
if (args.bundle.promptHistory && args.bundle.promptHistory.length > 0) {
|
|
7002
7322
|
await saveHistory(
|
|
@@ -7005,14 +7325,20 @@ var SessionManager = class {
|
|
|
7005
7325
|
).catch(() => void 0);
|
|
7006
7326
|
}
|
|
7007
7327
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7328
|
+
const isFork = args.forkedFromSessionId !== void 0;
|
|
7008
7329
|
await this.enqueueMetaWrite(args.sessionId, async () => {
|
|
7009
7330
|
await this.store.write({
|
|
7010
7331
|
sessionId: args.sessionId,
|
|
7011
7332
|
lineageId: args.bundle.session.lineageId,
|
|
7012
7333
|
upstreamSessionId: "",
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7334
|
+
...isFork ? {
|
|
7335
|
+
forkedFromSessionId: args.forkedFromSessionId,
|
|
7336
|
+
forkedFromMessageId: args.forkedFromMessageId
|
|
7337
|
+
} : {
|
|
7338
|
+
importedFromSessionId: args.bundle.session.sessionId,
|
|
7339
|
+
importedFromUpstreamSessionId: args.bundle.session.upstreamSessionId,
|
|
7340
|
+
importedFromMachine: args.bundle.exportedFrom.machine
|
|
7341
|
+
},
|
|
7016
7342
|
agentId: args.bundle.session.agentId,
|
|
7017
7343
|
cwd: args.cwd ?? args.bundle.session.cwd,
|
|
7018
7344
|
title: args.bundle.session.title,
|
|
@@ -7146,6 +7472,14 @@ var SessionManager = class {
|
|
|
7146
7472
|
}
|
|
7147
7473
|
await Promise.allSettled(pending);
|
|
7148
7474
|
}
|
|
7475
|
+
// Wait for every pending history.jsonl write to settle. markClosed
|
|
7476
|
+
// broadcasts turn_complete(interrupted) for the in-flight turn via a
|
|
7477
|
+
// fire-and-forget store.append; without flushing, a SIGTERM can exit
|
|
7478
|
+
// before that append hits disk, leaving an unmatched prompt_received
|
|
7479
|
+
// in history that leaks pendingTurns on every client that replays it.
|
|
7480
|
+
async flushHistoryWrites() {
|
|
7481
|
+
await this.histories.flushAll();
|
|
7482
|
+
}
|
|
7149
7483
|
// Startup hook: scan persisted sessions for non-empty queue files,
|
|
7150
7484
|
// apply the TTL, resurrect anything with surviving entries, and
|
|
7151
7485
|
// replay them through the normal queue path. Called from the daemon
|
|
@@ -7244,6 +7578,8 @@ function mergeForPersistence(session, existing) {
|
|
|
7244
7578
|
agentModes,
|
|
7245
7579
|
agentModels,
|
|
7246
7580
|
parentSessionId: session.parentSessionId ?? existing?.parentSessionId,
|
|
7581
|
+
forkedFromSessionId: session.forkedFromSessionId ?? existing?.forkedFromSessionId,
|
|
7582
|
+
forkedFromMessageId: session.forkedFromMessageId ?? existing?.forkedFromMessageId,
|
|
7247
7583
|
originatingClient: session.originatingClient ?? existing?.originatingClient,
|
|
7248
7584
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
7249
7585
|
});
|
|
@@ -7513,9 +7849,26 @@ function parseModesList(list) {
|
|
|
7513
7849
|
}
|
|
7514
7850
|
return out;
|
|
7515
7851
|
}
|
|
7852
|
+
function findLastTurnComplete(history) {
|
|
7853
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
7854
|
+
const entry = history[i];
|
|
7855
|
+
if (!entry || entry.method !== "session/update") {
|
|
7856
|
+
continue;
|
|
7857
|
+
}
|
|
7858
|
+
const update = entry.params?.update;
|
|
7859
|
+
if (update?.sessionUpdate !== "turn_complete") {
|
|
7860
|
+
continue;
|
|
7861
|
+
}
|
|
7862
|
+
if (typeof update.messageId !== "string" || update.messageId.length === 0) {
|
|
7863
|
+
continue;
|
|
7864
|
+
}
|
|
7865
|
+
return { index: i, messageId: update.messageId };
|
|
7866
|
+
}
|
|
7867
|
+
return void 0;
|
|
7868
|
+
}
|
|
7516
7869
|
async function loadPromptHistorySafely(sessionId) {
|
|
7517
7870
|
try {
|
|
7518
|
-
const raw = await
|
|
7871
|
+
const raw = await fs11.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
7519
7872
|
const out = [];
|
|
7520
7873
|
for (const line of raw.split("\n")) {
|
|
7521
7874
|
if (line.length === 0) {
|
|
@@ -7536,7 +7889,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
7536
7889
|
}
|
|
7537
7890
|
async function historyMtimeIso(sessionId) {
|
|
7538
7891
|
try {
|
|
7539
|
-
const st = await
|
|
7892
|
+
const st = await fs11.stat(paths.historyFile(sessionId));
|
|
7540
7893
|
return new Date(st.mtimeMs).toISOString();
|
|
7541
7894
|
} catch {
|
|
7542
7895
|
return void 0;
|
|
@@ -7545,9 +7898,9 @@ async function historyMtimeIso(sessionId) {
|
|
|
7545
7898
|
|
|
7546
7899
|
// src/core/extensions.ts
|
|
7547
7900
|
import { spawn as spawn4 } from "child_process";
|
|
7548
|
-
import * as
|
|
7901
|
+
import * as fs12 from "fs";
|
|
7549
7902
|
import * as fsp5 from "fs/promises";
|
|
7550
|
-
import * as
|
|
7903
|
+
import * as path8 from "path";
|
|
7551
7904
|
var RESTART_BASE_MS = 1e3;
|
|
7552
7905
|
var RESTART_CAP_MS = 6e4;
|
|
7553
7906
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -7604,9 +7957,9 @@ var ExtensionManager = class {
|
|
|
7604
7957
|
} catch {
|
|
7605
7958
|
}
|
|
7606
7959
|
tasks.push(
|
|
7607
|
-
new Promise((
|
|
7960
|
+
new Promise((resolve4) => {
|
|
7608
7961
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7609
|
-
|
|
7962
|
+
resolve4();
|
|
7610
7963
|
return;
|
|
7611
7964
|
}
|
|
7612
7965
|
const timer = setTimeout(() => {
|
|
@@ -7614,11 +7967,11 @@ var ExtensionManager = class {
|
|
|
7614
7967
|
child.kill("SIGKILL");
|
|
7615
7968
|
} catch {
|
|
7616
7969
|
}
|
|
7617
|
-
|
|
7970
|
+
resolve4();
|
|
7618
7971
|
}, STOP_GRACE_MS);
|
|
7619
7972
|
child.on("exit", () => {
|
|
7620
7973
|
clearTimeout(timer);
|
|
7621
|
-
|
|
7974
|
+
resolve4();
|
|
7622
7975
|
});
|
|
7623
7976
|
})
|
|
7624
7977
|
);
|
|
@@ -7726,8 +8079,8 @@ var ExtensionManager = class {
|
|
|
7726
8079
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7727
8080
|
return;
|
|
7728
8081
|
}
|
|
7729
|
-
const exited = new Promise((
|
|
7730
|
-
entry.exitWaiters.push(
|
|
8082
|
+
const exited = new Promise((resolve4) => {
|
|
8083
|
+
entry.exitWaiters.push(resolve4);
|
|
7731
8084
|
});
|
|
7732
8085
|
try {
|
|
7733
8086
|
child.kill("SIGTERM");
|
|
@@ -7802,7 +8155,7 @@ var ExtensionManager = class {
|
|
|
7802
8155
|
if (!entry.endsWith(".pid")) {
|
|
7803
8156
|
continue;
|
|
7804
8157
|
}
|
|
7805
|
-
const pidPath =
|
|
8158
|
+
const pidPath = path8.join(paths.extensionsDir(), entry);
|
|
7806
8159
|
let pid;
|
|
7807
8160
|
try {
|
|
7808
8161
|
const raw = await fsp5.readFile(pidPath, "utf8");
|
|
@@ -7841,7 +8194,7 @@ var ExtensionManager = class {
|
|
|
7841
8194
|
}
|
|
7842
8195
|
const ext = entry.config;
|
|
7843
8196
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
7844
|
-
const logStream =
|
|
8197
|
+
const logStream = fs12.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
7845
8198
|
flags: "a"
|
|
7846
8199
|
});
|
|
7847
8200
|
logStream.write(
|
|
@@ -7894,7 +8247,7 @@ var ExtensionManager = class {
|
|
|
7894
8247
|
}
|
|
7895
8248
|
if (typeof child.pid === "number") {
|
|
7896
8249
|
try {
|
|
7897
|
-
|
|
8250
|
+
fs12.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
7898
8251
|
`, {
|
|
7899
8252
|
encoding: "utf8",
|
|
7900
8253
|
mode: 384
|
|
@@ -7919,7 +8272,7 @@ var ExtensionManager = class {
|
|
|
7919
8272
|
});
|
|
7920
8273
|
child.on("exit", (code, signal) => {
|
|
7921
8274
|
try {
|
|
7922
|
-
|
|
8275
|
+
fs12.unlinkSync(paths.extensionPidFile(ext.name));
|
|
7923
8276
|
} catch {
|
|
7924
8277
|
}
|
|
7925
8278
|
logStream.write(
|
|
@@ -7934,8 +8287,8 @@ var ExtensionManager = class {
|
|
|
7934
8287
|
entry.processToken = void 0;
|
|
7935
8288
|
}
|
|
7936
8289
|
const waiters = entry.exitWaiters.splice(0);
|
|
7937
|
-
for (const
|
|
7938
|
-
|
|
8290
|
+
for (const resolve4 of waiters) {
|
|
8291
|
+
resolve4();
|
|
7939
8292
|
}
|
|
7940
8293
|
if (this.stopping || entry.manuallyStopped) {
|
|
7941
8294
|
try {
|
|
@@ -7981,9 +8334,9 @@ function withCode2(err, code) {
|
|
|
7981
8334
|
|
|
7982
8335
|
// src/core/transformer-manager.ts
|
|
7983
8336
|
import { spawn as spawn5 } from "child_process";
|
|
7984
|
-
import * as
|
|
8337
|
+
import * as fs13 from "fs";
|
|
7985
8338
|
import * as fsp6 from "fs/promises";
|
|
7986
|
-
import * as
|
|
8339
|
+
import * as path9 from "path";
|
|
7987
8340
|
var RESTART_BASE_MS2 = 1e3;
|
|
7988
8341
|
var RESTART_CAP_MS2 = 6e4;
|
|
7989
8342
|
var STOP_GRACE_MS2 = 3e3;
|
|
@@ -8067,9 +8420,9 @@ var TransformerManager = class {
|
|
|
8067
8420
|
} catch {
|
|
8068
8421
|
}
|
|
8069
8422
|
tasks.push(
|
|
8070
|
-
new Promise((
|
|
8423
|
+
new Promise((resolve4) => {
|
|
8071
8424
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
8072
|
-
|
|
8425
|
+
resolve4();
|
|
8073
8426
|
return;
|
|
8074
8427
|
}
|
|
8075
8428
|
const timer = setTimeout(() => {
|
|
@@ -8077,11 +8430,11 @@ var TransformerManager = class {
|
|
|
8077
8430
|
child.kill("SIGKILL");
|
|
8078
8431
|
} catch {
|
|
8079
8432
|
}
|
|
8080
|
-
|
|
8433
|
+
resolve4();
|
|
8081
8434
|
}, STOP_GRACE_MS2);
|
|
8082
8435
|
child.on("exit", () => {
|
|
8083
8436
|
clearTimeout(timer);
|
|
8084
|
-
|
|
8437
|
+
resolve4();
|
|
8085
8438
|
});
|
|
8086
8439
|
})
|
|
8087
8440
|
);
|
|
@@ -8186,8 +8539,8 @@ var TransformerManager = class {
|
|
|
8186
8539
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
8187
8540
|
return;
|
|
8188
8541
|
}
|
|
8189
|
-
const exited = new Promise((
|
|
8190
|
-
entry.exitWaiters.push(
|
|
8542
|
+
const exited = new Promise((resolve4) => {
|
|
8543
|
+
entry.exitWaiters.push(resolve4);
|
|
8191
8544
|
});
|
|
8192
8545
|
try {
|
|
8193
8546
|
child.kill("SIGTERM");
|
|
@@ -8262,7 +8615,7 @@ var TransformerManager = class {
|
|
|
8262
8615
|
if (!entry.endsWith(".pid")) {
|
|
8263
8616
|
continue;
|
|
8264
8617
|
}
|
|
8265
|
-
const pidPath =
|
|
8618
|
+
const pidPath = path9.join(paths.transformersDir(), entry);
|
|
8266
8619
|
let pid;
|
|
8267
8620
|
try {
|
|
8268
8621
|
const raw = await fsp6.readFile(pidPath, "utf8");
|
|
@@ -8301,7 +8654,7 @@ var TransformerManager = class {
|
|
|
8301
8654
|
}
|
|
8302
8655
|
const t = entry.config;
|
|
8303
8656
|
const command = t.command.length > 0 ? t.command : [t.name];
|
|
8304
|
-
const logStream =
|
|
8657
|
+
const logStream = fs13.createWriteStream(paths.transformerLogFile(t.name), {
|
|
8305
8658
|
flags: "a"
|
|
8306
8659
|
});
|
|
8307
8660
|
logStream.write(
|
|
@@ -8354,7 +8707,7 @@ var TransformerManager = class {
|
|
|
8354
8707
|
}
|
|
8355
8708
|
if (typeof child.pid === "number") {
|
|
8356
8709
|
try {
|
|
8357
|
-
|
|
8710
|
+
fs13.writeFileSync(paths.transformerPidFile(t.name), `${child.pid}
|
|
8358
8711
|
`, {
|
|
8359
8712
|
encoding: "utf8",
|
|
8360
8713
|
mode: 384
|
|
@@ -8379,7 +8732,7 @@ var TransformerManager = class {
|
|
|
8379
8732
|
});
|
|
8380
8733
|
child.on("exit", (code, signal) => {
|
|
8381
8734
|
try {
|
|
8382
|
-
|
|
8735
|
+
fs13.unlinkSync(paths.transformerPidFile(t.name));
|
|
8383
8736
|
} catch {
|
|
8384
8737
|
}
|
|
8385
8738
|
logStream.write(
|
|
@@ -8394,8 +8747,8 @@ var TransformerManager = class {
|
|
|
8394
8747
|
entry.processToken = void 0;
|
|
8395
8748
|
}
|
|
8396
8749
|
const waiters = entry.exitWaiters.splice(0);
|
|
8397
|
-
for (const
|
|
8398
|
-
|
|
8750
|
+
for (const resolve4 of waiters) {
|
|
8751
|
+
resolve4();
|
|
8399
8752
|
}
|
|
8400
8753
|
if (this.stopping || entry.manuallyStopped) {
|
|
8401
8754
|
try {
|
|
@@ -8490,7 +8843,7 @@ var ExtensionCommandRegistry = class {
|
|
|
8490
8843
|
|
|
8491
8844
|
// src/core/agent-prune.ts
|
|
8492
8845
|
import * as fsp7 from "fs/promises";
|
|
8493
|
-
import * as
|
|
8846
|
+
import * as path10 from "path";
|
|
8494
8847
|
var logSink3 = (msg) => {
|
|
8495
8848
|
process.stderr.write(msg + "\n");
|
|
8496
8849
|
};
|
|
@@ -8508,7 +8861,7 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
8508
8861
|
desiredByAgent.set(a.id, a.version ?? "current");
|
|
8509
8862
|
}
|
|
8510
8863
|
const activeByAgent = sessionManager.activeAgentVersions();
|
|
8511
|
-
const platformDir =
|
|
8864
|
+
const platformDir = path10.join(paths.agentsDir(), platformKey);
|
|
8512
8865
|
let agentEntries;
|
|
8513
8866
|
try {
|
|
8514
8867
|
agentEntries = await fsp7.readdir(platformDir, { withFileTypes: true });
|
|
@@ -8530,7 +8883,7 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
8530
8883
|
continue;
|
|
8531
8884
|
}
|
|
8532
8885
|
const activeVersions = activeByAgent.get(agentId) ?? /* @__PURE__ */ new Set();
|
|
8533
|
-
const agentDir =
|
|
8886
|
+
const agentDir = path10.join(platformDir, agentId);
|
|
8534
8887
|
let versionEntries;
|
|
8535
8888
|
try {
|
|
8536
8889
|
versionEntries = await fsp7.readdir(agentDir, { withFileTypes: true });
|
|
@@ -8554,7 +8907,7 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
8554
8907
|
if (version.includes(".partial-")) {
|
|
8555
8908
|
continue;
|
|
8556
8909
|
}
|
|
8557
|
-
const versionDir =
|
|
8910
|
+
const versionDir = path10.join(agentDir, version);
|
|
8558
8911
|
try {
|
|
8559
8912
|
await fsp7.rm(versionDir, { recursive: true, force: true });
|
|
8560
8913
|
logSink3(`hydra-acp: pruned stale ${agentId} ${version} (${versionDir})`);
|
|
@@ -8567,23 +8920,88 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
8567
8920
|
}
|
|
8568
8921
|
}
|
|
8569
8922
|
|
|
8923
|
+
// src/core/agent-sync-scheduler.ts
|
|
8924
|
+
function startAgentSyncScheduler(opts) {
|
|
8925
|
+
let timer;
|
|
8926
|
+
let stopped = false;
|
|
8927
|
+
let cursor = 0;
|
|
8928
|
+
const log = (level, msg) => {
|
|
8929
|
+
if (!opts.logger) {
|
|
8930
|
+
return;
|
|
8931
|
+
}
|
|
8932
|
+
opts.logger[level](`agent-sync: ${msg}`);
|
|
8933
|
+
};
|
|
8934
|
+
const tick = async () => {
|
|
8935
|
+
const installed = [];
|
|
8936
|
+
try {
|
|
8937
|
+
const doc = await opts.registry.load();
|
|
8938
|
+
for (const a of doc.agents) {
|
|
8939
|
+
const state = await agentInstallState(a);
|
|
8940
|
+
if (state === "yes") {
|
|
8941
|
+
installed.push(a.id);
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8944
|
+
} catch (err) {
|
|
8945
|
+
log("warn", `registry load failed: ${err.message}`);
|
|
8946
|
+
return opts.intervalMs;
|
|
8947
|
+
}
|
|
8948
|
+
if (installed.length === 0) {
|
|
8949
|
+
return opts.intervalMs;
|
|
8950
|
+
}
|
|
8951
|
+
const idx = cursor % installed.length;
|
|
8952
|
+
cursor = (cursor + 1) % installed.length;
|
|
8953
|
+
const agentId = installed[idx];
|
|
8954
|
+
try {
|
|
8955
|
+
const { synced, skipped } = await opts.manager.syncFromAgent(agentId);
|
|
8956
|
+
log(
|
|
8957
|
+
"info",
|
|
8958
|
+
`${agentId}: synced ${synced.length}, skipped ${skipped}`
|
|
8959
|
+
);
|
|
8960
|
+
} catch (err) {
|
|
8961
|
+
log("warn", `${agentId}: ${err.message}`);
|
|
8962
|
+
}
|
|
8963
|
+
return Math.max(1, Math.floor(opts.intervalMs / installed.length));
|
|
8964
|
+
};
|
|
8965
|
+
const scheduleNext = (delayMs) => {
|
|
8966
|
+
if (stopped) {
|
|
8967
|
+
return;
|
|
8968
|
+
}
|
|
8969
|
+
timer = setTimeout(() => {
|
|
8970
|
+
tick().then((nextDelay) => {
|
|
8971
|
+
scheduleNext(nextDelay);
|
|
8972
|
+
}).catch((err) => {
|
|
8973
|
+
log("warn", `tick crashed: ${err.message}`);
|
|
8974
|
+
scheduleNext(opts.intervalMs);
|
|
8975
|
+
});
|
|
8976
|
+
}, delayMs);
|
|
8977
|
+
timer.unref();
|
|
8978
|
+
};
|
|
8979
|
+
scheduleNext(opts.intervalMs);
|
|
8980
|
+
return () => {
|
|
8981
|
+
stopped = true;
|
|
8982
|
+
if (timer) {
|
|
8983
|
+
clearTimeout(timer);
|
|
8984
|
+
timer = void 0;
|
|
8985
|
+
}
|
|
8986
|
+
};
|
|
8987
|
+
}
|
|
8988
|
+
|
|
8570
8989
|
// src/core/session-tokens.ts
|
|
8571
|
-
import * as
|
|
8572
|
-
import
|
|
8573
|
-
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
8990
|
+
import * as path11 from "path";
|
|
8991
|
+
import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
8574
8992
|
var TOKEN_PREFIX = "hydra_session_";
|
|
8575
8993
|
var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
|
|
8576
8994
|
var ID_LENGTH = 12;
|
|
8577
8995
|
var TOKEN_BYTES = 32;
|
|
8578
8996
|
var WRITE_DEBOUNCE_MS = 50;
|
|
8579
8997
|
function tokensFilePath() {
|
|
8580
|
-
return
|
|
8998
|
+
return path11.join(paths.home(), "session-tokens.json");
|
|
8581
8999
|
}
|
|
8582
9000
|
function sha256Hex(input) {
|
|
8583
9001
|
return createHash("sha256").update(input).digest("hex");
|
|
8584
9002
|
}
|
|
8585
9003
|
function randomHex(bytes) {
|
|
8586
|
-
return
|
|
9004
|
+
return randomBytes2(bytes).toString("hex");
|
|
8587
9005
|
}
|
|
8588
9006
|
function generateId() {
|
|
8589
9007
|
return randomHex(ID_LENGTH).slice(0, ID_LENGTH * 2);
|
|
@@ -8603,17 +9021,11 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
8603
9021
|
}
|
|
8604
9022
|
static async load() {
|
|
8605
9023
|
let records = [];
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
}
|
|
8612
|
-
} catch (err) {
|
|
8613
|
-
const e = err;
|
|
8614
|
-
if (e.code !== "ENOENT") {
|
|
8615
|
-
throw err;
|
|
8616
|
-
}
|
|
9024
|
+
const parsed = await readJsonSafe(
|
|
9025
|
+
tokensFilePath()
|
|
9026
|
+
);
|
|
9027
|
+
if (parsed && Array.isArray(parsed.records)) {
|
|
9028
|
+
records = parsed.records.filter(isRecord);
|
|
8617
9029
|
}
|
|
8618
9030
|
const store = new _SessionTokenStore(records);
|
|
8619
9031
|
const removed = store.sweepExpired(/* @__PURE__ */ new Date());
|
|
@@ -8730,14 +9142,11 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
8730
9142
|
await this.writeInflight;
|
|
8731
9143
|
}
|
|
8732
9144
|
const records = Array.from(this.records.values());
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
mode: 384
|
|
8739
|
-
});
|
|
8740
|
-
})();
|
|
9145
|
+
this.writeInflight = writeJsonAtomic(
|
|
9146
|
+
tokensFilePath(),
|
|
9147
|
+
{ records },
|
|
9148
|
+
{ mode: 384 }
|
|
9149
|
+
);
|
|
8741
9150
|
try {
|
|
8742
9151
|
await this.writeInflight;
|
|
8743
9152
|
} finally {
|
|
@@ -8905,89 +9314,6 @@ var AuthRateLimiter = class {
|
|
|
8905
9314
|
// src/daemon/routes/sessions.ts
|
|
8906
9315
|
import * as os3 from "os";
|
|
8907
9316
|
|
|
8908
|
-
// src/core/bundle.ts
|
|
8909
|
-
import { z as z5 } from "zod";
|
|
8910
|
-
var HistoryEntrySchema = z5.object({
|
|
8911
|
-
method: z5.string(),
|
|
8912
|
-
params: z5.unknown(),
|
|
8913
|
-
recordedAt: z5.number()
|
|
8914
|
-
});
|
|
8915
|
-
var BundleSession = z5.object({
|
|
8916
|
-
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
8917
|
-
// the local namespace; lineageId is what survives across hops).
|
|
8918
|
-
sessionId: z5.string(),
|
|
8919
|
-
// Required on bundles — the export path backfills if the source
|
|
8920
|
-
// record was written before lineageId existed.
|
|
8921
|
-
lineageId: z5.string(),
|
|
8922
|
-
// The exporter's agent-side session id at export time. Carried so
|
|
8923
|
-
// importers can persist it as a breadcrumb (and, eventually, as the
|
|
8924
|
-
// handle a "connect back to origin" feature would need). Omitted on
|
|
8925
|
-
// bundles whose source record never bound to an agent (e.g. a
|
|
8926
|
-
// re-export of an imported, not-yet-attached session).
|
|
8927
|
-
upstreamSessionId: z5.string().optional(),
|
|
8928
|
-
agentId: z5.string(),
|
|
8929
|
-
cwd: z5.string(),
|
|
8930
|
-
title: z5.string().optional(),
|
|
8931
|
-
currentModel: z5.string().optional(),
|
|
8932
|
-
currentMode: z5.string().optional(),
|
|
8933
|
-
currentUsage: PersistedUsage.optional(),
|
|
8934
|
-
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
8935
|
-
agentModes: z5.array(PersistedAgentMode).optional(),
|
|
8936
|
-
createdAt: z5.string(),
|
|
8937
|
-
updatedAt: z5.string()
|
|
8938
|
-
});
|
|
8939
|
-
var Bundle = z5.object({
|
|
8940
|
-
version: z5.literal(1),
|
|
8941
|
-
exportedAt: z5.string(),
|
|
8942
|
-
exportedFrom: z5.object({
|
|
8943
|
-
hydraVersion: z5.string(),
|
|
8944
|
-
machine: z5.string(),
|
|
8945
|
-
// Externally-reachable name (and optional ":port") for the exporting
|
|
8946
|
-
// daemon, sourced from config.daemon.publicHost (or daemon.host when
|
|
8947
|
-
// non-loopback). Carried so an importer can construct a hydra:// URL
|
|
8948
|
-
// that dials back to the origin — e.g. over Tailscale. Omitted when
|
|
8949
|
-
// the exporter has no routable address; never falls back to loopback.
|
|
8950
|
-
hydraHost: z5.string().optional()
|
|
8951
|
-
}),
|
|
8952
|
-
session: BundleSession,
|
|
8953
|
-
history: z5.array(HistoryEntrySchema),
|
|
8954
|
-
promptHistory: z5.array(z5.string()).optional()
|
|
8955
|
-
});
|
|
8956
|
-
function encodeBundle(params) {
|
|
8957
|
-
const bundle = {
|
|
8958
|
-
version: 1,
|
|
8959
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8960
|
-
exportedFrom: {
|
|
8961
|
-
hydraVersion: params.hydraVersion,
|
|
8962
|
-
machine: params.machine,
|
|
8963
|
-
...params.hydraHost !== void 0 && params.hydraHost.length > 0 ? { hydraHost: params.hydraHost } : {}
|
|
8964
|
-
},
|
|
8965
|
-
session: {
|
|
8966
|
-
sessionId: params.record.sessionId,
|
|
8967
|
-
lineageId: params.record.lineageId,
|
|
8968
|
-
...params.record.upstreamSessionId ? { upstreamSessionId: params.record.upstreamSessionId } : {},
|
|
8969
|
-
agentId: params.record.agentId,
|
|
8970
|
-
cwd: params.record.cwd,
|
|
8971
|
-
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
8972
|
-
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
8973
|
-
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
8974
|
-
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
8975
|
-
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
8976
|
-
...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
|
|
8977
|
-
createdAt: params.record.createdAt,
|
|
8978
|
-
updatedAt: params.record.updatedAt
|
|
8979
|
-
},
|
|
8980
|
-
history: params.history
|
|
8981
|
-
};
|
|
8982
|
-
if (params.promptHistory !== void 0) {
|
|
8983
|
-
bundle.promptHistory = params.promptHistory;
|
|
8984
|
-
}
|
|
8985
|
-
return bundle;
|
|
8986
|
-
}
|
|
8987
|
-
function decodeBundle(raw) {
|
|
8988
|
-
return Bundle.parse(raw);
|
|
8989
|
-
}
|
|
8990
|
-
|
|
8991
9317
|
// src/core/render-update.ts
|
|
8992
9318
|
import stripAnsi from "strip-ansi";
|
|
8993
9319
|
var STRIP_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
@@ -9181,6 +9507,51 @@ function isExitPlanModeTool(name) {
|
|
|
9181
9507
|
const normalised = name.toLowerCase().replace(/[_\s-]/g, "");
|
|
9182
9508
|
return normalised === "exitplanmode";
|
|
9183
9509
|
}
|
|
9510
|
+
function extractEditDiff(u) {
|
|
9511
|
+
const content = u.content;
|
|
9512
|
+
if (Array.isArray(content)) {
|
|
9513
|
+
for (const block of content) {
|
|
9514
|
+
if (!block || typeof block !== "object") {
|
|
9515
|
+
continue;
|
|
9516
|
+
}
|
|
9517
|
+
const b = block;
|
|
9518
|
+
if (b.type !== "diff") {
|
|
9519
|
+
continue;
|
|
9520
|
+
}
|
|
9521
|
+
const oldText = typeof b.oldText === "string" ? b.oldText : void 0;
|
|
9522
|
+
const newText = typeof b.newText === "string" ? b.newText : void 0;
|
|
9523
|
+
if (oldText === void 0 && newText === void 0) {
|
|
9524
|
+
continue;
|
|
9525
|
+
}
|
|
9526
|
+
const path14 = typeof b.path === "string" ? b.path : void 0;
|
|
9527
|
+
return {
|
|
9528
|
+
...path14 !== void 0 ? { path: path14 } : {},
|
|
9529
|
+
oldText: oldText ?? "",
|
|
9530
|
+
newText: newText ?? ""
|
|
9531
|
+
};
|
|
9532
|
+
}
|
|
9533
|
+
}
|
|
9534
|
+
const rawInput = u.rawInput;
|
|
9535
|
+
if (rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)) {
|
|
9536
|
+
const r = rawInput;
|
|
9537
|
+
const filePath = typeof r.file_path === "string" ? r.file_path : typeof r.path === "string" ? r.path : void 0;
|
|
9538
|
+
if (typeof r.old_string === "string" && typeof r.new_string === "string") {
|
|
9539
|
+
return {
|
|
9540
|
+
...filePath !== void 0 ? { path: filePath } : {},
|
|
9541
|
+
oldText: r.old_string,
|
|
9542
|
+
newText: r.new_string
|
|
9543
|
+
};
|
|
9544
|
+
}
|
|
9545
|
+
if (typeof r.content === "string") {
|
|
9546
|
+
return {
|
|
9547
|
+
...filePath !== void 0 ? { path: filePath } : {},
|
|
9548
|
+
oldText: "",
|
|
9549
|
+
newText: r.content
|
|
9550
|
+
};
|
|
9551
|
+
}
|
|
9552
|
+
}
|
|
9553
|
+
return null;
|
|
9554
|
+
}
|
|
9184
9555
|
function readExitPlanMarkdown(u) {
|
|
9185
9556
|
const rawInput = u.rawInput;
|
|
9186
9557
|
if (!rawInput || typeof rawInput !== "object" || Array.isArray(rawInput)) {
|
|
@@ -9220,6 +9591,10 @@ function mapToolCall(u) {
|
|
|
9220
9591
|
if (rawKind !== void 0) {
|
|
9221
9592
|
event.rawKind = rawKind;
|
|
9222
9593
|
}
|
|
9594
|
+
const diff = extractEditDiff(u);
|
|
9595
|
+
if (diff !== null) {
|
|
9596
|
+
event.editDiff = diff;
|
|
9597
|
+
}
|
|
9223
9598
|
return event;
|
|
9224
9599
|
}
|
|
9225
9600
|
function mapToolCallUpdate(u) {
|
|
@@ -9230,7 +9605,8 @@ function mapToolCallUpdate(u) {
|
|
|
9230
9605
|
const rawTitle = readString(u, "title");
|
|
9231
9606
|
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
9232
9607
|
const status = readString(u, "status");
|
|
9233
|
-
const
|
|
9608
|
+
const diff = extractEditDiff(u);
|
|
9609
|
+
const meaningful = title !== void 0 || diff !== null || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
|
|
9234
9610
|
if (!meaningful) {
|
|
9235
9611
|
return null;
|
|
9236
9612
|
}
|
|
@@ -9253,6 +9629,9 @@ function mapToolCallUpdate(u) {
|
|
|
9253
9629
|
if (status !== void 0) {
|
|
9254
9630
|
event.status = status;
|
|
9255
9631
|
}
|
|
9632
|
+
if (diff !== null) {
|
|
9633
|
+
event.editDiff = diff;
|
|
9634
|
+
}
|
|
9256
9635
|
if (status === "failed") {
|
|
9257
9636
|
const errorText = extractToolFailureText(u);
|
|
9258
9637
|
if (errorText !== null) {
|
|
@@ -10172,6 +10551,48 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
10172
10551
|
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
10173
10552
|
reply.code(200).send(bundleToMarkdown(bundle));
|
|
10174
10553
|
});
|
|
10554
|
+
app.post("/v1/sessions/:id/fork", async (request, reply) => {
|
|
10555
|
+
const raw = request.params.id;
|
|
10556
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
10557
|
+
const body = request.body ?? {};
|
|
10558
|
+
const opts = {};
|
|
10559
|
+
if (body.forkAt !== void 0) {
|
|
10560
|
+
if (typeof body.forkAt !== "string" || body.forkAt.length === 0) {
|
|
10561
|
+
reply.code(400).send({ error: "forkAt must be a non-empty string" });
|
|
10562
|
+
return;
|
|
10563
|
+
}
|
|
10564
|
+
opts.forkAt = body.forkAt;
|
|
10565
|
+
}
|
|
10566
|
+
if (body.cwd !== void 0) {
|
|
10567
|
+
if (typeof body.cwd !== "string" || body.cwd.length === 0) {
|
|
10568
|
+
reply.code(400).send({ error: "cwd must be a non-empty string" });
|
|
10569
|
+
return;
|
|
10570
|
+
}
|
|
10571
|
+
opts.cwd = expandHome(body.cwd);
|
|
10572
|
+
}
|
|
10573
|
+
if (body.agentId !== void 0) {
|
|
10574
|
+
if (typeof body.agentId !== "string" || body.agentId.length === 0) {
|
|
10575
|
+
reply.code(400).send({ error: "agentId must be a non-empty string" });
|
|
10576
|
+
return;
|
|
10577
|
+
}
|
|
10578
|
+
opts.agentId = body.agentId;
|
|
10579
|
+
}
|
|
10580
|
+
try {
|
|
10581
|
+
const result = await manager.forkSession(id, opts);
|
|
10582
|
+
reply.code(201).send(result);
|
|
10583
|
+
} catch (err) {
|
|
10584
|
+
const e = err;
|
|
10585
|
+
if (e.code === JsonRpcErrorCodes.SessionNotFound) {
|
|
10586
|
+
reply.code(404).send({ error: e.message });
|
|
10587
|
+
return;
|
|
10588
|
+
}
|
|
10589
|
+
if (e.code === JsonRpcErrorCodes.InvalidParams || e.code === JsonRpcErrorCodes.AgentNotInstalled) {
|
|
10590
|
+
reply.code(400).send({ error: e.message });
|
|
10591
|
+
return;
|
|
10592
|
+
}
|
|
10593
|
+
reply.code(500).send({ error: e.message });
|
|
10594
|
+
}
|
|
10595
|
+
});
|
|
10175
10596
|
app.post("/v1/sessions/import", async (request, reply) => {
|
|
10176
10597
|
const body = request.body ?? {};
|
|
10177
10598
|
if (body.bundle === void 0) {
|
|
@@ -10284,15 +10705,20 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
10284
10705
|
function registerAgentRoutes(app, registry, manager, opts = {}) {
|
|
10285
10706
|
app.get("/v1/agents", async () => {
|
|
10286
10707
|
const doc = await registry.load();
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
agents: doc.agents.map((a) => ({
|
|
10708
|
+
const agents = await Promise.all(
|
|
10709
|
+
doc.agents.map(async (a) => ({
|
|
10290
10710
|
id: a.id,
|
|
10291
10711
|
name: a.name,
|
|
10292
10712
|
version: a.version,
|
|
10293
10713
|
description: a.description,
|
|
10294
|
-
distributions: Object.keys(a.distribution)
|
|
10714
|
+
distributions: Object.keys(a.distribution),
|
|
10715
|
+
installed: await agentInstallState(a)
|
|
10295
10716
|
}))
|
|
10717
|
+
);
|
|
10718
|
+
return {
|
|
10719
|
+
version: doc.version,
|
|
10720
|
+
fetchedAt: registry.lastFetchedAt(),
|
|
10721
|
+
agents
|
|
10296
10722
|
};
|
|
10297
10723
|
});
|
|
10298
10724
|
app.get("/v1/registry", async () => {
|
|
@@ -10611,12 +11037,12 @@ import { z as z6 } from "zod";
|
|
|
10611
11037
|
|
|
10612
11038
|
// src/core/password.ts
|
|
10613
11039
|
import * as fs14 from "fs/promises";
|
|
10614
|
-
import * as
|
|
10615
|
-
import { randomBytes as
|
|
11040
|
+
import * as path12 from "path";
|
|
11041
|
+
import { randomBytes as randomBytes3, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
10616
11042
|
import { promisify } from "util";
|
|
10617
11043
|
var scryptAsync = promisify(scrypt);
|
|
10618
11044
|
function passwordHashPath() {
|
|
10619
|
-
return
|
|
11045
|
+
return path12.join(paths.home(), "password-hash");
|
|
10620
11046
|
}
|
|
10621
11047
|
var DEFAULT_N = 1 << 15;
|
|
10622
11048
|
var MAX_MEM = 128 * 1024 * 1024;
|
|
@@ -10805,13 +11231,13 @@ function wsToMessageStream(ws) {
|
|
|
10805
11231
|
throw new Error("ws is closed");
|
|
10806
11232
|
}
|
|
10807
11233
|
const text = JSON.stringify(message);
|
|
10808
|
-
await new Promise((
|
|
11234
|
+
await new Promise((resolve4, reject) => {
|
|
10809
11235
|
ws.send(text, (err) => {
|
|
10810
11236
|
if (err) {
|
|
10811
11237
|
reject(err);
|
|
10812
11238
|
return;
|
|
10813
11239
|
}
|
|
10814
|
-
|
|
11240
|
+
resolve4();
|
|
10815
11241
|
});
|
|
10816
11242
|
});
|
|
10817
11243
|
},
|
|
@@ -10833,8 +11259,8 @@ function wsToMessageStream(ws) {
|
|
|
10833
11259
|
|
|
10834
11260
|
// src/daemon/acp-ws.ts
|
|
10835
11261
|
import * as os4 from "os";
|
|
10836
|
-
import * as
|
|
10837
|
-
import { randomBytes as
|
|
11262
|
+
import * as path13 from "path";
|
|
11263
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
10838
11264
|
function registerAcpWsEndpoint(app, deps) {
|
|
10839
11265
|
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
10840
11266
|
const token = tokenFromUpgradeRequest({
|
|
@@ -10916,6 +11342,50 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
10916
11342
|
registry.clear(processIdentity.name);
|
|
10917
11343
|
});
|
|
10918
11344
|
}
|
|
11345
|
+
if (processIdentity && deps.extensionMcp) {
|
|
11346
|
+
const mcpRegistry = deps.extensionMcp;
|
|
11347
|
+
connection.onRequest("hydra-acp/register_mcp_tools", async (raw) => {
|
|
11348
|
+
const params = raw ?? {};
|
|
11349
|
+
const instructions = typeof params.instructions === "string" ? params.instructions : void 0;
|
|
11350
|
+
const tools = Array.isArray(params.tools) ? params.tools.map((t) => {
|
|
11351
|
+
if (!t || typeof t !== "object") {
|
|
11352
|
+
return void 0;
|
|
11353
|
+
}
|
|
11354
|
+
const obj = t;
|
|
11355
|
+
if (typeof obj.name !== "string" || obj.name.length === 0) {
|
|
11356
|
+
return void 0;
|
|
11357
|
+
}
|
|
11358
|
+
if (typeof obj.description !== "string") {
|
|
11359
|
+
return void 0;
|
|
11360
|
+
}
|
|
11361
|
+
if (obj.inputSchema === null || typeof obj.inputSchema !== "object") {
|
|
11362
|
+
return void 0;
|
|
11363
|
+
}
|
|
11364
|
+
const spec = {
|
|
11365
|
+
name: obj.name,
|
|
11366
|
+
description: obj.description,
|
|
11367
|
+
inputSchema: obj.inputSchema
|
|
11368
|
+
};
|
|
11369
|
+
if (obj.outputSchema !== null && typeof obj.outputSchema === "object") {
|
|
11370
|
+
spec.outputSchema = obj.outputSchema;
|
|
11371
|
+
}
|
|
11372
|
+
return spec;
|
|
11373
|
+
}).filter((s) => s !== void 0) : [];
|
|
11374
|
+
if (tools.length === 0) {
|
|
11375
|
+
throw new Error("register_mcp_tools requires at least one tool");
|
|
11376
|
+
}
|
|
11377
|
+
mcpRegistry.register(
|
|
11378
|
+
processIdentity.name,
|
|
11379
|
+
connection,
|
|
11380
|
+
instructions,
|
|
11381
|
+
tools
|
|
11382
|
+
);
|
|
11383
|
+
return { ok: true, registered: tools.length };
|
|
11384
|
+
});
|
|
11385
|
+
connection.onClose(() => {
|
|
11386
|
+
mcpRegistry.clear(processIdentity.name);
|
|
11387
|
+
});
|
|
11388
|
+
}
|
|
10919
11389
|
if (processIdentity?.kind === "transformer") {
|
|
10920
11390
|
connection.onRequest("transformer/initialize", async (raw) => {
|
|
10921
11391
|
const params = raw ?? {};
|
|
@@ -10987,6 +11457,23 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
10987
11457
|
});
|
|
10988
11458
|
return { childSessionId: child.sessionId };
|
|
10989
11459
|
});
|
|
11460
|
+
connection.onRequest("hydra-acp/fork_session", async (raw) => {
|
|
11461
|
+
const params = raw ?? {};
|
|
11462
|
+
if (typeof params.sessionId !== "string") {
|
|
11463
|
+
throw Object.assign(
|
|
11464
|
+
new Error("fork_session requires sessionId"),
|
|
11465
|
+
{ code: JsonRpcErrorCodes.InvalidParams }
|
|
11466
|
+
);
|
|
11467
|
+
}
|
|
11468
|
+
const forkAt = typeof params.forkAt === "string" ? params.forkAt : void 0;
|
|
11469
|
+
const cwd = typeof params.cwd === "string" ? params.cwd : void 0;
|
|
11470
|
+
const agentId = typeof params.agentId === "string" ? params.agentId : void 0;
|
|
11471
|
+
return await deps.manager.forkSession(params.sessionId, {
|
|
11472
|
+
...forkAt !== void 0 ? { forkAt } : {},
|
|
11473
|
+
...cwd !== void 0 ? { cwd } : {},
|
|
11474
|
+
...agentId !== void 0 ? { agentId } : {}
|
|
11475
|
+
});
|
|
11476
|
+
});
|
|
10990
11477
|
connection.onRequest("hydra-acp/await_child", async (raw) => {
|
|
10991
11478
|
const params = raw ?? {};
|
|
10992
11479
|
const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
|
|
@@ -11002,13 +11489,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11002
11489
|
{ code: JsonRpcErrorCodes.SessionNotFound }
|
|
11003
11490
|
);
|
|
11004
11491
|
}
|
|
11005
|
-
return new Promise((
|
|
11492
|
+
return new Promise((resolve4) => {
|
|
11006
11493
|
const entries = [];
|
|
11007
11494
|
let unsubscribe;
|
|
11008
11495
|
const finish = () => {
|
|
11009
11496
|
clearTimeout(timer);
|
|
11010
11497
|
unsubscribe?.();
|
|
11011
|
-
|
|
11498
|
+
resolve4({ entries });
|
|
11012
11499
|
};
|
|
11013
11500
|
unsubscribe = child.onBroadcast((entry) => {
|
|
11014
11501
|
entries.push(entry);
|
|
@@ -11060,12 +11547,12 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11060
11547
|
let stdinToken;
|
|
11061
11548
|
let stdinReservation;
|
|
11062
11549
|
let augmentedMcpServers = params.mcpServers;
|
|
11063
|
-
if (hydraMeta.mcpStdin === true && deps.
|
|
11064
|
-
stdinToken =
|
|
11065
|
-
stdinReservation = deps.
|
|
11066
|
-
const url = `${deps.getDaemonOrigin()}/mcp/stdin`;
|
|
11550
|
+
if (hydraMeta.mcpStdin === true && deps.mcpTokenRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
|
|
11551
|
+
stdinToken = randomBytes4(32).toString("hex");
|
|
11552
|
+
stdinReservation = deps.mcpTokenRegistry.reserve(stdinToken);
|
|
11553
|
+
const url = `${deps.getDaemonOrigin()}/mcp/hydra-acp-stdin`;
|
|
11067
11554
|
const descriptor = {
|
|
11068
|
-
name: "
|
|
11555
|
+
name: "hydra-acp-stdin",
|
|
11069
11556
|
type: "http",
|
|
11070
11557
|
url,
|
|
11071
11558
|
headers: [
|
|
@@ -11074,6 +11561,28 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11074
11561
|
};
|
|
11075
11562
|
augmentedMcpServers = [...params.mcpServers ?? [], descriptor];
|
|
11076
11563
|
}
|
|
11564
|
+
let extMcpToken;
|
|
11565
|
+
let extMcpReservation;
|
|
11566
|
+
if (deps.extensionMcp !== void 0 && deps.mcpTokenRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
|
|
11567
|
+
const extNames = deps.extensionMcp.list();
|
|
11568
|
+
if (extNames.length > 0) {
|
|
11569
|
+
extMcpToken = randomBytes4(32).toString("hex");
|
|
11570
|
+
extMcpReservation = deps.mcpTokenRegistry.reserve(extMcpToken);
|
|
11571
|
+
const origin = deps.getDaemonOrigin();
|
|
11572
|
+
const descriptors = extNames.map((name) => ({
|
|
11573
|
+
name,
|
|
11574
|
+
type: "http",
|
|
11575
|
+
url: `${origin}/mcp/${name}`,
|
|
11576
|
+
headers: [
|
|
11577
|
+
{ name: "Authorization", value: `Bearer ${extMcpToken}` }
|
|
11578
|
+
]
|
|
11579
|
+
}));
|
|
11580
|
+
augmentedMcpServers = [
|
|
11581
|
+
...augmentedMcpServers ?? [],
|
|
11582
|
+
...descriptors
|
|
11583
|
+
];
|
|
11584
|
+
}
|
|
11585
|
+
}
|
|
11077
11586
|
let session;
|
|
11078
11587
|
try {
|
|
11079
11588
|
session = await deps.manager.create({
|
|
@@ -11091,16 +11600,27 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11091
11600
|
if (stdinReservation !== void 0) {
|
|
11092
11601
|
stdinReservation.abandon(err instanceof Error ? err : void 0);
|
|
11093
11602
|
}
|
|
11603
|
+
if (extMcpReservation !== void 0) {
|
|
11604
|
+
extMcpReservation.abandon(err instanceof Error ? err : void 0);
|
|
11605
|
+
}
|
|
11094
11606
|
throw err;
|
|
11095
11607
|
}
|
|
11096
|
-
if (stdinToken !== void 0 && stdinReservation !== void 0 && deps.
|
|
11608
|
+
if (stdinToken !== void 0 && stdinReservation !== void 0 && deps.mcpTokenRegistry !== void 0) {
|
|
11097
11609
|
const token2 = stdinToken;
|
|
11098
|
-
const registry = deps.
|
|
11610
|
+
const registry = deps.mcpTokenRegistry;
|
|
11099
11611
|
stdinReservation.complete(session);
|
|
11100
11612
|
session.onClose(() => {
|
|
11101
11613
|
void registry.unbind(token2);
|
|
11102
11614
|
});
|
|
11103
11615
|
}
|
|
11616
|
+
if (extMcpToken !== void 0 && extMcpReservation !== void 0 && deps.mcpTokenRegistry !== void 0) {
|
|
11617
|
+
const token2 = extMcpToken;
|
|
11618
|
+
const registry = deps.mcpTokenRegistry;
|
|
11619
|
+
extMcpReservation.complete(session);
|
|
11620
|
+
session.onClose(() => {
|
|
11621
|
+
void registry.unbind(token2);
|
|
11622
|
+
});
|
|
11623
|
+
}
|
|
11104
11624
|
const client = bindClientToSession(connection, session, state);
|
|
11105
11625
|
const { entries: replay } = await session.attach(client, "full");
|
|
11106
11626
|
state.attached.set(session.sessionId, {
|
|
@@ -11403,7 +11923,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11403
11923
|
openOpts.fileCapBytes = params.fileCapBytes;
|
|
11404
11924
|
}
|
|
11405
11925
|
if ((params.mode ?? "memory") === "file") {
|
|
11406
|
-
openOpts.filePathFor = (sid) =>
|
|
11926
|
+
openOpts.filePathFor = (sid) => path13.join(os4.tmpdir(), `hydra-acp-stdin-${sid}.log`);
|
|
11407
11927
|
}
|
|
11408
11928
|
return session.openStream(openOpts);
|
|
11409
11929
|
});
|
|
@@ -11821,26 +12341,25 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
|
|
|
11821
12341
|
};
|
|
11822
12342
|
}
|
|
11823
12343
|
|
|
11824
|
-
// src/daemon/mcp/
|
|
11825
|
-
var
|
|
12344
|
+
// src/daemon/mcp/token-registry.ts
|
|
12345
|
+
var McpTokenRegistry = class {
|
|
11826
12346
|
byToken = /* @__PURE__ */ new Map();
|
|
11827
|
-
// Reserve a token slot before the session exists. Used by acp-ws when
|
|
11828
|
-
// we need to inject the bearer into the agent's mcpServers BEFORE
|
|
11829
|
-
// manager.create() returns — claude-acp connects to /mcp/stdin during
|
|
11830
|
-
// session/new initialization (eagerly), so the route handler must be
|
|
11831
|
-
// able to find the token by the time the agent's first request lands.
|
|
11832
12347
|
reserve(token) {
|
|
11833
12348
|
if (this.byToken.has(token)) {
|
|
11834
|
-
throw new Error(
|
|
12349
|
+
throw new Error("mcp token already bound");
|
|
11835
12350
|
}
|
|
11836
12351
|
let resolveSession;
|
|
11837
12352
|
let rejectSession;
|
|
11838
|
-
const sessionReady = new Promise((
|
|
11839
|
-
resolveSession =
|
|
12353
|
+
const sessionReady = new Promise((resolve4, reject) => {
|
|
12354
|
+
resolveSession = resolve4;
|
|
11840
12355
|
rejectSession = reject;
|
|
11841
12356
|
});
|
|
11842
12357
|
sessionReady.catch(() => void 0);
|
|
11843
|
-
const entry = {
|
|
12358
|
+
const entry = {
|
|
12359
|
+
session: void 0,
|
|
12360
|
+
sessionReady,
|
|
12361
|
+
disposers: []
|
|
12362
|
+
};
|
|
11844
12363
|
this.byToken.set(token, entry);
|
|
11845
12364
|
return {
|
|
11846
12365
|
complete: (session) => {
|
|
@@ -11849,7 +12368,7 @@ var StdinMcpRegistry = class {
|
|
|
11849
12368
|
},
|
|
11850
12369
|
abandon: (reason) => {
|
|
11851
12370
|
this.byToken.delete(token);
|
|
11852
|
-
rejectSession(reason ?? new Error("
|
|
12371
|
+
rejectSession(reason ?? new Error("mcp token reservation abandoned"));
|
|
11853
12372
|
}
|
|
11854
12373
|
};
|
|
11855
12374
|
}
|
|
@@ -11862,29 +12381,26 @@ var StdinMcpRegistry = class {
|
|
|
11862
12381
|
lookup(token) {
|
|
11863
12382
|
return this.byToken.get(token);
|
|
11864
12383
|
}
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
12384
|
+
// Register a cleanup callback for this token. No-op if the token is
|
|
12385
|
+
// not currently bound — late additions after unbind() would never fire
|
|
12386
|
+
// anyway, so dropping them silently is safer than throwing into an
|
|
12387
|
+
// unrelated cleanup path.
|
|
12388
|
+
addDisposer(token, dispose) {
|
|
12389
|
+
const entry = this.byToken.get(token);
|
|
12390
|
+
if (entry === void 0) {
|
|
11868
12391
|
return;
|
|
11869
12392
|
}
|
|
11870
|
-
|
|
11871
|
-
ep.transport = transport;
|
|
12393
|
+
entry.disposers.push(dispose);
|
|
11872
12394
|
}
|
|
11873
12395
|
async unbind(token) {
|
|
11874
|
-
const
|
|
11875
|
-
if (
|
|
12396
|
+
const entry = this.byToken.get(token);
|
|
12397
|
+
if (entry === void 0) {
|
|
11876
12398
|
return;
|
|
11877
12399
|
}
|
|
11878
12400
|
this.byToken.delete(token);
|
|
11879
|
-
|
|
12401
|
+
for (const dispose of entry.disposers) {
|
|
11880
12402
|
try {
|
|
11881
|
-
await
|
|
11882
|
-
} catch {
|
|
11883
|
-
}
|
|
11884
|
-
}
|
|
11885
|
-
if (ep.server) {
|
|
11886
|
-
try {
|
|
11887
|
-
await ep.server.close();
|
|
12403
|
+
await dispose();
|
|
11888
12404
|
} catch {
|
|
11889
12405
|
}
|
|
11890
12406
|
}
|
|
@@ -11899,6 +12415,8 @@ import { randomUUID } from "crypto";
|
|
|
11899
12415
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11900
12416
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11901
12417
|
import { z as z7 } from "zod";
|
|
12418
|
+
|
|
12419
|
+
// src/daemon/mcp/bearer.ts
|
|
11902
12420
|
var BEARER_PREFIX2 = "Bearer ";
|
|
11903
12421
|
function extractBearer(req) {
|
|
11904
12422
|
const header = req.headers.authorization;
|
|
@@ -11911,15 +12429,17 @@ function extractBearer(req) {
|
|
|
11911
12429
|
const token = header.slice(BEARER_PREFIX2.length).trim();
|
|
11912
12430
|
return token.length > 0 ? token : void 0;
|
|
11913
12431
|
}
|
|
12432
|
+
|
|
12433
|
+
// src/daemon/mcp/stdin-server.ts
|
|
11914
12434
|
function buildMcpServer(session) {
|
|
11915
12435
|
const server = new McpServer(
|
|
11916
|
-
{ name: "hydra-stdin", version: "1.0.0" },
|
|
12436
|
+
{ name: "hydra-acp-stdin", version: "1.0.0" },
|
|
11917
12437
|
{
|
|
11918
|
-
instructions: "Piped input from `hydra cat --stream` is exposed here as a byte stream. Use `
|
|
12438
|
+
instructions: "Piped input from `hydra cat --stream` is exposed here as a byte stream. Use `tail` for the latest N bytes (good for finding the end of a log), `head` for the first N bytes (good for headers/preamble), `read` for windowed reads against an absolute byte cursor, `wait_for_more` to block until new bytes arrive past a cursor, and `info` for the current cursors/capacity/closed status. Byte payloads come back base64-encoded."
|
|
11919
12439
|
}
|
|
11920
12440
|
);
|
|
11921
12441
|
server.registerTool(
|
|
11922
|
-
"
|
|
12442
|
+
"tail",
|
|
11923
12443
|
{
|
|
11924
12444
|
description: "Return the most recent `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means older bytes existed but have been evicted from the ring.",
|
|
11925
12445
|
inputSchema: {
|
|
@@ -11940,7 +12460,7 @@ function buildMcpServer(session) {
|
|
|
11940
12460
|
}
|
|
11941
12461
|
);
|
|
11942
12462
|
server.registerTool(
|
|
11943
|
-
"
|
|
12463
|
+
"head",
|
|
11944
12464
|
{
|
|
11945
12465
|
description: "Return the first `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means the head has already been evicted from the ring and the returned bytes start at the oldest still-resident cursor.",
|
|
11946
12466
|
inputSchema: {
|
|
@@ -11961,7 +12481,7 @@ function buildMcpServer(session) {
|
|
|
11961
12481
|
}
|
|
11962
12482
|
);
|
|
11963
12483
|
server.registerTool(
|
|
11964
|
-
"
|
|
12484
|
+
"read",
|
|
11965
12485
|
{
|
|
11966
12486
|
description: "Read up to `max_bytes` bytes starting at absolute byte `cursor`. Returns `{bytes, nextCursor, gap?, eof?}` \u2014 `gap` is the number of bytes silently skipped because the ring had evicted them; `eof:true` means the producer closed and there is nothing left to read.",
|
|
11967
12487
|
inputSchema: {
|
|
@@ -12014,9 +12534,9 @@ function buildMcpServer(session) {
|
|
|
12014
12534
|
}
|
|
12015
12535
|
);
|
|
12016
12536
|
server.registerTool(
|
|
12017
|
-
"
|
|
12537
|
+
"grep",
|
|
12018
12538
|
{
|
|
12019
|
-
description: "Scan piped stdin line-by-line and return lines matching `pattern`. Prefer this over `
|
|
12539
|
+
description: "Scan piped stdin line-by-line and return lines matching `pattern`. Prefer this over `read` when the question is 'find lines that mention X' \u2014 it filters server-side so you don't pull and decode 64 KiB base64 windows. Returns `{matches: [{cursor, line, before?, after?}], truncated, nextCursor, gap?, scannedBytes, eof?}`. Lines come back as decoded UTF-8 strings (not base64). When `truncated:true`, re-call with `cursor: nextCursor` to resume.",
|
|
12020
12540
|
inputSchema: {
|
|
12021
12541
|
pattern: z7.string().min(1).describe(
|
|
12022
12542
|
"Search pattern. Treated as a JavaScript regular expression by default (set `regex:false` for a literal substring match)."
|
|
@@ -12068,7 +12588,7 @@ function buildMcpServer(session) {
|
|
|
12068
12588
|
}
|
|
12069
12589
|
);
|
|
12070
12590
|
server.registerTool(
|
|
12071
|
-
"
|
|
12591
|
+
"info",
|
|
12072
12592
|
{
|
|
12073
12593
|
description: "Report cursor / capacity / closed state of the stdin ring. Cheap; safe to call repeatedly.",
|
|
12074
12594
|
inputSchema: {}
|
|
@@ -12088,66 +12608,337 @@ function buildMcpServer(session) {
|
|
|
12088
12608
|
);
|
|
12089
12609
|
return server;
|
|
12090
12610
|
}
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
12611
|
+
var SESSION_READY_TIMEOUT_MS = 1e4;
|
|
12612
|
+
function registerStdinMcpRoutes(app, tokenRegistry) {
|
|
12613
|
+
const builtPerToken = /* @__PURE__ */ new Map();
|
|
12614
|
+
async function ensureTransport(token, session) {
|
|
12615
|
+
const existing = builtPerToken.get(token);
|
|
12616
|
+
if (existing !== void 0) {
|
|
12617
|
+
return existing.transport;
|
|
12618
|
+
}
|
|
12619
|
+
const server = buildMcpServer(session);
|
|
12620
|
+
const transport = new StreamableHTTPServerTransport({
|
|
12621
|
+
sessionIdGenerator: () => randomUUID()
|
|
12622
|
+
});
|
|
12623
|
+
await server.connect(transport);
|
|
12624
|
+
const pair = { server, transport };
|
|
12625
|
+
builtPerToken.set(token, pair);
|
|
12626
|
+
tokenRegistry.addDisposer(token, async () => {
|
|
12627
|
+
builtPerToken.delete(token);
|
|
12628
|
+
try {
|
|
12629
|
+
await transport.close();
|
|
12630
|
+
} catch {
|
|
12631
|
+
}
|
|
12632
|
+
try {
|
|
12633
|
+
await server.close();
|
|
12634
|
+
} catch {
|
|
12635
|
+
}
|
|
12636
|
+
});
|
|
12637
|
+
return transport;
|
|
12638
|
+
}
|
|
12639
|
+
async function handle(req, reply) {
|
|
12640
|
+
const token = extractBearer(req);
|
|
12641
|
+
if (token === void 0) {
|
|
12642
|
+
reply.code(401).send({ error: "missing bearer token" });
|
|
12643
|
+
return;
|
|
12644
|
+
}
|
|
12645
|
+
const entry = tokenRegistry.lookup(token);
|
|
12646
|
+
if (entry === void 0) {
|
|
12647
|
+
reply.code(404).send({ error: "unknown stdin token" });
|
|
12648
|
+
return;
|
|
12649
|
+
}
|
|
12650
|
+
let session;
|
|
12651
|
+
if (entry.session !== void 0) {
|
|
12652
|
+
session = entry.session;
|
|
12653
|
+
} else {
|
|
12654
|
+
let timer;
|
|
12655
|
+
const timeout = new Promise((resolve4) => {
|
|
12656
|
+
timer = setTimeout(() => resolve4(void 0), SESSION_READY_TIMEOUT_MS);
|
|
12657
|
+
});
|
|
12658
|
+
const resolved = await Promise.race([
|
|
12659
|
+
entry.sessionReady.catch(() => void 0),
|
|
12660
|
+
timeout
|
|
12661
|
+
]);
|
|
12662
|
+
if (timer !== void 0) {
|
|
12663
|
+
clearTimeout(timer);
|
|
12664
|
+
}
|
|
12665
|
+
if (resolved === void 0) {
|
|
12666
|
+
reply.code(503).send({ error: "session not ready" });
|
|
12667
|
+
return;
|
|
12668
|
+
}
|
|
12669
|
+
session = resolved;
|
|
12670
|
+
}
|
|
12671
|
+
const transport = await ensureTransport(token, session);
|
|
12672
|
+
reply.hijack();
|
|
12673
|
+
await transport.handleRequest(req.raw, reply.raw, req.body);
|
|
12095
12674
|
}
|
|
12096
|
-
const
|
|
12097
|
-
|
|
12098
|
-
|
|
12675
|
+
const opts = { config: { skipAuth: true } };
|
|
12676
|
+
app.post("/mcp/hydra-acp-stdin", opts, async (req, reply) => {
|
|
12677
|
+
await handle(req, reply);
|
|
12678
|
+
});
|
|
12679
|
+
app.get("/mcp/hydra-acp-stdin", opts, async (req, reply) => {
|
|
12680
|
+
await handle(req, reply);
|
|
12681
|
+
});
|
|
12682
|
+
app.delete("/mcp/hydra-acp-stdin", opts, async (req, reply) => {
|
|
12683
|
+
await handle(req, reply);
|
|
12099
12684
|
});
|
|
12100
|
-
await server.connect(transport);
|
|
12101
|
-
registry.attachTransport(token, server, transport);
|
|
12102
|
-
return transport;
|
|
12103
12685
|
}
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12686
|
+
|
|
12687
|
+
// src/core/extension-mcp.ts
|
|
12688
|
+
var ExtensionMcpRegistry = class {
|
|
12689
|
+
byName = /* @__PURE__ */ new Map();
|
|
12690
|
+
changeHandlers = [];
|
|
12691
|
+
// Set-the-whole-spec semantics, same as ExtensionCommandRegistry. A
|
|
12692
|
+
// second register for the same extName overwrites tools + instructions
|
|
12693
|
+
// wholesale; the change notification lets the route evict any cached
|
|
12694
|
+
// transports built against the old spec.
|
|
12695
|
+
register(extName, connection, instructions, tools) {
|
|
12696
|
+
this.byName.set(extName, {
|
|
12697
|
+
connection,
|
|
12698
|
+
instructions,
|
|
12699
|
+
tools: [...tools]
|
|
12700
|
+
});
|
|
12701
|
+
this.fireChanged(extName, "register");
|
|
12110
12702
|
}
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12703
|
+
clear(extName) {
|
|
12704
|
+
if (this.byName.delete(extName)) {
|
|
12705
|
+
this.fireChanged(extName, "clear");
|
|
12706
|
+
}
|
|
12115
12707
|
}
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12708
|
+
lookup(extName) {
|
|
12709
|
+
return this.byName.get(extName);
|
|
12710
|
+
}
|
|
12711
|
+
// List of currently-registered extension names. Used by session-create
|
|
12712
|
+
// to decide whether to mint an extension-MCP token and which mcpServers
|
|
12713
|
+
// entries to emit.
|
|
12714
|
+
list() {
|
|
12715
|
+
return Array.from(this.byName.keys());
|
|
12716
|
+
}
|
|
12717
|
+
onChange(handler) {
|
|
12718
|
+
this.changeHandlers.push(handler);
|
|
12719
|
+
return () => {
|
|
12720
|
+
const i = this.changeHandlers.indexOf(handler);
|
|
12721
|
+
if (i >= 0) {
|
|
12722
|
+
this.changeHandlers.splice(i, 1);
|
|
12723
|
+
}
|
|
12724
|
+
};
|
|
12725
|
+
}
|
|
12726
|
+
fireChanged(extName, kind) {
|
|
12727
|
+
for (const h of this.changeHandlers) {
|
|
12728
|
+
try {
|
|
12729
|
+
h(extName, kind);
|
|
12730
|
+
} catch {
|
|
12731
|
+
}
|
|
12732
|
+
}
|
|
12733
|
+
}
|
|
12734
|
+
};
|
|
12735
|
+
|
|
12736
|
+
// src/daemon/mcp/extension-route.ts
|
|
12737
|
+
import { StreamableHTTPServerTransport as StreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
12738
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
12739
|
+
|
|
12740
|
+
// src/daemon/mcp/build-extension-server.ts
|
|
12741
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12742
|
+
import {
|
|
12743
|
+
CallToolRequestSchema,
|
|
12744
|
+
ListToolsRequestSchema
|
|
12745
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12746
|
+
var DEFAULT_INVOKE_TIMEOUT_MS = 6e4;
|
|
12747
|
+
function buildExtensionServer(extensionName, entry, options = {}) {
|
|
12748
|
+
const invokeTimeoutMs = options.invokeTimeoutMs ?? DEFAULT_INVOKE_TIMEOUT_MS;
|
|
12749
|
+
const server = new Server(
|
|
12750
|
+
{ name: extensionName, version: "1.0.0" },
|
|
12751
|
+
{
|
|
12752
|
+
capabilities: {
|
|
12753
|
+
// listChanged: false matches the v1 strategy — the daemon closes
|
|
12754
|
+
// transports on re-register; agents reconnect and re-list against
|
|
12755
|
+
// the new spec naturally. Flipping to true is the upgrade path
|
|
12756
|
+
// if any supported agent caches tools/list across reconnects.
|
|
12757
|
+
tools: { listChanged: false }
|
|
12758
|
+
},
|
|
12759
|
+
...entry.instructions !== void 0 ? { instructions: entry.instructions } : {}
|
|
12760
|
+
}
|
|
12761
|
+
);
|
|
12762
|
+
const toolsByName = new Map(
|
|
12763
|
+
entry.tools.map((t) => [t.name, t])
|
|
12764
|
+
);
|
|
12765
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
12766
|
+
tools: entry.tools.map((t) => ({
|
|
12767
|
+
name: t.name,
|
|
12768
|
+
description: t.description,
|
|
12769
|
+
inputSchema: t.inputSchema,
|
|
12770
|
+
...t.outputSchema !== void 0 ? { outputSchema: t.outputSchema } : {}
|
|
12771
|
+
}))
|
|
12772
|
+
}));
|
|
12773
|
+
server.setRequestHandler(
|
|
12774
|
+
CallToolRequestSchema,
|
|
12775
|
+
async (req) => {
|
|
12776
|
+
const toolName = req.params.name;
|
|
12777
|
+
if (!toolsByName.has(toolName)) {
|
|
12778
|
+
return errorResult(`unknown tool: ${toolName}`);
|
|
12779
|
+
}
|
|
12780
|
+
try {
|
|
12781
|
+
const raw = await invokeWithTimeout(
|
|
12782
|
+
entry.connection,
|
|
12783
|
+
extensionName,
|
|
12784
|
+
toolName,
|
|
12785
|
+
req.params.arguments ?? {},
|
|
12786
|
+
invokeTimeoutMs
|
|
12787
|
+
);
|
|
12788
|
+
return normalizeToolResult(raw, toolName);
|
|
12789
|
+
} catch (err) {
|
|
12790
|
+
return errorResult(
|
|
12791
|
+
err instanceof Error ? err.message : String(err)
|
|
12792
|
+
);
|
|
12793
|
+
}
|
|
12794
|
+
}
|
|
12795
|
+
);
|
|
12796
|
+
return server;
|
|
12797
|
+
}
|
|
12798
|
+
async function invokeWithTimeout(connection, server, tool, args, timeoutMs) {
|
|
12799
|
+
let timer;
|
|
12800
|
+
const timeout = new Promise((_, reject) => {
|
|
12801
|
+
timer = setTimeout(
|
|
12802
|
+
() => reject(new Error(`extension timeout after ${timeoutMs}ms`)),
|
|
12803
|
+
timeoutMs
|
|
12804
|
+
);
|
|
12805
|
+
});
|
|
12806
|
+
try {
|
|
12807
|
+
return await Promise.race([
|
|
12808
|
+
connection.request("hydra-acp/invoke_mcp_tool", {
|
|
12809
|
+
server,
|
|
12810
|
+
tool,
|
|
12811
|
+
args
|
|
12812
|
+
}),
|
|
12126
12813
|
timeout
|
|
12127
12814
|
]);
|
|
12815
|
+
} finally {
|
|
12128
12816
|
if (timer !== void 0) {
|
|
12129
12817
|
clearTimeout(timer);
|
|
12130
12818
|
}
|
|
12131
|
-
|
|
12132
|
-
|
|
12819
|
+
}
|
|
12820
|
+
}
|
|
12821
|
+
function normalizeToolResult(raw, toolName) {
|
|
12822
|
+
if (raw === null || typeof raw !== "object") {
|
|
12823
|
+
return errorResult(`extension ${toolName} returned non-object`);
|
|
12824
|
+
}
|
|
12825
|
+
const obj = raw;
|
|
12826
|
+
if (!Array.isArray(obj.content)) {
|
|
12827
|
+
return errorResult(`extension ${toolName} omitted content array`);
|
|
12828
|
+
}
|
|
12829
|
+
return obj;
|
|
12830
|
+
}
|
|
12831
|
+
function errorResult(message) {
|
|
12832
|
+
return {
|
|
12833
|
+
content: [{ type: "text", text: message }],
|
|
12834
|
+
isError: true
|
|
12835
|
+
};
|
|
12836
|
+
}
|
|
12837
|
+
|
|
12838
|
+
// src/daemon/mcp/extension-route.ts
|
|
12839
|
+
var SESSION_READY_TIMEOUT_MS2 = 1e4;
|
|
12840
|
+
function registerExtensionMcpRoutes(app, tokenRegistry, extensionMcp, options = {}) {
|
|
12841
|
+
const built = /* @__PURE__ */ new Map();
|
|
12842
|
+
async function disposeBuiltPair(pair) {
|
|
12843
|
+
try {
|
|
12844
|
+
await pair.transport.close();
|
|
12845
|
+
} catch {
|
|
12846
|
+
}
|
|
12847
|
+
try {
|
|
12848
|
+
await pair.server.close();
|
|
12849
|
+
} catch {
|
|
12850
|
+
}
|
|
12851
|
+
}
|
|
12852
|
+
function evictExtension(extName) {
|
|
12853
|
+
for (const tokenScope of built.values()) {
|
|
12854
|
+
const pair = tokenScope.get(extName);
|
|
12855
|
+
if (pair !== void 0) {
|
|
12856
|
+
tokenScope.delete(extName);
|
|
12857
|
+
void disposeBuiltPair(pair);
|
|
12858
|
+
}
|
|
12859
|
+
}
|
|
12860
|
+
}
|
|
12861
|
+
extensionMcp.onChange((extName) => {
|
|
12862
|
+
evictExtension(extName);
|
|
12863
|
+
});
|
|
12864
|
+
async function ensureTransport(token, extName) {
|
|
12865
|
+
let tokenScope = built.get(token);
|
|
12866
|
+
if (tokenScope === void 0) {
|
|
12867
|
+
tokenScope = /* @__PURE__ */ new Map();
|
|
12868
|
+
built.set(token, tokenScope);
|
|
12869
|
+
tokenRegistry.addDisposer(token, async () => {
|
|
12870
|
+
const scope = built.get(token);
|
|
12871
|
+
if (scope === void 0) {
|
|
12872
|
+
return;
|
|
12873
|
+
}
|
|
12874
|
+
built.delete(token);
|
|
12875
|
+
for (const pair of scope.values()) {
|
|
12876
|
+
await disposeBuiltPair(pair);
|
|
12877
|
+
}
|
|
12878
|
+
});
|
|
12879
|
+
}
|
|
12880
|
+
const existing = tokenScope.get(extName);
|
|
12881
|
+
if (existing !== void 0) {
|
|
12882
|
+
return existing.transport;
|
|
12883
|
+
}
|
|
12884
|
+
const entry = extensionMcp.lookup(extName);
|
|
12885
|
+
if (entry === void 0) {
|
|
12886
|
+
return void 0;
|
|
12887
|
+
}
|
|
12888
|
+
const server = buildExtensionServer(extName, entry, options.buildOptions);
|
|
12889
|
+
const transport = new StreamableHTTPServerTransport2({
|
|
12890
|
+
sessionIdGenerator: () => randomUUID2()
|
|
12891
|
+
});
|
|
12892
|
+
await server.connect(transport);
|
|
12893
|
+
tokenScope.set(extName, { server, transport });
|
|
12894
|
+
return transport;
|
|
12895
|
+
}
|
|
12896
|
+
async function handle(req, reply) {
|
|
12897
|
+
const token = extractBearer(req);
|
|
12898
|
+
if (token === void 0) {
|
|
12899
|
+
reply.code(401).send({ error: "missing bearer token" });
|
|
12900
|
+
return;
|
|
12901
|
+
}
|
|
12902
|
+
const entry = tokenRegistry.lookup(token);
|
|
12903
|
+
if (entry === void 0) {
|
|
12904
|
+
reply.code(404).send({ error: "unknown mcp token" });
|
|
12905
|
+
return;
|
|
12906
|
+
}
|
|
12907
|
+
if (entry.session === void 0) {
|
|
12908
|
+
let timer;
|
|
12909
|
+
const timeout = new Promise((resolve4) => {
|
|
12910
|
+
timer = setTimeout(() => resolve4(void 0), SESSION_READY_TIMEOUT_MS2);
|
|
12911
|
+
});
|
|
12912
|
+
const resolved = await Promise.race([
|
|
12913
|
+
entry.sessionReady.catch(() => void 0),
|
|
12914
|
+
timeout
|
|
12915
|
+
]);
|
|
12916
|
+
if (timer !== void 0) {
|
|
12917
|
+
clearTimeout(timer);
|
|
12918
|
+
}
|
|
12919
|
+
if (resolved === void 0) {
|
|
12920
|
+
reply.code(503).send({ error: "session not ready" });
|
|
12921
|
+
return;
|
|
12922
|
+
}
|
|
12923
|
+
}
|
|
12924
|
+
const extName = req.params.name;
|
|
12925
|
+
const transport = await ensureTransport(token, extName);
|
|
12926
|
+
if (transport === void 0) {
|
|
12927
|
+
reply.code(404).send({ error: `unknown mcp server: ${extName}` });
|
|
12133
12928
|
return;
|
|
12134
12929
|
}
|
|
12135
|
-
|
|
12930
|
+
reply.hijack();
|
|
12931
|
+
await transport.handleRequest(req.raw, reply.raw, req.body);
|
|
12136
12932
|
}
|
|
12137
|
-
const transport = await ensureTransport(token, session, registry);
|
|
12138
|
-
reply.hijack();
|
|
12139
|
-
await transport.handleRequest(req.raw, reply.raw, req.body);
|
|
12140
|
-
}
|
|
12141
|
-
function registerStdinMcpRoutes(app, registry) {
|
|
12142
12933
|
const opts = { config: { skipAuth: true } };
|
|
12143
|
-
app.post("/mcp
|
|
12144
|
-
await handle(req, reply
|
|
12934
|
+
app.post("/mcp/:name", opts, async (req, reply) => {
|
|
12935
|
+
await handle(req, reply);
|
|
12145
12936
|
});
|
|
12146
|
-
app.get("/mcp
|
|
12147
|
-
await handle(req, reply
|
|
12937
|
+
app.get("/mcp/:name", opts, async (req, reply) => {
|
|
12938
|
+
await handle(req, reply);
|
|
12148
12939
|
});
|
|
12149
|
-
app.delete("/mcp
|
|
12150
|
-
await handle(req, reply
|
|
12940
|
+
app.delete("/mcp/:name", opts, async (req, reply) => {
|
|
12941
|
+
await handle(req, reply);
|
|
12151
12942
|
});
|
|
12152
12943
|
}
|
|
12153
12944
|
|
|
@@ -12256,8 +13047,10 @@ async function startDaemon(config, serviceToken) {
|
|
|
12256
13047
|
store: sessionTokenStore,
|
|
12257
13048
|
rateLimiter: authRateLimiter
|
|
12258
13049
|
});
|
|
12259
|
-
const
|
|
12260
|
-
|
|
13050
|
+
const mcpTokenRegistry = new McpTokenRegistry();
|
|
13051
|
+
const extensionMcp = new ExtensionMcpRegistry();
|
|
13052
|
+
registerStdinMcpRoutes(app, mcpTokenRegistry);
|
|
13053
|
+
registerExtensionMcpRoutes(app, mcpTokenRegistry, extensionMcp);
|
|
12261
13054
|
let daemonOriginCached;
|
|
12262
13055
|
const getDaemonOrigin = () => {
|
|
12263
13056
|
if (daemonOriginCached !== void 0) {
|
|
@@ -12278,7 +13071,8 @@ async function startDaemon(config, serviceToken) {
|
|
|
12278
13071
|
onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
|
|
12279
13072
|
transformers,
|
|
12280
13073
|
extensionCommands,
|
|
12281
|
-
|
|
13074
|
+
mcpTokenRegistry,
|
|
13075
|
+
extensionMcp,
|
|
12282
13076
|
getDaemonOrigin
|
|
12283
13077
|
});
|
|
12284
13078
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
@@ -12314,13 +13108,24 @@ async function startDaemon(config, serviceToken) {
|
|
|
12314
13108
|
`queue replay scan failed: ${err.message}`
|
|
12315
13109
|
);
|
|
12316
13110
|
});
|
|
13111
|
+
const intervalMs = config.daemon.agentSyncIntervalMinutes * 60 * 1e3;
|
|
13112
|
+
const stopAgentSync = intervalMs > 0 ? startAgentSyncScheduler({
|
|
13113
|
+
registry,
|
|
13114
|
+
manager,
|
|
13115
|
+
intervalMs,
|
|
13116
|
+
logger: agentLogger
|
|
13117
|
+
}) : void 0;
|
|
12317
13118
|
const shutdown = async () => {
|
|
13119
|
+
if (stopAgentSync) {
|
|
13120
|
+
stopAgentSync();
|
|
13121
|
+
}
|
|
12318
13122
|
clearInterval(sweepInterval);
|
|
12319
13123
|
await sessionTokenStore.flush();
|
|
12320
13124
|
await extensions.stop();
|
|
12321
13125
|
await transformers.stop();
|
|
12322
13126
|
await manager.closeAll();
|
|
12323
13127
|
await manager.flushMetaWrites();
|
|
13128
|
+
await manager.flushHistoryWrites();
|
|
12324
13129
|
setBinaryInstallLogger(null);
|
|
12325
13130
|
setNpmInstallLogger(null);
|
|
12326
13131
|
setAgentPruneLogger(null);
|
|
@@ -12334,7 +13139,17 @@ async function startDaemon(config, serviceToken) {
|
|
|
12334
13139
|
} catch {
|
|
12335
13140
|
}
|
|
12336
13141
|
};
|
|
12337
|
-
return {
|
|
13142
|
+
return {
|
|
13143
|
+
app,
|
|
13144
|
+
manager,
|
|
13145
|
+
registry,
|
|
13146
|
+
extensions,
|
|
13147
|
+
transformers,
|
|
13148
|
+
mcpTokenRegistry,
|
|
13149
|
+
extensionMcp,
|
|
13150
|
+
processRegistry,
|
|
13151
|
+
shutdown
|
|
13152
|
+
};
|
|
12338
13153
|
}
|
|
12339
13154
|
async function buildLogStream(level) {
|
|
12340
13155
|
const fileStream = await createPinoRoll({
|