@hydra-acp/cli 0.1.60 → 0.1.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -107
- package/dist/cli.js +2324 -764
- package/dist/index.d.ts +231 -22
- package/dist/index.js +1372 -541
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/daemon/server.ts
|
|
2
|
-
import * as
|
|
2
|
+
import * as fs17 from "fs";
|
|
3
3
|
import * as fsp7 from "fs/promises";
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import websocketPlugin from "@fastify/websocket";
|
|
@@ -88,6 +88,23 @@ var paths = {
|
|
|
88
88
|
// the agent invocation (see Session.drainQueue) so a crash mid-
|
|
89
89
|
// generation doesn't double-run on restart.
|
|
90
90
|
queueFile: (id) => path.join(hydraHome(), "sessions", id, "queue.ndjson"),
|
|
91
|
+
// Tombstones for sessions that were deleted locally but might still
|
|
92
|
+
// be reported by an agent's session/list at the next periodic sync.
|
|
93
|
+
// One file per (agentId, upstreamSessionId); existence is the source
|
|
94
|
+
// of truth, contents are a small JSON blob for diagnostics and the
|
|
95
|
+
// "agent advanced past our snapshot → resurrect" decision. Hidden
|
|
96
|
+
// under sessions/ because SessionStore.read() filters non-conforming
|
|
97
|
+
// dir names (the leading dot fails SESSION_ID_PATTERN) so the
|
|
98
|
+
// directory cohabits safely with real session directories.
|
|
99
|
+
tombstonesDir: () => path.join(hydraHome(), "sessions", ".tombstones"),
|
|
100
|
+
tombstoneAgentDir: (agentId) => path.join(hydraHome(), "sessions", ".tombstones", encodeURIComponent(agentId)),
|
|
101
|
+
tombstoneFile: (agentId, upstreamSessionId) => path.join(
|
|
102
|
+
hydraHome(),
|
|
103
|
+
"sessions",
|
|
104
|
+
".tombstones",
|
|
105
|
+
encodeURIComponent(agentId),
|
|
106
|
+
encodeURIComponent(upstreamSessionId)
|
|
107
|
+
),
|
|
91
108
|
extensionsDir: () => path.join(hydraHome(), "extensions"),
|
|
92
109
|
extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
|
|
93
110
|
extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
|
|
@@ -267,7 +284,27 @@ var DaemonConfig = z.object({
|
|
|
267
284
|
});
|
|
268
285
|
var RegistryConfig = z.object({
|
|
269
286
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
270
|
-
ttlHours: z.number().positive().default(24)
|
|
287
|
+
ttlHours: z.number().positive().default(24),
|
|
288
|
+
// When true, the daemon never re-fetches the registry over the network:
|
|
289
|
+
// it serves whatever is in the on-disk cache (~/.hydra-acp/registry.json)
|
|
290
|
+
// indefinitely, ignoring ttlHours. An escape hatch for when a bad registry
|
|
291
|
+
// push breaks an agent — pin to the last-known-good cache until upstream
|
|
292
|
+
// is fixed. `hydra registry refresh` still forces a one-off fetch.
|
|
293
|
+
pinned: z.boolean().default(false)
|
|
294
|
+
});
|
|
295
|
+
var LocalAgentConfig = z.object({
|
|
296
|
+
name: z.string().optional(),
|
|
297
|
+
description: z.string().optional(),
|
|
298
|
+
// Optional: defaults to the agent id (the config.agents key), mirroring
|
|
299
|
+
// how extensions default their command to the extension name. Set it
|
|
300
|
+
// when the executable differs from the id, or to point at an absolute
|
|
301
|
+
// path / wrapper script.
|
|
302
|
+
command: z.string().optional(),
|
|
303
|
+
args: z.array(z.string()).optional(),
|
|
304
|
+
env: z.record(z.string()).optional()
|
|
305
|
+
});
|
|
306
|
+
var AgentOverrideConfig = z.object({
|
|
307
|
+
packageSpec: z.string().optional()
|
|
271
308
|
});
|
|
272
309
|
var TuiConfig = z.object({
|
|
273
310
|
// Minimum interval (ms) between full-screen repaints driven by content
|
|
@@ -385,7 +422,18 @@ var TransformerBody = z.object({
|
|
|
385
422
|
});
|
|
386
423
|
var HydraConfig = z.object({
|
|
387
424
|
daemon: DaemonConfig.default({}),
|
|
388
|
-
registry: RegistryConfig.default({
|
|
425
|
+
registry: RegistryConfig.default({
|
|
426
|
+
url: REGISTRY_URL_DEFAULT,
|
|
427
|
+
ttlHours: 24,
|
|
428
|
+
pinned: false
|
|
429
|
+
}),
|
|
430
|
+
// User-defined agents that bypass the network registry. Keyed by agent
|
|
431
|
+
// id; each is spawned via its `command`/`args` directly. Shadow registry
|
|
432
|
+
// agents of the same id.
|
|
433
|
+
agents: z.record(z.string(), LocalAgentConfig).default({}),
|
|
434
|
+
// Per-agent pin overrides applied to registry agents (e.g. force a
|
|
435
|
+
// specific npm version of opencode). Keyed by agent id.
|
|
436
|
+
agentOverrides: z.record(z.string(), AgentOverrideConfig).default({}),
|
|
389
437
|
defaultAgent: z.string().default("opencode"),
|
|
390
438
|
// Optional per-agent default model id. When a brand-new agent process
|
|
391
439
|
// is spawned (session/new path), hydra issues session/set_model with
|
|
@@ -1062,10 +1110,16 @@ var UvxDistribution = z2.object({
|
|
|
1062
1110
|
args: z2.array(z2.string()).optional(),
|
|
1063
1111
|
env: z2.record(z2.string()).optional()
|
|
1064
1112
|
});
|
|
1113
|
+
var ExecDistribution = z2.object({
|
|
1114
|
+
command: z2.string(),
|
|
1115
|
+
args: z2.array(z2.string()).optional(),
|
|
1116
|
+
env: z2.record(z2.string()).optional()
|
|
1117
|
+
});
|
|
1065
1118
|
var Distribution = z2.object({
|
|
1066
1119
|
npx: NpxDistribution.optional(),
|
|
1067
1120
|
binary: BinaryDistribution.optional(),
|
|
1068
|
-
uvx: UvxDistribution.optional()
|
|
1121
|
+
uvx: UvxDistribution.optional(),
|
|
1122
|
+
exec: ExecDistribution.optional()
|
|
1069
1123
|
});
|
|
1070
1124
|
var RegistryAgent = z2.object({
|
|
1071
1125
|
id: z2.string(),
|
|
@@ -1093,11 +1147,11 @@ var Registry = class {
|
|
|
1093
1147
|
options;
|
|
1094
1148
|
cache;
|
|
1095
1149
|
async load() {
|
|
1096
|
-
if (this.cache && this.isFresh(this.cache.fetchedAt)) {
|
|
1150
|
+
if (this.cache && (this.isPinned() || this.isFresh(this.cache.fetchedAt))) {
|
|
1097
1151
|
return this.cache.data;
|
|
1098
1152
|
}
|
|
1099
1153
|
const onDisk = await this.readDiskCache();
|
|
1100
|
-
if (onDisk && this.isFresh(onDisk.fetchedAt)) {
|
|
1154
|
+
if (onDisk && (this.isPinned() || this.isFresh(onDisk.fetchedAt))) {
|
|
1101
1155
|
this.cache = onDisk;
|
|
1102
1156
|
return onDisk.data;
|
|
1103
1157
|
}
|
|
@@ -1128,12 +1182,57 @@ var Registry = class {
|
|
|
1128
1182
|
return this.cache?.fetchedAt;
|
|
1129
1183
|
}
|
|
1130
1184
|
async getAgent(id) {
|
|
1185
|
+
const local = this.localAgents().find((a) => a.id === id);
|
|
1186
|
+
if (local) {
|
|
1187
|
+
return local;
|
|
1188
|
+
}
|
|
1131
1189
|
const doc = await this.load();
|
|
1132
1190
|
const exact = doc.agents.find((a) => a.id === id);
|
|
1133
1191
|
if (exact) {
|
|
1134
|
-
return exact;
|
|
1192
|
+
return this.applyOverride(exact);
|
|
1135
1193
|
}
|
|
1136
|
-
|
|
1194
|
+
const byBasename = doc.agents.find((a) => npxPackageBasename(a) === id);
|
|
1195
|
+
return byBasename ? this.applyOverride(byBasename) : void 0;
|
|
1196
|
+
}
|
|
1197
|
+
// Synthesize RegistryAgent entries from config.agents. These carry an
|
|
1198
|
+
// `exec` distribution and a fixed "local" version key (no install dir).
|
|
1199
|
+
localAgents() {
|
|
1200
|
+
return Object.entries(this.config.agents ?? {}).map(([id, def]) => ({
|
|
1201
|
+
id,
|
|
1202
|
+
name: def.name ?? id,
|
|
1203
|
+
description: def.description,
|
|
1204
|
+
version: "local",
|
|
1205
|
+
distribution: {
|
|
1206
|
+
exec: {
|
|
1207
|
+
// Default the command to the agent id (like extensions default
|
|
1208
|
+
// theirs to the extension name) — resolved off PATH at spawn.
|
|
1209
|
+
command: def.command ?? id,
|
|
1210
|
+
args: def.args,
|
|
1211
|
+
env: def.env
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}));
|
|
1215
|
+
}
|
|
1216
|
+
// Apply a config.agentOverrides[id] pin to a registry agent: swap the
|
|
1217
|
+
// npx package spec and key the install dir on the pinned version so it
|
|
1218
|
+
// never collides with the floating "current" install. No-op when the
|
|
1219
|
+
// agent has no override or isn't npx-distributed.
|
|
1220
|
+
applyOverride(agent) {
|
|
1221
|
+
const override = this.config.agentOverrides?.[agent.id];
|
|
1222
|
+
if (!override?.packageSpec || !agent.distribution.npx) {
|
|
1223
|
+
return agent;
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
...agent,
|
|
1227
|
+
version: versionKeyFromSpec(override.packageSpec),
|
|
1228
|
+
distribution: {
|
|
1229
|
+
...agent.distribution,
|
|
1230
|
+
npx: { ...agent.distribution.npx, package: override.packageSpec }
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
isPinned() {
|
|
1235
|
+
return this.config.registry?.pinned === true;
|
|
1137
1236
|
}
|
|
1138
1237
|
isFresh(fetchedAt) {
|
|
1139
1238
|
const ageMs = Date.now() - fetchedAt;
|
|
@@ -1175,6 +1274,12 @@ var Registry = class {
|
|
|
1175
1274
|
});
|
|
1176
1275
|
}
|
|
1177
1276
|
};
|
|
1277
|
+
function versionKeyFromSpec(spec) {
|
|
1278
|
+
const lastAt = spec.lastIndexOf("@");
|
|
1279
|
+
const version = lastAt > 0 ? spec.slice(lastAt + 1) : "";
|
|
1280
|
+
const sanitized = version.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1281
|
+
return sanitized.length > 0 ? `pin-${sanitized}` : "pinned";
|
|
1282
|
+
}
|
|
1178
1283
|
function npxPackageBasename(agent) {
|
|
1179
1284
|
const pkg = agent.distribution.npx?.package;
|
|
1180
1285
|
if (!pkg) {
|
|
@@ -1185,12 +1290,45 @@ function npxPackageBasename(agent) {
|
|
|
1185
1290
|
const atIdx = afterSlash.lastIndexOf("@");
|
|
1186
1291
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
1187
1292
|
}
|
|
1293
|
+
async function listAgents(registry) {
|
|
1294
|
+
const local = typeof registry.localAgents === "function" ? registry.localAgents() : [];
|
|
1295
|
+
let doc;
|
|
1296
|
+
try {
|
|
1297
|
+
doc = await registry.load();
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
if (local.length === 0) {
|
|
1300
|
+
throw err;
|
|
1301
|
+
}
|
|
1302
|
+
doc = { version: "local-only", agents: [] };
|
|
1303
|
+
}
|
|
1304
|
+
const localIds = new Set(local.map((a) => a.id));
|
|
1305
|
+
const merged = [...local, ...doc.agents.filter((a) => !localIds.has(a.id))];
|
|
1306
|
+
const agents = await Promise.all(
|
|
1307
|
+
merged.map(async (a) => ({
|
|
1308
|
+
id: a.id,
|
|
1309
|
+
name: a.name,
|
|
1310
|
+
version: a.version,
|
|
1311
|
+
description: a.description,
|
|
1312
|
+
distributions: Object.keys(a.distribution),
|
|
1313
|
+
installed: await agentInstallState(a),
|
|
1314
|
+
source: localIds.has(a.id) ? "local" : "registry"
|
|
1315
|
+
}))
|
|
1316
|
+
);
|
|
1317
|
+
return {
|
|
1318
|
+
version: doc.version,
|
|
1319
|
+
fetchedAt: registry.lastFetchedAt(),
|
|
1320
|
+
agents
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1188
1323
|
async function agentInstallState(agent) {
|
|
1189
1324
|
const platformKey = currentPlatformKey();
|
|
1190
1325
|
if (!platformKey) {
|
|
1191
1326
|
return "no";
|
|
1192
1327
|
}
|
|
1193
1328
|
const version = agent.version ?? "current";
|
|
1329
|
+
if (agent.distribution.exec) {
|
|
1330
|
+
return "yes";
|
|
1331
|
+
}
|
|
1194
1332
|
if (agent.distribution.binary) {
|
|
1195
1333
|
const target = pickBinaryTarget(agent.distribution.binary, platformKey);
|
|
1196
1334
|
if (target?.cmd) {
|
|
@@ -1287,6 +1425,16 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
1287
1425
|
version
|
|
1288
1426
|
};
|
|
1289
1427
|
}
|
|
1428
|
+
if (agent.distribution.exec) {
|
|
1429
|
+
const exec = agent.distribution.exec;
|
|
1430
|
+
const tail = callerArgs.length > 0 ? callerArgs : exec.args ?? [];
|
|
1431
|
+
return {
|
|
1432
|
+
command: exec.command,
|
|
1433
|
+
args: tail,
|
|
1434
|
+
env: exec.env ?? {},
|
|
1435
|
+
version
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1290
1438
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
1291
1439
|
}
|
|
1292
1440
|
|
|
@@ -1344,8 +1492,8 @@ var HistoryPolicy = z3.enum([
|
|
|
1344
1492
|
]);
|
|
1345
1493
|
var SessionNewParams = z3.object({
|
|
1346
1494
|
cwd: z3.string(),
|
|
1347
|
-
|
|
1348
|
-
|
|
1495
|
+
mcpServers: z3.array(z3.unknown()).optional(),
|
|
1496
|
+
_meta: z3.record(z3.unknown()).optional()
|
|
1349
1497
|
});
|
|
1350
1498
|
var SessionResumeHints = z3.object({
|
|
1351
1499
|
upstreamSessionId: z3.string(),
|
|
@@ -1373,23 +1521,10 @@ var SessionAttachParams = z3.object({
|
|
|
1373
1521
|
name: z3.string(),
|
|
1374
1522
|
version: z3.string().optional()
|
|
1375
1523
|
}).optional(),
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
// session/
|
|
1379
|
-
//
|
|
1380
|
-
// streams history from disk. Used by the TUI's view-only mode.
|
|
1381
|
-
readonly: z3.boolean().optional(),
|
|
1382
|
-
// Debug-only replay pacing. When "drip", the daemon skips chunk
|
|
1383
|
-
// coalescing and re-emits each recorded session/update individually,
|
|
1384
|
-
// spacing them by their original recordedAt deltas (scaled by
|
|
1385
|
-
// dripSpeed, with a per-gap cap) so a session's streaming render can
|
|
1386
|
-
// be reproduced at its real granularity for flicker investigation.
|
|
1387
|
-
// Omitted/"instant" preserves the normal coalesced, as-fast-as-possible
|
|
1388
|
-
// replay.
|
|
1389
|
-
replayMode: z3.enum(["instant", "drip"]).optional(),
|
|
1390
|
-
// Multiplier applied to original inter-entry gaps in drip mode. >1
|
|
1391
|
-
// compresses time (faster), <1 stretches it. Defaults to 1.
|
|
1392
|
-
dripSpeed: z3.number().positive().optional(),
|
|
1524
|
+
// Hydra-specific attach options (readonly, replayMode, dripSpeed) are
|
|
1525
|
+
// NOT top-level — they ride under `_meta["hydra-acp"]` (read via
|
|
1526
|
+
// extractHydraMeta) so session/attach carries only RFD #533's own
|
|
1527
|
+
// fields at the top level.
|
|
1393
1528
|
_meta: z3.record(z3.unknown()).optional()
|
|
1394
1529
|
});
|
|
1395
1530
|
var HYDRA_META_KEY = "hydra-acp";
|
|
@@ -1412,8 +1547,23 @@ function extractHydraMeta(meta) {
|
|
|
1412
1547
|
if (typeof obj.cwd === "string") {
|
|
1413
1548
|
out.cwd = obj.cwd;
|
|
1414
1549
|
}
|
|
1415
|
-
if (typeof obj.
|
|
1416
|
-
out.
|
|
1550
|
+
if (typeof obj.clientId === "string") {
|
|
1551
|
+
out.clientId = obj.clientId;
|
|
1552
|
+
}
|
|
1553
|
+
if (typeof obj.readonly === "boolean") {
|
|
1554
|
+
out.readonly = obj.readonly;
|
|
1555
|
+
}
|
|
1556
|
+
if (obj.replayMode === "instant" || obj.replayMode === "drip") {
|
|
1557
|
+
out.replayMode = obj.replayMode;
|
|
1558
|
+
}
|
|
1559
|
+
if (typeof obj.dripSpeed === "number" && obj.dripSpeed > 0) {
|
|
1560
|
+
out.dripSpeed = obj.dripSpeed;
|
|
1561
|
+
}
|
|
1562
|
+
if (obj.detachStatus === "detached") {
|
|
1563
|
+
out.detachStatus = obj.detachStatus;
|
|
1564
|
+
}
|
|
1565
|
+
if (typeof obj.title === "string") {
|
|
1566
|
+
out.title = obj.title;
|
|
1417
1567
|
}
|
|
1418
1568
|
if (Array.isArray(obj.agentArgs) && obj.agentArgs.every((a) => typeof a === "string")) {
|
|
1419
1569
|
out.agentArgs = obj.agentArgs;
|
|
@@ -1465,14 +1615,22 @@ function extractHydraMeta(meta) {
|
|
|
1465
1615
|
out.availableCommands = cmds;
|
|
1466
1616
|
}
|
|
1467
1617
|
}
|
|
1468
|
-
if (typeof obj.
|
|
1469
|
-
|
|
1618
|
+
if (obj.prompt && typeof obj.prompt === "object" && !Array.isArray(obj.prompt)) {
|
|
1619
|
+
const p = obj.prompt;
|
|
1620
|
+
const caps = {};
|
|
1621
|
+
if (typeof p.queueing === "boolean") caps.queueing = p.queueing;
|
|
1622
|
+
if (typeof p.cancelling === "boolean") caps.cancelling = p.cancelling;
|
|
1623
|
+
if (typeof p.updating === "boolean") caps.updating = p.updating;
|
|
1624
|
+
if (typeof p.amending === "boolean") caps.amending = p.amending;
|
|
1625
|
+
if (typeof p.pipelining === "boolean") caps.pipelining = p.pipelining;
|
|
1626
|
+
out.prompt = caps;
|
|
1470
1627
|
}
|
|
1471
|
-
if (typeof obj.
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1628
|
+
if (obj.agents && typeof obj.agents === "object" && !Array.isArray(obj.agents)) {
|
|
1629
|
+
const a = obj.agents;
|
|
1630
|
+
const caps = {};
|
|
1631
|
+
if (typeof a.list === "boolean") caps.list = a.list;
|
|
1632
|
+
if (typeof a.installProgress === "boolean") caps.installProgress = a.installProgress;
|
|
1633
|
+
out.agents = caps;
|
|
1476
1634
|
}
|
|
1477
1635
|
if (typeof obj.mcpStdin === "boolean") {
|
|
1478
1636
|
out.mcpStdin = obj.mcpStdin;
|
|
@@ -1483,12 +1641,6 @@ function extractHydraMeta(meta) {
|
|
|
1483
1641
|
if (typeof obj.ancillary === "boolean") {
|
|
1484
1642
|
out.ancillary = obj.ancillary;
|
|
1485
1643
|
}
|
|
1486
|
-
if (typeof obj.promptAmending === "boolean") {
|
|
1487
|
-
out.promptAmending = obj.promptAmending;
|
|
1488
|
-
}
|
|
1489
|
-
if (typeof obj.promptPipelining === "boolean") {
|
|
1490
|
-
out.promptPipelining = obj.promptPipelining;
|
|
1491
|
-
}
|
|
1492
1644
|
if (Array.isArray(obj.queue)) {
|
|
1493
1645
|
const entries = [];
|
|
1494
1646
|
for (const raw of obj.queue) {
|
|
@@ -1561,6 +1713,43 @@ function extractHydraMeta(meta) {
|
|
|
1561
1713
|
out.availableModels = models;
|
|
1562
1714
|
}
|
|
1563
1715
|
}
|
|
1716
|
+
if (obj.status === "live" || obj.status === "cold") {
|
|
1717
|
+
out.status = obj.status;
|
|
1718
|
+
}
|
|
1719
|
+
if (typeof obj.busy === "boolean") {
|
|
1720
|
+
out.busy = obj.busy;
|
|
1721
|
+
}
|
|
1722
|
+
if (typeof obj.awaitingInput === "boolean") {
|
|
1723
|
+
out.awaitingInput = obj.awaitingInput;
|
|
1724
|
+
}
|
|
1725
|
+
if (typeof obj.attachedClients === "number") {
|
|
1726
|
+
out.attachedClients = obj.attachedClients;
|
|
1727
|
+
}
|
|
1728
|
+
if (typeof obj.importedFromMachine === "string") {
|
|
1729
|
+
out.importedFromMachine = obj.importedFromMachine;
|
|
1730
|
+
}
|
|
1731
|
+
if (typeof obj.importedFromUpstreamSessionId === "string") {
|
|
1732
|
+
out.importedFromUpstreamSessionId = obj.importedFromUpstreamSessionId;
|
|
1733
|
+
}
|
|
1734
|
+
if (typeof obj.parentSessionId === "string") {
|
|
1735
|
+
out.parentSessionId = obj.parentSessionId;
|
|
1736
|
+
}
|
|
1737
|
+
if (typeof obj.forkedFromSessionId === "string") {
|
|
1738
|
+
out.forkedFromSessionId = obj.forkedFromSessionId;
|
|
1739
|
+
}
|
|
1740
|
+
if (typeof obj.forkedFromMessageId === "string") {
|
|
1741
|
+
out.forkedFromMessageId = obj.forkedFromMessageId;
|
|
1742
|
+
}
|
|
1743
|
+
if (obj.originatingClient && typeof obj.originatingClient === "object" && !Array.isArray(obj.originatingClient) && typeof obj.originatingClient.name === "string") {
|
|
1744
|
+
const oc = obj.originatingClient;
|
|
1745
|
+
out.originatingClient = {
|
|
1746
|
+
name: oc.name,
|
|
1747
|
+
...typeof oc.version === "string" ? { version: oc.version } : {}
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
if (obj.agentCapabilities !== void 0) {
|
|
1751
|
+
out.agentCapabilities = obj.agentCapabilities;
|
|
1752
|
+
}
|
|
1564
1753
|
return out;
|
|
1565
1754
|
}
|
|
1566
1755
|
function mergeMeta(passthrough, ours) {
|
|
@@ -1598,7 +1787,7 @@ var SessionListEntry = z3.object({
|
|
|
1598
1787
|
importedFromUpstreamSessionId: z3.string().optional(),
|
|
1599
1788
|
// Set when this session was spawned as a child by a transformer.
|
|
1600
1789
|
parentSessionId: z3.string().optional(),
|
|
1601
|
-
// Local-fork breadcrumbs set by hydra-acp/
|
|
1790
|
+
// Local-fork breadcrumbs set by hydra-acp/session/fork. Distinct from
|
|
1602
1791
|
// the imported* family above: a fork is a local branch off another
|
|
1603
1792
|
// local session, an import is a cross-machine takeover.
|
|
1604
1793
|
forkedFromSessionId: z3.string().optional(),
|
|
@@ -1636,36 +1825,89 @@ var SessionListResult = z3.object({
|
|
|
1636
1825
|
sessions: z3.array(SessionListEntryWire),
|
|
1637
1826
|
nextCursor: z3.string().optional()
|
|
1638
1827
|
});
|
|
1639
|
-
function
|
|
1640
|
-
const
|
|
1828
|
+
function buildHydraSessionMeta(entry, extras) {
|
|
1829
|
+
const meta = {
|
|
1641
1830
|
attachedClients: entry.attachedClients,
|
|
1642
1831
|
status: entry.status,
|
|
1643
1832
|
busy: entry.busy,
|
|
1644
1833
|
awaitingInput: entry.awaitingInput
|
|
1645
1834
|
};
|
|
1835
|
+
if (entry.cwd !== void 0) {
|
|
1836
|
+
meta.cwd = entry.cwd;
|
|
1837
|
+
}
|
|
1838
|
+
if (entry.title !== void 0) {
|
|
1839
|
+
meta.title = entry.title;
|
|
1840
|
+
}
|
|
1646
1841
|
if (entry.agentId !== void 0) {
|
|
1647
|
-
|
|
1842
|
+
meta.agentId = entry.agentId;
|
|
1648
1843
|
}
|
|
1649
1844
|
if (entry.upstreamSessionId !== void 0) {
|
|
1650
|
-
|
|
1845
|
+
meta.upstreamSessionId = entry.upstreamSessionId;
|
|
1651
1846
|
}
|
|
1652
1847
|
if (entry.currentModel !== void 0) {
|
|
1653
|
-
|
|
1848
|
+
meta.currentModel = entry.currentModel;
|
|
1654
1849
|
}
|
|
1655
1850
|
if (entry.currentUsage !== void 0) {
|
|
1656
|
-
|
|
1851
|
+
meta.currentUsage = entry.currentUsage;
|
|
1657
1852
|
}
|
|
1658
1853
|
if (entry.importedFromMachine !== void 0) {
|
|
1659
|
-
|
|
1854
|
+
meta.importedFromMachine = entry.importedFromMachine;
|
|
1660
1855
|
}
|
|
1661
1856
|
if (entry.importedFromUpstreamSessionId !== void 0) {
|
|
1662
|
-
|
|
1857
|
+
meta.importedFromUpstreamSessionId = entry.importedFromUpstreamSessionId;
|
|
1858
|
+
}
|
|
1859
|
+
if (entry.parentSessionId !== void 0) {
|
|
1860
|
+
meta.parentSessionId = entry.parentSessionId;
|
|
1861
|
+
}
|
|
1862
|
+
if (entry.forkedFromSessionId !== void 0) {
|
|
1863
|
+
meta.forkedFromSessionId = entry.forkedFromSessionId;
|
|
1864
|
+
}
|
|
1865
|
+
if (entry.forkedFromMessageId !== void 0) {
|
|
1866
|
+
meta.forkedFromMessageId = entry.forkedFromMessageId;
|
|
1867
|
+
}
|
|
1868
|
+
if (entry.originatingClient !== void 0) {
|
|
1869
|
+
meta.originatingClient = entry.originatingClient;
|
|
1870
|
+
}
|
|
1871
|
+
if (entry.interactive !== void 0) {
|
|
1872
|
+
meta.interactive = entry.interactive;
|
|
1873
|
+
}
|
|
1874
|
+
if (extras) {
|
|
1875
|
+
if (extras.clientId !== void 0) {
|
|
1876
|
+
meta.clientId = extras.clientId;
|
|
1877
|
+
}
|
|
1878
|
+
if (extras.currentMode !== void 0) {
|
|
1879
|
+
meta.currentMode = extras.currentMode;
|
|
1880
|
+
}
|
|
1881
|
+
if (extras.agentArgs !== void 0 && extras.agentArgs.length > 0) {
|
|
1882
|
+
meta.agentArgs = extras.agentArgs;
|
|
1883
|
+
}
|
|
1884
|
+
if (extras.availableCommands !== void 0 && extras.availableCommands.length > 0) {
|
|
1885
|
+
meta.availableCommands = extras.availableCommands;
|
|
1886
|
+
}
|
|
1887
|
+
if (extras.availableModes !== void 0 && extras.availableModes.length > 0) {
|
|
1888
|
+
meta.availableModes = extras.availableModes;
|
|
1889
|
+
}
|
|
1890
|
+
if (extras.availableModels !== void 0 && extras.availableModels.length > 0) {
|
|
1891
|
+
meta.availableModels = extras.availableModels;
|
|
1892
|
+
}
|
|
1893
|
+
if (extras.turnStartedAt !== void 0) {
|
|
1894
|
+
meta.turnStartedAt = extras.turnStartedAt;
|
|
1895
|
+
}
|
|
1896
|
+
if (extras.agentCapabilities !== void 0) {
|
|
1897
|
+
meta.agentCapabilities = extras.agentCapabilities;
|
|
1898
|
+
}
|
|
1899
|
+
if (extras.queue !== void 0 && extras.queue.length > 0) {
|
|
1900
|
+
meta.queue = extras.queue;
|
|
1901
|
+
}
|
|
1663
1902
|
}
|
|
1903
|
+
return meta;
|
|
1904
|
+
}
|
|
1905
|
+
function sessionListEntryToWire(entry) {
|
|
1664
1906
|
const wire = {
|
|
1665
1907
|
sessionId: entry.sessionId,
|
|
1666
1908
|
cwd: entry.cwd,
|
|
1667
1909
|
updatedAt: entry.updatedAt,
|
|
1668
|
-
_meta: mergeMeta(entry._meta,
|
|
1910
|
+
_meta: mergeMeta(entry._meta, buildHydraSessionMeta(entry))
|
|
1669
1911
|
};
|
|
1670
1912
|
if (entry.title !== void 0) {
|
|
1671
1913
|
wire.title = entry.title;
|
|
@@ -1753,65 +1995,6 @@ var PromptAmendedParams = z3.object({
|
|
|
1753
1995
|
originator: PromptOriginatorSchema,
|
|
1754
1996
|
amendedAt: z3.number()
|
|
1755
1997
|
});
|
|
1756
|
-
var StreamOpenParams = z3.object({
|
|
1757
|
-
sessionId: z3.string(),
|
|
1758
|
-
// 'memory' keeps the ring in RAM only — needed for the eventual MCP
|
|
1759
|
-
// tool surface. 'file' adds a temp file projection that the agent can
|
|
1760
|
-
// consume with shell tools (tail -f / head / grep) when MCP isn't
|
|
1761
|
-
// available. The temp file's path is returned in the response.
|
|
1762
|
-
mode: z3.enum(["memory", "file"]).optional(),
|
|
1763
|
-
// Ring capacity in bytes. Server clamps to a reasonable minimum and
|
|
1764
|
-
// its configured max; omitted falls back to the daemon default.
|
|
1765
|
-
capacityBytes: z3.number().int().positive().optional(),
|
|
1766
|
-
// File mode only. Soft cap in bytes; after this many bytes are
|
|
1767
|
-
// written to the file, further appends still land in the ring but
|
|
1768
|
-
// stop being mirrored to disk. The daemon emits one stream_truncated
|
|
1769
|
-
// session/update notification when the cap is first hit.
|
|
1770
|
-
fileCapBytes: z3.number().int().positive().optional()
|
|
1771
|
-
});
|
|
1772
|
-
var StreamOpenResult = z3.object({
|
|
1773
|
-
// Only present when mode === "file".
|
|
1774
|
-
filePath: z3.string().optional(),
|
|
1775
|
-
capacityBytes: z3.number().int().positive(),
|
|
1776
|
-
fileCapBytes: z3.number().int().positive().optional()
|
|
1777
|
-
});
|
|
1778
|
-
var StreamWriteParams = z3.object({
|
|
1779
|
-
sessionId: z3.string(),
|
|
1780
|
-
// Base64-encoded bytes. UTF-8 stdin gets re-encoded on the wire; the
|
|
1781
|
-
// ring is byte-exact so binary streams (audio, framed protocols) work
|
|
1782
|
-
// identically.
|
|
1783
|
-
chunk: z3.string(),
|
|
1784
|
-
// True on the final write. Pending long-poll reads / waits return with
|
|
1785
|
-
// eof:true once this is observed.
|
|
1786
|
-
eof: z3.boolean().optional()
|
|
1787
|
-
});
|
|
1788
|
-
var StreamWriteResult = z3.object({
|
|
1789
|
-
// Absolute writeCursor after this append landed.
|
|
1790
|
-
writeCursor: z3.number().int().nonnegative()
|
|
1791
|
-
});
|
|
1792
|
-
var StreamReadParams = z3.object({
|
|
1793
|
-
sessionId: z3.string(),
|
|
1794
|
-
cursor: z3.number().int().nonnegative(),
|
|
1795
|
-
// Cap on bytes returned. Server enforces a hard ceiling (STREAM_READ_MAX_BYTES,
|
|
1796
|
-
// currently 64 KiB) even when the caller asks for more.
|
|
1797
|
-
maxBytes: z3.number().int().positive().optional(),
|
|
1798
|
-
// Long-poll timeout in ms. 0 / omitted returns immediately with
|
|
1799
|
-
// whatever's available (possibly empty). Server cap 60s.
|
|
1800
|
-
waitMs: z3.number().int().nonnegative().optional()
|
|
1801
|
-
});
|
|
1802
|
-
var StreamReadResult = z3.object({
|
|
1803
|
-
// Base64-encoded bytes. Empty string when nothing new is available
|
|
1804
|
-
// and either waitMs was 0 or the long-poll expired without data.
|
|
1805
|
-
bytes: z3.string(),
|
|
1806
|
-
nextCursor: z3.number().int().nonnegative(),
|
|
1807
|
-
// Set when `cursor` pointed before the oldest still-resident byte —
|
|
1808
|
-
// value is the count of bytes that were evicted between the caller's
|
|
1809
|
-
// cursor and what we still have.
|
|
1810
|
-
gap: z3.number().int().nonnegative().optional(),
|
|
1811
|
-
// True when the producer has closed AND there are no more bytes
|
|
1812
|
-
// after nextCursor.
|
|
1813
|
-
eof: z3.boolean().optional()
|
|
1814
|
-
});
|
|
1815
1998
|
var AgentInstallProgressParams = z3.object({
|
|
1816
1999
|
agentId: z3.string(),
|
|
1817
2000
|
version: z3.string(),
|
|
@@ -1828,7 +2011,7 @@ var AgentInstallProgressParams = z3.object({
|
|
|
1828
2011
|
totalBytes: z3.number().optional(),
|
|
1829
2012
|
packageSpec: z3.string().optional()
|
|
1830
2013
|
});
|
|
1831
|
-
var AGENT_INSTALL_PROGRESS_METHOD = "hydra-acp/
|
|
2014
|
+
var AGENT_INSTALL_PROGRESS_METHOD = "hydra-acp/agents/install_progress";
|
|
1832
2015
|
|
|
1833
2016
|
// src/acp/framing.ts
|
|
1834
2017
|
function ndjsonStreamFromStdio(stdout, stdin) {
|
|
@@ -1931,6 +2114,13 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1931
2114
|
pending = /* @__PURE__ */ new Map();
|
|
1932
2115
|
closed = false;
|
|
1933
2116
|
closeHandlers = [];
|
|
2117
|
+
// Observers for error frames that arrive with no matching pending
|
|
2118
|
+
// request — e.g. an agent replying with an error to a notification
|
|
2119
|
+
// (which carries no id, so it can't be correlated the normal way).
|
|
2120
|
+
// session/cancel is the canonical case: an agent that doesn't support
|
|
2121
|
+
// it (current opencode) emits a MethodNotFound/UnsupportedOperation
|
|
2122
|
+
// error frame we'd otherwise silently drop. See handleResponse.
|
|
2123
|
+
orphanErrorHandlers = [];
|
|
1934
2124
|
onRequest(method, handler) {
|
|
1935
2125
|
this.requestHandlers.set(method, handler);
|
|
1936
2126
|
}
|
|
@@ -1966,6 +2156,10 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1966
2156
|
onClose(handler) {
|
|
1967
2157
|
this.closeHandlers.push(handler);
|
|
1968
2158
|
}
|
|
2159
|
+
// Subscribe to error frames that can't be matched to a pending request.
|
|
2160
|
+
onOrphanError(handler) {
|
|
2161
|
+
this.orphanErrorHandlers.push(handler);
|
|
2162
|
+
}
|
|
1969
2163
|
async request(method, params) {
|
|
1970
2164
|
return this.requestWithId(method, params).response;
|
|
1971
2165
|
}
|
|
@@ -2022,6 +2216,8 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
2022
2216
|
}
|
|
2023
2217
|
} else if ("id" in message) {
|
|
2024
2218
|
this.handleResponse(message);
|
|
2219
|
+
} else if ("error" in message) {
|
|
2220
|
+
this.handleResponse(message);
|
|
2025
2221
|
}
|
|
2026
2222
|
}
|
|
2027
2223
|
async handleRequest(req) {
|
|
@@ -2069,6 +2265,18 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
2069
2265
|
handleResponse(res) {
|
|
2070
2266
|
const pending = this.pending.get(res.id);
|
|
2071
2267
|
if (!pending) {
|
|
2268
|
+
if (res.error) {
|
|
2269
|
+
for (const handler of this.orphanErrorHandlers) {
|
|
2270
|
+
try {
|
|
2271
|
+
handler({
|
|
2272
|
+
code: res.error.code,
|
|
2273
|
+
message: res.error.message,
|
|
2274
|
+
data: res.error.data
|
|
2275
|
+
});
|
|
2276
|
+
} catch {
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2072
2280
|
return;
|
|
2073
2281
|
}
|
|
2074
2282
|
this.pending.delete(res.id);
|
|
@@ -2254,7 +2462,7 @@ function openAgentLog(agentId) {
|
|
|
2254
2462
|
}
|
|
2255
2463
|
|
|
2256
2464
|
// src/core/session-manager.ts
|
|
2257
|
-
import * as
|
|
2465
|
+
import * as fs14 from "fs/promises";
|
|
2258
2466
|
import * as os2 from "os";
|
|
2259
2467
|
import * as path9 from "path";
|
|
2260
2468
|
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
@@ -2298,8 +2506,8 @@ var SessionStreamBuffer = class {
|
|
|
2298
2506
|
fileCapReached = false;
|
|
2299
2507
|
onFileCapReached;
|
|
2300
2508
|
logWriteError;
|
|
2301
|
-
// Single-flight chain for file appends so concurrent
|
|
2302
|
-
//
|
|
2509
|
+
// Single-flight chain for file appends so concurrent stdin writes
|
|
2510
|
+
// don't interleave their file writes.
|
|
2303
2511
|
fileWriteChain = Promise.resolve();
|
|
2304
2512
|
constructor(opts = {}) {
|
|
2305
2513
|
this.maxCapacityBytes = opts.capacityBytes ?? DEFAULT_CAPACITY_BYTES;
|
|
@@ -2920,7 +3128,7 @@ function stripHydraSessionPrefix(id) {
|
|
|
2920
3128
|
var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
2921
3129
|
var TRANSFORMER_CLAIM_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2922
3130
|
var RECENTLY_TERMINAL_LIMIT = 64;
|
|
2923
|
-
var Session = class {
|
|
3131
|
+
var Session = class _Session {
|
|
2924
3132
|
sessionId;
|
|
2925
3133
|
cwd;
|
|
2926
3134
|
// agent / agentId / upstreamSessionId are mutable so /hydra agent can
|
|
@@ -2994,6 +3202,16 @@ var Session = class {
|
|
|
2994
3202
|
// endpoint uses this to tail a live session's conversation stream
|
|
2995
3203
|
// without participating in turns or prompts.
|
|
2996
3204
|
broadcastHandlers = [];
|
|
3205
|
+
// Epoch ms of the most recent session/cancel we sent to the agent.
|
|
3206
|
+
// Used to attribute an id-less error frame from the agent to a cancel
|
|
3207
|
+
// (see wireAgent's orphan-error observer). Window kept short so an
|
|
3208
|
+
// unrelated later error isn't mislabeled.
|
|
3209
|
+
lastCancelAt = 0;
|
|
3210
|
+
static CANCEL_ERROR_WINDOW_MS = 2e3;
|
|
3211
|
+
// Set by forceCancel() so the in-flight turn's agent-kill rejection is
|
|
3212
|
+
// reported to the originator as a clean "cancelled" stopReason instead of
|
|
3213
|
+
// a raw "connection closed" error.
|
|
3214
|
+
forceCancelling = false;
|
|
2997
3215
|
// True once we've observed our first session/prompt; gates the
|
|
2998
3216
|
// first-prompt-seeded title so subsequent prompts don't churn it.
|
|
2999
3217
|
// Also read by SessionManager's onClose hook to decide whether to
|
|
@@ -3069,6 +3287,7 @@ var Session = class {
|
|
|
3069
3287
|
agentCommandsHandlers = [];
|
|
3070
3288
|
agentModesHandlers = [];
|
|
3071
3289
|
agentModelsHandlers = [];
|
|
3290
|
+
availableAgentsFn;
|
|
3072
3291
|
modelHandlers = [];
|
|
3073
3292
|
modeHandlers = [];
|
|
3074
3293
|
interactiveHandlers = [];
|
|
@@ -3141,6 +3360,7 @@ var Session = class {
|
|
|
3141
3360
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
3142
3361
|
this.idleEventTimeoutMs = init.idleEventTimeoutMs ?? 3e4;
|
|
3143
3362
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
3363
|
+
this.availableAgentsFn = init.availableAgents;
|
|
3144
3364
|
this.listSessions = init.listSessions;
|
|
3145
3365
|
this.logger = init.logger;
|
|
3146
3366
|
this.transformChain = init.transformChain ?? [];
|
|
@@ -3218,6 +3438,11 @@ var Session = class {
|
|
|
3218
3438
|
agent.connection.onRequest("session/request_permission", async (params) => {
|
|
3219
3439
|
return this.handlePermissionRequest(params);
|
|
3220
3440
|
});
|
|
3441
|
+
if (typeof agent.connection.onOrphanError === "function") {
|
|
3442
|
+
agent.connection.onOrphanError((error) => {
|
|
3443
|
+
this.handleOrphanError(error);
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3221
3446
|
agent.onExit(() => {
|
|
3222
3447
|
if (this.agent !== agent) {
|
|
3223
3448
|
return;
|
|
@@ -3225,6 +3450,31 @@ var Session = class {
|
|
|
3225
3450
|
this.markClosed({ deleteRecord: false });
|
|
3226
3451
|
});
|
|
3227
3452
|
}
|
|
3453
|
+
// An error frame arrived from the agent that couldn't be matched to a
|
|
3454
|
+
// pending request. The canonical case is a reply to our id-less
|
|
3455
|
+
// session/cancel notification: agents that don't support cancellation
|
|
3456
|
+
// (current opencode) answer with MethodNotFound (-32601) or an
|
|
3457
|
+
// UnsupportedOperation error. If one lands within the cancel window,
|
|
3458
|
+
// surface it to attached clients so the TUI can tell the user the
|
|
3459
|
+
// cancel didn't take rather than silently dropping it.
|
|
3460
|
+
handleOrphanError(error) {
|
|
3461
|
+
const sinceCancel = Date.now() - this.lastCancelAt;
|
|
3462
|
+
if (this.lastCancelAt === 0 || sinceCancel > _Session.CANCEL_ERROR_WINDOW_MS) {
|
|
3463
|
+
this.logger?.warn(
|
|
3464
|
+
`agent ${this.agentId} sent uncorrelated error frame code=${error.code} message=${error.message}`
|
|
3465
|
+
);
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
this.lastCancelAt = 0;
|
|
3469
|
+
this.logger?.warn(
|
|
3470
|
+
`agent ${this.agentId} rejected session/cancel code=${error.code} message=${error.message}`
|
|
3471
|
+
);
|
|
3472
|
+
this.broadcastQueueNotification("hydra-acp/cancel_failed", {
|
|
3473
|
+
sessionId: this.sessionId,
|
|
3474
|
+
code: error.code,
|
|
3475
|
+
message: error.message
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3228
3478
|
// Runs the response-side transformer chain, then the snapshot interceptors,
|
|
3229
3479
|
// then recordAndBroadcast. All state mutation happens after the chain exits.
|
|
3230
3480
|
// See forwardRequest for originatedBy / startIdx semantics.
|
|
@@ -3242,7 +3492,7 @@ var Session = class {
|
|
|
3242
3492
|
const token = `t_${generateChainToken()}`;
|
|
3243
3493
|
let result;
|
|
3244
3494
|
try {
|
|
3245
|
-
result = await t.connection.request("transformer/message", {
|
|
3495
|
+
result = await t.connection.request("hydra-acp/transformer/message", {
|
|
3246
3496
|
token,
|
|
3247
3497
|
phase: "response",
|
|
3248
3498
|
method: "session/update",
|
|
@@ -3268,7 +3518,7 @@ var Session = class {
|
|
|
3268
3518
|
const timer = setTimeout(() => {
|
|
3269
3519
|
if (this.pendingClaims.delete(token)) {
|
|
3270
3520
|
this.broadcastQueueNotification(
|
|
3271
|
-
"hydra-acp/
|
|
3521
|
+
"hydra-acp/transformer/abandoned_request",
|
|
3272
3522
|
{ sessionId: this.sessionId, token, transformerName: t.name }
|
|
3273
3523
|
);
|
|
3274
3524
|
void this.runResponseChain(
|
|
@@ -3315,7 +3565,10 @@ var Session = class {
|
|
|
3315
3565
|
return;
|
|
3316
3566
|
}
|
|
3317
3567
|
if (this.maybeApplyAgentConfigOption(envelope)) {
|
|
3318
|
-
this.recordAndBroadcast(
|
|
3568
|
+
this.recordAndBroadcast(
|
|
3569
|
+
"session/update",
|
|
3570
|
+
this.mergeAgentOptionIntoEnvelope(envelope)
|
|
3571
|
+
);
|
|
3319
3572
|
return;
|
|
3320
3573
|
}
|
|
3321
3574
|
if (this.maybeApplyAgentUsage(rawParams)) {
|
|
@@ -3650,7 +3903,7 @@ var Session = class {
|
|
|
3650
3903
|
// prompt_received a single, useful meaning ("the agent is now taking
|
|
3651
3904
|
// a turn on this prompt"), which is how attached clients (notably
|
|
3652
3905
|
// agent-shell) consume it. The accept-time signal that peers can use
|
|
3653
|
-
// for queue chip rendering is hydra-acp/
|
|
3906
|
+
// for queue chip rendering is hydra-acp/prompt_queue/added instead.
|
|
3654
3907
|
broadcastPromptReceived(entry) {
|
|
3655
3908
|
const sentBy = { clientId: entry.originator.clientId };
|
|
3656
3909
|
if (entry.originator.name) {
|
|
@@ -3741,7 +3994,7 @@ var Session = class {
|
|
|
3741
3994
|
this.recentlyTerminal.delete(oldest);
|
|
3742
3995
|
}
|
|
3743
3996
|
}
|
|
3744
|
-
// Fire hydra-acp/
|
|
3997
|
+
// Fire hydra-acp/prompt/amended for the M1→M2 linkage. The amendment's
|
|
3745
3998
|
// current content is read live from the queue entry so any update_prompt
|
|
3746
3999
|
// calls during the amend window are reflected. Best-effort: if M2 has
|
|
3747
4000
|
// already been cancelled out of the queue by the time we get here, we
|
|
@@ -3761,7 +4014,7 @@ var Session = class {
|
|
|
3761
4014
|
amendedAt: Date.now()
|
|
3762
4015
|
};
|
|
3763
4016
|
this.broadcastQueueNotification(
|
|
3764
|
-
"hydra-acp/
|
|
4017
|
+
"hydra-acp/prompt/amended",
|
|
3765
4018
|
params
|
|
3766
4019
|
);
|
|
3767
4020
|
}
|
|
@@ -3804,17 +4057,17 @@ var Session = class {
|
|
|
3804
4057
|
"hydra-acp": { amending: options.amending }
|
|
3805
4058
|
};
|
|
3806
4059
|
}
|
|
3807
|
-
this.broadcastQueueNotification("hydra-acp/
|
|
4060
|
+
this.broadcastQueueNotification("hydra-acp/prompt_queue/added", params);
|
|
3808
4061
|
}
|
|
3809
4062
|
broadcastQueueUpdated(messageId, prompt) {
|
|
3810
|
-
this.broadcastQueueNotification("hydra-acp/
|
|
4063
|
+
this.broadcastQueueNotification("hydra-acp/prompt_queue/updated", {
|
|
3811
4064
|
sessionId: this.sessionId,
|
|
3812
4065
|
messageId,
|
|
3813
4066
|
prompt
|
|
3814
4067
|
});
|
|
3815
4068
|
}
|
|
3816
4069
|
broadcastQueueRemoved(messageId, reason) {
|
|
3817
|
-
this.broadcastQueueNotification("hydra-acp/
|
|
4070
|
+
this.broadcastQueueNotification("hydra-acp/prompt_queue/removed", {
|
|
3818
4071
|
sessionId: this.sessionId,
|
|
3819
4072
|
messageId,
|
|
3820
4073
|
reason
|
|
@@ -4095,6 +4348,7 @@ var Session = class {
|
|
|
4095
4348
|
JsonRpcErrorCodes.SessionNotFound
|
|
4096
4349
|
);
|
|
4097
4350
|
}
|
|
4351
|
+
this.lastCancelAt = Date.now();
|
|
4098
4352
|
await this.agent.connection.notify("session/cancel", {
|
|
4099
4353
|
sessionId: this.upstreamSessionId
|
|
4100
4354
|
});
|
|
@@ -4110,7 +4364,7 @@ var Session = class {
|
|
|
4110
4364
|
this.transformChain.push(ref);
|
|
4111
4365
|
}
|
|
4112
4366
|
if (ref.intercepts.has("lifecycle:session.opened")) {
|
|
4113
|
-
void ref.connection.notify("transformer/session_event", {
|
|
4367
|
+
void ref.connection.notify("hydra-acp/transformer/session_event", {
|
|
4114
4368
|
event: "session.opened",
|
|
4115
4369
|
sessionId: this.sessionId
|
|
4116
4370
|
}).catch(() => void 0);
|
|
@@ -4134,7 +4388,7 @@ var Session = class {
|
|
|
4134
4388
|
const token = `t_${generateChainToken()}`;
|
|
4135
4389
|
let result;
|
|
4136
4390
|
try {
|
|
4137
|
-
result = await t.connection.request("transformer/message", {
|
|
4391
|
+
result = await t.connection.request("hydra-acp/transformer/message", {
|
|
4138
4392
|
token,
|
|
4139
4393
|
phase: "request",
|
|
4140
4394
|
method,
|
|
@@ -4160,7 +4414,7 @@ var Session = class {
|
|
|
4160
4414
|
const timer = setTimeout(() => {
|
|
4161
4415
|
if (this.pendingClaims.delete(token)) {
|
|
4162
4416
|
this.broadcastQueueNotification(
|
|
4163
|
-
"hydra-acp/
|
|
4417
|
+
"hydra-acp/transformer/abandoned_request",
|
|
4164
4418
|
{ sessionId: this.sessionId, token, transformerName: t.name }
|
|
4165
4419
|
);
|
|
4166
4420
|
void this.forwardRequest(
|
|
@@ -4202,7 +4456,7 @@ var Session = class {
|
|
|
4202
4456
|
claim.resolve(result);
|
|
4203
4457
|
return true;
|
|
4204
4458
|
}
|
|
4205
|
-
// Called by the WS handler on hydra-acp/keep_alive.
|
|
4459
|
+
// Called by the WS handler on hydra-acp/connection/keep_alive.
|
|
4206
4460
|
// Resets the abandonment timer for an outstanding processing claim.
|
|
4207
4461
|
keepAliveClaim(token, estimatedRemainingMs) {
|
|
4208
4462
|
const claim = this.pendingClaims.get(token);
|
|
@@ -4214,7 +4468,7 @@ var Session = class {
|
|
|
4214
4468
|
const timer = setTimeout(() => {
|
|
4215
4469
|
if (this.pendingClaims.delete(token)) {
|
|
4216
4470
|
this.broadcastQueueNotification(
|
|
4217
|
-
"hydra-acp/
|
|
4471
|
+
"hydra-acp/transformer/abandoned_request",
|
|
4218
4472
|
{ sessionId: this.sessionId, token, transformerName: claim.transformerName }
|
|
4219
4473
|
);
|
|
4220
4474
|
if (claim.side === "response") {
|
|
@@ -4379,18 +4633,20 @@ var Session = class {
|
|
|
4379
4633
|
} catch {
|
|
4380
4634
|
}
|
|
4381
4635
|
}
|
|
4636
|
+
this.broadcastConfigOptions();
|
|
4382
4637
|
return true;
|
|
4383
4638
|
}
|
|
4384
|
-
// Apply an
|
|
4385
|
-
// (not the spec-shaped current_model_update /
|
|
4386
|
-
// to carry
|
|
4387
|
-
// The payload is `configOptions: [{ id
|
|
4388
|
-
// [{ value, name }] }, ...]`. We harvest
|
|
4389
|
-
//
|
|
4390
|
-
//
|
|
4391
|
-
//
|
|
4392
|
-
//
|
|
4393
|
-
//
|
|
4639
|
+
// Apply an agent-emitted config_option_update. claude-acp and opencode
|
|
4640
|
+
// emit this (not the spec-shaped current_model_update /
|
|
4641
|
+
// available_modes_update) to carry the current value AND option list for
|
|
4642
|
+
// model and mode. The payload is `configOptions: [{ id, currentValue,
|
|
4643
|
+
// options: [{ value, name }] }, ...]`. We harvest the "model" and "mode"
|
|
4644
|
+
// entries — other ids (e.g. "effort") are agent-internal and ignored.
|
|
4645
|
+
// Harvesting the mode list here is what populates availableModes for
|
|
4646
|
+
// agents that never send available_modes_update (so the TUI's Shift+Tab
|
|
4647
|
+
// cycle has something to cycle through). Returns true when recognized so
|
|
4648
|
+
// the wireAgent loop stops trying further extractors (the original frame
|
|
4649
|
+
// still broadcasts; config-options-aware clients render it directly).
|
|
4394
4650
|
maybeApplyAgentConfigOption(params) {
|
|
4395
4651
|
const obj = params ?? {};
|
|
4396
4652
|
const update = obj.update ?? {};
|
|
@@ -4406,24 +4662,37 @@ var Session = class {
|
|
|
4406
4662
|
continue;
|
|
4407
4663
|
}
|
|
4408
4664
|
const opt = raw;
|
|
4409
|
-
if (opt.id
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4665
|
+
if (opt.id === "model") {
|
|
4666
|
+
const models = parseModelsList(opt.options);
|
|
4667
|
+
if (models.length > 0) {
|
|
4668
|
+
this.setAgentAdvertisedModels(models);
|
|
4669
|
+
}
|
|
4670
|
+
const cv = opt.currentValue;
|
|
4671
|
+
if (typeof cv === "string") {
|
|
4672
|
+
const trimmed = cv.trim();
|
|
4673
|
+
if (trimmed && trimmed !== this.currentModel) {
|
|
4674
|
+
this.logger?.info(
|
|
4675
|
+
`live config_option_update(model): sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4676
|
+
);
|
|
4677
|
+
this.applyModelChange(trimmed);
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
} else if (opt.id === "mode") {
|
|
4681
|
+
const modes = parseModesList(opt.options);
|
|
4682
|
+
if (modes.length > 0) {
|
|
4683
|
+
this.setAgentAdvertisedModes(modes);
|
|
4684
|
+
}
|
|
4685
|
+
const cv = opt.currentValue;
|
|
4686
|
+
if (typeof cv === "string") {
|
|
4687
|
+
const trimmed = cv.trim();
|
|
4688
|
+
if (trimmed && trimmed !== this.currentMode) {
|
|
4689
|
+
this.logger?.info(
|
|
4690
|
+
`live config_option_update(mode): sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4691
|
+
);
|
|
4692
|
+
this.applyModeChange(trimmed);
|
|
4693
|
+
}
|
|
4424
4694
|
}
|
|
4425
4695
|
}
|
|
4426
|
-
break;
|
|
4427
4696
|
}
|
|
4428
4697
|
return true;
|
|
4429
4698
|
}
|
|
@@ -4451,6 +4720,7 @@ var Session = class {
|
|
|
4451
4720
|
} catch {
|
|
4452
4721
|
}
|
|
4453
4722
|
}
|
|
4723
|
+
this.broadcastConfigOptions();
|
|
4454
4724
|
return true;
|
|
4455
4725
|
}
|
|
4456
4726
|
// usage_update carries any subset of {used, size, cost.amount,
|
|
@@ -4661,6 +4931,7 @@ var Session = class {
|
|
|
4661
4931
|
sessionId: this.upstreamSessionId,
|
|
4662
4932
|
update
|
|
4663
4933
|
});
|
|
4934
|
+
this.broadcastConfigOptions();
|
|
4664
4935
|
}
|
|
4665
4936
|
// Apply a mode change initiated by a client request (session/set_mode)
|
|
4666
4937
|
// when the agent doesn't emit a current_mode_update notification on its
|
|
@@ -4697,6 +4968,115 @@ var Session = class {
|
|
|
4697
4968
|
sessionId: this.upstreamSessionId,
|
|
4698
4969
|
update
|
|
4699
4970
|
});
|
|
4971
|
+
this.broadcastConfigOptions();
|
|
4972
|
+
}
|
|
4973
|
+
// Assemble the spec-shaped configOptions snapshot for this session.
|
|
4974
|
+
// Order reflects the agent's preferred prominence: model and mode (the
|
|
4975
|
+
// dimensions users toggle constantly) first, then the hydra-native
|
|
4976
|
+
// `agent` selector. model/mode are included only when hydra actually
|
|
4977
|
+
// knows that dimension for the connected agent; `agent` is always
|
|
4978
|
+
// present since hydra owns the swap concept regardless of the backend.
|
|
4979
|
+
buildConfigOptions() {
|
|
4980
|
+
const out = [];
|
|
4981
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
4982
|
+
const options = this.agentAdvertisedModels.map(
|
|
4983
|
+
(m) => ({
|
|
4984
|
+
value: m.modelId,
|
|
4985
|
+
name: m.name ?? m.modelId,
|
|
4986
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
4987
|
+
})
|
|
4988
|
+
);
|
|
4989
|
+
const currentValue = this.currentModel && options.some((o) => o.value === this.currentModel) ? this.currentModel : options[0].value;
|
|
4990
|
+
out.push({
|
|
4991
|
+
id: "model",
|
|
4992
|
+
name: "Model",
|
|
4993
|
+
category: "model",
|
|
4994
|
+
type: "select",
|
|
4995
|
+
currentValue,
|
|
4996
|
+
options
|
|
4997
|
+
});
|
|
4998
|
+
}
|
|
4999
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
5000
|
+
const options = this.agentAdvertisedModes.map(
|
|
5001
|
+
(m) => ({
|
|
5002
|
+
value: m.id,
|
|
5003
|
+
name: m.name ?? m.id,
|
|
5004
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
5005
|
+
})
|
|
5006
|
+
);
|
|
5007
|
+
const currentValue = this.currentMode && options.some((o) => o.value === this.currentMode) ? this.currentMode : options[0].value;
|
|
5008
|
+
out.push({
|
|
5009
|
+
id: "mode",
|
|
5010
|
+
name: "Session Mode",
|
|
5011
|
+
category: "mode",
|
|
5012
|
+
type: "select",
|
|
5013
|
+
currentValue,
|
|
5014
|
+
options
|
|
5015
|
+
});
|
|
5016
|
+
}
|
|
5017
|
+
const agents = this.availableAgentsFn?.() ?? [];
|
|
5018
|
+
const agentOptions = agents.map((a) => ({
|
|
5019
|
+
value: a.id,
|
|
5020
|
+
name: a.name ?? a.id,
|
|
5021
|
+
...a.description !== void 0 ? { description: a.description } : {}
|
|
5022
|
+
}));
|
|
5023
|
+
if (!agentOptions.some((o) => o.value === this.agentId)) {
|
|
5024
|
+
agentOptions.unshift({ value: this.agentId, name: this.agentId });
|
|
5025
|
+
}
|
|
5026
|
+
out.push({
|
|
5027
|
+
id: "agent",
|
|
5028
|
+
name: "Agent",
|
|
5029
|
+
category: "_hydra_agent",
|
|
5030
|
+
type: "select",
|
|
5031
|
+
currentValue: this.agentId,
|
|
5032
|
+
options: agentOptions
|
|
5033
|
+
});
|
|
5034
|
+
return out;
|
|
5035
|
+
}
|
|
5036
|
+
// Broadcast a config_option_update carrying the full snapshot. Fired
|
|
5037
|
+
// alongside the legacy current_mode_update / current_model_update and on
|
|
5038
|
+
// agent swaps so config-options-aware clients stay in sync via the
|
|
5039
|
+
// spec mechanism. config_option_update is a STATE_UPDATE_KIND, so this
|
|
5040
|
+
// broadcasts live but is not recorded to history.
|
|
5041
|
+
broadcastConfigOptions() {
|
|
5042
|
+
this.recordAndBroadcast("session/update", {
|
|
5043
|
+
sessionId: this.upstreamSessionId,
|
|
5044
|
+
update: {
|
|
5045
|
+
sessionUpdate: "config_option_update",
|
|
5046
|
+
configOptions: this.buildConfigOptions()
|
|
5047
|
+
}
|
|
5048
|
+
});
|
|
5049
|
+
}
|
|
5050
|
+
// Return a shallow clone of an agent-emitted config_option_update
|
|
5051
|
+
// envelope with the hydra-native `agent` option appended to its
|
|
5052
|
+
// configOptions (unless the agent already advertised one). Preserves
|
|
5053
|
+
// the agent's own options (mode/model/effort/…) and their order; the
|
|
5054
|
+
// `agent` selector rides last, matching its lower prominence. The
|
|
5055
|
+
// original envelope is not mutated.
|
|
5056
|
+
mergeAgentOptionIntoEnvelope(envelope) {
|
|
5057
|
+
if (!envelope || typeof envelope !== "object") {
|
|
5058
|
+
return envelope;
|
|
5059
|
+
}
|
|
5060
|
+
const env = envelope;
|
|
5061
|
+
if (!env.update || typeof env.update !== "object") {
|
|
5062
|
+
return envelope;
|
|
5063
|
+
}
|
|
5064
|
+
const update = env.update;
|
|
5065
|
+
const list = Array.isArray(update.configOptions) ? [...update.configOptions] : [];
|
|
5066
|
+
const hasAgent = list.some(
|
|
5067
|
+
(o) => o && typeof o === "object" && o.id === "agent"
|
|
5068
|
+
);
|
|
5069
|
+
if (hasAgent) {
|
|
5070
|
+
return envelope;
|
|
5071
|
+
}
|
|
5072
|
+
const agentOption = this.buildConfigOptions().find((o) => o.id === "agent");
|
|
5073
|
+
if (!agentOption) {
|
|
5074
|
+
return envelope;
|
|
5075
|
+
}
|
|
5076
|
+
return {
|
|
5077
|
+
...env,
|
|
5078
|
+
update: { ...update, configOptions: [...list, agentOption] }
|
|
5079
|
+
};
|
|
4700
5080
|
}
|
|
4701
5081
|
onUsageChange(handler) {
|
|
4702
5082
|
this.usageHandlers.push(handler);
|
|
@@ -4832,7 +5212,7 @@ var Session = class {
|
|
|
4832
5212
|
// "/hydra <name> <verb> [args]" — name matches a registered extension
|
|
4833
5213
|
// or transformer. We split the remainder into verb + args, validate the
|
|
4834
5214
|
// verb against what the process advertised, and forward as a
|
|
4835
|
-
// hydra-acp/
|
|
5215
|
+
// hydra-acp/commands/invoke request on the process's WS connection.
|
|
4836
5216
|
// The reply's text (if any) is broadcast as a synthetic
|
|
4837
5217
|
// agent_message_chunk so it appears in the conversation alongside the
|
|
4838
5218
|
// user's invocation.
|
|
@@ -4861,7 +5241,7 @@ var Session = class {
|
|
|
4861
5241
|
}
|
|
4862
5242
|
let reply;
|
|
4863
5243
|
try {
|
|
4864
|
-
reply = await entry.connection.request("hydra-acp/
|
|
5244
|
+
reply = await entry.connection.request("hydra-acp/commands/invoke", {
|
|
4865
5245
|
sessionId: this.sessionId,
|
|
4866
5246
|
verb,
|
|
4867
5247
|
args
|
|
@@ -5074,6 +5454,14 @@ ${body}
|
|
|
5074
5454
|
// record. Spawns the new agent first so a failure leaves the old one
|
|
5075
5455
|
// intact; then injects a synthesized transcript so the new agent has
|
|
5076
5456
|
// context for the next turn.
|
|
5457
|
+
// Public entry for swapping the underlying agent from a client request
|
|
5458
|
+
// (session/set_config_option with configId "agent"), the protocol twin
|
|
5459
|
+
// of the `/hydra agent` text command. Delegates to the same swap
|
|
5460
|
+
// machinery so both paths share validation, transcript replay, and the
|
|
5461
|
+
// config_option_update broadcast.
|
|
5462
|
+
setAgent(newAgentId) {
|
|
5463
|
+
return this.runAgentCommand(newAgentId);
|
|
5464
|
+
}
|
|
5077
5465
|
runAgentCommand(newAgentId) {
|
|
5078
5466
|
if (!newAgentId) {
|
|
5079
5467
|
throw withCode(
|
|
@@ -5112,12 +5500,10 @@ ${body}
|
|
|
5112
5500
|
this.agentCapabilities = fresh.agentCapabilities;
|
|
5113
5501
|
this.agentAdvertisedCommands = [];
|
|
5114
5502
|
this.broadcastMergedCommands();
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
this.setAgentAdvertisedModes([]);
|
|
5120
|
-
}
|
|
5503
|
+
this.currentModel = fresh.initialModel;
|
|
5504
|
+
this.currentMode = fresh.initialMode;
|
|
5505
|
+
this.setAgentAdvertisedModels(fresh.initialModels ?? []);
|
|
5506
|
+
this.setAgentAdvertisedModes(fresh.initialModes ?? []);
|
|
5121
5507
|
await oldAgent.kill().catch(() => void 0);
|
|
5122
5508
|
if (transcript) {
|
|
5123
5509
|
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
@@ -5141,7 +5527,7 @@ ${body}
|
|
|
5141
5527
|
// down any in-flight request as a side effect. The record is kept
|
|
5142
5528
|
// (deleteRecord:false) so the session goes cold and can be resurrected.
|
|
5143
5529
|
// Returns end_turn so the prompt() caller's response resolves normally,
|
|
5144
|
-
// but every attached client has already received hydra-acp/
|
|
5530
|
+
// but every attached client has already received hydra-acp/session/closed
|
|
5145
5531
|
// by the time this returns.
|
|
5146
5532
|
async runKillCommand() {
|
|
5147
5533
|
await this.close({ deleteRecord: false });
|
|
@@ -5159,45 +5545,71 @@ ${body}
|
|
|
5159
5545
|
JsonRpcErrorCodes.InternalError
|
|
5160
5546
|
);
|
|
5161
5547
|
}
|
|
5162
|
-
const spawnAgent = this.spawnReplacementAgent;
|
|
5163
|
-
const agentId = this.agentId;
|
|
5164
5548
|
return this.enqueuePrompt(async () => {
|
|
5165
|
-
|
|
5166
|
-
const fresh = await spawnAgent({
|
|
5167
|
-
agentId,
|
|
5168
|
-
cwd: this.cwd,
|
|
5169
|
-
agentArgs: this.agentArgs
|
|
5170
|
-
});
|
|
5171
|
-
this.accumulateAndResetCost();
|
|
5172
|
-
this.wireAgent(fresh.agent);
|
|
5173
|
-
const oldAgent = this.agent;
|
|
5174
|
-
this.agent = fresh.agent;
|
|
5175
|
-
this.upstreamSessionId = fresh.upstreamSessionId;
|
|
5176
|
-
this.agentMeta = fresh.agentMeta;
|
|
5177
|
-
this.agentCapabilities = fresh.agentCapabilities;
|
|
5178
|
-
this.agentAdvertisedCommands = [];
|
|
5179
|
-
this.broadcastMergedCommands();
|
|
5180
|
-
if (this.agentAdvertisedModels.length > 0) {
|
|
5181
|
-
this.setAgentAdvertisedModels([]);
|
|
5182
|
-
}
|
|
5183
|
-
if (this.agentAdvertisedModes.length > 0) {
|
|
5184
|
-
this.setAgentAdvertisedModes([]);
|
|
5185
|
-
}
|
|
5186
|
-
await oldAgent.kill().catch(() => void 0);
|
|
5187
|
-
if (transcript) {
|
|
5188
|
-
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
5189
|
-
}
|
|
5190
|
-
this.broadcastAgentSwitch(agentId, agentId);
|
|
5191
|
-
const info = { agentId, upstreamSessionId: this.upstreamSessionId };
|
|
5192
|
-
for (const handler of this.agentChangeHandlers) {
|
|
5193
|
-
try {
|
|
5194
|
-
handler(info);
|
|
5195
|
-
} catch {
|
|
5196
|
-
}
|
|
5197
|
-
}
|
|
5549
|
+
await this.respawnAgent();
|
|
5198
5550
|
return { stopReason: "end_turn" };
|
|
5199
5551
|
});
|
|
5200
5552
|
}
|
|
5553
|
+
// Last-resort cancellation. When an agent ignores session/cancel (current
|
|
5554
|
+
// opencode returns UnsupportedOperation and keeps generating), the only
|
|
5555
|
+
// way to actually stop the turn is to tear the subprocess down. Rather
|
|
5556
|
+
// than respawn + replay the transcript (slow, and renders as an agent
|
|
5557
|
+
// "switch"), we close the session keeping its record: agent.kill() aborts
|
|
5558
|
+
// the in-flight turn, and the next prompt auto-resurrects via session/load
|
|
5559
|
+
// (cheap — the agent restores its own context). The forceCancelling flag
|
|
5560
|
+
// makes runQueueEntry render the aborted turn as "cancelled" rather than a
|
|
5561
|
+
// raw connection error. Runs OUTSIDE the prompt queue so a wedged turn
|
|
5562
|
+
// can't block it.
|
|
5563
|
+
async forceCancel() {
|
|
5564
|
+
if (this.closed || this.closing) {
|
|
5565
|
+
throw withCode(
|
|
5566
|
+
new Error("session is closing"),
|
|
5567
|
+
JsonRpcErrorCodes.SessionClosing
|
|
5568
|
+
);
|
|
5569
|
+
}
|
|
5570
|
+
this.lastCancelAt = 0;
|
|
5571
|
+
this.forceCancelling = true;
|
|
5572
|
+
await this.close({ deleteRecord: false });
|
|
5573
|
+
return { stopReason: "cancelled" };
|
|
5574
|
+
}
|
|
5575
|
+
// Shared kill-and-respawn used by /hydra restart (queued) and forceCancel
|
|
5576
|
+
// (immediate). Spawns a fresh agent, swaps it in, kills the old one, and
|
|
5577
|
+
// re-seeds the conversation transcript so the new process has context.
|
|
5578
|
+
async respawnAgent() {
|
|
5579
|
+
const spawnAgent = this.spawnReplacementAgent;
|
|
5580
|
+
const agentId = this.agentId;
|
|
5581
|
+
const transcript = await this.buildSwitchTranscript(agentId);
|
|
5582
|
+
const fresh = await spawnAgent({
|
|
5583
|
+
agentId,
|
|
5584
|
+
cwd: this.cwd,
|
|
5585
|
+
agentArgs: this.agentArgs
|
|
5586
|
+
});
|
|
5587
|
+
this.accumulateAndResetCost();
|
|
5588
|
+
this.wireAgent(fresh.agent);
|
|
5589
|
+
const oldAgent = this.agent;
|
|
5590
|
+
this.agent = fresh.agent;
|
|
5591
|
+
this.upstreamSessionId = fresh.upstreamSessionId;
|
|
5592
|
+
this.agentMeta = fresh.agentMeta;
|
|
5593
|
+
this.agentCapabilities = fresh.agentCapabilities;
|
|
5594
|
+
this.agentAdvertisedCommands = [];
|
|
5595
|
+
this.broadcastMergedCommands();
|
|
5596
|
+
this.currentModel = fresh.initialModel;
|
|
5597
|
+
this.currentMode = fresh.initialMode;
|
|
5598
|
+
this.setAgentAdvertisedModels(fresh.initialModels ?? []);
|
|
5599
|
+
this.setAgentAdvertisedModes(fresh.initialModes ?? []);
|
|
5600
|
+
await oldAgent.kill().catch(() => void 0);
|
|
5601
|
+
if (transcript) {
|
|
5602
|
+
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
5603
|
+
}
|
|
5604
|
+
this.broadcastAgentSwitch(agentId, agentId);
|
|
5605
|
+
const info = { agentId, upstreamSessionId: this.upstreamSessionId };
|
|
5606
|
+
for (const handler of this.agentChangeHandlers) {
|
|
5607
|
+
try {
|
|
5608
|
+
handler(info);
|
|
5609
|
+
} catch {
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5201
5613
|
// Walk the persisted history and produce a labeled transcript suitable
|
|
5202
5614
|
// for handing to a fresh agent. Includes user prompts, agent replies,
|
|
5203
5615
|
// and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
|
|
@@ -5298,15 +5710,13 @@ ${body}
|
|
|
5298
5710
|
return void 0;
|
|
5299
5711
|
});
|
|
5300
5712
|
}
|
|
5301
|
-
// Tell every attached client
|
|
5302
|
-
//
|
|
5713
|
+
// Tell every attached client the agent identity has changed via
|
|
5714
|
+
// session_info_update carrying agentId inside _meta["hydra-acp"] —
|
|
5303
5715
|
// the ACP schema for session_info_update is just title/updatedAt/_meta,
|
|
5304
5716
|
// so non-hydra clients harmlessly ignore the extension; hydra-aware
|
|
5305
|
-
// ones read it and relabel
|
|
5306
|
-
// transcript
|
|
5307
|
-
|
|
5308
|
-
// so a future /hydra agent's transcript builder filters them out.
|
|
5309
|
-
broadcastAgentSwitch(oldAgentId, newAgentId) {
|
|
5717
|
+
// ones read it and relabel. synthetic=true so a future /hydra agent's
|
|
5718
|
+
// transcript builder filters it out.
|
|
5719
|
+
broadcastAgentSwitch(_oldAgentId, newAgentId) {
|
|
5310
5720
|
this.recordAndBroadcast("session/update", {
|
|
5311
5721
|
sessionId: this.sessionId,
|
|
5312
5722
|
update: {
|
|
@@ -5314,19 +5724,7 @@ ${body}
|
|
|
5314
5724
|
_meta: { "hydra-acp": { synthetic: true, agentId: newAgentId } }
|
|
5315
5725
|
}
|
|
5316
5726
|
});
|
|
5317
|
-
this.
|
|
5318
|
-
sessionId: this.sessionId,
|
|
5319
|
-
update: {
|
|
5320
|
-
sessionUpdate: "agent_message_chunk",
|
|
5321
|
-
content: {
|
|
5322
|
-
type: "text",
|
|
5323
|
-
text: `
|
|
5324
|
-
_(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
5325
|
-
`
|
|
5326
|
-
},
|
|
5327
|
-
_meta: { "hydra-acp": { synthetic: true } }
|
|
5328
|
-
}
|
|
5329
|
-
});
|
|
5727
|
+
this.broadcastConfigOptions();
|
|
5330
5728
|
}
|
|
5331
5729
|
// stdin-stream lifecycle. Cat --stream calls openStream() once after
|
|
5332
5730
|
// session/new, then forwards stdin chunks via streamWrite(). The agent
|
|
@@ -5457,7 +5855,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5457
5855
|
requireStreamBuffer() {
|
|
5458
5856
|
if (this.streamBuffer === void 0) {
|
|
5459
5857
|
const err = new Error(
|
|
5460
|
-
`session ${this.sessionId} has no stream buffer;
|
|
5858
|
+
`session ${this.sessionId} has no stream buffer; POST /v1/sessions/:id/stdin/open first`
|
|
5461
5859
|
);
|
|
5462
5860
|
err.code = JsonRpcErrorCodes.StreamNotEnabled;
|
|
5463
5861
|
throw err;
|
|
@@ -5500,7 +5898,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5500
5898
|
const sessionId = this.sessionId;
|
|
5501
5899
|
this.queueWriteChain = this.queueWriteChain.catch(() => void 0).then(() => deleteQueue(sessionId).catch(() => void 0));
|
|
5502
5900
|
for (const client of this.clients.values()) {
|
|
5503
|
-
void client.connection.notify("hydra-acp/
|
|
5901
|
+
void client.connection.notify("hydra-acp/session/closed", { sessionId: this.sessionId }).catch(() => void 0);
|
|
5504
5902
|
}
|
|
5505
5903
|
this.clients.clear();
|
|
5506
5904
|
if (this.streamBuffer !== void 0) {
|
|
@@ -5607,7 +6005,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5607
6005
|
if (!t.intercepts.has(intercept)) {
|
|
5608
6006
|
continue;
|
|
5609
6007
|
}
|
|
5610
|
-
void t.connection.notify("transformer/session_event", {
|
|
6008
|
+
void t.connection.notify("hydra-acp/transformer/session_event", {
|
|
5611
6009
|
event,
|
|
5612
6010
|
sessionId: this.sessionId,
|
|
5613
6011
|
payload
|
|
@@ -5887,6 +6285,10 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5887
6285
|
}
|
|
5888
6286
|
);
|
|
5889
6287
|
} catch (err) {
|
|
6288
|
+
if (this.forceCancelling) {
|
|
6289
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
6290
|
+
return { stopReason: "cancelled" };
|
|
6291
|
+
}
|
|
5890
6292
|
if (!this.closed) {
|
|
5891
6293
|
this.broadcastTurnComplete(
|
|
5892
6294
|
entry.clientId,
|
|
@@ -6002,6 +6404,31 @@ function parseModelsList(list) {
|
|
|
6002
6404
|
}
|
|
6003
6405
|
return out;
|
|
6004
6406
|
}
|
|
6407
|
+
function parseModesList(list) {
|
|
6408
|
+
if (!Array.isArray(list)) {
|
|
6409
|
+
return [];
|
|
6410
|
+
}
|
|
6411
|
+
const out = [];
|
|
6412
|
+
for (const raw of list) {
|
|
6413
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
6414
|
+
continue;
|
|
6415
|
+
}
|
|
6416
|
+
const r = raw;
|
|
6417
|
+
const id = typeof r.value === "string" && r.value.trim() || typeof r.id === "string" && r.id.trim() || void 0;
|
|
6418
|
+
if (!id) {
|
|
6419
|
+
continue;
|
|
6420
|
+
}
|
|
6421
|
+
const mode = { id };
|
|
6422
|
+
if (typeof r.name === "string" && r.name.length > 0) {
|
|
6423
|
+
mode.name = r.name;
|
|
6424
|
+
}
|
|
6425
|
+
if (typeof r.description === "string" && r.description.length > 0) {
|
|
6426
|
+
mode.description = r.description;
|
|
6427
|
+
}
|
|
6428
|
+
out.push(mode);
|
|
6429
|
+
}
|
|
6430
|
+
return out;
|
|
6431
|
+
}
|
|
6005
6432
|
function extractAdvertisedModes(params) {
|
|
6006
6433
|
const obj = params ?? {};
|
|
6007
6434
|
const update = obj.update ?? {};
|
|
@@ -6378,9 +6805,9 @@ var SessionRecord = z5.object({
|
|
|
6378
6805
|
// memory. Cleared after that first resurrect completes.
|
|
6379
6806
|
pendingHistorySync: z5.boolean().optional(),
|
|
6380
6807
|
// Set when this session was spawned as a child by a transformer via
|
|
6381
|
-
// hydra-acp/
|
|
6808
|
+
// hydra-acp/child_session/spawn. Points to the spawning session's id.
|
|
6382
6809
|
parentSessionId: z5.string().optional(),
|
|
6383
|
-
// Set when this session was created by hydra-acp/
|
|
6810
|
+
// Set when this session was created by hydra-acp/session/fork.
|
|
6384
6811
|
// forkedFromSessionId points to the local source session; forkedFromMessageId
|
|
6385
6812
|
// is the resolved forkAt — the messageId of the turn_complete the slice
|
|
6386
6813
|
// ended at. Kept so future UI can show "branched from turn N of session X".
|
|
@@ -6521,20 +6948,167 @@ function recordFromMemorySession(args) {
|
|
|
6521
6948
|
};
|
|
6522
6949
|
}
|
|
6523
6950
|
|
|
6951
|
+
// src/core/tombstone-store.ts
|
|
6952
|
+
import * as fs9 from "fs/promises";
|
|
6953
|
+
import { z as z6 } from "zod";
|
|
6954
|
+
var Tombstone = z6.object({
|
|
6955
|
+
version: z6.literal(1),
|
|
6956
|
+
agentId: z6.string(),
|
|
6957
|
+
upstreamSessionId: z6.string(),
|
|
6958
|
+
deletedAt: z6.string(),
|
|
6959
|
+
// Agent's last-reported updatedAt for this session at the moment we
|
|
6960
|
+
// deleted, snapshotted from SessionRecord.updatedAt. Compared against
|
|
6961
|
+
// the listing's updatedAt on subsequent syncs to detect that the
|
|
6962
|
+
// conversation has moved on (the agent / user revived it), in which
|
|
6963
|
+
// case the tombstone is dropped and the session re-imports. Absent
|
|
6964
|
+
// when the deleted record never carried a meaningful updatedAt.
|
|
6965
|
+
upstreamUpdatedAt: z6.string().optional(),
|
|
6966
|
+
cwd: z6.string().optional(),
|
|
6967
|
+
title: z6.string().optional(),
|
|
6968
|
+
reason: z6.enum(["user", "expired"]).optional()
|
|
6969
|
+
});
|
|
6970
|
+
var TombstoneStore = class {
|
|
6971
|
+
async add(t) {
|
|
6972
|
+
const full = { version: 1, ...t };
|
|
6973
|
+
await writeJsonAtomic(
|
|
6974
|
+
paths.tombstoneFile(t.agentId, t.upstreamSessionId),
|
|
6975
|
+
full,
|
|
6976
|
+
{ mode: 384 }
|
|
6977
|
+
);
|
|
6978
|
+
}
|
|
6979
|
+
async has(agentId, upstreamSessionId) {
|
|
6980
|
+
try {
|
|
6981
|
+
await fs9.access(paths.tombstoneFile(agentId, upstreamSessionId));
|
|
6982
|
+
return true;
|
|
6983
|
+
} catch {
|
|
6984
|
+
return false;
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
// Returns the tombstone payload if the file exists. An unreadable or
|
|
6988
|
+
// unparseable file still counts as a tombstone — we synthesize a
|
|
6989
|
+
// bare record so the caller's "is this dead?" check stays correct,
|
|
6990
|
+
// but with no upstreamUpdatedAt the resurrection rule treats any
|
|
6991
|
+
// listed updatedAt as advancement (see SessionManager.syncFromAgent).
|
|
6992
|
+
async read(agentId, upstreamSessionId) {
|
|
6993
|
+
const file = paths.tombstoneFile(agentId, upstreamSessionId);
|
|
6994
|
+
const parsed = await readJsonSafe(file);
|
|
6995
|
+
if (parsed === void 0) {
|
|
6996
|
+
if (await this.has(agentId, upstreamSessionId)) {
|
|
6997
|
+
return {
|
|
6998
|
+
version: 1,
|
|
6999
|
+
agentId,
|
|
7000
|
+
upstreamSessionId,
|
|
7001
|
+
deletedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
7002
|
+
};
|
|
7003
|
+
}
|
|
7004
|
+
return void 0;
|
|
7005
|
+
}
|
|
7006
|
+
try {
|
|
7007
|
+
return Tombstone.parse(parsed);
|
|
7008
|
+
} catch {
|
|
7009
|
+
return {
|
|
7010
|
+
version: 1,
|
|
7011
|
+
agentId,
|
|
7012
|
+
upstreamSessionId,
|
|
7013
|
+
deletedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
7014
|
+
};
|
|
7015
|
+
}
|
|
7016
|
+
}
|
|
7017
|
+
async remove(agentId, upstreamSessionId) {
|
|
7018
|
+
try {
|
|
7019
|
+
await fs9.unlink(paths.tombstoneFile(agentId, upstreamSessionId));
|
|
7020
|
+
} catch (err) {
|
|
7021
|
+
const e = err;
|
|
7022
|
+
if (e.code !== "ENOENT") {
|
|
7023
|
+
throw err;
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
try {
|
|
7027
|
+
await fs9.rmdir(paths.tombstoneAgentDir(agentId));
|
|
7028
|
+
} catch (err) {
|
|
7029
|
+
const e = err;
|
|
7030
|
+
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
7031
|
+
throw err;
|
|
7032
|
+
}
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
async list(agentId) {
|
|
7036
|
+
if (agentId !== void 0) {
|
|
7037
|
+
return this.listForAgent(agentId);
|
|
7038
|
+
}
|
|
7039
|
+
let agents;
|
|
7040
|
+
try {
|
|
7041
|
+
agents = await fs9.readdir(paths.tombstonesDir());
|
|
7042
|
+
} catch (err) {
|
|
7043
|
+
const e = err;
|
|
7044
|
+
if (e.code === "ENOENT") {
|
|
7045
|
+
return [];
|
|
7046
|
+
}
|
|
7047
|
+
throw err;
|
|
7048
|
+
}
|
|
7049
|
+
const out = [];
|
|
7050
|
+
for (const enc of agents) {
|
|
7051
|
+
let decoded;
|
|
7052
|
+
try {
|
|
7053
|
+
decoded = decodeURIComponent(enc);
|
|
7054
|
+
} catch {
|
|
7055
|
+
continue;
|
|
7056
|
+
}
|
|
7057
|
+
out.push(...await this.listForAgent(decoded));
|
|
7058
|
+
}
|
|
7059
|
+
return out;
|
|
7060
|
+
}
|
|
7061
|
+
async listForAgent(agentId) {
|
|
7062
|
+
let files;
|
|
7063
|
+
try {
|
|
7064
|
+
files = await fs9.readdir(paths.tombstoneAgentDir(agentId));
|
|
7065
|
+
} catch (err) {
|
|
7066
|
+
const e = err;
|
|
7067
|
+
if (e.code === "ENOENT") {
|
|
7068
|
+
return [];
|
|
7069
|
+
}
|
|
7070
|
+
throw err;
|
|
7071
|
+
}
|
|
7072
|
+
const out = [];
|
|
7073
|
+
for (const f of files) {
|
|
7074
|
+
let upstreamId;
|
|
7075
|
+
try {
|
|
7076
|
+
upstreamId = decodeURIComponent(f);
|
|
7077
|
+
} catch {
|
|
7078
|
+
continue;
|
|
7079
|
+
}
|
|
7080
|
+
const t = await this.read(agentId, upstreamId);
|
|
7081
|
+
if (t) {
|
|
7082
|
+
out.push(t);
|
|
7083
|
+
}
|
|
7084
|
+
}
|
|
7085
|
+
return out;
|
|
7086
|
+
}
|
|
7087
|
+
};
|
|
7088
|
+
function shouldResurrectFromUpstream(tombstone, listingUpdatedAt) {
|
|
7089
|
+
if (listingUpdatedAt === void 0) {
|
|
7090
|
+
return false;
|
|
7091
|
+
}
|
|
7092
|
+
if (tombstone.upstreamUpdatedAt === void 0) {
|
|
7093
|
+
return true;
|
|
7094
|
+
}
|
|
7095
|
+
return listingUpdatedAt > tombstone.upstreamUpdatedAt;
|
|
7096
|
+
}
|
|
7097
|
+
|
|
6524
7098
|
// src/core/synopsis-coordinator.ts
|
|
6525
|
-
import * as
|
|
7099
|
+
import * as fs11 from "fs/promises";
|
|
6526
7100
|
|
|
6527
7101
|
// src/core/hydra-version.ts
|
|
6528
7102
|
import { fileURLToPath } from "url";
|
|
6529
7103
|
import * as path7 from "path";
|
|
6530
|
-
import * as
|
|
7104
|
+
import * as fs10 from "fs";
|
|
6531
7105
|
function resolveVersion() {
|
|
6532
7106
|
try {
|
|
6533
7107
|
let dir = path7.dirname(fileURLToPath(import.meta.url));
|
|
6534
7108
|
for (let i = 0; i < 8; i += 1) {
|
|
6535
7109
|
const candidate = path7.join(dir, "package.json");
|
|
6536
|
-
if (
|
|
6537
|
-
const pkg = JSON.parse(
|
|
7110
|
+
if (fs10.existsSync(candidate)) {
|
|
7111
|
+
const pkg = JSON.parse(fs10.readFileSync(candidate, "utf8"));
|
|
6538
7112
|
if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
|
|
6539
7113
|
return pkg.version;
|
|
6540
7114
|
}
|
|
@@ -7058,7 +7632,7 @@ var SynopsisCoordinator = class {
|
|
|
7058
7632
|
});
|
|
7059
7633
|
const modelId = this.opts.synopsisModel;
|
|
7060
7634
|
const synopsisCwd = paths.sessionDir(sessionId);
|
|
7061
|
-
await
|
|
7635
|
+
await fs11.mkdir(synopsisCwd, { recursive: true }).catch(() => void 0);
|
|
7062
7636
|
this.opts.logger?.info(
|
|
7063
7637
|
`synopsis: start sessionId=${sessionId} agentId=${synopsisAgentId} historyLen=${history.length} model=${JSON.stringify(modelId ?? "(default)")} cwd=${synopsisCwd}`
|
|
7064
7638
|
);
|
|
@@ -7161,7 +7735,7 @@ function describeFields(s) {
|
|
|
7161
7735
|
}
|
|
7162
7736
|
|
|
7163
7737
|
// src/core/history-store.ts
|
|
7164
|
-
import * as
|
|
7738
|
+
import * as fs12 from "fs/promises";
|
|
7165
7739
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
7166
7740
|
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
7167
7741
|
var HistoryStore = class {
|
|
@@ -7178,9 +7752,9 @@ var HistoryStore = class {
|
|
|
7178
7752
|
return;
|
|
7179
7753
|
}
|
|
7180
7754
|
return this.enqueue(sessionId, async () => {
|
|
7181
|
-
await
|
|
7755
|
+
await fs12.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
7182
7756
|
const line = JSON.stringify(entry) + "\n";
|
|
7183
|
-
await
|
|
7757
|
+
await fs12.appendFile(paths.historyFile(sessionId), line, {
|
|
7184
7758
|
encoding: "utf8",
|
|
7185
7759
|
mode: 384
|
|
7186
7760
|
});
|
|
@@ -7191,9 +7765,9 @@ var HistoryStore = class {
|
|
|
7191
7765
|
return;
|
|
7192
7766
|
}
|
|
7193
7767
|
return this.enqueue(sessionId, async () => {
|
|
7194
|
-
await
|
|
7768
|
+
await fs12.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
7195
7769
|
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
7196
|
-
await
|
|
7770
|
+
await fs12.writeFile(paths.historyFile(sessionId), body, {
|
|
7197
7771
|
encoding: "utf8",
|
|
7198
7772
|
mode: 384
|
|
7199
7773
|
});
|
|
@@ -7210,7 +7784,7 @@ var HistoryStore = class {
|
|
|
7210
7784
|
return this.enqueue(sessionId, async () => {
|
|
7211
7785
|
let raw;
|
|
7212
7786
|
try {
|
|
7213
|
-
raw = await
|
|
7787
|
+
raw = await fs12.readFile(paths.historyFile(sessionId), "utf8");
|
|
7214
7788
|
} catch (err) {
|
|
7215
7789
|
const e = err;
|
|
7216
7790
|
if (e.code === "ENOENT") {
|
|
@@ -7223,7 +7797,7 @@ var HistoryStore = class {
|
|
|
7223
7797
|
return;
|
|
7224
7798
|
}
|
|
7225
7799
|
const trimmed = lines.slice(-maxEntries);
|
|
7226
|
-
await
|
|
7800
|
+
await fs12.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
7227
7801
|
encoding: "utf8",
|
|
7228
7802
|
mode: 384
|
|
7229
7803
|
});
|
|
@@ -7239,7 +7813,7 @@ var HistoryStore = class {
|
|
|
7239
7813
|
}
|
|
7240
7814
|
let raw;
|
|
7241
7815
|
try {
|
|
7242
|
-
raw = await
|
|
7816
|
+
raw = await fs12.readFile(paths.historyFile(sessionId), "utf8");
|
|
7243
7817
|
} catch (err) {
|
|
7244
7818
|
const e = err;
|
|
7245
7819
|
if (e.code === "ENOENT") {
|
|
@@ -7298,7 +7872,7 @@ var HistoryStore = class {
|
|
|
7298
7872
|
}
|
|
7299
7873
|
return this.enqueue(sessionId, async () => {
|
|
7300
7874
|
try {
|
|
7301
|
-
await
|
|
7875
|
+
await fs12.unlink(paths.historyFile(sessionId));
|
|
7302
7876
|
} catch (err) {
|
|
7303
7877
|
const e = err;
|
|
7304
7878
|
if (e.code !== "ENOENT") {
|
|
@@ -7306,7 +7880,7 @@ var HistoryStore = class {
|
|
|
7306
7880
|
}
|
|
7307
7881
|
}
|
|
7308
7882
|
try {
|
|
7309
|
-
await
|
|
7883
|
+
await fs12.rmdir(paths.sessionDir(sessionId));
|
|
7310
7884
|
} catch (err) {
|
|
7311
7885
|
const e = err;
|
|
7312
7886
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -7330,74 +7904,74 @@ var HistoryStore = class {
|
|
|
7330
7904
|
};
|
|
7331
7905
|
|
|
7332
7906
|
// src/tui/history.ts
|
|
7333
|
-
import { promises as
|
|
7907
|
+
import { promises as fs13 } from "fs";
|
|
7334
7908
|
import * as path8 from "path";
|
|
7335
7909
|
async function saveHistory(file, history) {
|
|
7336
|
-
await
|
|
7910
|
+
await fs13.mkdir(path8.dirname(file), { recursive: true });
|
|
7337
7911
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
7338
|
-
await
|
|
7912
|
+
await fs13.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
7339
7913
|
}
|
|
7340
7914
|
|
|
7341
7915
|
// src/core/bundle.ts
|
|
7342
|
-
import { z as
|
|
7343
|
-
var HistoryEntrySchema =
|
|
7344
|
-
method:
|
|
7345
|
-
params:
|
|
7346
|
-
recordedAt:
|
|
7916
|
+
import { z as z7 } from "zod";
|
|
7917
|
+
var HistoryEntrySchema = z7.object({
|
|
7918
|
+
method: z7.string(),
|
|
7919
|
+
params: z7.unknown(),
|
|
7920
|
+
recordedAt: z7.number()
|
|
7347
7921
|
});
|
|
7348
|
-
var BundleSession =
|
|
7922
|
+
var BundleSession = z7.object({
|
|
7349
7923
|
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
7350
7924
|
// the local namespace; lineageId is what survives across hops).
|
|
7351
|
-
sessionId:
|
|
7925
|
+
sessionId: z7.string(),
|
|
7352
7926
|
// Required on bundles — the export path backfills if the source
|
|
7353
7927
|
// record was written before lineageId existed.
|
|
7354
|
-
lineageId:
|
|
7928
|
+
lineageId: z7.string(),
|
|
7355
7929
|
// The exporter's agent-side session id at export time. Carried so
|
|
7356
7930
|
// importers can persist it as a breadcrumb (and, eventually, as the
|
|
7357
7931
|
// handle a "connect back to origin" feature would need). Omitted on
|
|
7358
7932
|
// bundles whose source record never bound to an agent (e.g. a
|
|
7359
7933
|
// re-export of an imported, not-yet-attached session).
|
|
7360
|
-
upstreamSessionId:
|
|
7361
|
-
agentId:
|
|
7362
|
-
cwd:
|
|
7363
|
-
title:
|
|
7934
|
+
upstreamSessionId: z7.string().optional(),
|
|
7935
|
+
agentId: z7.string(),
|
|
7936
|
+
cwd: z7.string(),
|
|
7937
|
+
title: z7.string().optional(),
|
|
7364
7938
|
// Structured snapshot. Carried across export/import so a sync'd
|
|
7365
7939
|
// session on another machine surfaces the same synopsis in its
|
|
7366
7940
|
// picker / list / info views without re-asking the agent.
|
|
7367
7941
|
synopsis: SessionSynopsis.optional(),
|
|
7368
|
-
summarizedThroughEntry:
|
|
7369
|
-
currentModel:
|
|
7370
|
-
currentMode:
|
|
7942
|
+
summarizedThroughEntry: z7.number().int().nonnegative().optional(),
|
|
7943
|
+
currentModel: z7.string().optional(),
|
|
7944
|
+
currentMode: z7.string().optional(),
|
|
7371
7945
|
currentUsage: PersistedUsage.optional(),
|
|
7372
|
-
agentCommands:
|
|
7373
|
-
agentModes:
|
|
7946
|
+
agentCommands: z7.array(PersistedAgentCommand).optional(),
|
|
7947
|
+
agentModes: z7.array(PersistedAgentMode).optional(),
|
|
7374
7948
|
// Raw interactive tristate (NOT the resolved effectiveInteractive) so
|
|
7375
7949
|
// the value stays promotable on the destination: a cat/empty source
|
|
7376
7950
|
// arrives as undefined and a real turn there can still flip it to
|
|
7377
7951
|
// true. Carried alongside originatingClient so the importer's
|
|
7378
7952
|
// effectiveInteractive can re-apply the cat-name hint at read time
|
|
7379
7953
|
// without freezing a sticky `false` into the record.
|
|
7380
|
-
interactive:
|
|
7954
|
+
interactive: z7.boolean().optional(),
|
|
7381
7955
|
originatingClient: PersistedOriginatingClient.optional(),
|
|
7382
|
-
createdAt:
|
|
7383
|
-
updatedAt:
|
|
7956
|
+
createdAt: z7.string(),
|
|
7957
|
+
updatedAt: z7.string()
|
|
7384
7958
|
});
|
|
7385
|
-
var Bundle =
|
|
7386
|
-
version:
|
|
7387
|
-
exportedAt:
|
|
7388
|
-
exportedFrom:
|
|
7389
|
-
hydraVersion:
|
|
7390
|
-
machine:
|
|
7959
|
+
var Bundle = z7.object({
|
|
7960
|
+
version: z7.literal(1),
|
|
7961
|
+
exportedAt: z7.string(),
|
|
7962
|
+
exportedFrom: z7.object({
|
|
7963
|
+
hydraVersion: z7.string(),
|
|
7964
|
+
machine: z7.string(),
|
|
7391
7965
|
// Externally-reachable name (and optional ":port") for the exporting
|
|
7392
7966
|
// daemon, sourced from config.daemon.publicHost (or daemon.host when
|
|
7393
7967
|
// non-loopback). Carried so an importer can construct a hydra:// URL
|
|
7394
7968
|
// that dials back to the origin — e.g. over Tailscale. Omitted when
|
|
7395
7969
|
// the exporter has no routable address; never falls back to loopback.
|
|
7396
|
-
hydraHost:
|
|
7970
|
+
hydraHost: z7.string().optional()
|
|
7397
7971
|
}),
|
|
7398
7972
|
session: BundleSession,
|
|
7399
|
-
history:
|
|
7400
|
-
promptHistory:
|
|
7973
|
+
history: z7.array(HistoryEntrySchema),
|
|
7974
|
+
promptHistory: z7.array(z7.string()).optional()
|
|
7401
7975
|
});
|
|
7402
7976
|
function encodeBundle(params) {
|
|
7403
7977
|
const bundle = {
|
|
@@ -7447,6 +8021,7 @@ var SessionManager = class {
|
|
|
7447
8021
|
this.registry = registry;
|
|
7448
8022
|
this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
|
|
7449
8023
|
this.store = store ?? new SessionStore();
|
|
8024
|
+
this.tombstones = options.tombstones ?? new TombstoneStore();
|
|
7450
8025
|
this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
|
|
7451
8026
|
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
7452
8027
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
@@ -7478,12 +8053,14 @@ var SessionManager = class {
|
|
|
7478
8053
|
logger: this.logger,
|
|
7479
8054
|
npmRegistry: this.npmRegistry
|
|
7480
8055
|
});
|
|
8056
|
+
void this.refreshAgentCatalog();
|
|
7481
8057
|
}
|
|
7482
8058
|
registry;
|
|
7483
8059
|
sessions = /* @__PURE__ */ new Map();
|
|
7484
8060
|
resurrectionInflight = /* @__PURE__ */ new Map();
|
|
7485
8061
|
spawner;
|
|
7486
8062
|
store;
|
|
8063
|
+
tombstones;
|
|
7487
8064
|
histories;
|
|
7488
8065
|
idleTimeoutMs;
|
|
7489
8066
|
defaultModels;
|
|
@@ -7505,6 +8082,25 @@ var SessionManager = class {
|
|
|
7505
8082
|
// out-of-band so session close is instant; persists synopsis/title
|
|
7506
8083
|
// via the same enqueueMetaWrite path the in-session handlers used.
|
|
7507
8084
|
synopsisCoordinator;
|
|
8085
|
+
// Cached agent catalog used to populate the `agent` config option's
|
|
8086
|
+
// value list. Refreshed lazily (fire-and-forget) since the underlying
|
|
8087
|
+
// registry load may hit the network; sessions read whatever snapshot is
|
|
8088
|
+
// current and always inject their own live agent if it's missing.
|
|
8089
|
+
agentCatalog = [];
|
|
8090
|
+
// Refresh the cached agent catalog from the registry. Fire-and-forget;
|
|
8091
|
+
// failures leave the prior snapshot in place. Called at construction and
|
|
8092
|
+
// after each session creation so the list tracks newly-installed agents.
|
|
8093
|
+
async refreshAgentCatalog() {
|
|
8094
|
+
try {
|
|
8095
|
+
const { agents } = await listAgents(this.registry);
|
|
8096
|
+
this.agentCatalog = agents.map((a) => ({
|
|
8097
|
+
id: a.id,
|
|
8098
|
+
name: a.name,
|
|
8099
|
+
...a.description !== void 0 ? { description: a.description } : {}
|
|
8100
|
+
}));
|
|
8101
|
+
} catch {
|
|
8102
|
+
}
|
|
8103
|
+
}
|
|
7508
8104
|
async create(params) {
|
|
7509
8105
|
const fresh = await this.bootstrapAgent({
|
|
7510
8106
|
agentId: params.agentId,
|
|
@@ -7521,7 +8117,7 @@ var SessionManager = class {
|
|
|
7521
8117
|
continue;
|
|
7522
8118
|
}
|
|
7523
8119
|
try {
|
|
7524
|
-
const result = await t.connection.request("transformer/message", {
|
|
8120
|
+
const result = await t.connection.request("hydra-acp/transformer/message", {
|
|
7525
8121
|
token: `t_${generateRawSessionId()}`,
|
|
7526
8122
|
phase: "response",
|
|
7527
8123
|
method: "initialize",
|
|
@@ -7551,6 +8147,7 @@ var SessionManager = class {
|
|
|
7551
8147
|
logger: this.logger,
|
|
7552
8148
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7553
8149
|
listSessions: () => this.list(),
|
|
8150
|
+
availableAgents: () => this.agentCatalog,
|
|
7554
8151
|
historyStore: this.histories,
|
|
7555
8152
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
7556
8153
|
currentModel: fresh.initialModel,
|
|
@@ -7718,6 +8315,7 @@ var SessionManager = class {
|
|
|
7718
8315
|
logger: this.logger,
|
|
7719
8316
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7720
8317
|
listSessions: () => this.list(),
|
|
8318
|
+
availableAgents: () => this.agentCatalog,
|
|
7721
8319
|
historyStore: this.histories,
|
|
7722
8320
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
7723
8321
|
currentModel: effectiveModel,
|
|
@@ -7798,6 +8396,7 @@ var SessionManager = class {
|
|
|
7798
8396
|
logger: this.logger,
|
|
7799
8397
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7800
8398
|
listSessions: () => this.list(),
|
|
8399
|
+
availableAgents: () => this.agentCatalog,
|
|
7801
8400
|
historyStore: this.histories,
|
|
7802
8401
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
7803
8402
|
currentModel: effectiveModel,
|
|
@@ -7821,7 +8420,7 @@ var SessionManager = class {
|
|
|
7821
8420
|
}
|
|
7822
8421
|
async dirExists(cwd) {
|
|
7823
8422
|
try {
|
|
7824
|
-
return (await
|
|
8423
|
+
return (await fs14.stat(cwd)).isDirectory();
|
|
7825
8424
|
} catch {
|
|
7826
8425
|
return false;
|
|
7827
8426
|
}
|
|
@@ -7927,7 +8526,7 @@ var SessionManager = class {
|
|
|
7927
8526
|
for (const rec of stored) {
|
|
7928
8527
|
existing.add(`${rec.agentId}::${rec.upstreamSessionId}`);
|
|
7929
8528
|
}
|
|
7930
|
-
const
|
|
8529
|
+
const synopsisSandboxDir = paths.sessionsDir();
|
|
7931
8530
|
const synced = [];
|
|
7932
8531
|
let skipped = 0;
|
|
7933
8532
|
for (const entry of entries) {
|
|
@@ -7936,10 +8535,21 @@ var SessionManager = class {
|
|
|
7936
8535
|
skipped += 1;
|
|
7937
8536
|
continue;
|
|
7938
8537
|
}
|
|
7939
|
-
if (
|
|
8538
|
+
if (isSynopsisSession(entry.cwd, synopsisSandboxDir)) {
|
|
7940
8539
|
skipped += 1;
|
|
7941
8540
|
continue;
|
|
7942
8541
|
}
|
|
8542
|
+
const tombstone = await this.tombstones.read(agentId, entry.sessionId).catch(() => void 0);
|
|
8543
|
+
if (tombstone) {
|
|
8544
|
+
if (!shouldResurrectFromUpstream(tombstone, entry.updatedAt)) {
|
|
8545
|
+
skipped += 1;
|
|
8546
|
+
continue;
|
|
8547
|
+
}
|
|
8548
|
+
await this.tombstones.remove(agentId, entry.sessionId).catch(() => void 0);
|
|
8549
|
+
this.logger?.info(
|
|
8550
|
+
`syncFromAgent: resurrecting tombstoned ${agentId}/${entry.sessionId} (upstream updatedAt advanced past ${tombstone.upstreamUpdatedAt ?? "<unset>"})`
|
|
8551
|
+
);
|
|
8552
|
+
}
|
|
7943
8553
|
existing.add(dedupeKey);
|
|
7944
8554
|
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
7945
8555
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -8095,6 +8705,17 @@ var SessionManager = class {
|
|
|
8095
8705
|
session.onClose(({ deleteRecord }) => {
|
|
8096
8706
|
this.sessions.delete(session.sessionId);
|
|
8097
8707
|
if (deleteRecord) {
|
|
8708
|
+
if (session.upstreamSessionId) {
|
|
8709
|
+
void this.tombstones.add({
|
|
8710
|
+
agentId: session.agentId,
|
|
8711
|
+
upstreamSessionId: session.upstreamSessionId,
|
|
8712
|
+
deletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8713
|
+
upstreamUpdatedAt: new Date(session.updatedAt).toISOString(),
|
|
8714
|
+
cwd: session.cwd,
|
|
8715
|
+
title: session.title,
|
|
8716
|
+
reason: "user"
|
|
8717
|
+
}).catch(() => void 0);
|
|
8718
|
+
}
|
|
8098
8719
|
void this.store.delete(session.sessionId).catch(() => void 0);
|
|
8099
8720
|
void this.histories.delete(session.sessionId).catch(() => void 0);
|
|
8100
8721
|
return;
|
|
@@ -8304,6 +8925,35 @@ var SessionManager = class {
|
|
|
8304
8925
|
}
|
|
8305
8926
|
return session;
|
|
8306
8927
|
}
|
|
8928
|
+
// Synchronous SessionListEntry for a resident session. Mirrors the
|
|
8929
|
+
// live-session branch of list() but skips the async history probe:
|
|
8930
|
+
// callers on the attach/new hot path already hold the Session and
|
|
8931
|
+
// don't need the history-derived `interactive` inference (they pass
|
|
8932
|
+
// through the session's own tristate) or the history mtime (the
|
|
8933
|
+
// session's updatedAt is current). Used to build the reconciled
|
|
8934
|
+
// session/new + session/attach response `_meta["hydra-acp"]` from the
|
|
8935
|
+
// same shape session/list emits.
|
|
8936
|
+
liveListEntry(session) {
|
|
8937
|
+
return {
|
|
8938
|
+
sessionId: session.sessionId,
|
|
8939
|
+
upstreamSessionId: session.upstreamSessionId,
|
|
8940
|
+
cwd: session.cwd,
|
|
8941
|
+
title: session.title,
|
|
8942
|
+
agentId: session.agentId,
|
|
8943
|
+
currentModel: session.currentModel,
|
|
8944
|
+
currentUsage: session.currentUsage,
|
|
8945
|
+
parentSessionId: session.parentSessionId,
|
|
8946
|
+
forkedFromSessionId: session.forkedFromSessionId,
|
|
8947
|
+
forkedFromMessageId: session.forkedFromMessageId,
|
|
8948
|
+
originatingClient: session.originatingClient,
|
|
8949
|
+
interactive: session.interactive,
|
|
8950
|
+
updatedAt: new Date(session.updatedAt).toISOString(),
|
|
8951
|
+
attachedClients: session.attachedCount,
|
|
8952
|
+
status: "live",
|
|
8953
|
+
busy: session.turnStartedAt !== void 0,
|
|
8954
|
+
awaitingInput: session.awaitingInput
|
|
8955
|
+
};
|
|
8956
|
+
}
|
|
8307
8957
|
async list(filter = {}) {
|
|
8308
8958
|
const entries = [];
|
|
8309
8959
|
const liveIds = /* @__PURE__ */ new Set();
|
|
@@ -8582,7 +9232,7 @@ var SessionManager = class {
|
|
|
8582
9232
|
);
|
|
8583
9233
|
const sourceMtime = new Date(args.bundle.session.updatedAt);
|
|
8584
9234
|
if (!Number.isNaN(sourceMtime.getTime())) {
|
|
8585
|
-
await
|
|
9235
|
+
await fs14.utimes(paths.historyFile(args.sessionId), sourceMtime, sourceMtime).catch(() => void 0);
|
|
8586
9236
|
}
|
|
8587
9237
|
if (args.bundle.promptHistory && args.bundle.promptHistory.length > 0) {
|
|
8588
9238
|
await saveHistory(
|
|
@@ -8637,6 +9287,17 @@ var SessionManager = class {
|
|
|
8637
9287
|
if (!record) {
|
|
8638
9288
|
return false;
|
|
8639
9289
|
}
|
|
9290
|
+
if (record.upstreamSessionId) {
|
|
9291
|
+
await this.tombstones.add({
|
|
9292
|
+
agentId: record.agentId,
|
|
9293
|
+
upstreamSessionId: record.upstreamSessionId,
|
|
9294
|
+
deletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9295
|
+
upstreamUpdatedAt: record.updatedAt,
|
|
9296
|
+
cwd: record.cwd,
|
|
9297
|
+
title: record.title,
|
|
9298
|
+
reason: "user"
|
|
9299
|
+
}).catch(() => void 0);
|
|
9300
|
+
}
|
|
8640
9301
|
await this.store.delete(sessionId).catch(() => void 0);
|
|
8641
9302
|
return true;
|
|
8642
9303
|
}
|
|
@@ -8846,13 +9507,13 @@ var SessionManager = class {
|
|
|
8846
9507
|
}
|
|
8847
9508
|
}
|
|
8848
9509
|
};
|
|
8849
|
-
function
|
|
9510
|
+
function isSynopsisSession(cwd, sandboxDir) {
|
|
8850
9511
|
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
8851
9512
|
return false;
|
|
8852
9513
|
}
|
|
8853
9514
|
const resolved = path9.resolve(cwd);
|
|
8854
|
-
const
|
|
8855
|
-
return resolved ===
|
|
9515
|
+
const base = path9.resolve(sandboxDir);
|
|
9516
|
+
return resolved === base || resolved.startsWith(base + path9.sep);
|
|
8856
9517
|
}
|
|
8857
9518
|
function mergeForPersistence(session, existing) {
|
|
8858
9519
|
const persistedCommands = session.mergedAvailableCommands().length > 0 ? session.agentOnlyAdvertisedCommands().map((c) => {
|
|
@@ -8977,6 +9638,13 @@ function extractInitialModel(result) {
|
|
|
8977
9638
|
}
|
|
8978
9639
|
}
|
|
8979
9640
|
}
|
|
9641
|
+
const fromConfig = findConfigOptionEntry(result, "model");
|
|
9642
|
+
if (fromConfig) {
|
|
9643
|
+
const cv = asString(fromConfig.currentValue);
|
|
9644
|
+
if (cv) {
|
|
9645
|
+
return cv;
|
|
9646
|
+
}
|
|
9647
|
+
}
|
|
8980
9648
|
return void 0;
|
|
8981
9649
|
}
|
|
8982
9650
|
function asString(value) {
|
|
@@ -8986,6 +9654,22 @@ function asString(value) {
|
|
|
8986
9654
|
const trimmed = value.trim();
|
|
8987
9655
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
8988
9656
|
}
|
|
9657
|
+
function findConfigOptionEntry(result, id) {
|
|
9658
|
+
const list = result.configOptions;
|
|
9659
|
+
if (!Array.isArray(list)) {
|
|
9660
|
+
return void 0;
|
|
9661
|
+
}
|
|
9662
|
+
for (const raw of list) {
|
|
9663
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
9664
|
+
continue;
|
|
9665
|
+
}
|
|
9666
|
+
const entry = raw;
|
|
9667
|
+
if (entry.id === id) {
|
|
9668
|
+
return entry;
|
|
9669
|
+
}
|
|
9670
|
+
}
|
|
9671
|
+
return void 0;
|
|
9672
|
+
}
|
|
8989
9673
|
function nonEmptyOrUndefined(arr) {
|
|
8990
9674
|
return arr.length > 0 ? arr : void 0;
|
|
8991
9675
|
}
|
|
@@ -9021,6 +9705,13 @@ function extractInitialModels(result) {
|
|
|
9021
9705
|
}
|
|
9022
9706
|
}
|
|
9023
9707
|
}
|
|
9708
|
+
const fromConfig = findConfigOptionEntry(result, "model");
|
|
9709
|
+
if (fromConfig) {
|
|
9710
|
+
const parsed = parseModelsList(fromConfig.options);
|
|
9711
|
+
if (parsed.length > 0) {
|
|
9712
|
+
return parsed;
|
|
9713
|
+
}
|
|
9714
|
+
}
|
|
9024
9715
|
return [];
|
|
9025
9716
|
}
|
|
9026
9717
|
function extractInitialModes(result) {
|
|
@@ -9055,6 +9746,13 @@ function extractInitialModes(result) {
|
|
|
9055
9746
|
}
|
|
9056
9747
|
}
|
|
9057
9748
|
}
|
|
9749
|
+
const fromConfig = findConfigOptionEntry(result, "mode");
|
|
9750
|
+
if (fromConfig) {
|
|
9751
|
+
const parsed = parseModesList(fromConfig.options);
|
|
9752
|
+
if (parsed.length > 0) {
|
|
9753
|
+
return parsed;
|
|
9754
|
+
}
|
|
9755
|
+
}
|
|
9058
9756
|
return [];
|
|
9059
9757
|
}
|
|
9060
9758
|
function extractInitialCurrentMode(result) {
|
|
@@ -9085,6 +9783,13 @@ function extractInitialCurrentMode(result) {
|
|
|
9085
9783
|
}
|
|
9086
9784
|
}
|
|
9087
9785
|
}
|
|
9786
|
+
const fromConfig = findConfigOptionEntry(result, "mode");
|
|
9787
|
+
if (fromConfig) {
|
|
9788
|
+
const cv = asString(fromConfig.currentValue);
|
|
9789
|
+
if (cv) {
|
|
9790
|
+
return cv;
|
|
9791
|
+
}
|
|
9792
|
+
}
|
|
9088
9793
|
return void 0;
|
|
9089
9794
|
}
|
|
9090
9795
|
async function restoreCurrentMode(opts) {
|
|
@@ -9155,33 +9860,6 @@ async function restoreCurrentModel(opts) {
|
|
|
9155
9860
|
return agentReportedModel;
|
|
9156
9861
|
}
|
|
9157
9862
|
}
|
|
9158
|
-
function parseModesList(list) {
|
|
9159
|
-
if (!Array.isArray(list)) {
|
|
9160
|
-
return [];
|
|
9161
|
-
}
|
|
9162
|
-
const out = [];
|
|
9163
|
-
for (const raw of list) {
|
|
9164
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
9165
|
-
continue;
|
|
9166
|
-
}
|
|
9167
|
-
const r = raw;
|
|
9168
|
-
const id = asString(r.id) ?? asString(r.modeId);
|
|
9169
|
-
if (!id) {
|
|
9170
|
-
continue;
|
|
9171
|
-
}
|
|
9172
|
-
const mode = { id };
|
|
9173
|
-
const name = asString(r.name);
|
|
9174
|
-
if (name) {
|
|
9175
|
-
mode.name = name;
|
|
9176
|
-
}
|
|
9177
|
-
const description = asString(r.description);
|
|
9178
|
-
if (description) {
|
|
9179
|
-
mode.description = description;
|
|
9180
|
-
}
|
|
9181
|
-
out.push(mode);
|
|
9182
|
-
}
|
|
9183
|
-
return out;
|
|
9184
|
-
}
|
|
9185
9863
|
function findLastTurnComplete(history) {
|
|
9186
9864
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
9187
9865
|
const entry = history[i];
|
|
@@ -9201,7 +9879,7 @@ function findLastTurnComplete(history) {
|
|
|
9201
9879
|
}
|
|
9202
9880
|
async function loadPromptHistorySafely(sessionId) {
|
|
9203
9881
|
try {
|
|
9204
|
-
const raw = await
|
|
9882
|
+
const raw = await fs14.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
9205
9883
|
const out = [];
|
|
9206
9884
|
for (const line of raw.split("\n")) {
|
|
9207
9885
|
if (line.length === 0) {
|
|
@@ -9222,7 +9900,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
9222
9900
|
}
|
|
9223
9901
|
async function historyStatus(sessionId) {
|
|
9224
9902
|
try {
|
|
9225
|
-
const st = await
|
|
9903
|
+
const st = await fs14.stat(paths.historyFile(sessionId));
|
|
9226
9904
|
return {
|
|
9227
9905
|
mtime: new Date(st.mtimeMs).toISOString(),
|
|
9228
9906
|
hasContent: st.size > 0
|
|
@@ -9243,7 +9921,7 @@ function effectiveInteractive(record, hasContent) {
|
|
|
9243
9921
|
|
|
9244
9922
|
// src/core/child-supervisor.ts
|
|
9245
9923
|
import { spawn as spawn4 } from "child_process";
|
|
9246
|
-
import * as
|
|
9924
|
+
import * as fs15 from "fs";
|
|
9247
9925
|
import * as fsp5 from "fs/promises";
|
|
9248
9926
|
import * as path10 from "path";
|
|
9249
9927
|
|
|
@@ -9616,7 +10294,7 @@ var ChildSupervisor = class {
|
|
|
9616
10294
|
}
|
|
9617
10295
|
const cfg = entry.config;
|
|
9618
10296
|
const command = cfg.command.length > 0 ? cfg.command : [cfg.name];
|
|
9619
|
-
const logStream =
|
|
10297
|
+
const logStream = fs15.createWriteStream(
|
|
9620
10298
|
this.adapter.paths.logFile(cfg.name),
|
|
9621
10299
|
{ flags: "a" }
|
|
9622
10300
|
);
|
|
@@ -9672,7 +10350,7 @@ var ChildSupervisor = class {
|
|
|
9672
10350
|
}
|
|
9673
10351
|
if (typeof child.pid === "number") {
|
|
9674
10352
|
try {
|
|
9675
|
-
|
|
10353
|
+
fs15.writeFileSync(
|
|
9676
10354
|
this.adapter.paths.pidFile(cfg.name),
|
|
9677
10355
|
`${child.pid}
|
|
9678
10356
|
`,
|
|
@@ -9698,7 +10376,7 @@ var ChildSupervisor = class {
|
|
|
9698
10376
|
});
|
|
9699
10377
|
child.on("exit", (code, signal) => {
|
|
9700
10378
|
try {
|
|
9701
|
-
|
|
10379
|
+
fs15.unlinkSync(this.adapter.paths.pidFile(cfg.name));
|
|
9702
10380
|
} catch {
|
|
9703
10381
|
}
|
|
9704
10382
|
logStream.write(
|
|
@@ -9808,13 +10486,13 @@ var TRANSFORMER_ADAPTER = {
|
|
|
9808
10486
|
}
|
|
9809
10487
|
};
|
|
9810
10488
|
var TransformerManager = class extends ChildSupervisor {
|
|
9811
|
-
// Transformers that have completed transformer/initialize and are ready to
|
|
10489
|
+
// Transformers that have completed hydra-acp/transformer/initialize and are ready to
|
|
9812
10490
|
// participate in chains. Keyed by transformer name.
|
|
9813
10491
|
connected = /* @__PURE__ */ new Map();
|
|
9814
10492
|
constructor(transformers, context, options = {}) {
|
|
9815
10493
|
super(transformers, TRANSFORMER_ADAPTER, context, options);
|
|
9816
10494
|
}
|
|
9817
|
-
// Called by the WS handler after transformer/initialize completes. The
|
|
10495
|
+
// Called by the WS handler after hydra-acp/transformer/initialize completes. The
|
|
9818
10496
|
// transformer is now eligible to participate in session chains.
|
|
9819
10497
|
registerConnection(name, connection, intercepts) {
|
|
9820
10498
|
this.connected.set(name, {
|
|
@@ -10064,20 +10742,27 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
10064
10742
|
// keyed by hash
|
|
10065
10743
|
writeTimer = null;
|
|
10066
10744
|
writeInflight = null;
|
|
10067
|
-
|
|
10745
|
+
// Bound at construction so a debounced write that fires after the
|
|
10746
|
+
// process's HYDRA_ACP_HOME has changed (e.g. between tests) targets the
|
|
10747
|
+
// path this store was loaded from rather than re-resolving paths.home()
|
|
10748
|
+
// and clobbering an unrelated home's token file.
|
|
10749
|
+
filePath;
|
|
10750
|
+
constructor(records, filePath) {
|
|
10751
|
+
this.filePath = filePath;
|
|
10068
10752
|
for (const r of records) {
|
|
10069
10753
|
this.records.set(r.hash, r);
|
|
10070
10754
|
}
|
|
10071
10755
|
}
|
|
10072
10756
|
static async load() {
|
|
10073
10757
|
let records = [];
|
|
10758
|
+
const filePath = tokensFilePath();
|
|
10074
10759
|
const parsed = await readJsonSafe(
|
|
10075
|
-
|
|
10760
|
+
filePath
|
|
10076
10761
|
);
|
|
10077
10762
|
if (parsed && Array.isArray(parsed.records)) {
|
|
10078
10763
|
records = parsed.records.filter(isRecord);
|
|
10079
10764
|
}
|
|
10080
|
-
const store = new _SessionTokenStore(records);
|
|
10765
|
+
const store = new _SessionTokenStore(records, filePath);
|
|
10081
10766
|
const removed = store.sweepExpired(/* @__PURE__ */ new Date());
|
|
10082
10767
|
if (removed > 0) {
|
|
10083
10768
|
await store.flush();
|
|
@@ -10187,21 +10872,29 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
10187
10872
|
});
|
|
10188
10873
|
}, WRITE_DEBOUNCE_MS);
|
|
10189
10874
|
}
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10875
|
+
// Serialize writes by chaining onto whatever write is in flight. Two
|
|
10876
|
+
// concurrent persists (e.g. a debounced scheduleWrite timer firing while
|
|
10877
|
+
// flush() awaits) would otherwise both write a temp file and race their
|
|
10878
|
+
// renames onto the same path — the older snapshot could win. Chaining
|
|
10879
|
+
// guarantees writes run one after another, each snapshotting records at
|
|
10880
|
+
// the moment it actually runs, so the final on-disk state reflects the
|
|
10881
|
+
// latest records. The returned promise resolves once THIS persist's
|
|
10882
|
+
// write has completed.
|
|
10883
|
+
persist() {
|
|
10884
|
+
const run2 = (this.writeInflight ?? Promise.resolve()).catch(() => void 0).then(
|
|
10885
|
+
() => writeJsonAtomic(
|
|
10886
|
+
this.filePath,
|
|
10887
|
+
{ records: Array.from(this.records.values()) },
|
|
10888
|
+
{ mode: 384 }
|
|
10889
|
+
)
|
|
10199
10890
|
);
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10891
|
+
this.writeInflight = run2;
|
|
10892
|
+
run2.catch(() => void 0).finally(() => {
|
|
10893
|
+
if (this.writeInflight === run2) {
|
|
10894
|
+
this.writeInflight = null;
|
|
10895
|
+
}
|
|
10896
|
+
});
|
|
10897
|
+
return run2;
|
|
10205
10898
|
}
|
|
10206
10899
|
};
|
|
10207
10900
|
function isRecord(value) {
|
|
@@ -10363,6 +11056,7 @@ var AuthRateLimiter = class {
|
|
|
10363
11056
|
|
|
10364
11057
|
// src/daemon/routes/sessions.ts
|
|
10365
11058
|
import * as os3 from "os";
|
|
11059
|
+
import * as path13 from "path";
|
|
10366
11060
|
|
|
10367
11061
|
// src/core/render-update.ts
|
|
10368
11062
|
import stripAnsi from "strip-ansi";
|
|
@@ -10412,10 +11106,52 @@ function mapUpdate(update) {
|
|
|
10412
11106
|
return mapAvailableModes(u);
|
|
10413
11107
|
case "session_info_update":
|
|
10414
11108
|
return mapSessionInfo(u);
|
|
11109
|
+
case "config_option_update":
|
|
11110
|
+
return mapConfigOptions(u);
|
|
10415
11111
|
default:
|
|
10416
11112
|
return { kind: "unknown", sessionUpdate: tag, raw: update };
|
|
10417
11113
|
}
|
|
10418
11114
|
}
|
|
11115
|
+
function mapConfigOptions(u) {
|
|
11116
|
+
const list = u.configOptions;
|
|
11117
|
+
if (!Array.isArray(list)) {
|
|
11118
|
+
return null;
|
|
11119
|
+
}
|
|
11120
|
+
const options = [];
|
|
11121
|
+
for (const raw of list) {
|
|
11122
|
+
if (!raw || typeof raw !== "object") {
|
|
11123
|
+
continue;
|
|
11124
|
+
}
|
|
11125
|
+
const o = raw;
|
|
11126
|
+
if (typeof o.id !== "string" || typeof o.currentValue !== "string" || !Array.isArray(o.options)) {
|
|
11127
|
+
continue;
|
|
11128
|
+
}
|
|
11129
|
+
const values = [];
|
|
11130
|
+
for (const v of o.options) {
|
|
11131
|
+
if (!v || typeof v !== "object") {
|
|
11132
|
+
continue;
|
|
11133
|
+
}
|
|
11134
|
+
const vv = v;
|
|
11135
|
+
if (typeof vv.value !== "string") {
|
|
11136
|
+
continue;
|
|
11137
|
+
}
|
|
11138
|
+
values.push({
|
|
11139
|
+
value: vv.value,
|
|
11140
|
+
name: typeof vv.name === "string" ? vv.name : vv.value,
|
|
11141
|
+
...typeof vv.description === "string" ? { description: vv.description } : {}
|
|
11142
|
+
});
|
|
11143
|
+
}
|
|
11144
|
+
options.push({
|
|
11145
|
+
id: o.id,
|
|
11146
|
+
name: typeof o.name === "string" ? o.name : o.id,
|
|
11147
|
+
type: "select",
|
|
11148
|
+
currentValue: o.currentValue,
|
|
11149
|
+
options: values,
|
|
11150
|
+
...typeof o.category === "string" ? { category: o.category } : {}
|
|
11151
|
+
});
|
|
11152
|
+
}
|
|
11153
|
+
return { kind: "config-options", options };
|
|
11154
|
+
}
|
|
10419
11155
|
function mapSessionInfo(u) {
|
|
10420
11156
|
const rawTitle = readString(u, "title");
|
|
10421
11157
|
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
@@ -11518,6 +12254,53 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
11518
12254
|
}
|
|
11519
12255
|
reply.code(204).send();
|
|
11520
12256
|
});
|
|
12257
|
+
app.post("/v1/sessions/:id/stdin/open", async (request, reply) => {
|
|
12258
|
+
const raw = request.params.id;
|
|
12259
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
12260
|
+
const session = manager.get(id);
|
|
12261
|
+
if (!session) {
|
|
12262
|
+
reply.code(404).send({ error: "session not found" });
|
|
12263
|
+
return reply;
|
|
12264
|
+
}
|
|
12265
|
+
const body = request.body ?? {};
|
|
12266
|
+
const openOpts = {};
|
|
12267
|
+
if (body.mode === "memory" || body.mode === "file") {
|
|
12268
|
+
openOpts.mode = body.mode;
|
|
12269
|
+
}
|
|
12270
|
+
if (typeof body.capacityBytes === "number") {
|
|
12271
|
+
openOpts.capacityBytes = body.capacityBytes;
|
|
12272
|
+
}
|
|
12273
|
+
if (typeof body.fileCapBytes === "number") {
|
|
12274
|
+
openOpts.fileCapBytes = body.fileCapBytes;
|
|
12275
|
+
}
|
|
12276
|
+
if ((openOpts.mode ?? "memory") === "file") {
|
|
12277
|
+
openOpts.filePathFor = (sid) => path13.join(os3.tmpdir(), `hydra-acp-stdin-${sid}.log`);
|
|
12278
|
+
}
|
|
12279
|
+
try {
|
|
12280
|
+
return session.openStream(openOpts);
|
|
12281
|
+
} catch (err) {
|
|
12282
|
+
reply.code(409).send({ error: err.message });
|
|
12283
|
+
return reply;
|
|
12284
|
+
}
|
|
12285
|
+
});
|
|
12286
|
+
app.post("/v1/sessions/:id/stdin", async (request, reply) => {
|
|
12287
|
+
const raw = request.params.id;
|
|
12288
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
12289
|
+
const session = manager.get(id);
|
|
12290
|
+
if (!session) {
|
|
12291
|
+
reply.code(404).send({ error: "session not found" });
|
|
12292
|
+
return reply;
|
|
12293
|
+
}
|
|
12294
|
+
const body = request.body ?? {};
|
|
12295
|
+
const chunk = typeof body.chunk === "string" ? body.chunk : "";
|
|
12296
|
+
const eof = body.eof === true;
|
|
12297
|
+
try {
|
|
12298
|
+
return session.streamWrite(chunk, eof);
|
|
12299
|
+
} catch (err) {
|
|
12300
|
+
reply.code(409).send({ error: err.message });
|
|
12301
|
+
return reply;
|
|
12302
|
+
}
|
|
12303
|
+
});
|
|
11521
12304
|
app.patch("/v1/sessions/:id", async (request, reply) => {
|
|
11522
12305
|
const raw = request.params.id;
|
|
11523
12306
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -11754,22 +12537,7 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
11754
12537
|
// src/daemon/routes/agents.ts
|
|
11755
12538
|
function registerAgentRoutes(app, registry, manager, opts = {}) {
|
|
11756
12539
|
app.get("/v1/agents", async () => {
|
|
11757
|
-
|
|
11758
|
-
const agents = await Promise.all(
|
|
11759
|
-
doc.agents.map(async (a) => ({
|
|
11760
|
-
id: a.id,
|
|
11761
|
-
name: a.name,
|
|
11762
|
-
version: a.version,
|
|
11763
|
-
description: a.description,
|
|
11764
|
-
distributions: Object.keys(a.distribution),
|
|
11765
|
-
installed: await agentInstallState(a)
|
|
11766
|
-
}))
|
|
11767
|
-
);
|
|
11768
|
-
return {
|
|
11769
|
-
version: doc.version,
|
|
11770
|
-
fetchedAt: registry.lastFetchedAt(),
|
|
11771
|
-
agents
|
|
11772
|
-
};
|
|
12540
|
+
return listAgents(registry);
|
|
11773
12541
|
});
|
|
11774
12542
|
app.get("/v1/registry", async () => {
|
|
11775
12543
|
return registry.load();
|
|
@@ -12078,22 +12846,22 @@ function registerConfigRoutes(app, snapshot) {
|
|
|
12078
12846
|
}
|
|
12079
12847
|
|
|
12080
12848
|
// src/daemon/routes/auth.ts
|
|
12081
|
-
import { z as
|
|
12849
|
+
import { z as z8 } from "zod";
|
|
12082
12850
|
|
|
12083
12851
|
// src/core/password.ts
|
|
12084
|
-
import * as
|
|
12085
|
-
import * as
|
|
12852
|
+
import * as fs16 from "fs/promises";
|
|
12853
|
+
import * as path14 from "path";
|
|
12086
12854
|
import { randomBytes as randomBytes3, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
12087
12855
|
import { promisify } from "util";
|
|
12088
12856
|
var scryptAsync = promisify(scrypt);
|
|
12089
12857
|
function passwordHashPath() {
|
|
12090
|
-
return
|
|
12858
|
+
return path14.join(paths.home(), "password-hash");
|
|
12091
12859
|
}
|
|
12092
12860
|
var DEFAULT_N = 1 << 15;
|
|
12093
12861
|
var MAX_MEM = 128 * 1024 * 1024;
|
|
12094
12862
|
async function hasPassword() {
|
|
12095
12863
|
try {
|
|
12096
|
-
const text = await
|
|
12864
|
+
const text = await fs16.readFile(passwordHashPath(), "utf8");
|
|
12097
12865
|
return text.trim().length > 0;
|
|
12098
12866
|
} catch (err) {
|
|
12099
12867
|
const e = err;
|
|
@@ -12109,7 +12877,7 @@ async function verifyPassword(plaintext) {
|
|
|
12109
12877
|
}
|
|
12110
12878
|
let line;
|
|
12111
12879
|
try {
|
|
12112
|
-
line = (await
|
|
12880
|
+
line = (await fs16.readFile(passwordHashPath(), "utf8")).trim();
|
|
12113
12881
|
} catch (err) {
|
|
12114
12882
|
const e = err;
|
|
12115
12883
|
if (e.code === "ENOENT") {
|
|
@@ -12145,13 +12913,13 @@ async function verifyPassword(plaintext) {
|
|
|
12145
12913
|
}
|
|
12146
12914
|
|
|
12147
12915
|
// src/daemon/routes/auth.ts
|
|
12148
|
-
var LoginBody =
|
|
12149
|
-
password:
|
|
12150
|
-
label:
|
|
12151
|
-
ttlSec:
|
|
12916
|
+
var LoginBody = z8.object({
|
|
12917
|
+
password: z8.string().min(1),
|
|
12918
|
+
label: z8.string().min(1).max(256).optional(),
|
|
12919
|
+
ttlSec: z8.number().int().positive().optional()
|
|
12152
12920
|
});
|
|
12153
|
-
var LogoutBody =
|
|
12154
|
-
id:
|
|
12921
|
+
var LogoutBody = z8.object({
|
|
12922
|
+
id: z8.string().optional()
|
|
12155
12923
|
}).optional();
|
|
12156
12924
|
function registerAuthRoutes(app, deps) {
|
|
12157
12925
|
app.post(
|
|
@@ -12303,8 +13071,6 @@ function wsToMessageStream(ws) {
|
|
|
12303
13071
|
}
|
|
12304
13072
|
|
|
12305
13073
|
// src/daemon/acp-ws.ts
|
|
12306
|
-
import * as os4 from "os";
|
|
12307
|
-
import * as path14 from "path";
|
|
12308
13074
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
12309
13075
|
function registerAcpWsEndpoint(app, deps) {
|
|
12310
13076
|
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
@@ -12361,7 +13127,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12361
13127
|
});
|
|
12362
13128
|
if (processIdentity && deps.extensionCommands) {
|
|
12363
13129
|
const registry = deps.extensionCommands;
|
|
12364
|
-
connection.onRequest("hydra-acp/
|
|
13130
|
+
connection.onRequest("hydra-acp/commands/register", async (raw) => {
|
|
12365
13131
|
const params = raw ?? {};
|
|
12366
13132
|
const commands = Array.isArray(params.commands) ? params.commands.map((c) => {
|
|
12367
13133
|
if (!c || typeof c !== "object") {
|
|
@@ -12389,7 +13155,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12389
13155
|
}
|
|
12390
13156
|
if (processIdentity && deps.extensionMcp) {
|
|
12391
13157
|
const mcpRegistry = deps.extensionMcp;
|
|
12392
|
-
connection.onRequest("hydra-acp/
|
|
13158
|
+
connection.onRequest("hydra-acp/mcp_tools/register", async (raw) => {
|
|
12393
13159
|
const params = raw ?? {};
|
|
12394
13160
|
const instructions = typeof params.instructions === "string" ? params.instructions : void 0;
|
|
12395
13161
|
const tools = Array.isArray(params.tools) ? params.tools.map((t) => {
|
|
@@ -12432,7 +13198,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12432
13198
|
});
|
|
12433
13199
|
}
|
|
12434
13200
|
if (processIdentity?.kind === "transformer") {
|
|
12435
|
-
connection.onRequest("transformer/initialize", async (raw) => {
|
|
13201
|
+
connection.onRequest("hydra-acp/transformer/initialize", async (raw) => {
|
|
12436
13202
|
const params = raw ?? {};
|
|
12437
13203
|
const intercepts = Array.isArray(params.intercepts) ? params.intercepts.filter(
|
|
12438
13204
|
(v) => typeof v === "string"
|
|
@@ -12457,7 +13223,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12457
13223
|
connection.onClose(() => {
|
|
12458
13224
|
deps.transformers?.deregisterConnection(processIdentity.name);
|
|
12459
13225
|
});
|
|
12460
|
-
connection.onRequest("hydra-acp/
|
|
13226
|
+
connection.onRequest("hydra-acp/message/emit", async (raw) => {
|
|
12461
13227
|
const params = raw ?? {};
|
|
12462
13228
|
const sessionId = typeof params.sessionId === "string" ? params.sessionId : void 0;
|
|
12463
13229
|
const method = typeof params.method === "string" ? params.method : void 0;
|
|
@@ -12485,7 +13251,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12485
13251
|
}
|
|
12486
13252
|
throw Object.assign(new Error(`unsupported route: ${JSON.stringify(route)}`), { code: -32602 });
|
|
12487
13253
|
});
|
|
12488
|
-
connection.onRequest("hydra-acp/
|
|
13254
|
+
connection.onRequest("hydra-acp/child_session/spawn", async (raw) => {
|
|
12489
13255
|
const params = raw ?? {};
|
|
12490
13256
|
const agentId = typeof params.agentId === "string" ? params.agentId : deps.defaultAgent;
|
|
12491
13257
|
const cwd = typeof params.cwd === "string" ? params.cwd : void 0;
|
|
@@ -12502,7 +13268,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12502
13268
|
});
|
|
12503
13269
|
return { childSessionId: child.sessionId };
|
|
12504
13270
|
});
|
|
12505
|
-
connection.onRequest("hydra-acp/
|
|
13271
|
+
connection.onRequest("hydra-acp/session/fork", async (raw) => {
|
|
12506
13272
|
const params = raw ?? {};
|
|
12507
13273
|
if (typeof params.sessionId !== "string") {
|
|
12508
13274
|
throw Object.assign(
|
|
@@ -12519,7 +13285,30 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12519
13285
|
...agentId !== void 0 ? { agentId } : {}
|
|
12520
13286
|
});
|
|
12521
13287
|
});
|
|
12522
|
-
connection.onRequest("hydra-acp/
|
|
13288
|
+
connection.onRequest("hydra-acp/session/delete", async (raw) => {
|
|
13289
|
+
const params = raw ?? {};
|
|
13290
|
+
if (typeof params.sessionId !== "string") {
|
|
13291
|
+
throw Object.assign(
|
|
13292
|
+
new Error("hydra-acp/session/delete requires sessionId"),
|
|
13293
|
+
{ code: JsonRpcErrorCodes.InvalidParams }
|
|
13294
|
+
);
|
|
13295
|
+
}
|
|
13296
|
+
const id = await deps.manager.resolveCanonicalId(params.sessionId) ?? params.sessionId;
|
|
13297
|
+
const live = deps.manager.get(id);
|
|
13298
|
+
if (live) {
|
|
13299
|
+
await live.close({ deleteRecord: true });
|
|
13300
|
+
return { deleted: true, sessionId: id };
|
|
13301
|
+
}
|
|
13302
|
+
const removed = await deps.manager.deleteRecord(id);
|
|
13303
|
+
if (!removed) {
|
|
13304
|
+
throw Object.assign(
|
|
13305
|
+
new Error(`session ${id} not found`),
|
|
13306
|
+
{ code: JsonRpcErrorCodes.SessionNotFound }
|
|
13307
|
+
);
|
|
13308
|
+
}
|
|
13309
|
+
return { deleted: true, sessionId: id };
|
|
13310
|
+
});
|
|
13311
|
+
connection.onRequest("hydra-acp/child_session/await", async (raw) => {
|
|
12523
13312
|
const params = raw ?? {};
|
|
12524
13313
|
const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
|
|
12525
13314
|
const until = params.until === "idle" ? "idle" : "turn_complete";
|
|
@@ -12558,7 +13347,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12558
13347
|
child.onClose(() => finish());
|
|
12559
13348
|
});
|
|
12560
13349
|
});
|
|
12561
|
-
connection.onRequest("hydra-acp/
|
|
13350
|
+
connection.onRequest("hydra-acp/child_session/close", async (raw) => {
|
|
12562
13351
|
const params = raw ?? {};
|
|
12563
13352
|
const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
|
|
12564
13353
|
if (!childSessionId) {
|
|
@@ -12570,7 +13359,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12570
13359
|
}
|
|
12571
13360
|
return { ok: true };
|
|
12572
13361
|
});
|
|
12573
|
-
connection.onRequest("hydra-acp/keep_alive", async (raw) => {
|
|
13362
|
+
connection.onRequest("hydra-acp/connection/keep_alive", async (raw) => {
|
|
12574
13363
|
const params = raw ?? {};
|
|
12575
13364
|
const token2 = typeof params.token === "string" ? params.token : void 0;
|
|
12576
13365
|
const sessionId = typeof params.sessionId === "string" ? params.sessionId : void 0;
|
|
@@ -12632,9 +13421,9 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12632
13421
|
try {
|
|
12633
13422
|
session = await deps.manager.create({
|
|
12634
13423
|
cwd: params.cwd,
|
|
12635
|
-
agentId:
|
|
13424
|
+
agentId: hydraMeta.agentId ?? deps.defaultAgent,
|
|
12636
13425
|
mcpServers: augmentedMcpServers,
|
|
12637
|
-
title: hydraMeta.
|
|
13426
|
+
title: hydraMeta.title,
|
|
12638
13427
|
agentArgs: hydraMeta.agentArgs,
|
|
12639
13428
|
model: hydraMeta.model,
|
|
12640
13429
|
onInstallProgress: makeInstallProgressForwarder(connection),
|
|
@@ -12685,14 +13474,16 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12685
13474
|
const modelsPayload = buildModelsPayload(session);
|
|
12686
13475
|
return {
|
|
12687
13476
|
sessionId: session.sessionId,
|
|
12688
|
-
// session/new is implicitly an attach; mirror session/attach's
|
|
12689
|
-
// shape by including the clientId so deferred-echo clients
|
|
12690
|
-
// (TUI's queue work) can recognize their own prompt_queue_added
|
|
12691
|
-
// events without an extra round-trip.
|
|
12692
|
-
clientId: client.clientId,
|
|
12693
13477
|
...modesPayload ? { modes: modesPayload } : {},
|
|
12694
13478
|
...modelsPayload ? { models: modelsPayload } : {},
|
|
12695
|
-
|
|
13479
|
+
configOptions: session.buildConfigOptions(),
|
|
13480
|
+
// session/new is a core ACP spec method, so the per-attachment
|
|
13481
|
+
// clientId rides under _meta["hydra-acp"] rather than top-level.
|
|
13482
|
+
// Deferred-echo clients (TUI's queue work) read it from there to
|
|
13483
|
+
// recognize their own prompt_queue_added events.
|
|
13484
|
+
_meta: buildResponseMeta(deps.manager, session, {
|
|
13485
|
+
clientId: client.clientId
|
|
13486
|
+
})
|
|
12696
13487
|
};
|
|
12697
13488
|
});
|
|
12698
13489
|
connection.onRequest("session/attach", async (raw) => {
|
|
@@ -12705,8 +13496,9 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12705
13496
|
deps.onTransformerVersion?.(processIdentity.name, attachVersion);
|
|
12706
13497
|
}
|
|
12707
13498
|
}
|
|
12708
|
-
const
|
|
12709
|
-
const
|
|
13499
|
+
const hydraAttach = extractHydraMeta(params._meta);
|
|
13500
|
+
const hydraHints = hydraAttach.resume;
|
|
13501
|
+
const readonly = hydraAttach.readonly === true;
|
|
12710
13502
|
app.log.info(
|
|
12711
13503
|
`session/attach sessionId=${params.sessionId} hasResumeHints=${!!hydraHints} readonly=${readonly}`
|
|
12712
13504
|
);
|
|
@@ -12781,7 +13573,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12781
13573
|
params.clientInfo,
|
|
12782
13574
|
params.clientId
|
|
12783
13575
|
);
|
|
12784
|
-
const drip =
|
|
13576
|
+
const drip = hydraAttach.replayMode === "drip";
|
|
12785
13577
|
const { entries: replay, appliedPolicy } = await session.attach(
|
|
12786
13578
|
client,
|
|
12787
13579
|
params.historyPolicy,
|
|
@@ -12796,7 +13588,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12796
13588
|
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length} readonly=${readonly}${drip ? " replayMode=drip" : ""}`
|
|
12797
13589
|
);
|
|
12798
13590
|
if (drip) {
|
|
12799
|
-
const speed =
|
|
13591
|
+
const speed = hydraAttach.dripSpeed && hydraAttach.dripSpeed > 0 ? hydraAttach.dripSpeed : 1;
|
|
12800
13592
|
const MAX_GAP_MS = 750;
|
|
12801
13593
|
void (async () => {
|
|
12802
13594
|
let prev = null;
|
|
@@ -12838,7 +13630,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12838
13630
|
replayed: replay.length,
|
|
12839
13631
|
...modesPayload ? { modes: modesPayload } : {},
|
|
12840
13632
|
...modelsPayload ? { models: modelsPayload } : {},
|
|
12841
|
-
|
|
13633
|
+
configOptions: session.buildConfigOptions(),
|
|
13634
|
+
_meta: buildResponseMeta(deps.manager, session)
|
|
12842
13635
|
};
|
|
12843
13636
|
});
|
|
12844
13637
|
connection.onRequest("session/detach", async (raw) => {
|
|
@@ -12855,7 +13648,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12855
13648
|
if (session) {
|
|
12856
13649
|
void deps.manager.reapIfOrphanedNonInteractive(params.sessionId);
|
|
12857
13650
|
}
|
|
12858
|
-
return {
|
|
13651
|
+
return {
|
|
13652
|
+
sessionId: params.sessionId,
|
|
13653
|
+
_meta: { [HYDRA_META_KEY]: { detachStatus: "detached" } }
|
|
13654
|
+
};
|
|
12859
13655
|
});
|
|
12860
13656
|
connection.onRequest("session/list", async (raw) => {
|
|
12861
13657
|
const params = SessionListParams.parse(raw ?? {});
|
|
@@ -12868,6 +13664,14 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12868
13664
|
};
|
|
12869
13665
|
return result;
|
|
12870
13666
|
});
|
|
13667
|
+
connection.onRequest("hydra-acp/agents/list", async () => {
|
|
13668
|
+
if (!deps.registry) {
|
|
13669
|
+
const err = new Error("agent registry unavailable");
|
|
13670
|
+
err.code = JsonRpcErrorCodes.InternalError;
|
|
13671
|
+
throw err;
|
|
13672
|
+
}
|
|
13673
|
+
return listAgents(deps.registry);
|
|
13674
|
+
});
|
|
12871
13675
|
connection.onRequest("session/prompt", async (raw) => {
|
|
12872
13676
|
const params = SessionPromptParams.parse(raw);
|
|
12873
13677
|
denyIfReadonly(params.sessionId, "session/prompt");
|
|
@@ -12943,9 +13747,9 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12943
13747
|
handleCancelParams(raw);
|
|
12944
13748
|
return null;
|
|
12945
13749
|
});
|
|
12946
|
-
connection.onRequest("hydra-acp/
|
|
13750
|
+
connection.onRequest("hydra-acp/prompt/cancel", async (raw) => {
|
|
12947
13751
|
const params = CancelPromptParams.parse(raw);
|
|
12948
|
-
denyIfReadonly(params.sessionId, "hydra-acp/
|
|
13752
|
+
denyIfReadonly(params.sessionId, "hydra-acp/prompt/cancel");
|
|
12949
13753
|
const session = deps.manager.get(params.sessionId);
|
|
12950
13754
|
if (!session) {
|
|
12951
13755
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
@@ -12954,78 +13758,44 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12954
13758
|
}
|
|
12955
13759
|
return session.cancelQueuedPrompt(params.messageId);
|
|
12956
13760
|
});
|
|
12957
|
-
connection.onRequest("hydra-acp/
|
|
12958
|
-
const params =
|
|
12959
|
-
denyIfReadonly(params.sessionId, "hydra-acp/
|
|
12960
|
-
const session = deps.manager.get(params.sessionId);
|
|
12961
|
-
if (!session) {
|
|
12962
|
-
const err = new Error(`session ${params.sessionId} not found`);
|
|
12963
|
-
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12964
|
-
throw err;
|
|
12965
|
-
}
|
|
12966
|
-
return session.updateQueuedPrompt(params.messageId, params.prompt);
|
|
12967
|
-
});
|
|
12968
|
-
connection.onRequest("hydra-acp/amend_prompt", async (raw) => {
|
|
12969
|
-
const params = AmendPromptParams.parse(raw);
|
|
12970
|
-
denyIfReadonly(params.sessionId, "hydra-acp/amend_prompt");
|
|
12971
|
-
const att = state.attached.get(params.sessionId);
|
|
12972
|
-
if (!att) {
|
|
12973
|
-
const err = new Error("not attached to session");
|
|
12974
|
-
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12975
|
-
throw err;
|
|
12976
|
-
}
|
|
13761
|
+
connection.onRequest("hydra-acp/session/force_cancel", async (raw) => {
|
|
13762
|
+
const params = SessionCancelParams.parse(raw);
|
|
13763
|
+
denyIfReadonly(params.sessionId, "hydra-acp/session/force_cancel");
|
|
12977
13764
|
const session = deps.manager.get(params.sessionId);
|
|
12978
13765
|
if (!session) {
|
|
12979
13766
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
12980
13767
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12981
13768
|
throw err;
|
|
12982
13769
|
}
|
|
12983
|
-
return session.
|
|
13770
|
+
return session.forceCancel();
|
|
12984
13771
|
});
|
|
12985
|
-
connection.onRequest("hydra-acp/
|
|
12986
|
-
const params =
|
|
12987
|
-
denyIfReadonly(params.sessionId, "hydra-acp/
|
|
13772
|
+
connection.onRequest("hydra-acp/prompt/update", async (raw) => {
|
|
13773
|
+
const params = UpdatePromptParams.parse(raw);
|
|
13774
|
+
denyIfReadonly(params.sessionId, "hydra-acp/prompt/update");
|
|
12988
13775
|
const session = deps.manager.get(params.sessionId);
|
|
12989
13776
|
if (!session) {
|
|
12990
13777
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
12991
13778
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12992
13779
|
throw err;
|
|
12993
13780
|
}
|
|
12994
|
-
|
|
12995
|
-
if (params.mode !== void 0) {
|
|
12996
|
-
openOpts.mode = params.mode;
|
|
12997
|
-
}
|
|
12998
|
-
if (params.capacityBytes !== void 0) {
|
|
12999
|
-
openOpts.capacityBytes = params.capacityBytes;
|
|
13000
|
-
}
|
|
13001
|
-
if (params.fileCapBytes !== void 0) {
|
|
13002
|
-
openOpts.fileCapBytes = params.fileCapBytes;
|
|
13003
|
-
}
|
|
13004
|
-
if ((params.mode ?? "memory") === "file") {
|
|
13005
|
-
openOpts.filePathFor = (sid) => path14.join(os4.tmpdir(), `hydra-acp-stdin-${sid}.log`);
|
|
13006
|
-
}
|
|
13007
|
-
return session.openStream(openOpts);
|
|
13781
|
+
return session.updateQueuedPrompt(params.messageId, params.prompt);
|
|
13008
13782
|
});
|
|
13009
|
-
connection.onRequest("hydra-acp/
|
|
13010
|
-
const params =
|
|
13011
|
-
denyIfReadonly(params.sessionId, "hydra-acp/
|
|
13012
|
-
const
|
|
13013
|
-
if (!
|
|
13014
|
-
const err = new Error(
|
|
13783
|
+
connection.onRequest("hydra-acp/prompt/amend", async (raw) => {
|
|
13784
|
+
const params = AmendPromptParams.parse(raw);
|
|
13785
|
+
denyIfReadonly(params.sessionId, "hydra-acp/prompt/amend");
|
|
13786
|
+
const att = state.attached.get(params.sessionId);
|
|
13787
|
+
if (!att) {
|
|
13788
|
+
const err = new Error("not attached to session");
|
|
13015
13789
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
13016
13790
|
throw err;
|
|
13017
13791
|
}
|
|
13018
|
-
return session.streamWrite(params.chunk, params.eof);
|
|
13019
|
-
});
|
|
13020
|
-
connection.onRequest("hydra-acp/stream_read", async (raw) => {
|
|
13021
|
-
const params = StreamReadParams.parse(raw);
|
|
13022
13792
|
const session = deps.manager.get(params.sessionId);
|
|
13023
13793
|
if (!session) {
|
|
13024
13794
|
const err = new Error(`session ${params.sessionId} not found`);
|
|
13025
13795
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
13026
13796
|
throw err;
|
|
13027
13797
|
}
|
|
13028
|
-
return session.
|
|
13798
|
+
return session.amendPrompt(att.clientId, params);
|
|
13029
13799
|
});
|
|
13030
13800
|
connection.onRequest("session/load", async (raw) => {
|
|
13031
13801
|
const rawObj = raw ?? {};
|
|
@@ -13064,12 +13834,15 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
13064
13834
|
const modelsPayload = buildModelsPayload(session);
|
|
13065
13835
|
return {
|
|
13066
13836
|
sessionId: session.sessionId,
|
|
13067
|
-
// Same as session/new: include clientId so the deferred-echo
|
|
13068
|
-
// path in queue-aware clients can recognize own broadcasts.
|
|
13069
|
-
clientId: client.clientId,
|
|
13070
13837
|
...modesPayload ? { modes: modesPayload } : {},
|
|
13071
13838
|
...modelsPayload ? { models: modelsPayload } : {},
|
|
13072
|
-
|
|
13839
|
+
configOptions: session.buildConfigOptions(),
|
|
13840
|
+
// session/load is a core ACP spec method: clientId rides under
|
|
13841
|
+
// _meta["hydra-acp"] (not top-level), same as session/new. Lets
|
|
13842
|
+
// deferred-echo clients recognize their own broadcasts.
|
|
13843
|
+
_meta: buildResponseMeta(deps.manager, session, {
|
|
13844
|
+
clientId: client.clientId
|
|
13845
|
+
})
|
|
13073
13846
|
};
|
|
13074
13847
|
});
|
|
13075
13848
|
connection.onRequest("session/set_model", async (rawParams) => {
|
|
@@ -13127,6 +13900,79 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
13127
13900
|
session.applyModeChange(params.modeId);
|
|
13128
13901
|
return result;
|
|
13129
13902
|
});
|
|
13903
|
+
connection.onRequest("session/set_config_option", async (rawParams) => {
|
|
13904
|
+
const params = rawParams;
|
|
13905
|
+
const invalid = (msg) => {
|
|
13906
|
+
const err = new Error(msg);
|
|
13907
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
13908
|
+
return err;
|
|
13909
|
+
};
|
|
13910
|
+
const sessionIdField = params?.sessionId;
|
|
13911
|
+
if (typeof sessionIdField === "string") {
|
|
13912
|
+
denyIfReadonly(sessionIdField, "session/set_config_option");
|
|
13913
|
+
}
|
|
13914
|
+
if (!params || typeof params.sessionId !== "string") {
|
|
13915
|
+
throw invalid("session/set_config_option requires string sessionId");
|
|
13916
|
+
}
|
|
13917
|
+
if (typeof params.configId !== "string") {
|
|
13918
|
+
throw invalid("session/set_config_option requires string configId");
|
|
13919
|
+
}
|
|
13920
|
+
if (typeof params.value !== "string") {
|
|
13921
|
+
throw invalid("session/set_config_option requires string value");
|
|
13922
|
+
}
|
|
13923
|
+
const session = deps.manager.get(params.sessionId);
|
|
13924
|
+
if (!session) {
|
|
13925
|
+
const err = new Error(
|
|
13926
|
+
`session ${params.sessionId} not found`
|
|
13927
|
+
);
|
|
13928
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
13929
|
+
throw err;
|
|
13930
|
+
}
|
|
13931
|
+
const option = session.buildConfigOptions().find((o) => o.id === params.configId);
|
|
13932
|
+
if (!option) {
|
|
13933
|
+
throw invalid(
|
|
13934
|
+
`unknown configId ${JSON.stringify(params.configId)} for this session`
|
|
13935
|
+
);
|
|
13936
|
+
}
|
|
13937
|
+
if (!option.options.some((v) => v.value === params.value)) {
|
|
13938
|
+
throw invalid(
|
|
13939
|
+
`value ${JSON.stringify(params.value)} is not valid for configId ${JSON.stringify(params.configId)}`
|
|
13940
|
+
);
|
|
13941
|
+
}
|
|
13942
|
+
switch (params.configId) {
|
|
13943
|
+
case "model": {
|
|
13944
|
+
if (params.value !== session.currentModel) {
|
|
13945
|
+
await session.forwardRequest("session/set_model", {
|
|
13946
|
+
sessionId: params.sessionId,
|
|
13947
|
+
modelId: params.value
|
|
13948
|
+
});
|
|
13949
|
+
}
|
|
13950
|
+
session.applyModelChange(params.value);
|
|
13951
|
+
break;
|
|
13952
|
+
}
|
|
13953
|
+
case "mode": {
|
|
13954
|
+
if (params.value !== session.currentMode) {
|
|
13955
|
+
await session.forwardRequest("session/set_mode", {
|
|
13956
|
+
sessionId: params.sessionId,
|
|
13957
|
+
modeId: params.value
|
|
13958
|
+
});
|
|
13959
|
+
}
|
|
13960
|
+
session.applyModeChange(params.value);
|
|
13961
|
+
break;
|
|
13962
|
+
}
|
|
13963
|
+
case "agent": {
|
|
13964
|
+
if (params.value !== session.agentId) {
|
|
13965
|
+
await session.setAgent(params.value);
|
|
13966
|
+
}
|
|
13967
|
+
break;
|
|
13968
|
+
}
|
|
13969
|
+
default:
|
|
13970
|
+
throw invalid(
|
|
13971
|
+
`configId ${JSON.stringify(params.configId)} is not settable`
|
|
13972
|
+
);
|
|
13973
|
+
}
|
|
13974
|
+
return { configOptions: session.buildConfigOptions() };
|
|
13975
|
+
});
|
|
13130
13976
|
connection.setDefaultHandler(async (rawParams, method) => {
|
|
13131
13977
|
if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
|
|
13132
13978
|
const err = new Error(`Method not found: ${method}`);
|
|
@@ -13279,81 +14125,57 @@ function decideSetModel(rawParams, manager) {
|
|
|
13279
14125
|
};
|
|
13280
14126
|
}
|
|
13281
14127
|
function buildViewerResponseMeta(fromDisk) {
|
|
13282
|
-
const
|
|
14128
|
+
const entry = {
|
|
14129
|
+
sessionId: fromDisk.hydraSessionId,
|
|
13283
14130
|
upstreamSessionId: fromDisk.upstreamSessionId,
|
|
14131
|
+
cwd: fromDisk.cwd,
|
|
14132
|
+
title: fromDisk.title,
|
|
13284
14133
|
agentId: fromDisk.agentId,
|
|
13285
|
-
|
|
14134
|
+
currentModel: fromDisk.currentModel,
|
|
14135
|
+
currentUsage: fromDisk.currentUsage,
|
|
14136
|
+
forkedFromSessionId: fromDisk.forkedFromSessionId,
|
|
14137
|
+
forkedFromMessageId: fromDisk.forkedFromMessageId,
|
|
14138
|
+
originatingClient: fromDisk.originatingClient,
|
|
14139
|
+
interactive: fromDisk.interactive,
|
|
14140
|
+
updatedAt: fromDisk.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
14141
|
+
attachedClients: 0,
|
|
14142
|
+
status: "cold",
|
|
14143
|
+
busy: false,
|
|
14144
|
+
awaitingInput: false
|
|
13286
14145
|
};
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
}
|
|
13296
|
-
if (fromDisk.currentMode !== void 0) {
|
|
13297
|
-
ours.currentMode = fromDisk.currentMode;
|
|
13298
|
-
}
|
|
13299
|
-
if (fromDisk.currentUsage !== void 0) {
|
|
13300
|
-
ours.currentUsage = fromDisk.currentUsage;
|
|
13301
|
-
}
|
|
13302
|
-
if (fromDisk.agentCommands && fromDisk.agentCommands.length > 0) {
|
|
13303
|
-
ours.availableCommands = fromDisk.agentCommands;
|
|
13304
|
-
}
|
|
13305
|
-
if (fromDisk.agentModes && fromDisk.agentModes.length > 0) {
|
|
13306
|
-
ours.availableModes = fromDisk.agentModes;
|
|
13307
|
-
}
|
|
13308
|
-
if (fromDisk.agentModels && fromDisk.agentModels.length > 0) {
|
|
13309
|
-
ours.availableModels = fromDisk.agentModels;
|
|
13310
|
-
}
|
|
13311
|
-
return { [HYDRA_META_KEY]: ours };
|
|
14146
|
+
const extras = {
|
|
14147
|
+
currentMode: fromDisk.currentMode,
|
|
14148
|
+
agentArgs: fromDisk.agentArgs,
|
|
14149
|
+
availableCommands: fromDisk.agentCommands,
|
|
14150
|
+
availableModes: fromDisk.agentModes,
|
|
14151
|
+
availableModels: fromDisk.agentModels
|
|
14152
|
+
};
|
|
14153
|
+
return { [HYDRA_META_KEY]: buildHydraSessionMeta(entry, extras) };
|
|
13312
14154
|
}
|
|
13313
|
-
function buildResponseMeta(session) {
|
|
13314
|
-
const
|
|
13315
|
-
|
|
13316
|
-
|
|
13317
|
-
|
|
14155
|
+
function buildResponseMeta(manager, session, opts = {}) {
|
|
14156
|
+
const entry = manager.liveListEntry(session);
|
|
14157
|
+
const extras = {
|
|
14158
|
+
clientId: opts.clientId,
|
|
14159
|
+
currentMode: session.currentMode,
|
|
14160
|
+
agentArgs: session.agentArgs,
|
|
14161
|
+
availableCommands: session.mergedAvailableCommands(),
|
|
14162
|
+
availableModes: session.availableModes(),
|
|
14163
|
+
availableModels: session.availableModels(),
|
|
14164
|
+
// Mid-turn at attach time: hand the client the original prompt's
|
|
14165
|
+
// recordedAt so it can boot directly into "busy · Ns" instead of
|
|
14166
|
+
// sitting on "ready" until the next live notification.
|
|
14167
|
+
turnStartedAt: session.turnStartedAt,
|
|
14168
|
+
// The underlying agent's own initialize-time capability claim, captured
|
|
14169
|
+
// verbatim. Lets capability-aware clients (cat --stream) pick the right
|
|
14170
|
+
// consumption surface without re-probing the agent.
|
|
14171
|
+
agentCapabilities: session.agentCapabilities,
|
|
14172
|
+
// Snapshot of the daemon-owned prompt queue. Lets a late attacher
|
|
14173
|
+
// paint queue chips for entries that landed before it joined without
|
|
14174
|
+
// waiting for new prompt_queue_added notifications. Omitted entirely
|
|
14175
|
+
// when the queue is empty (the common case).
|
|
14176
|
+
queue: session.queueSnapshot()
|
|
13318
14177
|
};
|
|
13319
|
-
|
|
13320
|
-
ours.name = session.title;
|
|
13321
|
-
}
|
|
13322
|
-
if (session.agentArgs && session.agentArgs.length > 0) {
|
|
13323
|
-
ours.agentArgs = session.agentArgs;
|
|
13324
|
-
}
|
|
13325
|
-
if (session.currentModel !== void 0) {
|
|
13326
|
-
ours.currentModel = session.currentModel;
|
|
13327
|
-
}
|
|
13328
|
-
if (session.currentMode !== void 0) {
|
|
13329
|
-
ours.currentMode = session.currentMode;
|
|
13330
|
-
}
|
|
13331
|
-
if (session.currentUsage !== void 0) {
|
|
13332
|
-
ours.currentUsage = session.currentUsage;
|
|
13333
|
-
}
|
|
13334
|
-
const commands = session.mergedAvailableCommands();
|
|
13335
|
-
if (commands.length > 0) {
|
|
13336
|
-
ours.availableCommands = commands;
|
|
13337
|
-
}
|
|
13338
|
-
const modes = session.availableModes();
|
|
13339
|
-
if (modes.length > 0) {
|
|
13340
|
-
ours.availableModes = modes;
|
|
13341
|
-
}
|
|
13342
|
-
const models = session.availableModels();
|
|
13343
|
-
if (models.length > 0) {
|
|
13344
|
-
ours.availableModels = models;
|
|
13345
|
-
}
|
|
13346
|
-
if (session.turnStartedAt !== void 0) {
|
|
13347
|
-
ours.turnStartedAt = session.turnStartedAt;
|
|
13348
|
-
}
|
|
13349
|
-
if (session.agentCapabilities !== void 0) {
|
|
13350
|
-
ours.agentCapabilities = session.agentCapabilities;
|
|
13351
|
-
}
|
|
13352
|
-
const queue = session.queueSnapshot();
|
|
13353
|
-
if (queue.length > 0) {
|
|
13354
|
-
ours.queue = queue;
|
|
13355
|
-
}
|
|
13356
|
-
return mergeMeta(session.agentMeta, ours);
|
|
14178
|
+
return mergeMeta(session.agentMeta, buildHydraSessionMeta(entry, extras));
|
|
13357
14179
|
}
|
|
13358
14180
|
function buildInitializeResult() {
|
|
13359
14181
|
return {
|
|
@@ -13384,18 +14206,26 @@ function buildInitializeResult() {
|
|
|
13384
14206
|
description: "Bearer token presented at WS upgrade"
|
|
13385
14207
|
}
|
|
13386
14208
|
],
|
|
13387
|
-
// Advertise hydra-only capabilities via _meta["hydra-acp"]
|
|
13388
|
-
//
|
|
13389
|
-
//
|
|
13390
|
-
//
|
|
13391
|
-
// streaming-input probe lands
|
|
13392
|
-
//
|
|
14209
|
+
// Advertise hydra-only capabilities via _meta["hydra-acp"], grouped by
|
|
14210
|
+
// resource to mirror the hydra-acp/<resource>/<action> method
|
|
14211
|
+
// namespaces. Generic ACP clients ignore the field; capability-aware
|
|
14212
|
+
// clients probe here to gate UI before calling a method. `pipelining`
|
|
14213
|
+
// is false until the streaming-input probe lands; the rest are
|
|
14214
|
+
// unconditional method-availability flags. (Named `prompt`/`agents`,
|
|
14215
|
+
// not `promptCapabilities`/`agentCapabilities` — those are ACP spec
|
|
14216
|
+
// names with different meanings.)
|
|
13393
14217
|
_meta: mergeMeta(void 0, {
|
|
13394
|
-
|
|
13395
|
-
|
|
13396
|
-
|
|
13397
|
-
|
|
13398
|
-
|
|
14218
|
+
prompt: {
|
|
14219
|
+
queueing: true,
|
|
14220
|
+
cancelling: true,
|
|
14221
|
+
updating: true,
|
|
14222
|
+
amending: true,
|
|
14223
|
+
pipelining: false
|
|
14224
|
+
},
|
|
14225
|
+
agents: {
|
|
14226
|
+
list: true,
|
|
14227
|
+
installProgress: true
|
|
14228
|
+
}
|
|
13399
14229
|
})
|
|
13400
14230
|
};
|
|
13401
14231
|
}
|
|
@@ -13493,7 +14323,7 @@ var McpTokenRegistry = class {
|
|
|
13493
14323
|
import { randomUUID } from "crypto";
|
|
13494
14324
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13495
14325
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13496
|
-
import { z as
|
|
14326
|
+
import { z as z9 } from "zod";
|
|
13497
14327
|
|
|
13498
14328
|
// src/daemon/mcp/bearer.ts
|
|
13499
14329
|
var BEARER_PREFIX2 = "Bearer ";
|
|
@@ -13522,7 +14352,7 @@ function buildMcpServer(session) {
|
|
|
13522
14352
|
{
|
|
13523
14353
|
description: "Return the most recent `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means older bytes existed but have been evicted from the ring.",
|
|
13524
14354
|
inputSchema: {
|
|
13525
|
-
bytes:
|
|
14355
|
+
bytes: z9.number().int().min(1).describe("How many trailing bytes to return.")
|
|
13526
14356
|
}
|
|
13527
14357
|
},
|
|
13528
14358
|
async ({ bytes }) => {
|
|
@@ -13543,7 +14373,7 @@ function buildMcpServer(session) {
|
|
|
13543
14373
|
{
|
|
13544
14374
|
description: "Return the first `bytes` bytes of piped stdin (capped server-side, default 64 KiB max). `truncated:true` means the head has already been evicted from the ring and the returned bytes start at the oldest still-resident cursor.",
|
|
13545
14375
|
inputSchema: {
|
|
13546
|
-
bytes:
|
|
14376
|
+
bytes: z9.number().int().min(1).describe("How many leading bytes to return.")
|
|
13547
14377
|
}
|
|
13548
14378
|
},
|
|
13549
14379
|
async ({ bytes }) => {
|
|
@@ -13564,13 +14394,13 @@ function buildMcpServer(session) {
|
|
|
13564
14394
|
{
|
|
13565
14395
|
description: "Read up to `max_bytes` bytes starting at absolute byte `cursor`. Returns `{bytes, nextCursor, gap?, eof?}` \u2014 `gap` is the number of bytes silently skipped because the ring had evicted them; `eof:true` means the producer closed and there is nothing left to read.",
|
|
13566
14396
|
inputSchema: {
|
|
13567
|
-
cursor:
|
|
14397
|
+
cursor: z9.number().int().min(0).describe(
|
|
13568
14398
|
"Absolute byte offset to start reading from. Use 0 to read from the very beginning (may produce a gap if old bytes have been evicted)."
|
|
13569
14399
|
),
|
|
13570
|
-
max_bytes:
|
|
14400
|
+
max_bytes: z9.number().int().min(1).optional().describe(
|
|
13571
14401
|
"Optional cap on how many bytes to return. Server caps at 64 KiB regardless."
|
|
13572
14402
|
),
|
|
13573
|
-
wait_ms:
|
|
14403
|
+
wait_ms: z9.number().int().min(0).optional().describe(
|
|
13574
14404
|
"If no bytes are available, block up to this many ms for more (capped server-side at 60_000)."
|
|
13575
14405
|
)
|
|
13576
14406
|
}
|
|
@@ -13593,8 +14423,8 @@ function buildMcpServer(session) {
|
|
|
13593
14423
|
{
|
|
13594
14424
|
description: "Block until bytes are available past `cursor`, the stream closes, or `timeout_ms` elapses. Returns one of {data, eof, timeout} plus the current `writeCursor`. Use this when you've consumed everything up to a cursor and want to wait for more without busy-polling.",
|
|
13595
14425
|
inputSchema: {
|
|
13596
|
-
cursor:
|
|
13597
|
-
timeout_ms:
|
|
14426
|
+
cursor: z9.number().int().min(0).describe("The cursor you've already consumed up to."),
|
|
14427
|
+
timeout_ms: z9.number().int().min(0).describe("Maximum ms to block (server caps at 60_000).")
|
|
13598
14428
|
}
|
|
13599
14429
|
},
|
|
13600
14430
|
async ({ cursor, timeout_ms }) => {
|
|
@@ -13617,17 +14447,17 @@ function buildMcpServer(session) {
|
|
|
13617
14447
|
{
|
|
13618
14448
|
description: "Scan piped stdin line-by-line and return lines matching `pattern`. Prefer this over `read` when the question is 'find lines that mention X' \u2014 it filters server-side so you don't pull and decode 64 KiB base64 windows. Returns `{matches: [{cursor, line, before?, after?}], truncated, nextCursor, gap?, scannedBytes, eof?}`. Lines come back as decoded UTF-8 strings (not base64). When `truncated:true`, re-call with `cursor: nextCursor` to resume.",
|
|
13619
14449
|
inputSchema: {
|
|
13620
|
-
pattern:
|
|
14450
|
+
pattern: z9.string().min(1).describe(
|
|
13621
14451
|
"Search pattern. Treated as a JavaScript regular expression by default (set `regex:false` for a literal substring match)."
|
|
13622
14452
|
),
|
|
13623
|
-
regex:
|
|
13624
|
-
case_insensitive:
|
|
13625
|
-
invert:
|
|
13626
|
-
max_matches:
|
|
13627
|
-
max_bytes:
|
|
13628
|
-
context_before:
|
|
13629
|
-
context_after:
|
|
13630
|
-
cursor:
|
|
14453
|
+
regex: z9.boolean().optional().describe("Default true. Pass false to treat `pattern` as a literal substring."),
|
|
14454
|
+
case_insensitive: z9.boolean().optional().describe("Default false. Pass true for case-insensitive matching."),
|
|
14455
|
+
invert: z9.boolean().optional().describe("Default false. Pass true to return lines that do NOT match the pattern."),
|
|
14456
|
+
max_matches: z9.number().int().min(1).optional().describe("Default 100. Capped server-side at 1000."),
|
|
14457
|
+
max_bytes: z9.number().int().min(1).optional().describe("Default 64 KiB output. Capped server-side at 256 KiB."),
|
|
14458
|
+
context_before: z9.number().int().min(0).optional().describe("Default 0. Number of lines before each match to include (capped at 20)."),
|
|
14459
|
+
context_after: z9.number().int().min(0).optional().describe("Default 0. Number of lines after each match to include (capped at 20)."),
|
|
14460
|
+
cursor: z9.number().int().min(0).optional().describe(
|
|
13631
14461
|
"Optional absolute byte offset to start scanning from. Omit to scan from the oldest still-resident byte. Pass the `nextCursor` from a previous truncated call to resume."
|
|
13632
14462
|
)
|
|
13633
14463
|
}
|
|
@@ -13884,7 +14714,7 @@ async function invokeWithTimeout(connection, server, tool, args, timeoutMs) {
|
|
|
13884
14714
|
});
|
|
13885
14715
|
try {
|
|
13886
14716
|
return await Promise.race([
|
|
13887
|
-
connection.request("hydra-acp/
|
|
14717
|
+
connection.request("hydra-acp/mcp_tools/invoke", {
|
|
13888
14718
|
server,
|
|
13889
14719
|
tool,
|
|
13890
14720
|
args
|
|
@@ -14161,7 +14991,8 @@ async function startDaemon(config, serviceToken) {
|
|
|
14161
14991
|
extensionCommands,
|
|
14162
14992
|
mcpTokenRegistry,
|
|
14163
14993
|
extensionMcp,
|
|
14164
|
-
getDaemonOrigin
|
|
14994
|
+
getDaemonOrigin,
|
|
14995
|
+
registry
|
|
14165
14996
|
});
|
|
14166
14997
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
14167
14998
|
const address = app.server.address();
|
|
@@ -14221,7 +15052,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
14221
15052
|
setAgentPruneLogger(null);
|
|
14222
15053
|
await app.close();
|
|
14223
15054
|
try {
|
|
14224
|
-
|
|
15055
|
+
fs17.unlinkSync(paths.pidFile());
|
|
14225
15056
|
} catch {
|
|
14226
15057
|
}
|
|
14227
15058
|
try {
|