@hydra-acp/cli 0.1.44 → 0.1.46
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 +60 -6
- package/dist/cli.js +5726 -2916
- package/dist/index.d.ts +242 -7
- package/dist/index.js +2271 -182
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/daemon/server.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
2
|
+
import * as fs15 from "fs";
|
|
3
|
+
import * as fsp8 from "fs/promises";
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import websocketPlugin from "@fastify/websocket";
|
|
6
6
|
|
|
@@ -86,6 +86,13 @@ var paths = {
|
|
|
86
86
|
extensionsDir: () => path.join(hydraHome(), "extensions"),
|
|
87
87
|
extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
|
|
88
88
|
extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
|
|
89
|
+
transformersDir: () => path.join(hydraHome(), "transformers"),
|
|
90
|
+
transformerLogFile: (name) => path.join(hydraHome(), "transformers", `${name}.log`),
|
|
91
|
+
transformerPidFile: (name) => path.join(hydraHome(), "transformers", `${name}.pid`),
|
|
92
|
+
// Per-session scratch directory for transformer state. Each transformer
|
|
93
|
+
// gets an isolated directory keyed by session + transformer name so
|
|
94
|
+
// multiple transformers on the same session don't collide.
|
|
95
|
+
transformerState: (sessionId, transformerName) => path.join(hydraHome(), "sessions", sessionId, "transformer-state", transformerName),
|
|
89
96
|
tuiHistoryFile: (id) => path.join(hydraHome(), "sessions", id, "prompt-history"),
|
|
90
97
|
// Cross-session prompt history. Up-arrow / ^R fall through to this
|
|
91
98
|
// after the per-session list is exhausted. JSONL, one entry per
|
|
@@ -166,7 +173,7 @@ var DaemonConfig = z.object({
|
|
|
166
173
|
// Compaction trims to this many on a periodic basis; reads also slice
|
|
167
174
|
// to the tail at this length as a defensive measure against older
|
|
168
175
|
// daemons that may have written unbounded files.
|
|
169
|
-
sessionHistoryMaxEntries: z.number().int().positive().default(
|
|
176
|
+
sessionHistoryMaxEntries: z.number().int().positive().default(1e4),
|
|
170
177
|
// Bytes of trailing agent stderr buffered per AgentInstance so the
|
|
171
178
|
// daemon can include it in the diagnostic message when a spawn fails.
|
|
172
179
|
// Bump if your agents emit large tracebacks you want surfaced.
|
|
@@ -226,7 +233,13 @@ var TuiConfig = z.object({
|
|
|
226
233
|
// streaming lines beneath the live thinking block. Set false to
|
|
227
234
|
// suppress them — the TUI hotkey ^T toggles this at runtime without
|
|
228
235
|
// persisting back to config.
|
|
229
|
-
showThoughts: z.boolean().default(true)
|
|
236
|
+
showThoughts: z.boolean().default(true),
|
|
237
|
+
// Cap on entries kept in the cross-session global prompt-history file
|
|
238
|
+
// (~/.hydra-acp/prompt-history). This is the ^P / ^R recall list
|
|
239
|
+
// shared across all sessions; it's append-only on disk, so long-lived
|
|
240
|
+
// installs can grow past this — it's enforced at load time and per
|
|
241
|
+
// append in memory.
|
|
242
|
+
promptHistoryMaxEntries: z.number().int().positive().default(2e3)
|
|
230
243
|
});
|
|
231
244
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
232
245
|
var ExtensionBody = z.object({
|
|
@@ -238,6 +251,12 @@ var ExtensionBody = z.object({
|
|
|
238
251
|
env: z.record(z.string()).default({}),
|
|
239
252
|
enabled: z.boolean().default(true)
|
|
240
253
|
});
|
|
254
|
+
var TransformerBody = z.object({
|
|
255
|
+
command: z.array(z.string()).default([]),
|
|
256
|
+
args: z.array(z.string()).default([]),
|
|
257
|
+
env: z.record(z.string()).default({}),
|
|
258
|
+
enabled: z.boolean().default(true)
|
|
259
|
+
});
|
|
241
260
|
var HydraConfig = z.object({
|
|
242
261
|
daemon: DaemonConfig.default({}),
|
|
243
262
|
registry: RegistryConfig.default({ url: REGISTRY_URL_DEFAULT, ttlHours: 24 }),
|
|
@@ -259,6 +278,8 @@ var HydraConfig = z.object({
|
|
|
259
278
|
// recency and truncated to this count. `--all` overrides in the CLI.
|
|
260
279
|
sessionListColdLimit: z.number().int().nonnegative().default(20),
|
|
261
280
|
extensions: z.record(ExtensionName, ExtensionBody).default({}),
|
|
281
|
+
transformers: z.record(ExtensionName, TransformerBody).default({}),
|
|
282
|
+
defaultTransformers: z.array(z.string()).default([]),
|
|
262
283
|
// npm registry URL used when installing npm-distributed agents into
|
|
263
284
|
// ~/.hydra-acp/agents. Overrides the global ~/.npmrc registry so a
|
|
264
285
|
// corporate .npmrc pointing at an internal registry doesn't break
|
|
@@ -272,7 +293,8 @@ var HydraConfig = z.object({
|
|
|
272
293
|
cwdColumnMaxWidth: 24,
|
|
273
294
|
progressIndicator: true,
|
|
274
295
|
defaultEnterAction: "amend",
|
|
275
|
-
showThoughts: true
|
|
296
|
+
showThoughts: true,
|
|
297
|
+
promptHistoryMaxEntries: 2e3
|
|
276
298
|
})
|
|
277
299
|
});
|
|
278
300
|
function extensionList(config) {
|
|
@@ -281,6 +303,12 @@ function extensionList(config) {
|
|
|
281
303
|
...body
|
|
282
304
|
}));
|
|
283
305
|
}
|
|
306
|
+
function transformerList(config) {
|
|
307
|
+
return Object.entries(config.transformers).map(([name, body]) => ({
|
|
308
|
+
name,
|
|
309
|
+
...body
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
284
312
|
async function readConfigFile() {
|
|
285
313
|
let raw;
|
|
286
314
|
try {
|
|
@@ -1138,7 +1166,8 @@ var JsonRpcErrorCodes = {
|
|
|
1138
1166
|
// can't collide with future spec assignments.
|
|
1139
1167
|
BundleAlreadyImported: -32010,
|
|
1140
1168
|
PermissionDenied: -32011,
|
|
1141
|
-
AlreadyAttached: -32012
|
|
1169
|
+
AlreadyAttached: -32012,
|
|
1170
|
+
StreamNotEnabled: -32013
|
|
1142
1171
|
};
|
|
1143
1172
|
var InitializeParams = z3.object({
|
|
1144
1173
|
protocolVersion: z3.number().optional(),
|
|
@@ -1219,6 +1248,9 @@ function extractHydraMeta(meta) {
|
|
|
1219
1248
|
if (Array.isArray(obj.agentArgs) && obj.agentArgs.every((a) => typeof a === "string")) {
|
|
1220
1249
|
out.agentArgs = obj.agentArgs;
|
|
1221
1250
|
}
|
|
1251
|
+
if (Array.isArray(obj.transformers) && obj.transformers.every((t) => typeof t === "string")) {
|
|
1252
|
+
out.transformers = obj.transformers;
|
|
1253
|
+
}
|
|
1222
1254
|
if (obj.resume) {
|
|
1223
1255
|
const parsed = SessionResumeHints.safeParse(obj.resume);
|
|
1224
1256
|
if (parsed.success) {
|
|
@@ -1385,6 +1417,8 @@ var SessionListEntry = z3.object({
|
|
|
1385
1417
|
// future "connect back to origin" callers would dial both.
|
|
1386
1418
|
importedFromMachine: z3.string().optional(),
|
|
1387
1419
|
importedFromUpstreamSessionId: z3.string().optional(),
|
|
1420
|
+
// Set when this session was spawned as a child by a transformer.
|
|
1421
|
+
parentSessionId: z3.string().optional(),
|
|
1388
1422
|
updatedAt: z3.string(),
|
|
1389
1423
|
attachedClients: z3.number().int().nonnegative(),
|
|
1390
1424
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
@@ -1518,6 +1552,65 @@ var PromptAmendedParams = z3.object({
|
|
|
1518
1552
|
originator: PromptOriginatorSchema,
|
|
1519
1553
|
amendedAt: z3.number()
|
|
1520
1554
|
});
|
|
1555
|
+
var StreamOpenParams = z3.object({
|
|
1556
|
+
sessionId: z3.string(),
|
|
1557
|
+
// 'memory' keeps the ring in RAM only — needed for the eventual MCP
|
|
1558
|
+
// tool surface. 'file' adds a temp file projection that the agent can
|
|
1559
|
+
// consume with shell tools (tail -f / head / grep) when MCP isn't
|
|
1560
|
+
// available. The temp file's path is returned in the response.
|
|
1561
|
+
mode: z3.enum(["memory", "file"]).optional(),
|
|
1562
|
+
// Ring capacity in bytes. Server clamps to a reasonable minimum and
|
|
1563
|
+
// its configured max; omitted falls back to the daemon default.
|
|
1564
|
+
capacityBytes: z3.number().int().positive().optional(),
|
|
1565
|
+
// File mode only. Soft cap in bytes; after this many bytes are
|
|
1566
|
+
// written to the file, further appends still land in the ring but
|
|
1567
|
+
// stop being mirrored to disk. The daemon emits one stream_truncated
|
|
1568
|
+
// session/update notification when the cap is first hit.
|
|
1569
|
+
fileCapBytes: z3.number().int().positive().optional()
|
|
1570
|
+
});
|
|
1571
|
+
var StreamOpenResult = z3.object({
|
|
1572
|
+
// Only present when mode === "file".
|
|
1573
|
+
filePath: z3.string().optional(),
|
|
1574
|
+
capacityBytes: z3.number().int().positive(),
|
|
1575
|
+
fileCapBytes: z3.number().int().positive().optional()
|
|
1576
|
+
});
|
|
1577
|
+
var StreamWriteParams = z3.object({
|
|
1578
|
+
sessionId: z3.string(),
|
|
1579
|
+
// Base64-encoded bytes. UTF-8 stdin gets re-encoded on the wire; the
|
|
1580
|
+
// ring is byte-exact so binary streams (audio, framed protocols) work
|
|
1581
|
+
// identically.
|
|
1582
|
+
chunk: z3.string(),
|
|
1583
|
+
// True on the final write. Pending long-poll reads / waits return with
|
|
1584
|
+
// eof:true once this is observed.
|
|
1585
|
+
eof: z3.boolean().optional()
|
|
1586
|
+
});
|
|
1587
|
+
var StreamWriteResult = z3.object({
|
|
1588
|
+
// Absolute writeCursor after this append landed.
|
|
1589
|
+
writeCursor: z3.number().int().nonnegative()
|
|
1590
|
+
});
|
|
1591
|
+
var StreamReadParams = z3.object({
|
|
1592
|
+
sessionId: z3.string(),
|
|
1593
|
+
cursor: z3.number().int().nonnegative(),
|
|
1594
|
+
// Cap on bytes returned. Server enforces a hard ceiling (STREAM_READ_MAX_BYTES,
|
|
1595
|
+
// currently 64 KiB) even when the caller asks for more.
|
|
1596
|
+
maxBytes: z3.number().int().positive().optional(),
|
|
1597
|
+
// Long-poll timeout in ms. 0 / omitted returns immediately with
|
|
1598
|
+
// whatever's available (possibly empty). Server cap 60s.
|
|
1599
|
+
waitMs: z3.number().int().nonnegative().optional()
|
|
1600
|
+
});
|
|
1601
|
+
var StreamReadResult = z3.object({
|
|
1602
|
+
// Base64-encoded bytes. Empty string when nothing new is available
|
|
1603
|
+
// and either waitMs was 0 or the long-poll expired without data.
|
|
1604
|
+
bytes: z3.string(),
|
|
1605
|
+
nextCursor: z3.number().int().nonnegative(),
|
|
1606
|
+
// Set when `cursor` pointed before the oldest still-resident byte —
|
|
1607
|
+
// value is the count of bytes that were evicted between the caller's
|
|
1608
|
+
// cursor and what we still have.
|
|
1609
|
+
gap: z3.number().int().nonnegative().optional(),
|
|
1610
|
+
// True when the producer has closed AND there are no more bytes
|
|
1611
|
+
// after nextCursor.
|
|
1612
|
+
eof: z3.boolean().optional()
|
|
1613
|
+
});
|
|
1521
1614
|
var AgentInstallProgressParams = z3.object({
|
|
1522
1615
|
agentId: z3.string(),
|
|
1523
1616
|
version: z3.string(),
|
|
@@ -1923,6 +2016,264 @@ import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
|
1923
2016
|
// src/core/session.ts
|
|
1924
2017
|
import { customAlphabet } from "nanoid";
|
|
1925
2018
|
|
|
2019
|
+
// src/core/stream-buffer.ts
|
|
2020
|
+
import * as fsp3 from "fs/promises";
|
|
2021
|
+
var DEFAULT_CAPACITY_BYTES = 16 * 1024 * 1024;
|
|
2022
|
+
var STREAM_READ_MAX_BYTES = 64 * 1024;
|
|
2023
|
+
var STREAM_WAIT_MAX_MS = 6e4;
|
|
2024
|
+
var SessionStreamBuffer = class {
|
|
2025
|
+
storage;
|
|
2026
|
+
capacityBytes;
|
|
2027
|
+
// Absolute monotonic byte offset of the next byte to be written. Also
|
|
2028
|
+
// the count of bytes ever appended. `writeCursor - capacityBytes`
|
|
2029
|
+
// (clamped at 0) is the oldest still-resident byte's cursor.
|
|
2030
|
+
writeCursor = 0;
|
|
2031
|
+
closed = false;
|
|
2032
|
+
waiters = [];
|
|
2033
|
+
filePath;
|
|
2034
|
+
fileCapBytes;
|
|
2035
|
+
fileBytesWritten = 0;
|
|
2036
|
+
fileCapReached = false;
|
|
2037
|
+
onFileCapReached;
|
|
2038
|
+
logWriteError;
|
|
2039
|
+
// Single-flight chain for file appends so concurrent stream_write
|
|
2040
|
+
// calls don't interleave their writes.
|
|
2041
|
+
fileWriteChain = Promise.resolve();
|
|
2042
|
+
constructor(opts = {}) {
|
|
2043
|
+
this.capacityBytes = opts.capacityBytes ?? DEFAULT_CAPACITY_BYTES;
|
|
2044
|
+
if (this.capacityBytes <= 0) {
|
|
2045
|
+
throw new Error("capacityBytes must be > 0");
|
|
2046
|
+
}
|
|
2047
|
+
this.storage = Buffer.alloc(this.capacityBytes);
|
|
2048
|
+
this.filePath = opts.filePath;
|
|
2049
|
+
this.fileCapBytes = opts.fileCapBytes ?? Number.POSITIVE_INFINITY;
|
|
2050
|
+
this.onFileCapReached = opts.onFileCapReached;
|
|
2051
|
+
this.logWriteError = opts.logWriteError;
|
|
2052
|
+
}
|
|
2053
|
+
get capacity() {
|
|
2054
|
+
return this.capacityBytes;
|
|
2055
|
+
}
|
|
2056
|
+
get writeCursorPos() {
|
|
2057
|
+
return this.writeCursor;
|
|
2058
|
+
}
|
|
2059
|
+
get oldestAvailable() {
|
|
2060
|
+
return Math.max(0, this.writeCursor - this.capacityBytes);
|
|
2061
|
+
}
|
|
2062
|
+
get isClosed() {
|
|
2063
|
+
return this.closed;
|
|
2064
|
+
}
|
|
2065
|
+
// Append-or-noop. Calls after close() are silently dropped (the
|
|
2066
|
+
// producer ought not to keep writing, but it's not worth throwing if
|
|
2067
|
+
// a chunk arrives late).
|
|
2068
|
+
append(chunk) {
|
|
2069
|
+
if (this.closed || chunk.length === 0) {
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
this.writeRing(chunk);
|
|
2073
|
+
this.writeCursor += chunk.length;
|
|
2074
|
+
if (this.filePath !== void 0) {
|
|
2075
|
+
this.scheduleFileWrite(chunk);
|
|
2076
|
+
}
|
|
2077
|
+
this.wakeWaiters("data");
|
|
2078
|
+
}
|
|
2079
|
+
close() {
|
|
2080
|
+
if (this.closed) {
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
this.closed = true;
|
|
2084
|
+
this.wakeWaiters("eof");
|
|
2085
|
+
}
|
|
2086
|
+
// Read up to `maxBytes` bytes starting at `cursor`. If `cursor` is
|
|
2087
|
+
// behind the oldest still-resident byte, gap-skip to the oldest and
|
|
2088
|
+
// report how many bytes were dropped. If `cursor` is at writeCursor
|
|
2089
|
+
// and the buffer is closed, return eof:true.
|
|
2090
|
+
read(cursor, maxBytes) {
|
|
2091
|
+
const cap = Math.max(0, Math.min(maxBytes, STREAM_READ_MAX_BYTES));
|
|
2092
|
+
if (cap === 0) {
|
|
2093
|
+
const tail = {
|
|
2094
|
+
bytes: Buffer.alloc(0),
|
|
2095
|
+
nextCursor: cursor
|
|
2096
|
+
};
|
|
2097
|
+
if (this.closed && cursor >= this.writeCursor) {
|
|
2098
|
+
tail.eof = true;
|
|
2099
|
+
}
|
|
2100
|
+
return tail;
|
|
2101
|
+
}
|
|
2102
|
+
let from = cursor;
|
|
2103
|
+
let gap = 0;
|
|
2104
|
+
const oldest = this.oldestAvailable;
|
|
2105
|
+
if (from < oldest) {
|
|
2106
|
+
gap = oldest - from;
|
|
2107
|
+
from = oldest;
|
|
2108
|
+
}
|
|
2109
|
+
const available = this.writeCursor - from;
|
|
2110
|
+
if (available <= 0) {
|
|
2111
|
+
const result2 = {
|
|
2112
|
+
bytes: Buffer.alloc(0),
|
|
2113
|
+
nextCursor: from
|
|
2114
|
+
};
|
|
2115
|
+
if (gap > 0) {
|
|
2116
|
+
result2.gap = gap;
|
|
2117
|
+
}
|
|
2118
|
+
if (this.closed) {
|
|
2119
|
+
result2.eof = true;
|
|
2120
|
+
}
|
|
2121
|
+
return result2;
|
|
2122
|
+
}
|
|
2123
|
+
const take = Math.min(available, cap);
|
|
2124
|
+
const bytes = this.sliceFromRing(from, take);
|
|
2125
|
+
const result = {
|
|
2126
|
+
bytes,
|
|
2127
|
+
nextCursor: from + take
|
|
2128
|
+
};
|
|
2129
|
+
if (gap > 0) {
|
|
2130
|
+
result.gap = gap;
|
|
2131
|
+
}
|
|
2132
|
+
if (this.closed && from + take >= this.writeCursor) {
|
|
2133
|
+
result.eof = true;
|
|
2134
|
+
}
|
|
2135
|
+
return result;
|
|
2136
|
+
}
|
|
2137
|
+
// Latest N bytes from the tail, capped at capacity / STREAM_READ_MAX_BYTES.
|
|
2138
|
+
// truncated:true when the requested span extends past the oldest
|
|
2139
|
+
// still-resident byte (i.e. there was more upstream that we don't have
|
|
2140
|
+
// anymore).
|
|
2141
|
+
tail(bytes) {
|
|
2142
|
+
const want = Math.max(0, Math.min(bytes, STREAM_READ_MAX_BYTES));
|
|
2143
|
+
const oldest = this.oldestAvailable;
|
|
2144
|
+
const startWant = this.writeCursor - want;
|
|
2145
|
+
const start = Math.max(oldest, startWant);
|
|
2146
|
+
const truncated = startWant < oldest;
|
|
2147
|
+
const slice = this.sliceFromRing(start, this.writeCursor - start);
|
|
2148
|
+
return {
|
|
2149
|
+
bytes: slice,
|
|
2150
|
+
startCursor: start,
|
|
2151
|
+
endCursor: this.writeCursor,
|
|
2152
|
+
truncated
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
// First N bytes since the stream began. Returns truncated:true when
|
|
2156
|
+
// the head has already been evicted (cursor 0 is no longer resident).
|
|
2157
|
+
head(bytes) {
|
|
2158
|
+
const want = Math.max(0, Math.min(bytes, STREAM_READ_MAX_BYTES));
|
|
2159
|
+
const oldest = this.oldestAvailable;
|
|
2160
|
+
const truncated = oldest > 0;
|
|
2161
|
+
const start = oldest;
|
|
2162
|
+
const end = Math.min(this.writeCursor, start + want);
|
|
2163
|
+
const slice = this.sliceFromRing(start, end - start);
|
|
2164
|
+
return {
|
|
2165
|
+
bytes: slice,
|
|
2166
|
+
startCursor: start,
|
|
2167
|
+
endCursor: end,
|
|
2168
|
+
truncated
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
// Long-poll until new bytes arrive past `cursor`, the buffer closes, or
|
|
2172
|
+
// the timeout expires. Resolves with "data" / "eof" / "timeout".
|
|
2173
|
+
waitForData(cursor, timeoutMs) {
|
|
2174
|
+
if (cursor < this.writeCursor) {
|
|
2175
|
+
return Promise.resolve("data");
|
|
2176
|
+
}
|
|
2177
|
+
if (this.closed) {
|
|
2178
|
+
return Promise.resolve("eof");
|
|
2179
|
+
}
|
|
2180
|
+
const cap = Math.max(0, Math.min(timeoutMs, STREAM_WAIT_MAX_MS));
|
|
2181
|
+
if (cap === 0) {
|
|
2182
|
+
return Promise.resolve("timeout");
|
|
2183
|
+
}
|
|
2184
|
+
return new Promise((resolve3) => {
|
|
2185
|
+
const waiter = {
|
|
2186
|
+
resolve: (outcome) => {
|
|
2187
|
+
if (waiter.timer !== void 0) {
|
|
2188
|
+
clearTimeout(waiter.timer);
|
|
2189
|
+
waiter.timer = void 0;
|
|
2190
|
+
}
|
|
2191
|
+
resolve3(outcome);
|
|
2192
|
+
},
|
|
2193
|
+
timer: setTimeout(() => {
|
|
2194
|
+
const idx = this.waiters.indexOf(waiter);
|
|
2195
|
+
if (idx >= 0) {
|
|
2196
|
+
this.waiters.splice(idx, 1);
|
|
2197
|
+
}
|
|
2198
|
+
waiter.timer = void 0;
|
|
2199
|
+
resolve3("timeout");
|
|
2200
|
+
}, cap)
|
|
2201
|
+
};
|
|
2202
|
+
this.waiters.push(waiter);
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
wakeWaiters(outcome) {
|
|
2206
|
+
if (this.waiters.length === 0) {
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
const wake = this.waiters;
|
|
2210
|
+
this.waiters = [];
|
|
2211
|
+
for (const w of wake) {
|
|
2212
|
+
w.resolve(outcome);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
writeRing(chunk) {
|
|
2216
|
+
const len = chunk.length;
|
|
2217
|
+
if (len >= this.capacityBytes) {
|
|
2218
|
+
const tailStart = len - this.capacityBytes;
|
|
2219
|
+
chunk.copy(this.storage, 0, tailStart, len);
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const offset = this.writeCursor % this.capacityBytes;
|
|
2223
|
+
const tailRoom = this.capacityBytes - offset;
|
|
2224
|
+
if (len <= tailRoom) {
|
|
2225
|
+
chunk.copy(this.storage, offset, 0, len);
|
|
2226
|
+
} else {
|
|
2227
|
+
chunk.copy(this.storage, offset, 0, tailRoom);
|
|
2228
|
+
chunk.copy(this.storage, 0, tailRoom, len);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
sliceFromRing(fromCursor, length) {
|
|
2232
|
+
if (length <= 0) {
|
|
2233
|
+
return Buffer.alloc(0);
|
|
2234
|
+
}
|
|
2235
|
+
const out = Buffer.alloc(length);
|
|
2236
|
+
const offset = fromCursor % this.capacityBytes;
|
|
2237
|
+
const tailLen = Math.min(length, this.capacityBytes - offset);
|
|
2238
|
+
this.storage.copy(out, 0, offset, offset + tailLen);
|
|
2239
|
+
if (tailLen < length) {
|
|
2240
|
+
this.storage.copy(out, tailLen, 0, length - tailLen);
|
|
2241
|
+
}
|
|
2242
|
+
return out;
|
|
2243
|
+
}
|
|
2244
|
+
scheduleFileWrite(chunk) {
|
|
2245
|
+
const path13 = this.filePath;
|
|
2246
|
+
if (path13 === void 0) {
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
if (this.fileCapReached) {
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
const remaining = this.fileCapBytes - this.fileBytesWritten;
|
|
2253
|
+
if (remaining <= 0) {
|
|
2254
|
+
this.fileCapReached = true;
|
|
2255
|
+
this.onFileCapReached?.();
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
const slice = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining);
|
|
2259
|
+
this.fileBytesWritten += slice.length;
|
|
2260
|
+
const willHitCap = this.fileBytesWritten >= this.fileCapBytes;
|
|
2261
|
+
this.fileWriteChain = this.fileWriteChain.then(() => fsp3.appendFile(path13, slice)).catch((err) => {
|
|
2262
|
+
this.logWriteError?.(err);
|
|
2263
|
+
});
|
|
2264
|
+
if (willHitCap && !this.fileCapReached) {
|
|
2265
|
+
this.fileCapReached = true;
|
|
2266
|
+
this.onFileCapReached?.();
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
// Wait for any pending file appends to flush. Used by tests and by
|
|
2270
|
+
// session close handlers that want to ensure the file is durable
|
|
2271
|
+
// before unlinking.
|
|
2272
|
+
async drainFileWrites() {
|
|
2273
|
+
await this.fileWriteChain.catch(() => void 0);
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
|
|
1926
2277
|
// src/core/hydra-commands.ts
|
|
1927
2278
|
var HYDRA_COMMANDS = [
|
|
1928
2279
|
{
|
|
@@ -1940,6 +2291,11 @@ var HYDRA_COMMANDS = [
|
|
|
1940
2291
|
verb: "kill",
|
|
1941
2292
|
name: "hydra kill",
|
|
1942
2293
|
description: "Close this session (kills the agent; record is kept so it can be resumed later)"
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
verb: "restart",
|
|
2297
|
+
name: "hydra restart",
|
|
2298
|
+
description: "Restart the agent with a fresh session/new while preserving conversation history (useful when the proxy has changed available models)"
|
|
1943
2299
|
}
|
|
1944
2300
|
];
|
|
1945
2301
|
var VERB_INDEX = new Map(HYDRA_COMMANDS.map((c) => [c.verb, c]));
|
|
@@ -1992,8 +2348,10 @@ async function deleteQueue(sessionId) {
|
|
|
1992
2348
|
}
|
|
1993
2349
|
|
|
1994
2350
|
// src/core/session.ts
|
|
2351
|
+
import * as fsp4 from "fs/promises";
|
|
1995
2352
|
var HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
1996
2353
|
var generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
2354
|
+
var generateChainToken = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
1997
2355
|
var HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
1998
2356
|
function generateMessageId() {
|
|
1999
2357
|
return `m_${generateHydraId()}`;
|
|
@@ -2002,6 +2360,7 @@ function stripHydraSessionPrefix(id) {
|
|
|
2002
2360
|
return id.startsWith(HYDRA_SESSION_PREFIX) ? id.slice(HYDRA_SESSION_PREFIX.length) : id;
|
|
2003
2361
|
}
|
|
2004
2362
|
var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
2363
|
+
var TRANSFORMER_CLAIM_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2005
2364
|
var RECENTLY_TERMINAL_LIMIT = 64;
|
|
2006
2365
|
var Session = class {
|
|
2007
2366
|
sessionId;
|
|
@@ -2014,14 +2373,18 @@ var Session = class {
|
|
|
2014
2373
|
agent;
|
|
2015
2374
|
upstreamSessionId;
|
|
2016
2375
|
agentMeta;
|
|
2376
|
+
agentCapabilities;
|
|
2017
2377
|
agentArgs;
|
|
2378
|
+
parentSessionId;
|
|
2018
2379
|
title;
|
|
2019
2380
|
// Snapshot state delivered to attaching clients via the attach
|
|
2020
2381
|
// response _meta rather than via history replay (which would be
|
|
2021
2382
|
// stale-prone for snapshot-shaped events).
|
|
2022
2383
|
currentModel;
|
|
2023
2384
|
currentMode;
|
|
2024
|
-
|
|
2385
|
+
// Raw per-agent-life usage. Never read directly outside this class —
|
|
2386
|
+
// always access via the currentUsage getter which adds cumulativeCost.
|
|
2387
|
+
_currentUsage;
|
|
2025
2388
|
updatedAt;
|
|
2026
2389
|
createdAt;
|
|
2027
2390
|
clients = /* @__PURE__ */ new Map();
|
|
@@ -2076,6 +2439,10 @@ var Session = class {
|
|
|
2076
2439
|
internalPromptCapture;
|
|
2077
2440
|
idleTimeoutMs;
|
|
2078
2441
|
idleTimer;
|
|
2442
|
+
// Separate timer that fires session.idle to the transformer chain after
|
|
2443
|
+
// a quiet period. Distinct from idleTimer, which drives session close.
|
|
2444
|
+
idleEventTimer;
|
|
2445
|
+
idleEventTimeoutMs;
|
|
2079
2446
|
// Time of the last recordable broadcast (or session creation, if
|
|
2080
2447
|
// none yet). Drives the inactivity-based idle close; deliberately
|
|
2081
2448
|
// does NOT include snapshot state pings (model/mode/title/commands)
|
|
@@ -2083,7 +2450,11 @@ var Session = class {
|
|
|
2083
2450
|
// and noisy state churn keep a quiet session alive forever.
|
|
2084
2451
|
lastRecordedAt;
|
|
2085
2452
|
spawnReplacementAgent;
|
|
2453
|
+
listSessions;
|
|
2086
2454
|
logger;
|
|
2455
|
+
transformChain;
|
|
2456
|
+
// Outstanding "processing" claims: token → claim waiting for respondsTo discharge.
|
|
2457
|
+
pendingClaims = /* @__PURE__ */ new Map();
|
|
2087
2458
|
agentChangeHandlers = [];
|
|
2088
2459
|
// Last available_commands_update we observed from the agent. Stored
|
|
2089
2460
|
// so we can re-broadcast a merged (hydra ∪ agent) list whenever
|
|
@@ -2110,6 +2481,24 @@ var Session = class {
|
|
|
2110
2481
|
modelHandlers = [];
|
|
2111
2482
|
modeHandlers = [];
|
|
2112
2483
|
usageHandlers = [];
|
|
2484
|
+
cumulativeCost = 0;
|
|
2485
|
+
// Total cost across all agent lives. costAmount in the returned snapshot
|
|
2486
|
+
// is cumulativeCost + the current agent's raw amount so every consumer
|
|
2487
|
+
// gets the right figure without knowing about the internal split.
|
|
2488
|
+
// cumulativeCost is stripped from the return value so it never leaks
|
|
2489
|
+
// into persistence paths via session.currentUsage.
|
|
2490
|
+
get currentUsage() {
|
|
2491
|
+
if (!this._currentUsage && !this.cumulativeCost) {
|
|
2492
|
+
return void 0;
|
|
2493
|
+
}
|
|
2494
|
+
const base = this._currentUsage ?? {};
|
|
2495
|
+
const total = this.cumulativeCost + (base.costAmount ?? 0);
|
|
2496
|
+
return {
|
|
2497
|
+
...base,
|
|
2498
|
+
costAmount: total || void 0,
|
|
2499
|
+
cumulativeCost: void 0
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2113
2502
|
// Set by amendPrompt at the start of a cancel-and-resubmit dance.
|
|
2114
2503
|
// broadcastTurnComplete reads it to attach the _meta.amended marker
|
|
2115
2504
|
// to the cancelled turn's turn_complete notification, and to fire the
|
|
@@ -2124,6 +2513,11 @@ var Session = class {
|
|
|
2124
2513
|
// older entries fall out and resolve to target_not_found, which is
|
|
2125
2514
|
// the correct behavior.
|
|
2126
2515
|
recentlyTerminal = /* @__PURE__ */ new Map();
|
|
2516
|
+
// Optional ring buffer for piped stdin, populated by openStream() when
|
|
2517
|
+
// a cat --stream session attaches. Lifecycle follows the session — the
|
|
2518
|
+
// markClosed path closes the buffer and unlinks any file projection.
|
|
2519
|
+
streamBuffer;
|
|
2520
|
+
streamFilePath;
|
|
2127
2521
|
constructor(init) {
|
|
2128
2522
|
this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
|
|
2129
2523
|
this.cwd = init.cwd;
|
|
@@ -2131,11 +2525,14 @@ var Session = class {
|
|
|
2131
2525
|
this.agent = init.agent;
|
|
2132
2526
|
this.upstreamSessionId = init.upstreamSessionId;
|
|
2133
2527
|
this.agentMeta = init.agentMeta;
|
|
2528
|
+
this.agentCapabilities = init.agentCapabilities;
|
|
2134
2529
|
this.agentArgs = init.agentArgs;
|
|
2530
|
+
this.parentSessionId = init.parentSessionId;
|
|
2135
2531
|
this.title = init.title;
|
|
2136
2532
|
this.currentModel = init.currentModel;
|
|
2137
2533
|
this.currentMode = init.currentMode;
|
|
2138
|
-
this.
|
|
2534
|
+
this._currentUsage = init.currentUsage;
|
|
2535
|
+
this.cumulativeCost = init.currentUsage?.cumulativeCost ?? 0;
|
|
2139
2536
|
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
2140
2537
|
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
2141
2538
|
}
|
|
@@ -2146,8 +2543,11 @@ var Session = class {
|
|
|
2146
2543
|
this.agentAdvertisedModels = [...init.agentModels];
|
|
2147
2544
|
}
|
|
2148
2545
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
2546
|
+
this.idleEventTimeoutMs = init.idleEventTimeoutMs ?? 3e4;
|
|
2149
2547
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
2548
|
+
this.listSessions = init.listSessions;
|
|
2150
2549
|
this.logger = init.logger;
|
|
2550
|
+
this.transformChain = init.transformChain ?? [];
|
|
2151
2551
|
if (init.firstPromptSeeded) {
|
|
2152
2552
|
this.firstPromptSeeded = true;
|
|
2153
2553
|
}
|
|
@@ -2159,10 +2559,14 @@ var Session = class {
|
|
|
2159
2559
|
this.lastRecordedAt = this.updatedAt;
|
|
2160
2560
|
this.wireAgent(this.agent);
|
|
2161
2561
|
this.scheduleIdleCheck();
|
|
2562
|
+
this.notifyChain("session.opened", {});
|
|
2162
2563
|
}
|
|
2163
2564
|
broadcastMergedCommands() {
|
|
2164
2565
|
const merged = [
|
|
2165
2566
|
...hydraCommandsAsAdvertised(),
|
|
2567
|
+
{ name: "model <model-id>", description: "Switch model; omit arg to list available models" },
|
|
2568
|
+
{ name: "sessions", description: "List all sessions" },
|
|
2569
|
+
{ name: "help", description: "Show available commands" },
|
|
2166
2570
|
...this.agentAdvertisedCommands
|
|
2167
2571
|
];
|
|
2168
2572
|
this.recordAndBroadcast("session/update", {
|
|
@@ -2211,34 +2615,7 @@ var Session = class {
|
|
|
2211
2615
|
captureInternalChunk(this.internalPromptCapture, params);
|
|
2212
2616
|
return;
|
|
2213
2617
|
}
|
|
2214
|
-
|
|
2215
|
-
if (agentCmds !== null) {
|
|
2216
|
-
this.setAgentAdvertisedCommands(agentCmds);
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
const agentModes = extractAdvertisedModes(params);
|
|
2220
|
-
if (agentModes !== null) {
|
|
2221
|
-
this.setAgentAdvertisedModes(agentModes);
|
|
2222
|
-
return;
|
|
2223
|
-
}
|
|
2224
|
-
if (this.maybeApplyAgentModel(params)) {
|
|
2225
|
-
this.recordAndBroadcast("session/update", params);
|
|
2226
|
-
return;
|
|
2227
|
-
}
|
|
2228
|
-
if (this.maybeApplyAgentMode(params)) {
|
|
2229
|
-
this.recordAndBroadcast("session/update", params);
|
|
2230
|
-
return;
|
|
2231
|
-
}
|
|
2232
|
-
if (this.maybeApplyAgentConfigOption(params)) {
|
|
2233
|
-
this.recordAndBroadcast("session/update", params);
|
|
2234
|
-
return;
|
|
2235
|
-
}
|
|
2236
|
-
if (this.maybeApplyAgentUsage(params)) {
|
|
2237
|
-
this.recordAndBroadcast("session/update", params);
|
|
2238
|
-
return;
|
|
2239
|
-
}
|
|
2240
|
-
this.maybeApplyAgentSessionInfo(params);
|
|
2241
|
-
this.recordAndBroadcast("session/update", params);
|
|
2618
|
+
void this.runResponseChain(params);
|
|
2242
2619
|
});
|
|
2243
2620
|
agent.connection.onRequest("session/request_permission", async (params) => {
|
|
2244
2621
|
return this.handlePermissionRequest(params);
|
|
@@ -2250,6 +2627,106 @@ var Session = class {
|
|
|
2250
2627
|
this.markClosed({ deleteRecord: false });
|
|
2251
2628
|
});
|
|
2252
2629
|
}
|
|
2630
|
+
// Runs the response-side transformer chain, then the snapshot interceptors,
|
|
2631
|
+
// then recordAndBroadcast. All state mutation happens after the chain exits.
|
|
2632
|
+
// See forwardRequest for originatedBy / startIdx semantics.
|
|
2633
|
+
async runResponseChain(params, originatedBy = /* @__PURE__ */ new Set(), startIdx = 0) {
|
|
2634
|
+
const rawParams = params;
|
|
2635
|
+
let envelope = this.injectCumulativeCost(params);
|
|
2636
|
+
for (let i = startIdx; i < this.transformChain.length; i++) {
|
|
2637
|
+
const t = this.transformChain[i];
|
|
2638
|
+
if (originatedBy.has(t.name)) {
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
if (!t.intercepts.has("response:session/update")) {
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
const token = `t_${generateChainToken()}`;
|
|
2645
|
+
let result;
|
|
2646
|
+
try {
|
|
2647
|
+
result = await t.connection.request("transformer/message", {
|
|
2648
|
+
token,
|
|
2649
|
+
phase: "response",
|
|
2650
|
+
method: "session/update",
|
|
2651
|
+
direction: "agent\u2192client",
|
|
2652
|
+
sessionId: this.sessionId,
|
|
2653
|
+
envelope
|
|
2654
|
+
});
|
|
2655
|
+
} catch (err) {
|
|
2656
|
+
this.logger?.warn(
|
|
2657
|
+
`transformer ${t.name} error on response:session/update: ${err.message}`
|
|
2658
|
+
);
|
|
2659
|
+
continue;
|
|
2660
|
+
}
|
|
2661
|
+
const action = result?.action ?? "continue";
|
|
2662
|
+
if (action === "stop") {
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
if (action === "processing") {
|
|
2666
|
+
const claimIdx = i;
|
|
2667
|
+
const claimEnvelope = envelope;
|
|
2668
|
+
const claimOriginatedBy = new Set(originatedBy);
|
|
2669
|
+
await new Promise((resolve3) => {
|
|
2670
|
+
const timer = setTimeout(() => {
|
|
2671
|
+
if (this.pendingClaims.delete(token)) {
|
|
2672
|
+
this.broadcastQueueNotification(
|
|
2673
|
+
"hydra-acp/transformer_abandoned_request",
|
|
2674
|
+
{ sessionId: this.sessionId, token, transformerName: t.name }
|
|
2675
|
+
);
|
|
2676
|
+
void this.runResponseChain(
|
|
2677
|
+
claimEnvelope,
|
|
2678
|
+
/* @__PURE__ */ new Set([...claimOriginatedBy, t.name]),
|
|
2679
|
+
claimIdx + 1
|
|
2680
|
+
).then(resolve3);
|
|
2681
|
+
}
|
|
2682
|
+
}, TRANSFORMER_CLAIM_TIMEOUT_MS);
|
|
2683
|
+
if (typeof timer.unref === "function") {
|
|
2684
|
+
timer.unref();
|
|
2685
|
+
}
|
|
2686
|
+
this.pendingClaims.set(token, {
|
|
2687
|
+
resolve: () => resolve3(),
|
|
2688
|
+
timer,
|
|
2689
|
+
transformerName: t.name,
|
|
2690
|
+
method: "session/update",
|
|
2691
|
+
envelope: claimEnvelope,
|
|
2692
|
+
chainIdx: claimIdx,
|
|
2693
|
+
originatedBy: claimOriginatedBy,
|
|
2694
|
+
side: "response"
|
|
2695
|
+
});
|
|
2696
|
+
});
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
originatedBy.add(t.name);
|
|
2700
|
+
}
|
|
2701
|
+
const agentCmds = extractAdvertisedCommands(envelope);
|
|
2702
|
+
if (agentCmds !== null) {
|
|
2703
|
+
this.setAgentAdvertisedCommands(agentCmds);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
const agentModes = extractAdvertisedModes(envelope);
|
|
2707
|
+
if (agentModes !== null) {
|
|
2708
|
+
this.setAgentAdvertisedModes(agentModes);
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
if (this.maybeApplyAgentModel(envelope)) {
|
|
2712
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
if (this.maybeApplyAgentMode(envelope)) {
|
|
2716
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
if (this.maybeApplyAgentConfigOption(envelope)) {
|
|
2720
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
if (this.maybeApplyAgentUsage(rawParams)) {
|
|
2724
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
this.maybeApplyAgentSessionInfo(envelope);
|
|
2728
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2729
|
+
}
|
|
2253
2730
|
onAgentChange(handler) {
|
|
2254
2731
|
this.agentChangeHandlers.push(handler);
|
|
2255
2732
|
}
|
|
@@ -2990,8 +3467,159 @@ var Session = class {
|
|
|
2990
3467
|
sessionId: this.upstreamSessionId
|
|
2991
3468
|
});
|
|
2992
3469
|
}
|
|
2993
|
-
|
|
2994
|
-
|
|
3470
|
+
// Add a transformer to this session's chain retroactively. No-ops if it's
|
|
3471
|
+
// already present. Fires session.opened on the new transformer so it gets
|
|
3472
|
+
// the same lifecycle signal it would have received at session creation.
|
|
3473
|
+
addTransformer(ref) {
|
|
3474
|
+
const existing = this.transformChain.findIndex((t) => t.name === ref.name);
|
|
3475
|
+
if (existing >= 0) {
|
|
3476
|
+
this.transformChain[existing] = ref;
|
|
3477
|
+
} else {
|
|
3478
|
+
this.transformChain.push(ref);
|
|
3479
|
+
}
|
|
3480
|
+
if (ref.intercepts.has("lifecycle:session.opened")) {
|
|
3481
|
+
void ref.connection.notify("transformer/session_event", {
|
|
3482
|
+
event: "session.opened",
|
|
3483
|
+
sessionId: this.sessionId
|
|
3484
|
+
}).catch(() => void 0);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
// Walk the request-side chain then forward to the agent.
|
|
3488
|
+
// originatedBy: transformer names already in the lineage — skipped for loop
|
|
3489
|
+
// prevention and to implement resume-routing on re-entry from emit_message.
|
|
3490
|
+
// startIdx: chain position to start from (0 for normal, emitterIdx+1 for re-entry).
|
|
3491
|
+
async forwardRequest(method, params, originatedBy = /* @__PURE__ */ new Set(), startIdx = 0) {
|
|
3492
|
+
let envelope = this.rewriteForAgent(params);
|
|
3493
|
+
for (let i = startIdx; i < this.transformChain.length; i++) {
|
|
3494
|
+
const t = this.transformChain[i];
|
|
3495
|
+
if (originatedBy.has(t.name)) {
|
|
3496
|
+
continue;
|
|
3497
|
+
}
|
|
3498
|
+
const intercept = `request:${method}`;
|
|
3499
|
+
if (!t.intercepts.has(intercept)) {
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
const token = `t_${generateChainToken()}`;
|
|
3503
|
+
let result;
|
|
3504
|
+
try {
|
|
3505
|
+
result = await t.connection.request("transformer/message", {
|
|
3506
|
+
token,
|
|
3507
|
+
phase: "request",
|
|
3508
|
+
method,
|
|
3509
|
+
direction: "client\u2192agent",
|
|
3510
|
+
sessionId: this.sessionId,
|
|
3511
|
+
envelope
|
|
3512
|
+
});
|
|
3513
|
+
} catch (err) {
|
|
3514
|
+
this.logger?.warn(
|
|
3515
|
+
`transformer ${t.name} error on ${intercept}: ${err.message}`
|
|
3516
|
+
);
|
|
3517
|
+
continue;
|
|
3518
|
+
}
|
|
3519
|
+
const action = result?.action ?? "continue";
|
|
3520
|
+
if (action === "stop") {
|
|
3521
|
+
return result?.payload ?? defaultStopPayload(method);
|
|
3522
|
+
}
|
|
3523
|
+
if (action === "processing") {
|
|
3524
|
+
const claimIdx = i;
|
|
3525
|
+
const claimEnvelope = envelope;
|
|
3526
|
+
const claimOriginatedBy = new Set(originatedBy);
|
|
3527
|
+
return new Promise((resolve3) => {
|
|
3528
|
+
const timer = setTimeout(() => {
|
|
3529
|
+
if (this.pendingClaims.delete(token)) {
|
|
3530
|
+
this.broadcastQueueNotification(
|
|
3531
|
+
"hydra-acp/transformer_abandoned_request",
|
|
3532
|
+
{ sessionId: this.sessionId, token, transformerName: t.name }
|
|
3533
|
+
);
|
|
3534
|
+
void this.forwardRequest(
|
|
3535
|
+
method,
|
|
3536
|
+
claimEnvelope,
|
|
3537
|
+
/* @__PURE__ */ new Set([...claimOriginatedBy, t.name]),
|
|
3538
|
+
claimIdx + 1
|
|
3539
|
+
).then(resolve3).catch(() => resolve3(defaultStopPayload(method)));
|
|
3540
|
+
}
|
|
3541
|
+
}, TRANSFORMER_CLAIM_TIMEOUT_MS);
|
|
3542
|
+
if (typeof timer.unref === "function") {
|
|
3543
|
+
timer.unref();
|
|
3544
|
+
}
|
|
3545
|
+
this.pendingClaims.set(token, {
|
|
3546
|
+
resolve: resolve3,
|
|
3547
|
+
timer,
|
|
3548
|
+
transformerName: t.name,
|
|
3549
|
+
method,
|
|
3550
|
+
envelope: claimEnvelope,
|
|
3551
|
+
chainIdx: claimIdx,
|
|
3552
|
+
originatedBy: claimOriginatedBy,
|
|
3553
|
+
side: "request"
|
|
3554
|
+
});
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
originatedBy.add(t.name);
|
|
3558
|
+
}
|
|
3559
|
+
return this.agent.connection.request(method, envelope);
|
|
3560
|
+
}
|
|
3561
|
+
// Called by the WS handler when emit_message carries respondsTo.
|
|
3562
|
+
// Discharges the outstanding claim so the original requester unblocks.
|
|
3563
|
+
dischargeClaim(token, result) {
|
|
3564
|
+
const claim = this.pendingClaims.get(token);
|
|
3565
|
+
if (!claim) {
|
|
3566
|
+
return false;
|
|
3567
|
+
}
|
|
3568
|
+
clearTimeout(claim.timer);
|
|
3569
|
+
this.pendingClaims.delete(token);
|
|
3570
|
+
claim.resolve(result);
|
|
3571
|
+
return true;
|
|
3572
|
+
}
|
|
3573
|
+
// Called by the WS handler on hydra-acp/keep_alive.
|
|
3574
|
+
// Resets the abandonment timer for an outstanding processing claim.
|
|
3575
|
+
keepAliveClaim(token, estimatedRemainingMs) {
|
|
3576
|
+
const claim = this.pendingClaims.get(token);
|
|
3577
|
+
if (!claim) {
|
|
3578
|
+
return false;
|
|
3579
|
+
}
|
|
3580
|
+
clearTimeout(claim.timer);
|
|
3581
|
+
const timeout = typeof estimatedRemainingMs === "number" && estimatedRemainingMs > 0 ? Math.min(estimatedRemainingMs * 1.5, 30 * 60 * 1e3) : TRANSFORMER_CLAIM_TIMEOUT_MS;
|
|
3582
|
+
const timer = setTimeout(() => {
|
|
3583
|
+
if (this.pendingClaims.delete(token)) {
|
|
3584
|
+
this.broadcastQueueNotification(
|
|
3585
|
+
"hydra-acp/transformer_abandoned_request",
|
|
3586
|
+
{ sessionId: this.sessionId, token, transformerName: claim.transformerName }
|
|
3587
|
+
);
|
|
3588
|
+
if (claim.side === "response") {
|
|
3589
|
+
void this.runResponseChain(
|
|
3590
|
+
claim.envelope,
|
|
3591
|
+
/* @__PURE__ */ new Set([...claim.originatedBy, claim.transformerName]),
|
|
3592
|
+
claim.chainIdx + 1
|
|
3593
|
+
).then(() => claim.resolve(void 0));
|
|
3594
|
+
} else {
|
|
3595
|
+
void this.forwardRequest(
|
|
3596
|
+
claim.method,
|
|
3597
|
+
claim.envelope,
|
|
3598
|
+
/* @__PURE__ */ new Set([...claim.originatedBy, claim.transformerName]),
|
|
3599
|
+
claim.chainIdx + 1
|
|
3600
|
+
).then(claim.resolve).catch(() => claim.resolve(defaultStopPayload(claim.method)));
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
}, timeout);
|
|
3604
|
+
if (typeof timer.unref === "function") {
|
|
3605
|
+
timer.unref();
|
|
3606
|
+
}
|
|
3607
|
+
claim.timer = timer;
|
|
3608
|
+
return true;
|
|
3609
|
+
}
|
|
3610
|
+
// Called by the WS handler when a transformer emits via route:"chain".
|
|
3611
|
+
// Finds the emitter's position and re-enters the appropriate chain walk
|
|
3612
|
+
// from the next slot, with the emitter in originatedBy so it cannot see
|
|
3613
|
+
// its own re-emission.
|
|
3614
|
+
async emitToChain(emitterName, method, envelope) {
|
|
3615
|
+
const emitterIdx = this.transformChain.findIndex((t) => t.name === emitterName);
|
|
3616
|
+
const startIdx = emitterIdx >= 0 ? emitterIdx + 1 : 0;
|
|
3617
|
+
const originatedBy = /* @__PURE__ */ new Set([emitterName]);
|
|
3618
|
+
if (method === "session/update") {
|
|
3619
|
+
await this.runResponseChain(envelope, originatedBy, startIdx);
|
|
3620
|
+
return;
|
|
3621
|
+
}
|
|
3622
|
+
await this.forwardRequest(method, envelope, originatedBy, startIdx);
|
|
2995
3623
|
}
|
|
2996
3624
|
rewriteForAgent(params) {
|
|
2997
3625
|
if (params && typeof params === "object" && !Array.isArray(params)) {
|
|
@@ -3180,6 +3808,9 @@ var Session = class {
|
|
|
3180
3808
|
if (!trimmed || trimmed === this.currentMode) {
|
|
3181
3809
|
return true;
|
|
3182
3810
|
}
|
|
3811
|
+
this.logger?.info(
|
|
3812
|
+
`current_mode_update: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
|
|
3813
|
+
);
|
|
3183
3814
|
this.currentMode = trimmed;
|
|
3184
3815
|
for (const handler of this.modeHandlers) {
|
|
3185
3816
|
try {
|
|
@@ -3199,7 +3830,7 @@ var Session = class {
|
|
|
3199
3830
|
if (update.sessionUpdate !== "usage_update") {
|
|
3200
3831
|
return false;
|
|
3201
3832
|
}
|
|
3202
|
-
const next = { ...this.
|
|
3833
|
+
const next = { ...this._currentUsage ?? {} };
|
|
3203
3834
|
let changed = false;
|
|
3204
3835
|
if (typeof update.used === "number" && next.used !== update.used) {
|
|
3205
3836
|
next.used = update.used;
|
|
@@ -3223,15 +3854,68 @@ var Session = class {
|
|
|
3223
3854
|
if (!changed) {
|
|
3224
3855
|
return true;
|
|
3225
3856
|
}
|
|
3226
|
-
this.
|
|
3857
|
+
this._currentUsage = next;
|
|
3858
|
+
const total = this.currentUsage ?? next;
|
|
3227
3859
|
for (const handler of this.usageHandlers) {
|
|
3228
3860
|
try {
|
|
3229
|
-
handler(
|
|
3861
|
+
handler(total);
|
|
3230
3862
|
} catch {
|
|
3231
3863
|
}
|
|
3232
3864
|
}
|
|
3233
3865
|
return true;
|
|
3234
3866
|
}
|
|
3867
|
+
// Move currentUsage.costAmount into cumulativeCost and clear it so the
|
|
3868
|
+
// next agent life starts accumulating from $0. Fires usageHandlers so
|
|
3869
|
+
// meta.json is updated before the new agent starts emitting.
|
|
3870
|
+
accumulateAndResetCost() {
|
|
3871
|
+
const amount = this._currentUsage?.costAmount;
|
|
3872
|
+
if (!amount)
|
|
3873
|
+
return;
|
|
3874
|
+
this.cumulativeCost += amount;
|
|
3875
|
+
const next = {
|
|
3876
|
+
...this._currentUsage ?? {},
|
|
3877
|
+
cumulativeCost: this.cumulativeCost,
|
|
3878
|
+
costAmount: void 0
|
|
3879
|
+
};
|
|
3880
|
+
this._currentUsage = next;
|
|
3881
|
+
for (const handler of this.usageHandlers) {
|
|
3882
|
+
try {
|
|
3883
|
+
handler(next);
|
|
3884
|
+
} catch {
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
// Returns a modified envelope with cost.amount replaced by the running
|
|
3889
|
+
// total (cumulativeCost + raw agent amount). No-op if the envelope
|
|
3890
|
+
// doesn't carry a numeric cost.amount.
|
|
3891
|
+
injectCumulativeCost(envelope) {
|
|
3892
|
+
if (!this.cumulativeCost)
|
|
3893
|
+
return envelope;
|
|
3894
|
+
if (!envelope || typeof envelope !== "object")
|
|
3895
|
+
return envelope;
|
|
3896
|
+
const obj = envelope;
|
|
3897
|
+
if (!obj.update || typeof obj.update !== "object")
|
|
3898
|
+
return envelope;
|
|
3899
|
+
const update = obj.update;
|
|
3900
|
+
if (update.sessionUpdate !== "usage_update")
|
|
3901
|
+
return envelope;
|
|
3902
|
+
if (!update.cost || typeof update.cost !== "object")
|
|
3903
|
+
return envelope;
|
|
3904
|
+
const cost = update.cost;
|
|
3905
|
+
if (typeof cost.amount !== "number")
|
|
3906
|
+
return envelope;
|
|
3907
|
+
return {
|
|
3908
|
+
...obj,
|
|
3909
|
+
update: {
|
|
3910
|
+
...update,
|
|
3911
|
+
cost: { ...cost, amount: this.cumulativeCost + cost.amount }
|
|
3912
|
+
}
|
|
3913
|
+
};
|
|
3914
|
+
}
|
|
3915
|
+
// Deprecated alias — currentUsage already returns the total.
|
|
3916
|
+
get totalUsage() {
|
|
3917
|
+
return this.currentUsage;
|
|
3918
|
+
}
|
|
3235
3919
|
// Update the cached agent command list, fire persist handlers, and
|
|
3236
3920
|
// broadcast the merged list to attached clients. Idempotent on a
|
|
3237
3921
|
// structurally identical list so we don't churn meta.json on noisy
|
|
@@ -3296,6 +3980,26 @@ var Session = class {
|
|
|
3296
3980
|
onModeChange(handler) {
|
|
3297
3981
|
this.modeHandlers.push(handler);
|
|
3298
3982
|
}
|
|
3983
|
+
// Apply a mode change initiated by a client request (session/set_mode)
|
|
3984
|
+
// when the agent doesn't emit a current_mode_update notification on its
|
|
3985
|
+
// own. Fires modeHandlers so the persistence hook and any other listeners
|
|
3986
|
+
// see the change, identical to the agent-notification path.
|
|
3987
|
+
applyModeChange(modeId) {
|
|
3988
|
+
const trimmed = modeId.trim();
|
|
3989
|
+
if (!trimmed || trimmed === this.currentMode) {
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
this.logger?.info(
|
|
3993
|
+
`applyModeChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
|
|
3994
|
+
);
|
|
3995
|
+
this.currentMode = trimmed;
|
|
3996
|
+
for (const handler of this.modeHandlers) {
|
|
3997
|
+
try {
|
|
3998
|
+
handler(trimmed);
|
|
3999
|
+
} catch {
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
3299
4003
|
onUsageChange(handler) {
|
|
3300
4004
|
this.usageHandlers.push(handler);
|
|
3301
4005
|
}
|
|
@@ -3378,6 +4082,8 @@ var Session = class {
|
|
|
3378
4082
|
return this.runAgentCommand(arg);
|
|
3379
4083
|
case "kill":
|
|
3380
4084
|
return this.runKillCommand();
|
|
4085
|
+
case "restart":
|
|
4086
|
+
return this.runRestartCommand();
|
|
3381
4087
|
default: {
|
|
3382
4088
|
const err = new Error(
|
|
3383
4089
|
`no dispatcher for /hydra verb ${verb}`
|
|
@@ -3387,6 +4093,92 @@ var Session = class {
|
|
|
3387
4093
|
}
|
|
3388
4094
|
}
|
|
3389
4095
|
}
|
|
4096
|
+
async handleSessionsCommand() {
|
|
4097
|
+
let text;
|
|
4098
|
+
if (!this.listSessions) {
|
|
4099
|
+
text = "_(session listing not available)_";
|
|
4100
|
+
} else {
|
|
4101
|
+
const sessions = await this.listSessions();
|
|
4102
|
+
if (sessions.length === 0) {
|
|
4103
|
+
text = "_(no sessions)_";
|
|
4104
|
+
} else {
|
|
4105
|
+
const lines = sessions.map((s) => {
|
|
4106
|
+
const id = s.sessionId.replace(/^hydra_session_/, "").slice(0, 12);
|
|
4107
|
+
const model = s.currentModel ? ` \xB7 ${s.currentModel}` : "";
|
|
4108
|
+
const marker = s.sessionId === this.sessionId ? " \u25C0" : "";
|
|
4109
|
+
const title = s.title ? ` ${s.title}` : "";
|
|
4110
|
+
return `\`${id}\` ${s.cwd}${model}${marker}${title}`;
|
|
4111
|
+
});
|
|
4112
|
+
text = `Sessions (${sessions.length}):
|
|
4113
|
+
${lines.join("\n")}`;
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
this.recordAndBroadcast("session/update", {
|
|
4117
|
+
sessionId: this.upstreamSessionId,
|
|
4118
|
+
update: {
|
|
4119
|
+
sessionUpdate: "agent_message_chunk",
|
|
4120
|
+
content: { type: "text", text: `
|
|
4121
|
+
${text}
|
|
4122
|
+
` },
|
|
4123
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
4124
|
+
}
|
|
4125
|
+
});
|
|
4126
|
+
return { stopReason: "end_turn" };
|
|
4127
|
+
}
|
|
4128
|
+
handleHelpCommand() {
|
|
4129
|
+
const commands = this.mergedAvailableCommands();
|
|
4130
|
+
const lines = commands.map((c) => {
|
|
4131
|
+
const desc = c.description ? ` ${c.description}` : "";
|
|
4132
|
+
return `\`/${c.name}\`${desc}`;
|
|
4133
|
+
});
|
|
4134
|
+
const text = lines.length > 0 ? `Available commands:
|
|
4135
|
+
${lines.join("\n")}` : "_(no commands advertised)_";
|
|
4136
|
+
this.recordAndBroadcast("session/update", {
|
|
4137
|
+
sessionId: this.upstreamSessionId,
|
|
4138
|
+
update: {
|
|
4139
|
+
sessionUpdate: "agent_message_chunk",
|
|
4140
|
+
content: { type: "text", text: `
|
|
4141
|
+
${text}
|
|
4142
|
+
` },
|
|
4143
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
4144
|
+
}
|
|
4145
|
+
});
|
|
4146
|
+
return Promise.resolve({ stopReason: "end_turn" });
|
|
4147
|
+
}
|
|
4148
|
+
async handleModelCommand(text) {
|
|
4149
|
+
const arg = text.slice("/model".length).trim();
|
|
4150
|
+
if (arg === "") {
|
|
4151
|
+
const models = this.agentAdvertisedModels;
|
|
4152
|
+
const current = this.currentModel;
|
|
4153
|
+
let body;
|
|
4154
|
+
if (models.length === 0) {
|
|
4155
|
+
body = current ? `Current model: ${current}` : "_(no models advertised yet)_";
|
|
4156
|
+
} else {
|
|
4157
|
+
const lines = models.map((m) => {
|
|
4158
|
+
const marker = m.modelId === current ? " \u25C0" : "";
|
|
4159
|
+
const desc = m.name && m.name !== m.modelId ? ` ${m.name}` : "";
|
|
4160
|
+
return `${m.modelId}${marker}${desc}`;
|
|
4161
|
+
});
|
|
4162
|
+
body = lines.join("\n");
|
|
4163
|
+
}
|
|
4164
|
+
this.recordAndBroadcast("session/update", {
|
|
4165
|
+
sessionId: this.upstreamSessionId,
|
|
4166
|
+
update: {
|
|
4167
|
+
sessionUpdate: "agent_message_chunk",
|
|
4168
|
+
content: { type: "text", text: `
|
|
4169
|
+
${body}
|
|
4170
|
+
` },
|
|
4171
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
4172
|
+
}
|
|
4173
|
+
});
|
|
4174
|
+
return { stopReason: "end_turn" };
|
|
4175
|
+
}
|
|
4176
|
+
await this.forwardRequest("session/set_model", {
|
|
4177
|
+
sessionId: this.sessionId,
|
|
4178
|
+
modelId: arg
|
|
4179
|
+
});
|
|
4180
|
+
return { stopReason: "end_turn" };
|
|
4181
|
+
}
|
|
3390
4182
|
// Runs as a normal queued prompt (so it serializes after any in-flight
|
|
3391
4183
|
// turn). With an arg, sets the title directly. Without one, runs a
|
|
3392
4184
|
// suppressed sub-prompt to the agent and uses its reply as the title.
|
|
@@ -3463,12 +4255,14 @@ var Session = class {
|
|
|
3463
4255
|
cwd: this.cwd,
|
|
3464
4256
|
agentArgs: this.agentArgs
|
|
3465
4257
|
});
|
|
4258
|
+
this.accumulateAndResetCost();
|
|
3466
4259
|
this.wireAgent(fresh.agent);
|
|
3467
4260
|
const oldAgent = this.agent;
|
|
3468
4261
|
this.agent = fresh.agent;
|
|
3469
4262
|
this.agentId = newAgentId;
|
|
3470
4263
|
this.upstreamSessionId = fresh.upstreamSessionId;
|
|
3471
4264
|
this.agentMeta = fresh.agentMeta;
|
|
4265
|
+
this.agentCapabilities = fresh.agentCapabilities;
|
|
3472
4266
|
this.agentAdvertisedCommands = [];
|
|
3473
4267
|
this.broadcastMergedCommands();
|
|
3474
4268
|
if (this.agentAdvertisedModels.length > 0) {
|
|
@@ -3506,21 +4300,72 @@ var Session = class {
|
|
|
3506
4300
|
await this.close({ deleteRecord: false });
|
|
3507
4301
|
return { stopReason: "end_turn" };
|
|
3508
4302
|
}
|
|
3509
|
-
//
|
|
3510
|
-
//
|
|
3511
|
-
//
|
|
3512
|
-
//
|
|
3513
|
-
//
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
const
|
|
3522
|
-
|
|
3523
|
-
|
|
4303
|
+
// Restart the underlying agent with a fresh session/new, preserving the
|
|
4304
|
+
// conversation history. Unlike /hydra agent, this allows the same agentId
|
|
4305
|
+
// — useful when the proxy has changed available models (e.g. opus became
|
|
4306
|
+
// available after a quota reset) and the resumed session is locked to a
|
|
4307
|
+
// stale model list.
|
|
4308
|
+
runRestartCommand() {
|
|
4309
|
+
if (!this.spawnReplacementAgent) {
|
|
4310
|
+
throw withCode(
|
|
4311
|
+
new Error("agent restart not configured for this session"),
|
|
4312
|
+
JsonRpcErrorCodes.InternalError
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
const spawnAgent = this.spawnReplacementAgent;
|
|
4316
|
+
const agentId = this.agentId;
|
|
4317
|
+
return this.enqueuePrompt(async () => {
|
|
4318
|
+
const transcript = await this.buildSwitchTranscript(agentId);
|
|
4319
|
+
const fresh = await spawnAgent({
|
|
4320
|
+
agentId,
|
|
4321
|
+
cwd: this.cwd,
|
|
4322
|
+
agentArgs: this.agentArgs
|
|
4323
|
+
});
|
|
4324
|
+
this.accumulateAndResetCost();
|
|
4325
|
+
this.wireAgent(fresh.agent);
|
|
4326
|
+
const oldAgent = this.agent;
|
|
4327
|
+
this.agent = fresh.agent;
|
|
4328
|
+
this.upstreamSessionId = fresh.upstreamSessionId;
|
|
4329
|
+
this.agentMeta = fresh.agentMeta;
|
|
4330
|
+
this.agentCapabilities = fresh.agentCapabilities;
|
|
4331
|
+
this.agentAdvertisedCommands = [];
|
|
4332
|
+
this.broadcastMergedCommands();
|
|
4333
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
4334
|
+
this.setAgentAdvertisedModels([]);
|
|
4335
|
+
}
|
|
4336
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
4337
|
+
this.setAgentAdvertisedModes([]);
|
|
4338
|
+
}
|
|
4339
|
+
await oldAgent.kill().catch(() => void 0);
|
|
4340
|
+
if (transcript) {
|
|
4341
|
+
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
4342
|
+
}
|
|
4343
|
+
this.broadcastAgentSwitch(agentId, agentId);
|
|
4344
|
+
const info = { agentId, upstreamSessionId: this.upstreamSessionId };
|
|
4345
|
+
for (const handler of this.agentChangeHandlers) {
|
|
4346
|
+
try {
|
|
4347
|
+
handler(info);
|
|
4348
|
+
} catch {
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
return { stopReason: "end_turn" };
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
// Walk the persisted history and produce a labeled transcript suitable
|
|
4355
|
+
// for handing to a fresh agent. Includes user prompts, agent replies,
|
|
4356
|
+
// and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
|
|
4357
|
+
// switches don't accumulate banners) and other update kinds we don't
|
|
4358
|
+
// think the next agent benefits from re-seeing (plans, thoughts,
|
|
4359
|
+
// mode/model/usage).
|
|
4360
|
+
//
|
|
4361
|
+
// The header text defaults to the agent-swap framing; callers like
|
|
4362
|
+
// seedFromImport pass a custom header when the new agent is taking
|
|
4363
|
+
// over an imported session rather than swapping mid-conversation.
|
|
4364
|
+
async buildSwitchTranscript(prevAgentId, headerOverride) {
|
|
4365
|
+
const lines = [];
|
|
4366
|
+
const history = await this.getHistorySnapshot();
|
|
4367
|
+
for (const note of history) {
|
|
4368
|
+
if (note.method !== "session/update") {
|
|
3524
4369
|
continue;
|
|
3525
4370
|
}
|
|
3526
4371
|
const params = note.params ?? {};
|
|
@@ -3636,12 +4481,120 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3636
4481
|
}
|
|
3637
4482
|
});
|
|
3638
4483
|
}
|
|
4484
|
+
// stdin-stream lifecycle. Cat --stream calls openStream() once after
|
|
4485
|
+
// session/new, then forwards stdin chunks via streamWrite(). The agent
|
|
4486
|
+
// either consumes via the file path (when shell tools are available)
|
|
4487
|
+
// or — once the MCP surface lands — via tool calls that route through
|
|
4488
|
+
// streamRead() / streamTail() / streamHead() / streamWaitFor().
|
|
4489
|
+
hasStreamBuffer() {
|
|
4490
|
+
return this.streamBuffer !== void 0;
|
|
4491
|
+
}
|
|
4492
|
+
get streamPath() {
|
|
4493
|
+
return this.streamFilePath;
|
|
4494
|
+
}
|
|
4495
|
+
openStream(opts) {
|
|
4496
|
+
if (this.streamBuffer !== void 0) {
|
|
4497
|
+
throw new Error(
|
|
4498
|
+
`stream buffer already open for session ${this.sessionId}`
|
|
4499
|
+
);
|
|
4500
|
+
}
|
|
4501
|
+
const mode = opts.mode ?? "memory";
|
|
4502
|
+
const filePath = mode === "file" && opts.filePathFor !== void 0 ? opts.filePathFor(this.sessionId) : void 0;
|
|
4503
|
+
const bufferOpts = {};
|
|
4504
|
+
if (opts.capacityBytes !== void 0) {
|
|
4505
|
+
bufferOpts.capacityBytes = opts.capacityBytes;
|
|
4506
|
+
}
|
|
4507
|
+
if (filePath !== void 0) {
|
|
4508
|
+
bufferOpts.filePath = filePath;
|
|
4509
|
+
}
|
|
4510
|
+
if (opts.fileCapBytes !== void 0) {
|
|
4511
|
+
bufferOpts.fileCapBytes = opts.fileCapBytes;
|
|
4512
|
+
}
|
|
4513
|
+
bufferOpts.onFileCapReached = () => {
|
|
4514
|
+
this.recordAndBroadcast("session/update", {
|
|
4515
|
+
sessionId: this.upstreamSessionId,
|
|
4516
|
+
update: {
|
|
4517
|
+
sessionUpdate: "stream_truncated",
|
|
4518
|
+
...filePath !== void 0 ? { filePath } : {},
|
|
4519
|
+
fileCapBytes: opts.fileCapBytes
|
|
4520
|
+
}
|
|
4521
|
+
});
|
|
4522
|
+
};
|
|
4523
|
+
const buf = new SessionStreamBuffer(bufferOpts);
|
|
4524
|
+
this.streamBuffer = buf;
|
|
4525
|
+
this.streamFilePath = filePath;
|
|
4526
|
+
const result = {
|
|
4527
|
+
capacityBytes: buf.capacity
|
|
4528
|
+
};
|
|
4529
|
+
if (filePath !== void 0) {
|
|
4530
|
+
result.filePath = filePath;
|
|
4531
|
+
}
|
|
4532
|
+
if (opts.fileCapBytes !== void 0) {
|
|
4533
|
+
result.fileCapBytes = opts.fileCapBytes;
|
|
4534
|
+
}
|
|
4535
|
+
return result;
|
|
4536
|
+
}
|
|
4537
|
+
streamWrite(chunkB64, eof) {
|
|
4538
|
+
const buf = this.requireStreamBuffer();
|
|
4539
|
+
if (chunkB64.length > 0) {
|
|
4540
|
+
const chunk = Buffer.from(chunkB64, "base64");
|
|
4541
|
+
buf.append(chunk);
|
|
4542
|
+
}
|
|
4543
|
+
if (eof === true) {
|
|
4544
|
+
buf.close();
|
|
4545
|
+
}
|
|
4546
|
+
return { writeCursor: buf.writeCursorPos };
|
|
4547
|
+
}
|
|
4548
|
+
async streamRead(cursor, maxBytes, waitMs) {
|
|
4549
|
+
const buf = this.requireStreamBuffer();
|
|
4550
|
+
const cap = Math.max(
|
|
4551
|
+
0,
|
|
4552
|
+
Math.min(maxBytes ?? STREAM_READ_MAX_BYTES, STREAM_READ_MAX_BYTES)
|
|
4553
|
+
);
|
|
4554
|
+
let r = buf.read(cursor, cap);
|
|
4555
|
+
if (r.bytes.length === 0 && r.eof !== true && waitMs !== void 0 && waitMs > 0) {
|
|
4556
|
+
const outcome = await buf.waitForData(r.nextCursor, waitMs);
|
|
4557
|
+
if (outcome === "data" || outcome === "eof") {
|
|
4558
|
+
r = buf.read(r.nextCursor, cap);
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
const out = {
|
|
4562
|
+
bytes: r.bytes.toString("base64"),
|
|
4563
|
+
nextCursor: r.nextCursor
|
|
4564
|
+
};
|
|
4565
|
+
if (r.gap !== void 0) {
|
|
4566
|
+
out.gap = r.gap;
|
|
4567
|
+
}
|
|
4568
|
+
if (r.eof === true) {
|
|
4569
|
+
out.eof = true;
|
|
4570
|
+
}
|
|
4571
|
+
return out;
|
|
4572
|
+
}
|
|
4573
|
+
requireStreamBuffer() {
|
|
4574
|
+
if (this.streamBuffer === void 0) {
|
|
4575
|
+
const err = new Error(
|
|
4576
|
+
`session ${this.sessionId} has no stream buffer; call hydra-acp/stream_open first`
|
|
4577
|
+
);
|
|
4578
|
+
err.code = JsonRpcErrorCodes.StreamNotEnabled;
|
|
4579
|
+
throw err;
|
|
4580
|
+
}
|
|
4581
|
+
return this.streamBuffer;
|
|
4582
|
+
}
|
|
3639
4583
|
markClosed(opts) {
|
|
3640
4584
|
if (this.closed) {
|
|
3641
4585
|
return;
|
|
3642
4586
|
}
|
|
3643
4587
|
this.closed = true;
|
|
3644
4588
|
this.cancelIdleTimer();
|
|
4589
|
+
if (this.currentEntry?.kind === "user") {
|
|
4590
|
+
this.broadcastTurnComplete(
|
|
4591
|
+
this.currentEntry.clientId,
|
|
4592
|
+
{ stopReason: "interrupted" },
|
|
4593
|
+
this.currentEntry.messageId,
|
|
4594
|
+
this.currentEntry.wasAmend
|
|
4595
|
+
);
|
|
4596
|
+
this.currentEntry = void 0;
|
|
4597
|
+
}
|
|
3645
4598
|
const stranded = this.promptQueue;
|
|
3646
4599
|
this.promptQueue = [];
|
|
3647
4600
|
for (const entry of stranded) {
|
|
@@ -3654,12 +4607,23 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3654
4607
|
} catch {
|
|
3655
4608
|
}
|
|
3656
4609
|
}
|
|
4610
|
+
this.notifyChain("session.closed", {});
|
|
3657
4611
|
const sessionId = this.sessionId;
|
|
3658
4612
|
this.queueWriteChain = this.queueWriteChain.catch(() => void 0).then(() => deleteQueue(sessionId).catch(() => void 0));
|
|
3659
4613
|
for (const client of this.clients.values()) {
|
|
3660
4614
|
void client.connection.notify("hydra-acp/session_closed", { sessionId: this.sessionId }).catch(() => void 0);
|
|
3661
4615
|
}
|
|
3662
4616
|
this.clients.clear();
|
|
4617
|
+
if (this.streamBuffer !== void 0) {
|
|
4618
|
+
const buf = this.streamBuffer;
|
|
4619
|
+
const path13 = this.streamFilePath;
|
|
4620
|
+
this.streamBuffer = void 0;
|
|
4621
|
+
this.streamFilePath = void 0;
|
|
4622
|
+
buf.close();
|
|
4623
|
+
if (path13 !== void 0) {
|
|
4624
|
+
void buf.drainFileWrites().then(() => fsp4.unlink(path13).catch(() => void 0));
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
3663
4627
|
for (const handler of this.closeHandlers) {
|
|
3664
4628
|
handler(opts);
|
|
3665
4629
|
}
|
|
@@ -3722,6 +4686,44 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3722
4686
|
clearTimeout(this.idleTimer);
|
|
3723
4687
|
this.idleTimer = void 0;
|
|
3724
4688
|
}
|
|
4689
|
+
this.cancelIdleEventTimer();
|
|
4690
|
+
}
|
|
4691
|
+
// ── Lifecycle event timer ────────────────────────────────────────────────
|
|
4692
|
+
scheduleIdleEvent() {
|
|
4693
|
+
if (this.closed || this.idleEventTimeoutMs <= 0 || this.transformChain.length === 0) {
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
if (this.idleEventTimer) {
|
|
4697
|
+
clearTimeout(this.idleEventTimer);
|
|
4698
|
+
}
|
|
4699
|
+
this.idleEventTimer = setTimeout(() => {
|
|
4700
|
+
this.idleEventTimer = void 0;
|
|
4701
|
+
this.notifyChain("session.idle", {});
|
|
4702
|
+
}, this.idleEventTimeoutMs);
|
|
4703
|
+
if (typeof this.idleEventTimer.unref === "function") {
|
|
4704
|
+
this.idleEventTimer.unref();
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
cancelIdleEventTimer() {
|
|
4708
|
+
if (this.idleEventTimer) {
|
|
4709
|
+
clearTimeout(this.idleEventTimer);
|
|
4710
|
+
this.idleEventTimer = void 0;
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
// Send a lifecycle notification to every transformer in the chain that
|
|
4714
|
+
// declared the matching intercept. Fire-and-forget — no response expected.
|
|
4715
|
+
notifyChain(event, payload) {
|
|
4716
|
+
const intercept = `lifecycle:${event}`;
|
|
4717
|
+
for (const t of this.transformChain) {
|
|
4718
|
+
if (!t.intercepts.has(intercept)) {
|
|
4719
|
+
continue;
|
|
4720
|
+
}
|
|
4721
|
+
void t.connection.notify("transformer/session_event", {
|
|
4722
|
+
event,
|
|
4723
|
+
sessionId: this.sessionId,
|
|
4724
|
+
payload
|
|
4725
|
+
}).catch(() => void 0);
|
|
4726
|
+
}
|
|
3725
4727
|
}
|
|
3726
4728
|
rewriteForClient(params) {
|
|
3727
4729
|
if (params && typeof params === "object" && !Array.isArray(params)) {
|
|
@@ -3761,6 +4763,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3761
4763
|
}
|
|
3762
4764
|
}
|
|
3763
4765
|
this.scheduleIdleCheck();
|
|
4766
|
+
this.scheduleIdleEvent();
|
|
3764
4767
|
}
|
|
3765
4768
|
this.updatedAt = Date.now();
|
|
3766
4769
|
for (const client of this.clients.values()) {
|
|
@@ -3917,6 +4920,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3917
4920
|
return;
|
|
3918
4921
|
}
|
|
3919
4922
|
this.promptInFlight = true;
|
|
4923
|
+
await new Promise((r) => setImmediate(r));
|
|
3920
4924
|
try {
|
|
3921
4925
|
while (this.promptQueue.length > 0) {
|
|
3922
4926
|
const next = this.promptQueue.shift();
|
|
@@ -3936,7 +4940,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3936
4940
|
try {
|
|
3937
4941
|
const result = await this.runQueueEntry(next);
|
|
3938
4942
|
next.resolve(result);
|
|
3939
|
-
await Promise
|
|
4943
|
+
await new Promise((r) => setImmediate(r));
|
|
3940
4944
|
} catch (err) {
|
|
3941
4945
|
next.reject(err);
|
|
3942
4946
|
} finally {
|
|
@@ -3963,6 +4967,22 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3963
4967
|
return entry.task();
|
|
3964
4968
|
}
|
|
3965
4969
|
this.broadcastPromptReceived(entry);
|
|
4970
|
+
const promptText = extractPromptText(entry.prompt).trim();
|
|
4971
|
+
if (promptText === "/model" || promptText.startsWith("/model ") || promptText === "/sessions" || promptText === "/help") {
|
|
4972
|
+
let result;
|
|
4973
|
+
if (promptText === "/sessions") {
|
|
4974
|
+
result = await this.handleSessionsCommand();
|
|
4975
|
+
} else if (promptText === "/help") {
|
|
4976
|
+
result = await this.handleHelpCommand();
|
|
4977
|
+
} else {
|
|
4978
|
+
result = await this.handleModelCommand(promptText);
|
|
4979
|
+
}
|
|
4980
|
+
if (!this.closed) {
|
|
4981
|
+
this.broadcastTurnComplete(entry.clientId, result, entry.messageId, entry.wasAmend);
|
|
4982
|
+
}
|
|
4983
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
4984
|
+
return result;
|
|
4985
|
+
}
|
|
3966
4986
|
let response;
|
|
3967
4987
|
try {
|
|
3968
4988
|
response = await this.agent.connection.request(
|
|
@@ -3973,21 +4993,25 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3973
4993
|
}
|
|
3974
4994
|
);
|
|
3975
4995
|
} catch (err) {
|
|
4996
|
+
if (!this.closed) {
|
|
4997
|
+
this.broadcastTurnComplete(
|
|
4998
|
+
entry.clientId,
|
|
4999
|
+
{ stopReason: "error" },
|
|
5000
|
+
entry.messageId,
|
|
5001
|
+
entry.wasAmend
|
|
5002
|
+
);
|
|
5003
|
+
}
|
|
5004
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
5005
|
+
throw err;
|
|
5006
|
+
}
|
|
5007
|
+
if (!this.closed) {
|
|
3976
5008
|
this.broadcastTurnComplete(
|
|
3977
5009
|
entry.clientId,
|
|
3978
|
-
|
|
5010
|
+
response,
|
|
3979
5011
|
entry.messageId,
|
|
3980
5012
|
entry.wasAmend
|
|
3981
5013
|
);
|
|
3982
|
-
this.clearAmendIfMatches(entry.messageId);
|
|
3983
|
-
throw err;
|
|
3984
5014
|
}
|
|
3985
|
-
this.broadcastTurnComplete(
|
|
3986
|
-
entry.clientId,
|
|
3987
|
-
response,
|
|
3988
|
-
entry.messageId,
|
|
3989
|
-
entry.wasAmend
|
|
3990
|
-
);
|
|
3991
5015
|
this.clearAmendIfMatches(entry.messageId);
|
|
3992
5016
|
return response;
|
|
3993
5017
|
}
|
|
@@ -4252,6 +5276,12 @@ function extractPromptText(prompt) {
|
|
|
4252
5276
|
return "";
|
|
4253
5277
|
}).join("");
|
|
4254
5278
|
}
|
|
5279
|
+
function defaultStopPayload(method) {
|
|
5280
|
+
if (method === "session/prompt") {
|
|
5281
|
+
return { stopReason: "stopped" };
|
|
5282
|
+
}
|
|
5283
|
+
return {};
|
|
5284
|
+
}
|
|
4255
5285
|
function firstLine(text, max) {
|
|
4256
5286
|
for (const raw of text.split(/\r?\n/)) {
|
|
4257
5287
|
const line = raw.trim();
|
|
@@ -4292,7 +5322,8 @@ var PersistedUsage = z4.object({
|
|
|
4292
5322
|
used: z4.number().optional(),
|
|
4293
5323
|
size: z4.number().optional(),
|
|
4294
5324
|
costAmount: z4.number().optional(),
|
|
4295
|
-
costCurrency: z4.string().optional()
|
|
5325
|
+
costCurrency: z4.string().optional(),
|
|
5326
|
+
cumulativeCost: z4.number().optional()
|
|
4296
5327
|
});
|
|
4297
5328
|
var SessionRecord = z4.object({
|
|
4298
5329
|
version: z4.literal(1),
|
|
@@ -4341,6 +5372,9 @@ var SessionRecord = z4.object({
|
|
|
4341
5372
|
// it) so the local history.jsonl gets populated from the agent's
|
|
4342
5373
|
// memory. Cleared after that first resurrect completes.
|
|
4343
5374
|
pendingHistorySync: z4.boolean().optional(),
|
|
5375
|
+
// Set when this session was spawned as a child by a transformer via
|
|
5376
|
+
// hydra-acp/spawn_child_session. Points to the spawning session's id.
|
|
5377
|
+
parentSessionId: z4.string().optional(),
|
|
4344
5378
|
createdAt: z4.string(),
|
|
4345
5379
|
updatedAt: z4.string()
|
|
4346
5380
|
});
|
|
@@ -4462,6 +5496,7 @@ function recordFromMemorySession(args) {
|
|
|
4462
5496
|
agentModes: args.agentModes,
|
|
4463
5497
|
agentModels: args.agentModels,
|
|
4464
5498
|
pendingHistorySync: args.pendingHistorySync,
|
|
5499
|
+
parentSessionId: args.parentSessionId,
|
|
4465
5500
|
createdAt: args.createdAt ?? now,
|
|
4466
5501
|
updatedAt: args.updatedAt ?? now
|
|
4467
5502
|
};
|
|
@@ -4671,7 +5706,9 @@ var SessionManager = class {
|
|
|
4671
5706
|
this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
|
|
4672
5707
|
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
4673
5708
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
5709
|
+
this.idleEventTimeoutMs = options.idleEventTimeoutMs ?? 3e4;
|
|
4674
5710
|
this.defaultModels = options.defaultModels ?? {};
|
|
5711
|
+
this.defaultTransformers = options.defaultTransformers ?? [];
|
|
4675
5712
|
this.logger = options.logger;
|
|
4676
5713
|
this.npmRegistry = options.npmRegistry;
|
|
4677
5714
|
}
|
|
@@ -4683,6 +5720,8 @@ var SessionManager = class {
|
|
|
4683
5720
|
histories;
|
|
4684
5721
|
idleTimeoutMs;
|
|
4685
5722
|
defaultModels;
|
|
5723
|
+
defaultTransformers;
|
|
5724
|
+
idleEventTimeoutMs;
|
|
4686
5725
|
sessionHistoryMaxEntries;
|
|
4687
5726
|
// Serialize meta.json read-modify-write operations per session id so
|
|
4688
5727
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
@@ -4699,23 +5738,51 @@ var SessionManager = class {
|
|
|
4699
5738
|
model: params.model,
|
|
4700
5739
|
onInstallProgress: params.onInstallProgress
|
|
4701
5740
|
});
|
|
5741
|
+
if (params.transformChain && params.transformChain.length > 0) {
|
|
5742
|
+
let caps = { ...fresh.agentCapabilities ?? {} };
|
|
5743
|
+
for (const t of params.transformChain) {
|
|
5744
|
+
if (!t.intercepts.has("agent:initialize")) {
|
|
5745
|
+
continue;
|
|
5746
|
+
}
|
|
5747
|
+
try {
|
|
5748
|
+
const result = await t.connection.request("transformer/message", {
|
|
5749
|
+
token: `t_${generateRawSessionId()}`,
|
|
5750
|
+
phase: "response",
|
|
5751
|
+
method: "initialize",
|
|
5752
|
+
direction: "agent\u2192daemon",
|
|
5753
|
+
sessionId: "(pre-session)",
|
|
5754
|
+
envelope: caps
|
|
5755
|
+
});
|
|
5756
|
+
if (result.action === "stop" && result.payload) {
|
|
5757
|
+
caps = result.payload;
|
|
5758
|
+
}
|
|
5759
|
+
} catch {
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
fresh.agentCapabilities = caps;
|
|
5763
|
+
}
|
|
4702
5764
|
const session = new Session({
|
|
4703
5765
|
cwd: params.cwd,
|
|
4704
5766
|
agentId: params.agentId,
|
|
4705
5767
|
agent: fresh.agent,
|
|
4706
5768
|
upstreamSessionId: fresh.upstreamSessionId,
|
|
4707
5769
|
agentMeta: fresh.agentMeta,
|
|
5770
|
+
agentCapabilities: fresh.agentCapabilities,
|
|
4708
5771
|
title: params.title,
|
|
4709
5772
|
agentArgs: params.agentArgs,
|
|
4710
5773
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
5774
|
+
idleEventTimeoutMs: this.idleEventTimeoutMs,
|
|
4711
5775
|
logger: this.logger,
|
|
4712
5776
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
5777
|
+
listSessions: () => this.list(),
|
|
4713
5778
|
historyStore: this.histories,
|
|
4714
5779
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
4715
5780
|
currentModel: fresh.initialModel,
|
|
4716
5781
|
currentMode: fresh.initialMode,
|
|
4717
5782
|
agentModes: fresh.initialModes,
|
|
4718
|
-
agentModels: fresh.initialModels
|
|
5783
|
+
agentModels: fresh.initialModels,
|
|
5784
|
+
transformChain: params.transformChain,
|
|
5785
|
+
parentSessionId: params.parentSessionId
|
|
4719
5786
|
});
|
|
4720
5787
|
await this.attachManagerHooks(session);
|
|
4721
5788
|
return session;
|
|
@@ -4769,12 +5836,17 @@ var SessionManager = class {
|
|
|
4769
5836
|
cwd: params.cwd,
|
|
4770
5837
|
plan
|
|
4771
5838
|
});
|
|
5839
|
+
let agentCapabilities;
|
|
4772
5840
|
try {
|
|
4773
|
-
await agent.connection.request(
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
5841
|
+
const initResult = await agent.connection.request(
|
|
5842
|
+
"initialize",
|
|
5843
|
+
{
|
|
5844
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
5845
|
+
clientCapabilities: {},
|
|
5846
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
5847
|
+
}
|
|
5848
|
+
);
|
|
5849
|
+
agentCapabilities = initResult.agentCapabilities;
|
|
4778
5850
|
} catch (err) {
|
|
4779
5851
|
await agent.kill().catch(() => void 0);
|
|
4780
5852
|
throw err;
|
|
@@ -4804,6 +5876,22 @@ var SessionManager = class {
|
|
|
4804
5876
|
} else {
|
|
4805
5877
|
agent.connection.drainBuffered("session/update");
|
|
4806
5878
|
}
|
|
5879
|
+
const agentReportedMode = extractInitialCurrentMode(loadResult ?? {});
|
|
5880
|
+
const advertisedModes = params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {}));
|
|
5881
|
+
this.logger?.info(
|
|
5882
|
+
`resurrect: sessionId=${params.hydraSessionId} persistedMode=${JSON.stringify(params.currentMode)} agentReportedMode=${JSON.stringify(agentReportedMode)} advertisedModes=${JSON.stringify(advertisedModes?.map((m) => m.id))}`
|
|
5883
|
+
);
|
|
5884
|
+
const effectiveMode = await restoreCurrentMode({
|
|
5885
|
+
agent,
|
|
5886
|
+
upstreamSessionId: params.upstreamSessionId,
|
|
5887
|
+
persistedMode: params.currentMode,
|
|
5888
|
+
agentReportedMode,
|
|
5889
|
+
advertisedModes,
|
|
5890
|
+
logger: this.logger
|
|
5891
|
+
});
|
|
5892
|
+
this.logger?.info(
|
|
5893
|
+
`resurrect: effectiveMode=${JSON.stringify(effectiveMode)} for sessionId=${params.hydraSessionId}`
|
|
5894
|
+
);
|
|
4807
5895
|
const session = new Session({
|
|
4808
5896
|
sessionId: params.hydraSessionId,
|
|
4809
5897
|
cwd: params.cwd,
|
|
@@ -4811,11 +5899,13 @@ var SessionManager = class {
|
|
|
4811
5899
|
agent,
|
|
4812
5900
|
upstreamSessionId: params.upstreamSessionId,
|
|
4813
5901
|
agentMeta: loadResult?._meta,
|
|
5902
|
+
agentCapabilities,
|
|
4814
5903
|
title: params.title,
|
|
4815
5904
|
agentArgs: params.agentArgs,
|
|
4816
5905
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
4817
5906
|
logger: this.logger,
|
|
4818
5907
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
5908
|
+
listSessions: () => this.list(),
|
|
4819
5909
|
historyStore: this.histories,
|
|
4820
5910
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
4821
5911
|
// Prefer what we previously stored from a current_model_update; if
|
|
@@ -4823,11 +5913,15 @@ var SessionManager = class {
|
|
|
4823
5913
|
// this fix), fall back to the model the agent ships in its
|
|
4824
5914
|
// session/load response body.
|
|
4825
5915
|
currentModel: params.currentModel ?? extractInitialModel(loadResult ?? {}),
|
|
4826
|
-
currentMode:
|
|
5916
|
+
currentMode: effectiveMode,
|
|
4827
5917
|
currentUsage: params.currentUsage,
|
|
4828
5918
|
agentCommands: params.agentCommands,
|
|
4829
|
-
agentModes:
|
|
4830
|
-
|
|
5919
|
+
agentModes: advertisedModes,
|
|
5920
|
+
// Always prefer the fresh list from session/load over the persisted
|
|
5921
|
+
// snapshot — the proxy's available models can change between daemon
|
|
5922
|
+
// restarts (quota resets, rollouts), so meta.json is intentionally
|
|
5923
|
+
// treated as a cold fallback here, not the authoritative source.
|
|
5924
|
+
agentModels: nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})) ?? params.agentModels,
|
|
4831
5925
|
// Only gate the first-prompt title heuristic when we actually have
|
|
4832
5926
|
// a title to preserve. A title-less session (lost to a write race
|
|
4833
5927
|
// or never seeded) should re-derive from the next prompt rather
|
|
@@ -4854,6 +5948,15 @@ var SessionManager = class {
|
|
|
4854
5948
|
mcpServers: [],
|
|
4855
5949
|
onInstallProgress: params.onInstallProgress
|
|
4856
5950
|
});
|
|
5951
|
+
const advertisedModes = params.agentModes ?? fresh.initialModes;
|
|
5952
|
+
const effectiveMode = await restoreCurrentMode({
|
|
5953
|
+
agent: fresh.agent,
|
|
5954
|
+
upstreamSessionId: fresh.upstreamSessionId,
|
|
5955
|
+
persistedMode: params.currentMode,
|
|
5956
|
+
agentReportedMode: fresh.initialMode,
|
|
5957
|
+
advertisedModes,
|
|
5958
|
+
logger: this.logger
|
|
5959
|
+
});
|
|
4857
5960
|
const session = new Session({
|
|
4858
5961
|
sessionId: params.hydraSessionId,
|
|
4859
5962
|
cwd,
|
|
@@ -4861,20 +5964,22 @@ var SessionManager = class {
|
|
|
4861
5964
|
agent: fresh.agent,
|
|
4862
5965
|
upstreamSessionId: fresh.upstreamSessionId,
|
|
4863
5966
|
agentMeta: fresh.agentMeta,
|
|
5967
|
+
agentCapabilities: fresh.agentCapabilities,
|
|
4864
5968
|
title: params.title,
|
|
4865
5969
|
agentArgs: params.agentArgs,
|
|
4866
5970
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
4867
5971
|
logger: this.logger,
|
|
4868
5972
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
5973
|
+
listSessions: () => this.list(),
|
|
4869
5974
|
historyStore: this.histories,
|
|
4870
5975
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
4871
5976
|
// Prefer the stored value (set by a previous current_model_update);
|
|
4872
5977
|
// fall back to whatever the agent ships in its session/new response.
|
|
4873
5978
|
currentModel: params.currentModel ?? fresh.initialModel,
|
|
4874
|
-
currentMode:
|
|
5979
|
+
currentMode: effectiveMode,
|
|
4875
5980
|
currentUsage: params.currentUsage,
|
|
4876
5981
|
agentCommands: params.agentCommands,
|
|
4877
|
-
agentModes:
|
|
5982
|
+
agentModes: advertisedModes,
|
|
4878
5983
|
agentModels: params.agentModels ?? fresh.initialModels,
|
|
4879
5984
|
firstPromptSeeded: !!params.title,
|
|
4880
5985
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
@@ -5042,11 +6147,15 @@ var SessionManager = class {
|
|
|
5042
6147
|
plan
|
|
5043
6148
|
});
|
|
5044
6149
|
try {
|
|
5045
|
-
await agent.connection.request(
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
6150
|
+
const initResult = await agent.connection.request(
|
|
6151
|
+
"initialize",
|
|
6152
|
+
{
|
|
6153
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
6154
|
+
clientCapabilities: {},
|
|
6155
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
6156
|
+
}
|
|
6157
|
+
);
|
|
6158
|
+
const agentCapabilities = initResult.agentCapabilities;
|
|
5050
6159
|
const newResult = await agent.connection.request(
|
|
5051
6160
|
"session/new",
|
|
5052
6161
|
{
|
|
@@ -5072,13 +6181,15 @@ var SessionManager = class {
|
|
|
5072
6181
|
modelId: desired
|
|
5073
6182
|
});
|
|
5074
6183
|
initialModel = desired;
|
|
5075
|
-
} catch {
|
|
6184
|
+
} catch (err) {
|
|
6185
|
+
this.logger?.warn(
|
|
6186
|
+
`defaultModels[${params.agentId}]=${JSON.stringify(desired)} rejected by agent (${err.message}); session will use ${JSON.stringify(initialModel)}`
|
|
6187
|
+
);
|
|
5076
6188
|
}
|
|
5077
6189
|
} else {
|
|
5078
6190
|
const known = initialModels.map((m) => m.modelId).join(", ");
|
|
5079
|
-
|
|
5080
|
-
`
|
|
5081
|
-
`
|
|
6191
|
+
this.logger?.warn(
|
|
6192
|
+
`defaultModels[${params.agentId}]=${JSON.stringify(desired)} not in agent's availableModels ([${known}]); skipping session/set_model, session will use ${JSON.stringify(initialModel)}`
|
|
5082
6193
|
);
|
|
5083
6194
|
}
|
|
5084
6195
|
}
|
|
@@ -5088,6 +6199,7 @@ var SessionManager = class {
|
|
|
5088
6199
|
agent,
|
|
5089
6200
|
upstreamSessionId: sessionIdRaw,
|
|
5090
6201
|
agentMeta: newResult._meta,
|
|
6202
|
+
agentCapabilities,
|
|
5091
6203
|
initialModel,
|
|
5092
6204
|
initialModels: initialModels.length > 0 ? initialModels : void 0,
|
|
5093
6205
|
initialModes: initialModes.length > 0 ? initialModes : void 0,
|
|
@@ -5208,7 +6320,13 @@ var SessionManager = class {
|
|
|
5208
6320
|
agentArgs: record.agentArgs,
|
|
5209
6321
|
currentModel: record.currentModel,
|
|
5210
6322
|
currentMode: record.currentMode,
|
|
5211
|
-
currentUsage: persistedUsageToSnapshot(
|
|
6323
|
+
currentUsage: persistedUsageToSnapshot(
|
|
6324
|
+
record.currentUsage ? {
|
|
6325
|
+
...record.currentUsage,
|
|
6326
|
+
cumulativeCost: (record.currentUsage.cumulativeCost ?? 0) + (record.currentUsage.costAmount ?? 0),
|
|
6327
|
+
costAmount: void 0
|
|
6328
|
+
} : void 0
|
|
6329
|
+
),
|
|
5212
6330
|
agentCommands: record.agentCommands,
|
|
5213
6331
|
agentModes: record.agentModes,
|
|
5214
6332
|
agentModels: record.agentModels,
|
|
@@ -5248,6 +6366,9 @@ var SessionManager = class {
|
|
|
5248
6366
|
get(sessionId) {
|
|
5249
6367
|
return this.sessions.get(sessionId);
|
|
5250
6368
|
}
|
|
6369
|
+
liveSessions() {
|
|
6370
|
+
return this.sessions.values();
|
|
6371
|
+
}
|
|
5251
6372
|
// Snapshot of which agent versions are currently in use by live
|
|
5252
6373
|
// sessions, keyed by agentId. Read by the registry-fetch prune sweep
|
|
5253
6374
|
// so it can skip install dirs that still back a running process.
|
|
@@ -5310,6 +6431,7 @@ var SessionManager = class {
|
|
|
5310
6431
|
agentId: session.agentId,
|
|
5311
6432
|
currentModel: session.currentModel,
|
|
5312
6433
|
currentUsage: session.currentUsage,
|
|
6434
|
+
parentSessionId: session.parentSessionId,
|
|
5313
6435
|
updatedAt: used,
|
|
5314
6436
|
attachedClients: session.attachedCount,
|
|
5315
6437
|
status: "live",
|
|
@@ -5332,9 +6454,13 @@ var SessionManager = class {
|
|
|
5332
6454
|
title: r.title,
|
|
5333
6455
|
agentId: r.agentId,
|
|
5334
6456
|
currentModel: r.currentModel,
|
|
5335
|
-
currentUsage: r.currentUsage
|
|
6457
|
+
currentUsage: r.currentUsage ? {
|
|
6458
|
+
...r.currentUsage,
|
|
6459
|
+
costAmount: (r.currentUsage.cumulativeCost ?? 0) + (r.currentUsage.costAmount ?? 0) || void 0
|
|
6460
|
+
} : void 0,
|
|
5336
6461
|
importedFromMachine: r.importedFromMachine,
|
|
5337
6462
|
importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
|
|
6463
|
+
parentSessionId: r.parentSessionId,
|
|
5338
6464
|
updatedAt: used,
|
|
5339
6465
|
attachedClients: 0,
|
|
5340
6466
|
status: "cold",
|
|
@@ -5456,6 +6582,7 @@ var SessionManager = class {
|
|
|
5456
6582
|
currentMode: args.bundle.session.currentMode,
|
|
5457
6583
|
currentUsage: args.bundle.session.currentUsage,
|
|
5458
6584
|
agentCommands: args.bundle.session.agentCommands,
|
|
6585
|
+
agentModes: args.bundle.session.agentModes,
|
|
5459
6586
|
createdAt: args.preservedCreatedAt ?? now,
|
|
5460
6587
|
// Fallback path for historyMtimeIso (used when the history file
|
|
5461
6588
|
// is missing). Keep this consistent with the utimes stamp above.
|
|
@@ -5678,6 +6805,7 @@ function mergeForPersistence(session, existing) {
|
|
|
5678
6805
|
agentCommands,
|
|
5679
6806
|
agentModes,
|
|
5680
6807
|
agentModels,
|
|
6808
|
+
parentSessionId: session.parentSessionId ?? existing?.parentSessionId,
|
|
5681
6809
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
5682
6810
|
});
|
|
5683
6811
|
}
|
|
@@ -5698,6 +6826,9 @@ function usageSnapshotToPersisted(usage) {
|
|
|
5698
6826
|
if (usage.costCurrency !== void 0) {
|
|
5699
6827
|
out.costCurrency = usage.costCurrency;
|
|
5700
6828
|
}
|
|
6829
|
+
if (usage.cumulativeCost !== void 0) {
|
|
6830
|
+
out.cumulativeCost = usage.cumulativeCost;
|
|
6831
|
+
}
|
|
5701
6832
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
5702
6833
|
}
|
|
5703
6834
|
function persistedUsageToSnapshot(usage) {
|
|
@@ -5841,6 +6972,47 @@ function extractInitialCurrentMode(result) {
|
|
|
5841
6972
|
}
|
|
5842
6973
|
return void 0;
|
|
5843
6974
|
}
|
|
6975
|
+
async function restoreCurrentMode(opts) {
|
|
6976
|
+
const {
|
|
6977
|
+
agent,
|
|
6978
|
+
upstreamSessionId,
|
|
6979
|
+
persistedMode,
|
|
6980
|
+
agentReportedMode,
|
|
6981
|
+
advertisedModes,
|
|
6982
|
+
logger
|
|
6983
|
+
} = opts;
|
|
6984
|
+
if (!persistedMode) {
|
|
6985
|
+
return agentReportedMode;
|
|
6986
|
+
}
|
|
6987
|
+
if (persistedMode === agentReportedMode) {
|
|
6988
|
+
return persistedMode;
|
|
6989
|
+
}
|
|
6990
|
+
if (advertisedModes && advertisedModes.length > 0 && !advertisedModes.some((m) => m.id === persistedMode)) {
|
|
6991
|
+
const known = advertisedModes.map((m) => m.id).join(", ");
|
|
6992
|
+
logger?.warn(
|
|
6993
|
+
`resurrect: persisted currentMode=${JSON.stringify(persistedMode)} not in agent's availableModes ([${known}]); skipping session/set_mode, session will use ${JSON.stringify(agentReportedMode)}`
|
|
6994
|
+
);
|
|
6995
|
+
return agentReportedMode;
|
|
6996
|
+
}
|
|
6997
|
+
try {
|
|
6998
|
+
logger?.info(
|
|
6999
|
+
`resurrect: pushing persisted modeId=${JSON.stringify(persistedMode)} to agent (agentReported=${JSON.stringify(agentReportedMode)})`
|
|
7000
|
+
);
|
|
7001
|
+
await agent.connection.request("session/set_mode", {
|
|
7002
|
+
sessionId: upstreamSessionId,
|
|
7003
|
+
modeId: persistedMode
|
|
7004
|
+
});
|
|
7005
|
+
logger?.info(
|
|
7006
|
+
`resurrect: session/set_mode accepted, effectiveMode=${JSON.stringify(persistedMode)}`
|
|
7007
|
+
);
|
|
7008
|
+
return persistedMode;
|
|
7009
|
+
} catch (err) {
|
|
7010
|
+
logger?.warn(
|
|
7011
|
+
`resurrect: session/set_mode rejected by agent for modeId=${JSON.stringify(persistedMode)} (${err.message}); session will use ${JSON.stringify(agentReportedMode)}`
|
|
7012
|
+
);
|
|
7013
|
+
return agentReportedMode;
|
|
7014
|
+
}
|
|
7015
|
+
}
|
|
5844
7016
|
function parseModesList(list) {
|
|
5845
7017
|
if (!Array.isArray(list)) {
|
|
5846
7018
|
return [];
|
|
@@ -5901,7 +7073,7 @@ async function historyMtimeIso(sessionId) {
|
|
|
5901
7073
|
// src/core/extensions.ts
|
|
5902
7074
|
import { spawn as spawn4 } from "child_process";
|
|
5903
7075
|
import * as fs11 from "fs";
|
|
5904
|
-
import * as
|
|
7076
|
+
import * as fsp5 from "fs/promises";
|
|
5905
7077
|
import * as path7 from "path";
|
|
5906
7078
|
var RESTART_BASE_MS = 1e3;
|
|
5907
7079
|
var RESTART_CAP_MS = 6e4;
|
|
@@ -5910,8 +7082,10 @@ var ExtensionManager = class {
|
|
|
5910
7082
|
entries = /* @__PURE__ */ new Map();
|
|
5911
7083
|
stopping = false;
|
|
5912
7084
|
context;
|
|
5913
|
-
|
|
7085
|
+
tokenRegistry;
|
|
7086
|
+
constructor(extensions, context, options = {}) {
|
|
5914
7087
|
this.context = context;
|
|
7088
|
+
this.tokenRegistry = options.tokenRegistry;
|
|
5915
7089
|
for (const ext of extensions) {
|
|
5916
7090
|
this.entries.set(ext.name, this.makeEntry(ext));
|
|
5917
7091
|
}
|
|
@@ -5919,11 +7093,482 @@ var ExtensionManager = class {
|
|
|
5919
7093
|
setContext(context) {
|
|
5920
7094
|
this.context = context;
|
|
5921
7095
|
}
|
|
7096
|
+
// Called by the WS handler after a process connects and calls initialize
|
|
7097
|
+
// with clientInfo.version. Stored on the entry and surfaced in list().
|
|
7098
|
+
reportVersion(name, version) {
|
|
7099
|
+
const entry = this.entries.get(name);
|
|
7100
|
+
if (entry) {
|
|
7101
|
+
entry.version = version;
|
|
7102
|
+
}
|
|
7103
|
+
}
|
|
7104
|
+
async start() {
|
|
7105
|
+
if (!this.context) {
|
|
7106
|
+
throw new Error("ExtensionManager: setContext must be called before start");
|
|
7107
|
+
}
|
|
7108
|
+
await fsp5.mkdir(paths.extensionsDir(), { recursive: true });
|
|
7109
|
+
await this.reapOrphans();
|
|
7110
|
+
for (const entry of this.entries.values()) {
|
|
7111
|
+
if (!entry.config.enabled) {
|
|
7112
|
+
continue;
|
|
7113
|
+
}
|
|
7114
|
+
this.spawn(entry, 0);
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
async stop() {
|
|
7118
|
+
this.stopping = true;
|
|
7119
|
+
const tasks = [];
|
|
7120
|
+
for (const entry of this.entries.values()) {
|
|
7121
|
+
if (entry.restartTimer) {
|
|
7122
|
+
clearTimeout(entry.restartTimer);
|
|
7123
|
+
entry.restartTimer = void 0;
|
|
7124
|
+
}
|
|
7125
|
+
const child = entry.child;
|
|
7126
|
+
if (!child) {
|
|
7127
|
+
continue;
|
|
7128
|
+
}
|
|
7129
|
+
try {
|
|
7130
|
+
child.kill("SIGTERM");
|
|
7131
|
+
} catch {
|
|
7132
|
+
}
|
|
7133
|
+
tasks.push(
|
|
7134
|
+
new Promise((resolve3) => {
|
|
7135
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7136
|
+
resolve3();
|
|
7137
|
+
return;
|
|
7138
|
+
}
|
|
7139
|
+
const timer = setTimeout(() => {
|
|
7140
|
+
try {
|
|
7141
|
+
child.kill("SIGKILL");
|
|
7142
|
+
} catch {
|
|
7143
|
+
}
|
|
7144
|
+
resolve3();
|
|
7145
|
+
}, STOP_GRACE_MS);
|
|
7146
|
+
child.on("exit", () => {
|
|
7147
|
+
clearTimeout(timer);
|
|
7148
|
+
resolve3();
|
|
7149
|
+
});
|
|
7150
|
+
})
|
|
7151
|
+
);
|
|
7152
|
+
}
|
|
7153
|
+
await Promise.allSettled(tasks);
|
|
7154
|
+
for (const entry of this.entries.values()) {
|
|
7155
|
+
try {
|
|
7156
|
+
entry.logStream?.end();
|
|
7157
|
+
} catch {
|
|
7158
|
+
}
|
|
7159
|
+
entry.child = void 0;
|
|
7160
|
+
entry.logStream = void 0;
|
|
7161
|
+
entry.pid = void 0;
|
|
7162
|
+
}
|
|
7163
|
+
}
|
|
7164
|
+
list() {
|
|
7165
|
+
return [...this.entries.values()].map((entry) => this.infoFor(entry));
|
|
7166
|
+
}
|
|
7167
|
+
get(name) {
|
|
7168
|
+
const entry = this.entries.get(name);
|
|
7169
|
+
return entry ? this.infoFor(entry) : void 0;
|
|
7170
|
+
}
|
|
7171
|
+
has(name) {
|
|
7172
|
+
return this.entries.has(name);
|
|
7173
|
+
}
|
|
7174
|
+
async startByName(name) {
|
|
7175
|
+
const entry = this.entries.get(name);
|
|
7176
|
+
if (!entry) {
|
|
7177
|
+
throw withCode2(new Error(`unknown extension: ${name}`), "NOT_FOUND");
|
|
7178
|
+
}
|
|
7179
|
+
if (entry.child) {
|
|
7180
|
+
throw withCode2(new Error(`extension ${name} already running`), "CONFLICT");
|
|
7181
|
+
}
|
|
7182
|
+
if (entry.restartTimer) {
|
|
7183
|
+
clearTimeout(entry.restartTimer);
|
|
7184
|
+
entry.restartTimer = void 0;
|
|
7185
|
+
}
|
|
7186
|
+
entry.manuallyStopped = false;
|
|
7187
|
+
entry.restartCount = 0;
|
|
7188
|
+
this.spawn(entry, 0);
|
|
7189
|
+
return this.infoFor(entry);
|
|
7190
|
+
}
|
|
7191
|
+
async stopByName(name) {
|
|
7192
|
+
const entry = this.entries.get(name);
|
|
7193
|
+
if (!entry) {
|
|
7194
|
+
throw withCode2(new Error(`unknown extension: ${name}`), "NOT_FOUND");
|
|
7195
|
+
}
|
|
7196
|
+
entry.manuallyStopped = true;
|
|
7197
|
+
if (entry.restartTimer) {
|
|
7198
|
+
clearTimeout(entry.restartTimer);
|
|
7199
|
+
entry.restartTimer = void 0;
|
|
7200
|
+
}
|
|
7201
|
+
const child = entry.child;
|
|
7202
|
+
if (!child) {
|
|
7203
|
+
return this.infoFor(entry);
|
|
7204
|
+
}
|
|
7205
|
+
await this.terminate(entry, child);
|
|
7206
|
+
return this.infoFor(entry);
|
|
7207
|
+
}
|
|
7208
|
+
async restartByName(name) {
|
|
7209
|
+
await this.stopByName(name);
|
|
7210
|
+
return this.startByName(name);
|
|
7211
|
+
}
|
|
7212
|
+
// Register a new extension and (if enabled) start it. Used by the
|
|
7213
|
+
// POST /v1/extensions endpoint so `hydra-acp extensions add` can take
|
|
7214
|
+
// effect without a daemon restart.
|
|
7215
|
+
register(config) {
|
|
7216
|
+
if (this.entries.has(config.name)) {
|
|
7217
|
+
throw withCode2(
|
|
7218
|
+
new Error(`extension ${config.name} already exists`),
|
|
7219
|
+
"CONFLICT"
|
|
7220
|
+
);
|
|
7221
|
+
}
|
|
7222
|
+
if (!this.context) {
|
|
7223
|
+
throw new Error("ExtensionManager: setContext must be called before register");
|
|
7224
|
+
}
|
|
7225
|
+
const entry = this.makeEntry(config);
|
|
7226
|
+
this.entries.set(config.name, entry);
|
|
7227
|
+
if (config.enabled) {
|
|
7228
|
+
this.spawn(entry, 0);
|
|
7229
|
+
}
|
|
7230
|
+
return this.infoFor(entry);
|
|
7231
|
+
}
|
|
7232
|
+
async unregister(name) {
|
|
7233
|
+
const entry = this.entries.get(name);
|
|
7234
|
+
if (!entry) {
|
|
7235
|
+
throw withCode2(new Error(`unknown extension: ${name}`), "NOT_FOUND");
|
|
7236
|
+
}
|
|
7237
|
+
entry.manuallyStopped = true;
|
|
7238
|
+
if (entry.restartTimer) {
|
|
7239
|
+
clearTimeout(entry.restartTimer);
|
|
7240
|
+
entry.restartTimer = void 0;
|
|
7241
|
+
}
|
|
7242
|
+
const child = entry.child;
|
|
7243
|
+
if (child) {
|
|
7244
|
+
await this.terminate(entry, child);
|
|
7245
|
+
}
|
|
7246
|
+
try {
|
|
7247
|
+
entry.logStream?.end();
|
|
7248
|
+
} catch {
|
|
7249
|
+
}
|
|
7250
|
+
this.entries.delete(name);
|
|
7251
|
+
}
|
|
7252
|
+
async terminate(entry, child) {
|
|
7253
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7254
|
+
return;
|
|
7255
|
+
}
|
|
7256
|
+
const exited = new Promise((resolve3) => {
|
|
7257
|
+
entry.exitWaiters.push(resolve3);
|
|
7258
|
+
});
|
|
7259
|
+
try {
|
|
7260
|
+
child.kill("SIGTERM");
|
|
7261
|
+
} catch {
|
|
7262
|
+
}
|
|
7263
|
+
const killTimer = setTimeout(() => {
|
|
7264
|
+
try {
|
|
7265
|
+
child.kill("SIGKILL");
|
|
7266
|
+
} catch {
|
|
7267
|
+
}
|
|
7268
|
+
}, STOP_GRACE_MS);
|
|
7269
|
+
if (typeof killTimer.unref === "function") {
|
|
7270
|
+
killTimer.unref();
|
|
7271
|
+
}
|
|
7272
|
+
try {
|
|
7273
|
+
await exited;
|
|
7274
|
+
} finally {
|
|
7275
|
+
clearTimeout(killTimer);
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
infoFor(entry) {
|
|
7279
|
+
let status;
|
|
7280
|
+
if (entry.child) {
|
|
7281
|
+
status = "running";
|
|
7282
|
+
} else if (entry.restartTimer) {
|
|
7283
|
+
status = "restarting";
|
|
7284
|
+
} else if (!entry.config.enabled) {
|
|
7285
|
+
status = "disabled";
|
|
7286
|
+
} else {
|
|
7287
|
+
status = "stopped";
|
|
7288
|
+
}
|
|
7289
|
+
return {
|
|
7290
|
+
name: entry.config.name,
|
|
7291
|
+
status,
|
|
7292
|
+
pid: entry.pid,
|
|
7293
|
+
enabled: entry.config.enabled,
|
|
7294
|
+
restartCount: entry.restartCount,
|
|
7295
|
+
startedAt: entry.startedAt,
|
|
7296
|
+
lastExitCode: entry.lastExitCode,
|
|
7297
|
+
logPath: paths.extensionLogFile(entry.config.name),
|
|
7298
|
+
version: entry.version
|
|
7299
|
+
};
|
|
7300
|
+
}
|
|
7301
|
+
makeEntry(config) {
|
|
7302
|
+
return {
|
|
7303
|
+
config,
|
|
7304
|
+
child: void 0,
|
|
7305
|
+
logStream: void 0,
|
|
7306
|
+
restartTimer: void 0,
|
|
7307
|
+
pid: void 0,
|
|
7308
|
+
startedAt: void 0,
|
|
7309
|
+
restartCount: 0,
|
|
7310
|
+
lastExitCode: void 0,
|
|
7311
|
+
manuallyStopped: false,
|
|
7312
|
+
exitWaiters: [],
|
|
7313
|
+
version: void 0,
|
|
7314
|
+
processToken: void 0
|
|
7315
|
+
};
|
|
7316
|
+
}
|
|
7317
|
+
async reapOrphans() {
|
|
7318
|
+
let entries;
|
|
7319
|
+
try {
|
|
7320
|
+
entries = await fsp5.readdir(paths.extensionsDir());
|
|
7321
|
+
} catch (err) {
|
|
7322
|
+
const e = err;
|
|
7323
|
+
if (e.code === "ENOENT") {
|
|
7324
|
+
return;
|
|
7325
|
+
}
|
|
7326
|
+
throw err;
|
|
7327
|
+
}
|
|
7328
|
+
for (const entry of entries) {
|
|
7329
|
+
if (!entry.endsWith(".pid")) {
|
|
7330
|
+
continue;
|
|
7331
|
+
}
|
|
7332
|
+
const pidPath = path7.join(paths.extensionsDir(), entry);
|
|
7333
|
+
let pid;
|
|
7334
|
+
try {
|
|
7335
|
+
const raw = await fsp5.readFile(pidPath, "utf8");
|
|
7336
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
7337
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
7338
|
+
pid = parsed;
|
|
7339
|
+
}
|
|
7340
|
+
} catch {
|
|
7341
|
+
}
|
|
7342
|
+
if (typeof pid === "number" && isAlive(pid)) {
|
|
7343
|
+
try {
|
|
7344
|
+
process.kill(pid, "SIGTERM");
|
|
7345
|
+
} catch {
|
|
7346
|
+
}
|
|
7347
|
+
const deadline = Date.now() + STOP_GRACE_MS;
|
|
7348
|
+
while (Date.now() < deadline && isAlive(pid)) {
|
|
7349
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
7350
|
+
}
|
|
7351
|
+
if (isAlive(pid)) {
|
|
7352
|
+
try {
|
|
7353
|
+
process.kill(pid, "SIGKILL");
|
|
7354
|
+
} catch {
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
await fsp5.unlink(pidPath).catch(() => void 0);
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
spawn(entry, attempt) {
|
|
7362
|
+
if (this.stopping || entry.manuallyStopped) {
|
|
7363
|
+
return;
|
|
7364
|
+
}
|
|
7365
|
+
const ctx = this.context;
|
|
7366
|
+
if (!ctx) {
|
|
7367
|
+
throw new Error("ExtensionManager.spawn called before setContext");
|
|
7368
|
+
}
|
|
7369
|
+
const ext = entry.config;
|
|
7370
|
+
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
7371
|
+
const logStream = fs11.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
7372
|
+
flags: "a"
|
|
7373
|
+
});
|
|
7374
|
+
logStream.write(
|
|
7375
|
+
`[hydra-acp] ${(/* @__PURE__ */ new Date()).toISOString()} starting extension ${ext.name} (attempt ${attempt + 1})
|
|
7376
|
+
`
|
|
7377
|
+
);
|
|
7378
|
+
const processToken = this.tokenRegistry?.mint(ext.name, "extension") ?? ctx.serviceToken;
|
|
7379
|
+
entry.processToken = processToken;
|
|
7380
|
+
entry.version = void 0;
|
|
7381
|
+
const env = {
|
|
7382
|
+
...process.env,
|
|
7383
|
+
HYDRA_ACP_DAEMON_URL: ctx.daemonUrl,
|
|
7384
|
+
HYDRA_ACP_DAEMON_HOST: ctx.daemonHost,
|
|
7385
|
+
HYDRA_ACP_DAEMON_PORT: String(ctx.daemonPort),
|
|
7386
|
+
HYDRA_ACP_TOKEN: processToken,
|
|
7387
|
+
HYDRA_ACP_WS_URL: ctx.daemonWsUrl,
|
|
7388
|
+
HYDRA_ACP_HOME: ctx.hydraHome,
|
|
7389
|
+
HYDRA_ACP_EXTENSION_NAME: ext.name,
|
|
7390
|
+
...ext.env
|
|
7391
|
+
};
|
|
7392
|
+
const [cmd, ...baseArgs] = command;
|
|
7393
|
+
if (cmd === void 0) {
|
|
7394
|
+
logStream.write(`[hydra-acp] extension ${ext.name} has empty command
|
|
7395
|
+
`);
|
|
7396
|
+
logStream.end();
|
|
7397
|
+
return;
|
|
7398
|
+
}
|
|
7399
|
+
const args = [...baseArgs, ...ext.args];
|
|
7400
|
+
let child;
|
|
7401
|
+
try {
|
|
7402
|
+
child = spawn4(cmd, args, {
|
|
7403
|
+
env,
|
|
7404
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7405
|
+
detached: false
|
|
7406
|
+
});
|
|
7407
|
+
} catch (err) {
|
|
7408
|
+
logStream.write(
|
|
7409
|
+
`[hydra-acp] failed to spawn ${ext.name}: ${err.message}
|
|
7410
|
+
`
|
|
7411
|
+
);
|
|
7412
|
+
logStream.end();
|
|
7413
|
+
this.scheduleRestart(entry, attempt);
|
|
7414
|
+
return;
|
|
7415
|
+
}
|
|
7416
|
+
if (child.stdout) {
|
|
7417
|
+
child.stdout.pipe(logStream, { end: false });
|
|
7418
|
+
}
|
|
7419
|
+
if (child.stderr) {
|
|
7420
|
+
child.stderr.pipe(logStream, { end: false });
|
|
7421
|
+
}
|
|
7422
|
+
if (typeof child.pid === "number") {
|
|
7423
|
+
try {
|
|
7424
|
+
fs11.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
7425
|
+
`, {
|
|
7426
|
+
encoding: "utf8",
|
|
7427
|
+
mode: 384
|
|
7428
|
+
});
|
|
7429
|
+
} catch (err) {
|
|
7430
|
+
logStream.write(
|
|
7431
|
+
`[hydra-acp] failed to write pid file for ${ext.name}: ${err.message}
|
|
7432
|
+
`
|
|
7433
|
+
);
|
|
7434
|
+
}
|
|
7435
|
+
}
|
|
7436
|
+
entry.child = child;
|
|
7437
|
+
entry.logStream = logStream;
|
|
7438
|
+
entry.pid = typeof child.pid === "number" ? child.pid : void 0;
|
|
7439
|
+
entry.startedAt = Date.now();
|
|
7440
|
+
entry.lastExitCode = void 0;
|
|
7441
|
+
child.on("error", (err) => {
|
|
7442
|
+
logStream.write(
|
|
7443
|
+
`[hydra-acp] extension ${ext.name} error: ${err.message}
|
|
7444
|
+
`
|
|
7445
|
+
);
|
|
7446
|
+
});
|
|
7447
|
+
child.on("exit", (code, signal) => {
|
|
7448
|
+
try {
|
|
7449
|
+
fs11.unlinkSync(paths.extensionPidFile(ext.name));
|
|
7450
|
+
} catch {
|
|
7451
|
+
}
|
|
7452
|
+
logStream.write(
|
|
7453
|
+
`[hydra-acp] extension ${ext.name} exited code=${code ?? "null"} signal=${signal ?? "null"}
|
|
7454
|
+
`
|
|
7455
|
+
);
|
|
7456
|
+
entry.child = void 0;
|
|
7457
|
+
entry.pid = void 0;
|
|
7458
|
+
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
7459
|
+
if (entry.processToken) {
|
|
7460
|
+
this.tokenRegistry?.revoke(ext.name);
|
|
7461
|
+
entry.processToken = void 0;
|
|
7462
|
+
}
|
|
7463
|
+
const waiters = entry.exitWaiters.splice(0);
|
|
7464
|
+
for (const resolve3 of waiters) {
|
|
7465
|
+
resolve3();
|
|
7466
|
+
}
|
|
7467
|
+
if (this.stopping || entry.manuallyStopped) {
|
|
7468
|
+
try {
|
|
7469
|
+
logStream.end();
|
|
7470
|
+
} catch {
|
|
7471
|
+
}
|
|
7472
|
+
entry.logStream = void 0;
|
|
7473
|
+
return;
|
|
7474
|
+
}
|
|
7475
|
+
entry.restartCount += 1;
|
|
7476
|
+
this.scheduleRestart(entry, attempt + 1);
|
|
7477
|
+
});
|
|
7478
|
+
}
|
|
7479
|
+
scheduleRestart(entry, attempt) {
|
|
7480
|
+
if (this.stopping || entry.manuallyStopped) {
|
|
7481
|
+
return;
|
|
7482
|
+
}
|
|
7483
|
+
const delay = Math.min(
|
|
7484
|
+
RESTART_BASE_MS * 2 ** Math.min(attempt, 10),
|
|
7485
|
+
RESTART_CAP_MS
|
|
7486
|
+
);
|
|
7487
|
+
entry.restartTimer = setTimeout(() => {
|
|
7488
|
+
entry.restartTimer = void 0;
|
|
7489
|
+
this.spawn(entry, attempt);
|
|
7490
|
+
}, delay);
|
|
7491
|
+
if (typeof entry.restartTimer.unref === "function") {
|
|
7492
|
+
entry.restartTimer.unref();
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7495
|
+
};
|
|
7496
|
+
function isAlive(pid) {
|
|
7497
|
+
try {
|
|
7498
|
+
process.kill(pid, 0);
|
|
7499
|
+
return true;
|
|
7500
|
+
} catch {
|
|
7501
|
+
return false;
|
|
7502
|
+
}
|
|
7503
|
+
}
|
|
7504
|
+
function withCode2(err, code) {
|
|
7505
|
+
err.code = code;
|
|
7506
|
+
return err;
|
|
7507
|
+
}
|
|
7508
|
+
|
|
7509
|
+
// src/core/transformer-manager.ts
|
|
7510
|
+
import { spawn as spawn5 } from "child_process";
|
|
7511
|
+
import * as fs12 from "fs";
|
|
7512
|
+
import * as fsp6 from "fs/promises";
|
|
7513
|
+
import * as path8 from "path";
|
|
7514
|
+
var RESTART_BASE_MS2 = 1e3;
|
|
7515
|
+
var RESTART_CAP_MS2 = 6e4;
|
|
7516
|
+
var STOP_GRACE_MS2 = 3e3;
|
|
7517
|
+
var TransformerManager = class {
|
|
7518
|
+
entries = /* @__PURE__ */ new Map();
|
|
7519
|
+
// Transformers that have completed transformer/initialize and are ready to
|
|
7520
|
+
// participate in chains. Keyed by transformer name.
|
|
7521
|
+
connected = /* @__PURE__ */ new Map();
|
|
7522
|
+
stopping = false;
|
|
7523
|
+
context;
|
|
7524
|
+
tokenRegistry;
|
|
7525
|
+
constructor(transformers, context, options = {}) {
|
|
7526
|
+
this.context = context;
|
|
7527
|
+
this.tokenRegistry = options.tokenRegistry;
|
|
7528
|
+
for (const t of transformers) {
|
|
7529
|
+
this.entries.set(t.name, this.makeEntry(t));
|
|
7530
|
+
}
|
|
7531
|
+
}
|
|
7532
|
+
setContext(context) {
|
|
7533
|
+
this.context = context;
|
|
7534
|
+
}
|
|
7535
|
+
reportVersion(name, version) {
|
|
7536
|
+
const entry = this.entries.get(name);
|
|
7537
|
+
if (entry) {
|
|
7538
|
+
entry.version = version;
|
|
7539
|
+
}
|
|
7540
|
+
}
|
|
7541
|
+
// Called by the WS handler after transformer/initialize completes. The
|
|
7542
|
+
// transformer is now eligible to participate in session chains.
|
|
7543
|
+
registerConnection(name, connection, intercepts) {
|
|
7544
|
+
this.connected.set(name, {
|
|
7545
|
+
name,
|
|
7546
|
+
connection,
|
|
7547
|
+
intercepts: new Set(intercepts)
|
|
7548
|
+
});
|
|
7549
|
+
}
|
|
7550
|
+
// Called by the WS handler when the transformer's WS connection closes.
|
|
7551
|
+
deregisterConnection(name) {
|
|
7552
|
+
this.connected.delete(name);
|
|
7553
|
+
}
|
|
7554
|
+
// Resolve a list of transformer names to their live TransformerRef objects.
|
|
7555
|
+
// Names that are configured but not yet connected are silently skipped
|
|
7556
|
+
// (fail-open: session proceeds without that transformer rather than failing).
|
|
7557
|
+
resolveChain(names) {
|
|
7558
|
+
const out = [];
|
|
7559
|
+
for (const name of names) {
|
|
7560
|
+
const ref = this.connected.get(name);
|
|
7561
|
+
if (ref) {
|
|
7562
|
+
out.push(ref);
|
|
7563
|
+
}
|
|
7564
|
+
}
|
|
7565
|
+
return out;
|
|
7566
|
+
}
|
|
5922
7567
|
async start() {
|
|
5923
7568
|
if (!this.context) {
|
|
5924
|
-
throw new Error("
|
|
7569
|
+
throw new Error("TransformerManager: setContext must be called before start");
|
|
5925
7570
|
}
|
|
5926
|
-
await
|
|
7571
|
+
await fsp6.mkdir(paths.transformersDir(), { recursive: true });
|
|
5927
7572
|
await this.reapOrphans();
|
|
5928
7573
|
for (const entry of this.entries.values()) {
|
|
5929
7574
|
if (!entry.config.enabled) {
|
|
@@ -5960,7 +7605,7 @@ var ExtensionManager = class {
|
|
|
5960
7605
|
} catch {
|
|
5961
7606
|
}
|
|
5962
7607
|
resolve3();
|
|
5963
|
-
},
|
|
7608
|
+
}, STOP_GRACE_MS2);
|
|
5964
7609
|
child.on("exit", () => {
|
|
5965
7610
|
clearTimeout(timer);
|
|
5966
7611
|
resolve3();
|
|
@@ -5992,10 +7637,10 @@ var ExtensionManager = class {
|
|
|
5992
7637
|
async startByName(name) {
|
|
5993
7638
|
const entry = this.entries.get(name);
|
|
5994
7639
|
if (!entry) {
|
|
5995
|
-
throw
|
|
7640
|
+
throw withCode3(new Error(`unknown transformer: ${name}`), "NOT_FOUND");
|
|
5996
7641
|
}
|
|
5997
7642
|
if (entry.child) {
|
|
5998
|
-
throw
|
|
7643
|
+
throw withCode3(new Error(`transformer ${name} already running`), "CONFLICT");
|
|
5999
7644
|
}
|
|
6000
7645
|
if (entry.restartTimer) {
|
|
6001
7646
|
clearTimeout(entry.restartTimer);
|
|
@@ -6009,7 +7654,7 @@ var ExtensionManager = class {
|
|
|
6009
7654
|
async stopByName(name) {
|
|
6010
7655
|
const entry = this.entries.get(name);
|
|
6011
7656
|
if (!entry) {
|
|
6012
|
-
throw
|
|
7657
|
+
throw withCode3(new Error(`unknown transformer: ${name}`), "NOT_FOUND");
|
|
6013
7658
|
}
|
|
6014
7659
|
entry.manuallyStopped = true;
|
|
6015
7660
|
if (entry.restartTimer) {
|
|
@@ -6027,18 +7672,15 @@ var ExtensionManager = class {
|
|
|
6027
7672
|
await this.stopByName(name);
|
|
6028
7673
|
return this.startByName(name);
|
|
6029
7674
|
}
|
|
6030
|
-
// Register a new extension and (if enabled) start it. Used by the
|
|
6031
|
-
// POST /v1/extensions endpoint so `hydra-acp extensions add` can take
|
|
6032
|
-
// effect without a daemon restart.
|
|
6033
7675
|
register(config) {
|
|
6034
7676
|
if (this.entries.has(config.name)) {
|
|
6035
|
-
throw
|
|
6036
|
-
new Error(`
|
|
7677
|
+
throw withCode3(
|
|
7678
|
+
new Error(`transformer ${config.name} already exists`),
|
|
6037
7679
|
"CONFLICT"
|
|
6038
7680
|
);
|
|
6039
7681
|
}
|
|
6040
7682
|
if (!this.context) {
|
|
6041
|
-
throw new Error("
|
|
7683
|
+
throw new Error("TransformerManager: setContext must be called before register");
|
|
6042
7684
|
}
|
|
6043
7685
|
const entry = this.makeEntry(config);
|
|
6044
7686
|
this.entries.set(config.name, entry);
|
|
@@ -6050,7 +7692,7 @@ var ExtensionManager = class {
|
|
|
6050
7692
|
async unregister(name) {
|
|
6051
7693
|
const entry = this.entries.get(name);
|
|
6052
7694
|
if (!entry) {
|
|
6053
|
-
throw
|
|
7695
|
+
throw withCode3(new Error(`unknown transformer: ${name}`), "NOT_FOUND");
|
|
6054
7696
|
}
|
|
6055
7697
|
entry.manuallyStopped = true;
|
|
6056
7698
|
if (entry.restartTimer) {
|
|
@@ -6083,7 +7725,7 @@ var ExtensionManager = class {
|
|
|
6083
7725
|
child.kill("SIGKILL");
|
|
6084
7726
|
} catch {
|
|
6085
7727
|
}
|
|
6086
|
-
},
|
|
7728
|
+
}, STOP_GRACE_MS2);
|
|
6087
7729
|
if (typeof killTimer.unref === "function") {
|
|
6088
7730
|
killTimer.unref();
|
|
6089
7731
|
}
|
|
@@ -6112,7 +7754,8 @@ var ExtensionManager = class {
|
|
|
6112
7754
|
restartCount: entry.restartCount,
|
|
6113
7755
|
startedAt: entry.startedAt,
|
|
6114
7756
|
lastExitCode: entry.lastExitCode,
|
|
6115
|
-
logPath: paths.
|
|
7757
|
+
logPath: paths.transformerLogFile(entry.config.name),
|
|
7758
|
+
version: entry.version
|
|
6116
7759
|
};
|
|
6117
7760
|
}
|
|
6118
7761
|
makeEntry(config) {
|
|
@@ -6126,13 +7769,15 @@ var ExtensionManager = class {
|
|
|
6126
7769
|
restartCount: 0,
|
|
6127
7770
|
lastExitCode: void 0,
|
|
6128
7771
|
manuallyStopped: false,
|
|
6129
|
-
exitWaiters: []
|
|
7772
|
+
exitWaiters: [],
|
|
7773
|
+
version: void 0,
|
|
7774
|
+
processToken: void 0
|
|
6130
7775
|
};
|
|
6131
7776
|
}
|
|
6132
7777
|
async reapOrphans() {
|
|
6133
7778
|
let entries;
|
|
6134
7779
|
try {
|
|
6135
|
-
entries = await
|
|
7780
|
+
entries = await fsp6.readdir(paths.transformersDir());
|
|
6136
7781
|
} catch (err) {
|
|
6137
7782
|
const e = err;
|
|
6138
7783
|
if (e.code === "ENOENT") {
|
|
@@ -6144,33 +7789,33 @@ var ExtensionManager = class {
|
|
|
6144
7789
|
if (!entry.endsWith(".pid")) {
|
|
6145
7790
|
continue;
|
|
6146
7791
|
}
|
|
6147
|
-
const pidPath =
|
|
7792
|
+
const pidPath = path8.join(paths.transformersDir(), entry);
|
|
6148
7793
|
let pid;
|
|
6149
7794
|
try {
|
|
6150
|
-
const raw = await
|
|
7795
|
+
const raw = await fsp6.readFile(pidPath, "utf8");
|
|
6151
7796
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
6152
7797
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
6153
7798
|
pid = parsed;
|
|
6154
7799
|
}
|
|
6155
7800
|
} catch {
|
|
6156
7801
|
}
|
|
6157
|
-
if (typeof pid === "number" &&
|
|
7802
|
+
if (typeof pid === "number" && isAlive2(pid)) {
|
|
6158
7803
|
try {
|
|
6159
7804
|
process.kill(pid, "SIGTERM");
|
|
6160
7805
|
} catch {
|
|
6161
7806
|
}
|
|
6162
|
-
const deadline = Date.now() +
|
|
6163
|
-
while (Date.now() < deadline &&
|
|
7807
|
+
const deadline = Date.now() + STOP_GRACE_MS2;
|
|
7808
|
+
while (Date.now() < deadline && isAlive2(pid)) {
|
|
6164
7809
|
await new Promise((r) => setTimeout(r, 50));
|
|
6165
7810
|
}
|
|
6166
|
-
if (
|
|
7811
|
+
if (isAlive2(pid)) {
|
|
6167
7812
|
try {
|
|
6168
7813
|
process.kill(pid, "SIGKILL");
|
|
6169
7814
|
} catch {
|
|
6170
7815
|
}
|
|
6171
7816
|
}
|
|
6172
7817
|
}
|
|
6173
|
-
await
|
|
7818
|
+
await fsp6.unlink(pidPath).catch(() => void 0);
|
|
6174
7819
|
}
|
|
6175
7820
|
}
|
|
6176
7821
|
spawn(entry, attempt) {
|
|
@@ -6179,46 +7824,49 @@ var ExtensionManager = class {
|
|
|
6179
7824
|
}
|
|
6180
7825
|
const ctx = this.context;
|
|
6181
7826
|
if (!ctx) {
|
|
6182
|
-
throw new Error("
|
|
7827
|
+
throw new Error("TransformerManager.spawn called before setContext");
|
|
6183
7828
|
}
|
|
6184
|
-
const
|
|
6185
|
-
const command =
|
|
6186
|
-
const logStream =
|
|
7829
|
+
const t = entry.config;
|
|
7830
|
+
const command = t.command.length > 0 ? t.command : [t.name];
|
|
7831
|
+
const logStream = fs12.createWriteStream(paths.transformerLogFile(t.name), {
|
|
6187
7832
|
flags: "a"
|
|
6188
7833
|
});
|
|
6189
7834
|
logStream.write(
|
|
6190
|
-
`[hydra-acp] ${(/* @__PURE__ */ new Date()).toISOString()} starting
|
|
7835
|
+
`[hydra-acp] ${(/* @__PURE__ */ new Date()).toISOString()} starting transformer ${t.name} (attempt ${attempt + 1})
|
|
6191
7836
|
`
|
|
6192
7837
|
);
|
|
7838
|
+
const processToken = this.tokenRegistry?.mint(t.name, "transformer") ?? ctx.serviceToken;
|
|
7839
|
+
entry.processToken = processToken;
|
|
7840
|
+
entry.version = void 0;
|
|
6193
7841
|
const env = {
|
|
6194
7842
|
...process.env,
|
|
6195
7843
|
HYDRA_ACP_DAEMON_URL: ctx.daemonUrl,
|
|
6196
7844
|
HYDRA_ACP_DAEMON_HOST: ctx.daemonHost,
|
|
6197
7845
|
HYDRA_ACP_DAEMON_PORT: String(ctx.daemonPort),
|
|
6198
|
-
HYDRA_ACP_TOKEN:
|
|
7846
|
+
HYDRA_ACP_TOKEN: processToken,
|
|
6199
7847
|
HYDRA_ACP_WS_URL: ctx.daemonWsUrl,
|
|
6200
7848
|
HYDRA_ACP_HOME: ctx.hydraHome,
|
|
6201
|
-
|
|
6202
|
-
...
|
|
7849
|
+
HYDRA_ACP_TRANSFORMER_NAME: t.name,
|
|
7850
|
+
...t.env
|
|
6203
7851
|
};
|
|
6204
7852
|
const [cmd, ...baseArgs] = command;
|
|
6205
7853
|
if (cmd === void 0) {
|
|
6206
|
-
logStream.write(`[hydra-acp]
|
|
7854
|
+
logStream.write(`[hydra-acp] transformer ${t.name} has empty command
|
|
6207
7855
|
`);
|
|
6208
7856
|
logStream.end();
|
|
6209
7857
|
return;
|
|
6210
7858
|
}
|
|
6211
|
-
const args = [...baseArgs, ...
|
|
7859
|
+
const args = [...baseArgs, ...t.args];
|
|
6212
7860
|
let child;
|
|
6213
7861
|
try {
|
|
6214
|
-
child =
|
|
7862
|
+
child = spawn5(cmd, args, {
|
|
6215
7863
|
env,
|
|
6216
7864
|
stdio: ["ignore", "pipe", "pipe"],
|
|
6217
7865
|
detached: false
|
|
6218
7866
|
});
|
|
6219
7867
|
} catch (err) {
|
|
6220
7868
|
logStream.write(
|
|
6221
|
-
`[hydra-acp] failed to spawn ${
|
|
7869
|
+
`[hydra-acp] failed to spawn ${t.name}: ${err.message}
|
|
6222
7870
|
`
|
|
6223
7871
|
);
|
|
6224
7872
|
logStream.end();
|
|
@@ -6233,14 +7881,14 @@ var ExtensionManager = class {
|
|
|
6233
7881
|
}
|
|
6234
7882
|
if (typeof child.pid === "number") {
|
|
6235
7883
|
try {
|
|
6236
|
-
|
|
7884
|
+
fs12.writeFileSync(paths.transformerPidFile(t.name), `${child.pid}
|
|
6237
7885
|
`, {
|
|
6238
7886
|
encoding: "utf8",
|
|
6239
7887
|
mode: 384
|
|
6240
7888
|
});
|
|
6241
7889
|
} catch (err) {
|
|
6242
7890
|
logStream.write(
|
|
6243
|
-
`[hydra-acp] failed to write pid file for ${
|
|
7891
|
+
`[hydra-acp] failed to write pid file for ${t.name}: ${err.message}
|
|
6244
7892
|
`
|
|
6245
7893
|
);
|
|
6246
7894
|
}
|
|
@@ -6252,22 +7900,26 @@ var ExtensionManager = class {
|
|
|
6252
7900
|
entry.lastExitCode = void 0;
|
|
6253
7901
|
child.on("error", (err) => {
|
|
6254
7902
|
logStream.write(
|
|
6255
|
-
`[hydra-acp]
|
|
7903
|
+
`[hydra-acp] transformer ${t.name} error: ${err.message}
|
|
6256
7904
|
`
|
|
6257
7905
|
);
|
|
6258
7906
|
});
|
|
6259
7907
|
child.on("exit", (code, signal) => {
|
|
6260
7908
|
try {
|
|
6261
|
-
|
|
7909
|
+
fs12.unlinkSync(paths.transformerPidFile(t.name));
|
|
6262
7910
|
} catch {
|
|
6263
7911
|
}
|
|
6264
7912
|
logStream.write(
|
|
6265
|
-
`[hydra-acp]
|
|
7913
|
+
`[hydra-acp] transformer ${t.name} exited code=${code ?? "null"} signal=${signal ?? "null"}
|
|
6266
7914
|
`
|
|
6267
7915
|
);
|
|
6268
7916
|
entry.child = void 0;
|
|
6269
7917
|
entry.pid = void 0;
|
|
6270
7918
|
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
7919
|
+
if (entry.processToken) {
|
|
7920
|
+
this.tokenRegistry?.revoke(t.name);
|
|
7921
|
+
entry.processToken = void 0;
|
|
7922
|
+
}
|
|
6271
7923
|
const waiters = entry.exitWaiters.splice(0);
|
|
6272
7924
|
for (const resolve3 of waiters) {
|
|
6273
7925
|
resolve3();
|
|
@@ -6289,8 +7941,8 @@ var ExtensionManager = class {
|
|
|
6289
7941
|
return;
|
|
6290
7942
|
}
|
|
6291
7943
|
const delay = Math.min(
|
|
6292
|
-
|
|
6293
|
-
|
|
7944
|
+
RESTART_BASE_MS2 * 2 ** Math.min(attempt, 10),
|
|
7945
|
+
RESTART_CAP_MS2
|
|
6294
7946
|
);
|
|
6295
7947
|
entry.restartTimer = setTimeout(() => {
|
|
6296
7948
|
entry.restartTimer = void 0;
|
|
@@ -6301,7 +7953,7 @@ var ExtensionManager = class {
|
|
|
6301
7953
|
}
|
|
6302
7954
|
}
|
|
6303
7955
|
};
|
|
6304
|
-
function
|
|
7956
|
+
function isAlive2(pid) {
|
|
6305
7957
|
try {
|
|
6306
7958
|
process.kill(pid, 0);
|
|
6307
7959
|
return true;
|
|
@@ -6309,14 +7961,14 @@ function isAlive(pid) {
|
|
|
6309
7961
|
return false;
|
|
6310
7962
|
}
|
|
6311
7963
|
}
|
|
6312
|
-
function
|
|
7964
|
+
function withCode3(err, code) {
|
|
6313
7965
|
err.code = code;
|
|
6314
7966
|
return err;
|
|
6315
7967
|
}
|
|
6316
7968
|
|
|
6317
7969
|
// src/core/agent-prune.ts
|
|
6318
|
-
import * as
|
|
6319
|
-
import * as
|
|
7970
|
+
import * as fsp7 from "fs/promises";
|
|
7971
|
+
import * as path9 from "path";
|
|
6320
7972
|
var logSink3 = (msg) => {
|
|
6321
7973
|
process.stderr.write(msg + "\n");
|
|
6322
7974
|
};
|
|
@@ -6334,10 +7986,10 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
6334
7986
|
desiredByAgent.set(a.id, a.version ?? "current");
|
|
6335
7987
|
}
|
|
6336
7988
|
const activeByAgent = sessionManager.activeAgentVersions();
|
|
6337
|
-
const platformDir =
|
|
7989
|
+
const platformDir = path9.join(paths.agentsDir(), platformKey);
|
|
6338
7990
|
let agentEntries;
|
|
6339
7991
|
try {
|
|
6340
|
-
agentEntries = await
|
|
7992
|
+
agentEntries = await fsp7.readdir(platformDir, { withFileTypes: true });
|
|
6341
7993
|
} catch (err) {
|
|
6342
7994
|
const e = err;
|
|
6343
7995
|
if (e.code === "ENOENT") {
|
|
@@ -6356,10 +8008,10 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
6356
8008
|
continue;
|
|
6357
8009
|
}
|
|
6358
8010
|
const activeVersions = activeByAgent.get(agentId) ?? /* @__PURE__ */ new Set();
|
|
6359
|
-
const agentDir =
|
|
8011
|
+
const agentDir = path9.join(platformDir, agentId);
|
|
6360
8012
|
let versionEntries;
|
|
6361
8013
|
try {
|
|
6362
|
-
versionEntries = await
|
|
8014
|
+
versionEntries = await fsp7.readdir(agentDir, { withFileTypes: true });
|
|
6363
8015
|
} catch (err) {
|
|
6364
8016
|
logSink3(
|
|
6365
8017
|
`hydra-acp: prune: failed to read ${agentDir}: ${err.message}`
|
|
@@ -6380,9 +8032,9 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
6380
8032
|
if (version.includes(".partial-")) {
|
|
6381
8033
|
continue;
|
|
6382
8034
|
}
|
|
6383
|
-
const versionDir =
|
|
8035
|
+
const versionDir = path9.join(agentDir, version);
|
|
6384
8036
|
try {
|
|
6385
|
-
await
|
|
8037
|
+
await fsp7.rm(versionDir, { recursive: true, force: true });
|
|
6386
8038
|
logSink3(`hydra-acp: pruned stale ${agentId} ${version} (${versionDir})`);
|
|
6387
8039
|
} catch (err) {
|
|
6388
8040
|
logSink3(
|
|
@@ -6394,8 +8046,8 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
6394
8046
|
}
|
|
6395
8047
|
|
|
6396
8048
|
// src/core/session-tokens.ts
|
|
6397
|
-
import * as
|
|
6398
|
-
import * as
|
|
8049
|
+
import * as fs13 from "fs/promises";
|
|
8050
|
+
import * as path10 from "path";
|
|
6399
8051
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
6400
8052
|
var TOKEN_PREFIX = "hydra_session_";
|
|
6401
8053
|
var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
|
|
@@ -6403,7 +8055,7 @@ var ID_LENGTH = 12;
|
|
|
6403
8055
|
var TOKEN_BYTES = 32;
|
|
6404
8056
|
var WRITE_DEBOUNCE_MS = 50;
|
|
6405
8057
|
function tokensFilePath() {
|
|
6406
|
-
return
|
|
8058
|
+
return path10.join(paths.home(), "session-tokens.json");
|
|
6407
8059
|
}
|
|
6408
8060
|
function sha256Hex(input) {
|
|
6409
8061
|
return createHash("sha256").update(input).digest("hex");
|
|
@@ -6430,7 +8082,7 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
6430
8082
|
static async load() {
|
|
6431
8083
|
let records = [];
|
|
6432
8084
|
try {
|
|
6433
|
-
const raw = await
|
|
8085
|
+
const raw = await fs13.readFile(tokensFilePath(), "utf8");
|
|
6434
8086
|
const parsed = JSON.parse(raw);
|
|
6435
8087
|
if (parsed && Array.isArray(parsed.records)) {
|
|
6436
8088
|
records = parsed.records.filter(isRecord);
|
|
@@ -6558,8 +8210,8 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
6558
8210
|
const records = Array.from(this.records.values());
|
|
6559
8211
|
const payload = JSON.stringify({ records }, null, 2) + "\n";
|
|
6560
8212
|
this.writeInflight = (async () => {
|
|
6561
|
-
await
|
|
6562
|
-
await
|
|
8213
|
+
await fs13.mkdir(paths.home(), { recursive: true });
|
|
8214
|
+
await fs13.writeFile(tokensFilePath(), payload, {
|
|
6563
8215
|
encoding: "utf8",
|
|
6564
8216
|
mode: 384
|
|
6565
8217
|
});
|
|
@@ -6655,6 +8307,34 @@ function tokenFromUpgradeRequest(req) {
|
|
|
6655
8307
|
}
|
|
6656
8308
|
return void 0;
|
|
6657
8309
|
}
|
|
8310
|
+
var ProcessTokenRegistry = class {
|
|
8311
|
+
tokens = /* @__PURE__ */ new Map();
|
|
8312
|
+
mint(name, kind) {
|
|
8313
|
+
const token = generateServiceToken();
|
|
8314
|
+
this.tokens.set(token, { name, kind });
|
|
8315
|
+
return token;
|
|
8316
|
+
}
|
|
8317
|
+
// Revoke all tokens associated with the named process. Called when a
|
|
8318
|
+
// process exits or is unregistered so stale tokens can't be reused if a
|
|
8319
|
+
// new process happens to reconnect before a full daemon restart.
|
|
8320
|
+
revoke(name) {
|
|
8321
|
+
for (const [token, identity] of this.tokens) {
|
|
8322
|
+
if (identity.name === name) {
|
|
8323
|
+
this.tokens.delete(token);
|
|
8324
|
+
}
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8327
|
+
resolve(token) {
|
|
8328
|
+
return this.tokens.get(token);
|
|
8329
|
+
}
|
|
8330
|
+
async validate(token) {
|
|
8331
|
+
const identity = this.tokens.get(token);
|
|
8332
|
+
if (!identity) {
|
|
8333
|
+
return void 0;
|
|
8334
|
+
}
|
|
8335
|
+
return `${identity.kind}:${identity.name}`;
|
|
8336
|
+
}
|
|
8337
|
+
};
|
|
6658
8338
|
function constantTimeEqual(a, b) {
|
|
6659
8339
|
if (a.length !== b.length) {
|
|
6660
8340
|
return false;
|
|
@@ -6739,7 +8419,13 @@ var Bundle = z5.object({
|
|
|
6739
8419
|
exportedAt: z5.string(),
|
|
6740
8420
|
exportedFrom: z5.object({
|
|
6741
8421
|
hydraVersion: z5.string(),
|
|
6742
|
-
machine: z5.string()
|
|
8422
|
+
machine: z5.string(),
|
|
8423
|
+
// Externally-reachable name (and optional ":port") for the exporting
|
|
8424
|
+
// daemon, sourced from config.daemon.publicHost (or daemon.host when
|
|
8425
|
+
// non-loopback). Carried so an importer can construct a hydra:// URL
|
|
8426
|
+
// that dials back to the origin — e.g. over Tailscale. Omitted when
|
|
8427
|
+
// the exporter has no routable address; never falls back to loopback.
|
|
8428
|
+
hydraHost: z5.string().optional()
|
|
6743
8429
|
}),
|
|
6744
8430
|
session: BundleSession,
|
|
6745
8431
|
history: z5.array(HistoryEntrySchema),
|
|
@@ -6751,7 +8437,8 @@ function encodeBundle(params) {
|
|
|
6751
8437
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6752
8438
|
exportedFrom: {
|
|
6753
8439
|
hydraVersion: params.hydraVersion,
|
|
6754
|
-
machine: params.machine
|
|
8440
|
+
machine: params.machine,
|
|
8441
|
+
...params.hydraHost !== void 0 && params.hydraHost.length > 0 ? { hydraHost: params.hydraHost } : {}
|
|
6755
8442
|
},
|
|
6756
8443
|
session: {
|
|
6757
8444
|
sessionId: params.record.sessionId,
|
|
@@ -7095,7 +8782,7 @@ function isUpstreamInterrupted(u, errorText) {
|
|
|
7095
8782
|
}
|
|
7096
8783
|
function mapPlan(u) {
|
|
7097
8784
|
const entries = u.entries;
|
|
7098
|
-
if (!Array.isArray(entries)) {
|
|
8785
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
7099
8786
|
return null;
|
|
7100
8787
|
}
|
|
7101
8788
|
const normalized = [];
|
|
@@ -7131,7 +8818,15 @@ function mapModel(u) {
|
|
|
7131
8818
|
if (!model) {
|
|
7132
8819
|
return null;
|
|
7133
8820
|
}
|
|
7134
|
-
|
|
8821
|
+
const raw = u.availableModels;
|
|
8822
|
+
const availableModels = Array.isArray(raw) ? raw.map(
|
|
8823
|
+
(m) => typeof m === "object" && m !== null ? m.modelId : typeof m === "string" ? m : void 0
|
|
8824
|
+
).filter((id) => typeof id === "string" && id.length > 0) : void 0;
|
|
8825
|
+
return {
|
|
8826
|
+
kind: "model-changed",
|
|
8827
|
+
model: sanitizeSingleLine(model),
|
|
8828
|
+
...availableModels && availableModels.length > 0 ? { availableModels } : {}
|
|
8829
|
+
};
|
|
7135
8830
|
}
|
|
7136
8831
|
function mapTurnComplete(u) {
|
|
7137
8832
|
const stopReason = readString(u, "stopReason");
|
|
@@ -7431,7 +9126,21 @@ function formatNumber(n) {
|
|
|
7431
9126
|
return n.toLocaleString("en-US");
|
|
7432
9127
|
}
|
|
7433
9128
|
|
|
9129
|
+
// src/core/remote-url.ts
|
|
9130
|
+
function isLoopbackHost(host) {
|
|
9131
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost" || host === "[::1]";
|
|
9132
|
+
}
|
|
9133
|
+
|
|
7434
9134
|
// src/daemon/routes/sessions.ts
|
|
9135
|
+
function resolveHydraHost(defaults) {
|
|
9136
|
+
if (defaults.publicHost && defaults.publicHost.length > 0) {
|
|
9137
|
+
return defaults.publicHost;
|
|
9138
|
+
}
|
|
9139
|
+
if (defaults.host && !isLoopbackHost(defaults.host)) {
|
|
9140
|
+
return defaults.port !== void 0 ? `${defaults.host}:${defaults.port}` : defaults.host;
|
|
9141
|
+
}
|
|
9142
|
+
return void 0;
|
|
9143
|
+
}
|
|
7435
9144
|
function registerSessionRoutes(app, manager, defaults) {
|
|
7436
9145
|
app.get("/v1/sessions", async (request) => {
|
|
7437
9146
|
const query = request.query;
|
|
@@ -7531,7 +9240,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7531
9240
|
history: exported.history,
|
|
7532
9241
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
7533
9242
|
hydraVersion: HYDRA_VERSION,
|
|
7534
|
-
machine: os3.hostname()
|
|
9243
|
+
machine: os3.hostname(),
|
|
9244
|
+
hydraHost: resolveHydraHost(defaults)
|
|
7535
9245
|
});
|
|
7536
9246
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7537
9247
|
reply.header(
|
|
@@ -7553,7 +9263,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7553
9263
|
history: exported.history,
|
|
7554
9264
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
7555
9265
|
hydraVersion: HYDRA_VERSION,
|
|
7556
|
-
machine: os3.hostname()
|
|
9266
|
+
machine: os3.hostname(),
|
|
9267
|
+
hydraHost: resolveHydraHost(defaults)
|
|
7557
9268
|
});
|
|
7558
9269
|
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
7559
9270
|
reply.code(200).send(bundleToMarkdown(bundle));
|
|
@@ -7866,6 +9577,122 @@ function parseRegisterBody(body) {
|
|
|
7866
9577
|
};
|
|
7867
9578
|
}
|
|
7868
9579
|
|
|
9580
|
+
// src/daemon/routes/transformers.ts
|
|
9581
|
+
var NAME_RE2 = /^[A-Za-z0-9._-]+$/;
|
|
9582
|
+
function registerTransformerRoutes(app, transformers) {
|
|
9583
|
+
app.get("/v1/transformers", async () => {
|
|
9584
|
+
return { transformers: transformers.list() };
|
|
9585
|
+
});
|
|
9586
|
+
app.get("/v1/transformers/:name", async (request, reply) => {
|
|
9587
|
+
const name = request.params.name;
|
|
9588
|
+
const info = transformers.get(name);
|
|
9589
|
+
if (!info) {
|
|
9590
|
+
reply.code(404).send({ error: `unknown transformer: ${name}` });
|
|
9591
|
+
return;
|
|
9592
|
+
}
|
|
9593
|
+
return info;
|
|
9594
|
+
});
|
|
9595
|
+
app.post("/v1/transformers", async (request, reply) => {
|
|
9596
|
+
const body = request.body ?? {};
|
|
9597
|
+
const parsed = parseRegisterBody2(body);
|
|
9598
|
+
if ("error" in parsed) {
|
|
9599
|
+
reply.code(400).send({ error: parsed.error });
|
|
9600
|
+
return;
|
|
9601
|
+
}
|
|
9602
|
+
try {
|
|
9603
|
+
const info = transformers.register(parsed.config);
|
|
9604
|
+
reply.code(201).send(info);
|
|
9605
|
+
} catch (err) {
|
|
9606
|
+
sendError2(reply, err);
|
|
9607
|
+
}
|
|
9608
|
+
});
|
|
9609
|
+
app.delete("/v1/transformers/:name", async (request, reply) => {
|
|
9610
|
+
const name = request.params.name;
|
|
9611
|
+
try {
|
|
9612
|
+
await transformers.unregister(name);
|
|
9613
|
+
reply.code(204).send();
|
|
9614
|
+
} catch (err) {
|
|
9615
|
+
sendError2(reply, err);
|
|
9616
|
+
}
|
|
9617
|
+
});
|
|
9618
|
+
app.post("/v1/transformers/:name/start", async (request, reply) => {
|
|
9619
|
+
const name = request.params.name;
|
|
9620
|
+
try {
|
|
9621
|
+
const info = await transformers.startByName(name);
|
|
9622
|
+
reply.code(200).send(info);
|
|
9623
|
+
} catch (err) {
|
|
9624
|
+
sendError2(reply, err);
|
|
9625
|
+
}
|
|
9626
|
+
});
|
|
9627
|
+
app.post("/v1/transformers/:name/stop", async (request, reply) => {
|
|
9628
|
+
const name = request.params.name;
|
|
9629
|
+
try {
|
|
9630
|
+
const info = await transformers.stopByName(name);
|
|
9631
|
+
reply.code(200).send(info);
|
|
9632
|
+
} catch (err) {
|
|
9633
|
+
sendError2(reply, err);
|
|
9634
|
+
}
|
|
9635
|
+
});
|
|
9636
|
+
app.post("/v1/transformers/:name/restart", async (request, reply) => {
|
|
9637
|
+
const name = request.params.name;
|
|
9638
|
+
try {
|
|
9639
|
+
const info = await transformers.restartByName(name);
|
|
9640
|
+
reply.code(200).send(info);
|
|
9641
|
+
} catch (err) {
|
|
9642
|
+
sendError2(reply, err);
|
|
9643
|
+
}
|
|
9644
|
+
});
|
|
9645
|
+
}
|
|
9646
|
+
function sendError2(reply, err) {
|
|
9647
|
+
const code = err.code;
|
|
9648
|
+
const message = err.message ?? "unknown error";
|
|
9649
|
+
if (code === "NOT_FOUND") {
|
|
9650
|
+
reply.code(404).send({ error: message });
|
|
9651
|
+
return;
|
|
9652
|
+
}
|
|
9653
|
+
if (code === "CONFLICT") {
|
|
9654
|
+
reply.code(409).send({ error: message });
|
|
9655
|
+
return;
|
|
9656
|
+
}
|
|
9657
|
+
reply.code(500).send({ error: message });
|
|
9658
|
+
}
|
|
9659
|
+
function parseRegisterBody2(body) {
|
|
9660
|
+
const name = body.name;
|
|
9661
|
+
if (typeof name !== "string" || !NAME_RE2.test(name)) {
|
|
9662
|
+
return { error: "name must match [A-Za-z0-9._-]+" };
|
|
9663
|
+
}
|
|
9664
|
+
const command = body.command;
|
|
9665
|
+
if (command !== void 0 && (!Array.isArray(command) || command.some((c) => typeof c !== "string"))) {
|
|
9666
|
+
return { error: "command must be string[]" };
|
|
9667
|
+
}
|
|
9668
|
+
const args = body.args;
|
|
9669
|
+
if (args !== void 0 && (!Array.isArray(args) || args.some((a) => typeof a !== "string"))) {
|
|
9670
|
+
return { error: "args must be string[]" };
|
|
9671
|
+
}
|
|
9672
|
+
const env = body.env;
|
|
9673
|
+
if (env !== void 0 && (typeof env !== "object" || env === null || Array.isArray(env))) {
|
|
9674
|
+
return { error: "env must be an object of string\u2192string" };
|
|
9675
|
+
}
|
|
9676
|
+
if (env && Object.values(env).some(
|
|
9677
|
+
(v) => typeof v !== "string"
|
|
9678
|
+
)) {
|
|
9679
|
+
return { error: "env values must be strings" };
|
|
9680
|
+
}
|
|
9681
|
+
const enabled = body.enabled;
|
|
9682
|
+
if (enabled !== void 0 && typeof enabled !== "boolean") {
|
|
9683
|
+
return { error: "enabled must be a boolean" };
|
|
9684
|
+
}
|
|
9685
|
+
return {
|
|
9686
|
+
config: {
|
|
9687
|
+
name,
|
|
9688
|
+
command: command ?? [],
|
|
9689
|
+
args: args ?? [],
|
|
9690
|
+
env: env ?? {},
|
|
9691
|
+
enabled: enabled === void 0 ? true : enabled
|
|
9692
|
+
}
|
|
9693
|
+
};
|
|
9694
|
+
}
|
|
9695
|
+
|
|
7869
9696
|
// src/daemon/routes/config.ts
|
|
7870
9697
|
function registerConfigRoutes(app, defaults) {
|
|
7871
9698
|
app.get("/v1/config", async () => {
|
|
@@ -7880,19 +9707,19 @@ function registerConfigRoutes(app, defaults) {
|
|
|
7880
9707
|
import { z as z6 } from "zod";
|
|
7881
9708
|
|
|
7882
9709
|
// src/core/password.ts
|
|
7883
|
-
import * as
|
|
7884
|
-
import * as
|
|
9710
|
+
import * as fs14 from "fs/promises";
|
|
9711
|
+
import * as path11 from "path";
|
|
7885
9712
|
import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
7886
9713
|
import { promisify } from "util";
|
|
7887
9714
|
var scryptAsync = promisify(scrypt);
|
|
7888
9715
|
function passwordHashPath() {
|
|
7889
|
-
return
|
|
9716
|
+
return path11.join(paths.home(), "password-hash");
|
|
7890
9717
|
}
|
|
7891
9718
|
var DEFAULT_N = 1 << 15;
|
|
7892
9719
|
var MAX_MEM = 128 * 1024 * 1024;
|
|
7893
9720
|
async function hasPassword() {
|
|
7894
9721
|
try {
|
|
7895
|
-
const text = await
|
|
9722
|
+
const text = await fs14.readFile(passwordHashPath(), "utf8");
|
|
7896
9723
|
return text.trim().length > 0;
|
|
7897
9724
|
} catch (err) {
|
|
7898
9725
|
const e = err;
|
|
@@ -7908,7 +9735,7 @@ async function verifyPassword(plaintext) {
|
|
|
7908
9735
|
}
|
|
7909
9736
|
let line;
|
|
7910
9737
|
try {
|
|
7911
|
-
line = (await
|
|
9738
|
+
line = (await fs14.readFile(passwordHashPath(), "utf8")).trim();
|
|
7912
9739
|
} catch (err) {
|
|
7913
9740
|
const e = err;
|
|
7914
9741
|
if (e.code === "ENOENT") {
|
|
@@ -8102,6 +9929,8 @@ function wsToMessageStream(ws) {
|
|
|
8102
9929
|
}
|
|
8103
9930
|
|
|
8104
9931
|
// src/daemon/acp-ws.ts
|
|
9932
|
+
import * as os4 from "os";
|
|
9933
|
+
import * as path12 from "path";
|
|
8105
9934
|
function registerAcpWsEndpoint(app, deps) {
|
|
8106
9935
|
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
8107
9936
|
const token = tokenFromUpgradeRequest({
|
|
@@ -8112,10 +9941,12 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8112
9941
|
socket.close(4401, "Unauthorized");
|
|
8113
9942
|
return;
|
|
8114
9943
|
}
|
|
9944
|
+
const processIdentity = deps.processRegistry?.resolve(token);
|
|
8115
9945
|
const stream = wsToMessageStream(socket);
|
|
8116
9946
|
const connection = new JsonRpcConnection(stream);
|
|
8117
9947
|
const state = {
|
|
8118
9948
|
clientId: `hydra_client_${nanoid2(12)}`,
|
|
9949
|
+
processIdentity,
|
|
8119
9950
|
attached: /* @__PURE__ */ new Map()
|
|
8120
9951
|
};
|
|
8121
9952
|
connection.onClose(() => {
|
|
@@ -8136,14 +9967,158 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8136
9967
|
}
|
|
8137
9968
|
};
|
|
8138
9969
|
connection.onRequest("initialize", async (raw) => {
|
|
8139
|
-
InitializeParams.parse(raw ?? {});
|
|
9970
|
+
const params = InitializeParams.parse(raw ?? {});
|
|
9971
|
+
const version = params.clientInfo?.version;
|
|
9972
|
+
if (version && processIdentity) {
|
|
9973
|
+
if (processIdentity.kind === "extension") {
|
|
9974
|
+
deps.onExtensionVersion?.(processIdentity.name, version);
|
|
9975
|
+
} else {
|
|
9976
|
+
deps.onTransformerVersion?.(processIdentity.name, version);
|
|
9977
|
+
}
|
|
9978
|
+
}
|
|
8140
9979
|
return buildInitializeResult();
|
|
8141
9980
|
});
|
|
9981
|
+
if (processIdentity?.kind === "transformer") {
|
|
9982
|
+
connection.onRequest("transformer/initialize", async (raw) => {
|
|
9983
|
+
const params = raw ?? {};
|
|
9984
|
+
const intercepts = Array.isArray(params.intercepts) ? params.intercepts.filter(
|
|
9985
|
+
(v) => typeof v === "string"
|
|
9986
|
+
) : [];
|
|
9987
|
+
if (deps.transformers) {
|
|
9988
|
+
deps.transformers.registerConnection(
|
|
9989
|
+
processIdentity.name,
|
|
9990
|
+
connection,
|
|
9991
|
+
intercepts
|
|
9992
|
+
);
|
|
9993
|
+
if (deps.manager?.defaultTransformers.includes(processIdentity.name)) {
|
|
9994
|
+
const ref = deps.transformers.resolveChain([processIdentity.name])[0];
|
|
9995
|
+
if (ref) {
|
|
9996
|
+
for (const session of deps.manager.liveSessions()) {
|
|
9997
|
+
session.addTransformer(ref);
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
10000
|
+
}
|
|
10001
|
+
}
|
|
10002
|
+
return { ack: true };
|
|
10003
|
+
});
|
|
10004
|
+
connection.onClose(() => {
|
|
10005
|
+
deps.transformers?.deregisterConnection(processIdentity.name);
|
|
10006
|
+
});
|
|
10007
|
+
connection.onRequest("hydra-acp/emit_message", async (raw) => {
|
|
10008
|
+
const params = raw ?? {};
|
|
10009
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : void 0;
|
|
10010
|
+
const method = typeof params.method === "string" ? params.method : void 0;
|
|
10011
|
+
const envelope = params.envelope;
|
|
10012
|
+
const route = params.route;
|
|
10013
|
+
if (!sessionId || !method) {
|
|
10014
|
+
throw Object.assign(new Error("emit_message requires sessionId and method"), { code: -32602 });
|
|
10015
|
+
}
|
|
10016
|
+
const session = deps.manager.get(sessionId);
|
|
10017
|
+
if (!session) {
|
|
10018
|
+
throw Object.assign(new Error(`session ${sessionId} not found`), { code: JsonRpcErrorCodes.SessionNotFound });
|
|
10019
|
+
}
|
|
10020
|
+
const respondsTo = typeof params.respondsTo === "string" ? params.respondsTo : void 0;
|
|
10021
|
+
if (respondsTo) {
|
|
10022
|
+
session.dischargeClaim(respondsTo, envelope);
|
|
10023
|
+
return { ok: true };
|
|
10024
|
+
}
|
|
10025
|
+
if (route === "chain") {
|
|
10026
|
+
await session.emitToChain(processIdentity.name, method, envelope);
|
|
10027
|
+
return { ok: true };
|
|
10028
|
+
}
|
|
10029
|
+
if (route === "daemon") {
|
|
10030
|
+
await session.emitToChain(processIdentity.name, method, envelope);
|
|
10031
|
+
return { ok: true };
|
|
10032
|
+
}
|
|
10033
|
+
throw Object.assign(new Error(`unsupported route: ${JSON.stringify(route)}`), { code: -32602 });
|
|
10034
|
+
});
|
|
10035
|
+
connection.onRequest("hydra-acp/spawn_child_session", async (raw) => {
|
|
10036
|
+
const params = raw ?? {};
|
|
10037
|
+
const agentId = typeof params.agentId === "string" ? params.agentId : deps.defaultAgent;
|
|
10038
|
+
const cwd = typeof params.cwd === "string" ? params.cwd : void 0;
|
|
10039
|
+
const parentSessionId = typeof params.parentSessionId === "string" ? params.parentSessionId : void 0;
|
|
10040
|
+
if (!cwd) {
|
|
10041
|
+
throw Object.assign(new Error("spawn_child_session requires cwd"), { code: -32602 });
|
|
10042
|
+
}
|
|
10043
|
+
const child = await deps.manager.create({
|
|
10044
|
+
agentId,
|
|
10045
|
+
cwd,
|
|
10046
|
+
parentSessionId,
|
|
10047
|
+
transformChain: []
|
|
10048
|
+
// children start with no chain by default
|
|
10049
|
+
});
|
|
10050
|
+
return { childSessionId: child.sessionId };
|
|
10051
|
+
});
|
|
10052
|
+
connection.onRequest("hydra-acp/await_child", async (raw) => {
|
|
10053
|
+
const params = raw ?? {};
|
|
10054
|
+
const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
|
|
10055
|
+
const until = params.until === "idle" ? "idle" : "turn_complete";
|
|
10056
|
+
const timeoutMs = typeof params.timeoutMs === "number" ? Math.min(params.timeoutMs, 30 * 6e4) : 5 * 6e4;
|
|
10057
|
+
if (!childSessionId) {
|
|
10058
|
+
throw Object.assign(new Error("await_child requires childSessionId"), { code: -32602 });
|
|
10059
|
+
}
|
|
10060
|
+
const child = deps.manager.get(childSessionId);
|
|
10061
|
+
if (!child) {
|
|
10062
|
+
throw Object.assign(
|
|
10063
|
+
new Error(`child session ${childSessionId} not found`),
|
|
10064
|
+
{ code: JsonRpcErrorCodes.SessionNotFound }
|
|
10065
|
+
);
|
|
10066
|
+
}
|
|
10067
|
+
return new Promise((resolve3) => {
|
|
10068
|
+
const entries = [];
|
|
10069
|
+
let unsubscribe;
|
|
10070
|
+
const finish = () => {
|
|
10071
|
+
clearTimeout(timer);
|
|
10072
|
+
unsubscribe?.();
|
|
10073
|
+
resolve3({ entries });
|
|
10074
|
+
};
|
|
10075
|
+
unsubscribe = child.onBroadcast((entry) => {
|
|
10076
|
+
entries.push(entry);
|
|
10077
|
+
if (until === "turn_complete") {
|
|
10078
|
+
const upd = entry.params?.update;
|
|
10079
|
+
if (upd?.sessionUpdate === "turn_complete") {
|
|
10080
|
+
finish();
|
|
10081
|
+
}
|
|
10082
|
+
}
|
|
10083
|
+
});
|
|
10084
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
10085
|
+
if (typeof timer.unref === "function") {
|
|
10086
|
+
timer.unref();
|
|
10087
|
+
}
|
|
10088
|
+
child.onClose(() => finish());
|
|
10089
|
+
});
|
|
10090
|
+
});
|
|
10091
|
+
connection.onRequest("hydra-acp/close_child_session", async (raw) => {
|
|
10092
|
+
const params = raw ?? {};
|
|
10093
|
+
const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
|
|
10094
|
+
if (!childSessionId) {
|
|
10095
|
+
throw Object.assign(new Error("close_child_session requires childSessionId"), { code: -32602 });
|
|
10096
|
+
}
|
|
10097
|
+
const child = deps.manager.get(childSessionId);
|
|
10098
|
+
if (child) {
|
|
10099
|
+
await child.close({ deleteRecord: false });
|
|
10100
|
+
}
|
|
10101
|
+
return { ok: true };
|
|
10102
|
+
});
|
|
10103
|
+
connection.onRequest("hydra-acp/keep_alive", async (raw) => {
|
|
10104
|
+
const params = raw ?? {};
|
|
10105
|
+
const token2 = typeof params.token === "string" ? params.token : void 0;
|
|
10106
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : void 0;
|
|
10107
|
+
const estimatedRemainingMs = typeof params.estimatedRemainingMs === "number" ? params.estimatedRemainingMs : void 0;
|
|
10108
|
+
if (token2 && sessionId) {
|
|
10109
|
+
const session = deps.manager.get(sessionId);
|
|
10110
|
+
session?.keepAliveClaim(token2, estimatedRemainingMs);
|
|
10111
|
+
}
|
|
10112
|
+
return { ok: true };
|
|
10113
|
+
});
|
|
10114
|
+
}
|
|
8142
10115
|
connection.onRequest("session/new", async (raw) => {
|
|
8143
10116
|
const params = SessionNewParams.parse(raw);
|
|
8144
10117
|
const hydraMeta = extractHydraMeta(
|
|
8145
10118
|
raw?._meta
|
|
8146
10119
|
);
|
|
10120
|
+
const transformerNames = Array.isArray(hydraMeta.transformers) && hydraMeta.transformers.every((t) => typeof t === "string") ? hydraMeta.transformers : deps.manager.defaultTransformers ?? [];
|
|
10121
|
+
const transformChain = deps.transformers?.resolveChain(transformerNames) ?? [];
|
|
8147
10122
|
const session = await deps.manager.create({
|
|
8148
10123
|
cwd: params.cwd,
|
|
8149
10124
|
agentId: params.agentId ?? deps.defaultAgent,
|
|
@@ -8151,7 +10126,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8151
10126
|
title: hydraMeta.name,
|
|
8152
10127
|
agentArgs: hydraMeta.agentArgs,
|
|
8153
10128
|
model: hydraMeta.model,
|
|
8154
|
-
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
10129
|
+
onInstallProgress: makeInstallProgressForwarder(connection),
|
|
10130
|
+
transformChain
|
|
8155
10131
|
});
|
|
8156
10132
|
const client = bindClientToSession(connection, session, state);
|
|
8157
10133
|
const { entries: replay } = await session.attach(client, "full");
|
|
@@ -8183,6 +10159,14 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8183
10159
|
});
|
|
8184
10160
|
connection.onRequest("session/attach", async (raw) => {
|
|
8185
10161
|
const params = SessionAttachParams.parse(raw);
|
|
10162
|
+
const attachVersion = params.clientInfo?.version;
|
|
10163
|
+
if (attachVersion && processIdentity) {
|
|
10164
|
+
if (processIdentity.kind === "extension") {
|
|
10165
|
+
deps.onExtensionVersion?.(processIdentity.name, attachVersion);
|
|
10166
|
+
} else {
|
|
10167
|
+
deps.onTransformerVersion?.(processIdentity.name, attachVersion);
|
|
10168
|
+
}
|
|
10169
|
+
}
|
|
8186
10170
|
const hydraHints = extractHydraMeta(params._meta).resume;
|
|
8187
10171
|
const readonly = params.readonly === true;
|
|
8188
10172
|
app.log.info(
|
|
@@ -8229,16 +10213,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8229
10213
|
let resurrectParams = fromDisk;
|
|
8230
10214
|
if (hydraHints) {
|
|
8231
10215
|
resurrectParams = {
|
|
10216
|
+
...fromDisk,
|
|
8232
10217
|
hydraSessionId: params.sessionId,
|
|
8233
10218
|
upstreamSessionId: hydraHints.upstreamSessionId,
|
|
8234
10219
|
agentId: hydraHints.agentId,
|
|
8235
10220
|
cwd: hydraHints.cwd,
|
|
8236
|
-
title: hydraHints.title
|
|
8237
|
-
agentArgs: hydraHints.agentArgs
|
|
8238
|
-
currentModel: fromDisk?.currentModel,
|
|
8239
|
-
currentMode: fromDisk?.currentMode,
|
|
8240
|
-
agentCommands: fromDisk?.agentCommands,
|
|
8241
|
-
createdAt: fromDisk?.createdAt
|
|
10221
|
+
...hydraHints.title !== void 0 ? { title: hydraHints.title } : {},
|
|
10222
|
+
...hydraHints.agentArgs !== void 0 ? { agentArgs: hydraHints.agentArgs } : {}
|
|
8242
10223
|
};
|
|
8243
10224
|
}
|
|
8244
10225
|
if (!resurrectParams) {
|
|
@@ -8252,6 +10233,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8252
10233
|
...resurrectParams,
|
|
8253
10234
|
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
8254
10235
|
});
|
|
10236
|
+
wireDefaultTransformers(session, deps);
|
|
8255
10237
|
}
|
|
8256
10238
|
const client = bindClientToSession(
|
|
8257
10239
|
connection,
|
|
@@ -8341,6 +10323,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8341
10323
|
`session/prompt auto-resurrecting cold sessionId=${params.sessionId}`
|
|
8342
10324
|
);
|
|
8343
10325
|
session = await deps.manager.resurrect(fromDisk);
|
|
10326
|
+
wireDefaultTransformers(session, deps);
|
|
8344
10327
|
const client = bindClientToSession(
|
|
8345
10328
|
connection,
|
|
8346
10329
|
session,
|
|
@@ -8428,6 +10411,51 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8428
10411
|
}
|
|
8429
10412
|
return session.amendPrompt(att.clientId, params);
|
|
8430
10413
|
});
|
|
10414
|
+
connection.onRequest("hydra-acp/stream_open", async (raw) => {
|
|
10415
|
+
const params = StreamOpenParams.parse(raw);
|
|
10416
|
+
denyIfReadonly(params.sessionId, "hydra-acp/stream_open");
|
|
10417
|
+
const session = deps.manager.get(params.sessionId);
|
|
10418
|
+
if (!session) {
|
|
10419
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
10420
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
10421
|
+
throw err;
|
|
10422
|
+
}
|
|
10423
|
+
const openOpts = {};
|
|
10424
|
+
if (params.mode !== void 0) {
|
|
10425
|
+
openOpts.mode = params.mode;
|
|
10426
|
+
}
|
|
10427
|
+
if (params.capacityBytes !== void 0) {
|
|
10428
|
+
openOpts.capacityBytes = params.capacityBytes;
|
|
10429
|
+
}
|
|
10430
|
+
if (params.fileCapBytes !== void 0) {
|
|
10431
|
+
openOpts.fileCapBytes = params.fileCapBytes;
|
|
10432
|
+
}
|
|
10433
|
+
if ((params.mode ?? "memory") === "file") {
|
|
10434
|
+
openOpts.filePathFor = (sid) => path12.join(os4.tmpdir(), `hydra-stdin-${sid}.log`);
|
|
10435
|
+
}
|
|
10436
|
+
return session.openStream(openOpts);
|
|
10437
|
+
});
|
|
10438
|
+
connection.onRequest("hydra-acp/stream_write", async (raw) => {
|
|
10439
|
+
const params = StreamWriteParams.parse(raw);
|
|
10440
|
+
denyIfReadonly(params.sessionId, "hydra-acp/stream_write");
|
|
10441
|
+
const session = deps.manager.get(params.sessionId);
|
|
10442
|
+
if (!session) {
|
|
10443
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
10444
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
10445
|
+
throw err;
|
|
10446
|
+
}
|
|
10447
|
+
return session.streamWrite(params.chunk, params.eof);
|
|
10448
|
+
});
|
|
10449
|
+
connection.onRequest("hydra-acp/stream_read", async (raw) => {
|
|
10450
|
+
const params = StreamReadParams.parse(raw);
|
|
10451
|
+
const session = deps.manager.get(params.sessionId);
|
|
10452
|
+
if (!session) {
|
|
10453
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
10454
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
10455
|
+
throw err;
|
|
10456
|
+
}
|
|
10457
|
+
return session.streamRead(params.cursor, params.maxBytes, params.waitMs);
|
|
10458
|
+
});
|
|
8431
10459
|
connection.onRequest("session/load", async (raw) => {
|
|
8432
10460
|
const rawObj = raw ?? {};
|
|
8433
10461
|
const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
|
|
@@ -8448,6 +10476,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8448
10476
|
throw err;
|
|
8449
10477
|
}
|
|
8450
10478
|
session = await deps.manager.resurrect(fromDisk);
|
|
10479
|
+
wireDefaultTransformers(session, deps);
|
|
8451
10480
|
}
|
|
8452
10481
|
const client = bindClientToSession(connection, session, state);
|
|
8453
10482
|
const { entries: replay } = await session.attach(client, "pending_only");
|
|
@@ -8498,6 +10527,32 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8498
10527
|
app.log.info(decision.logMessage);
|
|
8499
10528
|
return decision.session.forwardRequest("session/set_model", rawParams);
|
|
8500
10529
|
});
|
|
10530
|
+
connection.onRequest("session/set_mode", async (rawParams) => {
|
|
10531
|
+
const params = rawParams;
|
|
10532
|
+
const sessionIdField = params?.sessionId;
|
|
10533
|
+
if (typeof sessionIdField === "string") {
|
|
10534
|
+
denyIfReadonly(sessionIdField, "session/set_mode");
|
|
10535
|
+
}
|
|
10536
|
+
if (!params || typeof params.sessionId !== "string") {
|
|
10537
|
+
const err = new Error("session/set_mode requires string sessionId");
|
|
10538
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
10539
|
+
throw err;
|
|
10540
|
+
}
|
|
10541
|
+
if (typeof params.modeId !== "string") {
|
|
10542
|
+
const err = new Error("session/set_mode requires string modeId");
|
|
10543
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
10544
|
+
throw err;
|
|
10545
|
+
}
|
|
10546
|
+
const session = deps.manager.get(params.sessionId);
|
|
10547
|
+
if (!session) {
|
|
10548
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
10549
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
10550
|
+
throw err;
|
|
10551
|
+
}
|
|
10552
|
+
const result = await session.forwardRequest("session/set_mode", rawParams);
|
|
10553
|
+
session.applyModeChange(params.modeId);
|
|
10554
|
+
return result;
|
|
10555
|
+
});
|
|
8501
10556
|
connection.setDefaultHandler(async (rawParams, method) => {
|
|
8502
10557
|
if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
|
|
8503
10558
|
const err = new Error(`Method not found: ${method}`);
|
|
@@ -8717,6 +10772,9 @@ function buildResponseMeta(session) {
|
|
|
8717
10772
|
if (session.turnStartedAt !== void 0) {
|
|
8718
10773
|
ours.turnStartedAt = session.turnStartedAt;
|
|
8719
10774
|
}
|
|
10775
|
+
if (session.agentCapabilities !== void 0) {
|
|
10776
|
+
ours.agentCapabilities = session.agentCapabilities;
|
|
10777
|
+
}
|
|
8720
10778
|
const queue = session.queueSnapshot();
|
|
8721
10779
|
if (queue.length > 0) {
|
|
8722
10780
|
ours.queue = queue;
|
|
@@ -8767,6 +10825,17 @@ function buildInitializeResult() {
|
|
|
8767
10825
|
})
|
|
8768
10826
|
};
|
|
8769
10827
|
}
|
|
10828
|
+
function wireDefaultTransformers(session, deps) {
|
|
10829
|
+
if (!deps.transformers || !deps.manager) {
|
|
10830
|
+
return;
|
|
10831
|
+
}
|
|
10832
|
+
for (const name of deps.manager.defaultTransformers) {
|
|
10833
|
+
const ref = deps.transformers.resolveChain([name])[0];
|
|
10834
|
+
if (ref) {
|
|
10835
|
+
session.addTransformer(ref);
|
|
10836
|
+
}
|
|
10837
|
+
}
|
|
10838
|
+
}
|
|
8770
10839
|
function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
|
|
8771
10840
|
void state;
|
|
8772
10841
|
void session;
|
|
@@ -8781,10 +10850,10 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
|
|
|
8781
10850
|
async function startDaemon(config, serviceToken) {
|
|
8782
10851
|
ensureLoopbackOrTls(config);
|
|
8783
10852
|
const httpsOptions = config.daemon.tls ? {
|
|
8784
|
-
key: await
|
|
8785
|
-
cert: await
|
|
10853
|
+
key: await fsp8.readFile(config.daemon.tls.key),
|
|
10854
|
+
cert: await fsp8.readFile(config.daemon.tls.cert)
|
|
8786
10855
|
} : void 0;
|
|
8787
|
-
await
|
|
10856
|
+
await fsp8.mkdir(paths.home(), { recursive: true });
|
|
8788
10857
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
8789
10858
|
config.daemon.logLevel
|
|
8790
10859
|
);
|
|
@@ -8809,9 +10878,11 @@ async function startDaemon(config, serviceToken) {
|
|
|
8809
10878
|
});
|
|
8810
10879
|
const sessionTokenStore = await SessionTokenStore.load();
|
|
8811
10880
|
const authRateLimiter = new AuthRateLimiter();
|
|
10881
|
+
const processRegistry = new ProcessTokenRegistry();
|
|
8812
10882
|
const validator = new CompositeTokenValidator([
|
|
8813
10883
|
new StaticTokenValidator(serviceToken),
|
|
8814
|
-
new SessionTokenValidator(sessionTokenStore)
|
|
10884
|
+
new SessionTokenValidator(sessionTokenStore),
|
|
10885
|
+
processRegistry
|
|
8815
10886
|
]);
|
|
8816
10887
|
const auth = bearerAuth({ validator });
|
|
8817
10888
|
app.addHook("onRequest", async (request, reply) => {
|
|
@@ -8848,18 +10919,28 @@ async function startDaemon(config, serviceToken) {
|
|
|
8848
10919
|
const manager = new SessionManager(registry, spawner, void 0, {
|
|
8849
10920
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
8850
10921
|
defaultModels: config.defaultModels,
|
|
10922
|
+
defaultTransformers: config.defaultTransformers,
|
|
8851
10923
|
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
|
|
8852
10924
|
logger: agentLogger,
|
|
8853
10925
|
npmRegistry: config.npmRegistry
|
|
8854
10926
|
});
|
|
8855
|
-
const extensions = new ExtensionManager(extensionList(config)
|
|
10927
|
+
const extensions = new ExtensionManager(extensionList(config), void 0, {
|
|
10928
|
+
tokenRegistry: processRegistry
|
|
10929
|
+
});
|
|
10930
|
+
const transformers = new TransformerManager(transformerList(config), void 0, {
|
|
10931
|
+
tokenRegistry: processRegistry
|
|
10932
|
+
});
|
|
8856
10933
|
registerHealthRoutes(app, HYDRA_VERSION);
|
|
8857
10934
|
registerSessionRoutes(app, manager, {
|
|
8858
10935
|
agentId: config.defaultAgent,
|
|
8859
|
-
cwd: config.defaultCwd
|
|
10936
|
+
cwd: config.defaultCwd,
|
|
10937
|
+
publicHost: config.daemon.publicHost,
|
|
10938
|
+
host: config.daemon.host,
|
|
10939
|
+
port: config.daemon.port
|
|
8860
10940
|
});
|
|
8861
10941
|
registerAgentRoutes(app, registry, manager, { npmRegistry: config.npmRegistry });
|
|
8862
10942
|
registerExtensionRoutes(app, extensions);
|
|
10943
|
+
registerTransformerRoutes(app, transformers);
|
|
8863
10944
|
registerConfigRoutes(app, {
|
|
8864
10945
|
defaultAgent: config.defaultAgent,
|
|
8865
10946
|
defaultCwd: config.defaultCwd
|
|
@@ -8871,13 +10952,17 @@ async function startDaemon(config, serviceToken) {
|
|
|
8871
10952
|
registerAcpWsEndpoint(app, {
|
|
8872
10953
|
validator,
|
|
8873
10954
|
manager,
|
|
8874
|
-
defaultAgent: config.defaultAgent
|
|
10955
|
+
defaultAgent: config.defaultAgent,
|
|
10956
|
+
processRegistry,
|
|
10957
|
+
onExtensionVersion: (name, version) => extensions.reportVersion(name, version),
|
|
10958
|
+
onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
|
|
10959
|
+
transformers
|
|
8875
10960
|
});
|
|
8876
10961
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
8877
10962
|
const address = app.server.address();
|
|
8878
10963
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
8879
|
-
await
|
|
8880
|
-
await
|
|
10964
|
+
await fsp8.mkdir(paths.home(), { recursive: true });
|
|
10965
|
+
await fsp8.writeFile(
|
|
8881
10966
|
paths.pidFile(),
|
|
8882
10967
|
JSON.stringify({
|
|
8883
10968
|
pid: process.pid,
|
|
@@ -8889,15 +10974,18 @@ async function startDaemon(config, serviceToken) {
|
|
|
8889
10974
|
);
|
|
8890
10975
|
const scheme = config.daemon.tls ? "https" : "http";
|
|
8891
10976
|
const wsScheme = config.daemon.tls ? "wss" : "ws";
|
|
8892
|
-
|
|
10977
|
+
const processContext = {
|
|
8893
10978
|
daemonUrl: `${scheme}://${config.daemon.host}:${boundPort}`,
|
|
8894
10979
|
daemonHost: config.daemon.host,
|
|
8895
10980
|
daemonPort: boundPort,
|
|
8896
10981
|
serviceToken,
|
|
8897
10982
|
daemonWsUrl: `${wsScheme}://${config.daemon.host}:${boundPort}/acp`,
|
|
8898
10983
|
hydraHome: paths.home()
|
|
8899
|
-
}
|
|
10984
|
+
};
|
|
10985
|
+
extensions.setContext(processContext);
|
|
10986
|
+
transformers.setContext(processContext);
|
|
8900
10987
|
await extensions.start();
|
|
10988
|
+
await transformers.start();
|
|
8901
10989
|
void manager.resurrectPendingQueues().catch((err) => {
|
|
8902
10990
|
app.log.warn(
|
|
8903
10991
|
`queue replay scan failed: ${err.message}`
|
|
@@ -8907,6 +10995,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
8907
10995
|
clearInterval(sweepInterval);
|
|
8908
10996
|
await sessionTokenStore.flush();
|
|
8909
10997
|
await extensions.stop();
|
|
10998
|
+
await transformers.stop();
|
|
8910
10999
|
await manager.closeAll();
|
|
8911
11000
|
await manager.flushMetaWrites();
|
|
8912
11001
|
setBinaryInstallLogger(null);
|
|
@@ -8914,7 +11003,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
8914
11003
|
setAgentPruneLogger(null);
|
|
8915
11004
|
await app.close();
|
|
8916
11005
|
try {
|
|
8917
|
-
|
|
11006
|
+
fs15.unlinkSync(paths.pidFile());
|
|
8918
11007
|
} catch {
|
|
8919
11008
|
}
|
|
8920
11009
|
try {
|
|
@@ -8922,7 +11011,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
8922
11011
|
} catch {
|
|
8923
11012
|
}
|
|
8924
11013
|
};
|
|
8925
|
-
return { app, manager, registry, extensions, shutdown };
|
|
11014
|
+
return { app, manager, registry, extensions, transformers, shutdown };
|
|
8926
11015
|
}
|
|
8927
11016
|
async function buildLogStream(level) {
|
|
8928
11017
|
const fileStream = await createPinoRoll({
|