@hydra-acp/cli 0.1.43 → 0.1.45
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 +57 -3
- package/dist/cli.js +4475 -1827
- package/dist/index.d.ts +220 -6
- package/dist/index.js +1936 -163
- 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
|
|
@@ -238,6 +245,12 @@ var ExtensionBody = z.object({
|
|
|
238
245
|
env: z.record(z.string()).default({}),
|
|
239
246
|
enabled: z.boolean().default(true)
|
|
240
247
|
});
|
|
248
|
+
var TransformerBody = z.object({
|
|
249
|
+
command: z.array(z.string()).default([]),
|
|
250
|
+
args: z.array(z.string()).default([]),
|
|
251
|
+
env: z.record(z.string()).default({}),
|
|
252
|
+
enabled: z.boolean().default(true)
|
|
253
|
+
});
|
|
241
254
|
var HydraConfig = z.object({
|
|
242
255
|
daemon: DaemonConfig.default({}),
|
|
243
256
|
registry: RegistryConfig.default({ url: REGISTRY_URL_DEFAULT, ttlHours: 24 }),
|
|
@@ -259,6 +272,8 @@ var HydraConfig = z.object({
|
|
|
259
272
|
// recency and truncated to this count. `--all` overrides in the CLI.
|
|
260
273
|
sessionListColdLimit: z.number().int().nonnegative().default(20),
|
|
261
274
|
extensions: z.record(ExtensionName, ExtensionBody).default({}),
|
|
275
|
+
transformers: z.record(ExtensionName, TransformerBody).default({}),
|
|
276
|
+
defaultTransformers: z.array(z.string()).default([]),
|
|
262
277
|
// npm registry URL used when installing npm-distributed agents into
|
|
263
278
|
// ~/.hydra-acp/agents. Overrides the global ~/.npmrc registry so a
|
|
264
279
|
// corporate .npmrc pointing at an internal registry doesn't break
|
|
@@ -281,6 +296,12 @@ function extensionList(config) {
|
|
|
281
296
|
...body
|
|
282
297
|
}));
|
|
283
298
|
}
|
|
299
|
+
function transformerList(config) {
|
|
300
|
+
return Object.entries(config.transformers).map(([name, body]) => ({
|
|
301
|
+
name,
|
|
302
|
+
...body
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
284
305
|
async function readConfigFile() {
|
|
285
306
|
let raw;
|
|
286
307
|
try {
|
|
@@ -1138,7 +1159,8 @@ var JsonRpcErrorCodes = {
|
|
|
1138
1159
|
// can't collide with future spec assignments.
|
|
1139
1160
|
BundleAlreadyImported: -32010,
|
|
1140
1161
|
PermissionDenied: -32011,
|
|
1141
|
-
AlreadyAttached: -32012
|
|
1162
|
+
AlreadyAttached: -32012,
|
|
1163
|
+
StreamNotEnabled: -32013
|
|
1142
1164
|
};
|
|
1143
1165
|
var InitializeParams = z3.object({
|
|
1144
1166
|
protocolVersion: z3.number().optional(),
|
|
@@ -1219,6 +1241,9 @@ function extractHydraMeta(meta) {
|
|
|
1219
1241
|
if (Array.isArray(obj.agentArgs) && obj.agentArgs.every((a) => typeof a === "string")) {
|
|
1220
1242
|
out.agentArgs = obj.agentArgs;
|
|
1221
1243
|
}
|
|
1244
|
+
if (Array.isArray(obj.transformers) && obj.transformers.every((t) => typeof t === "string")) {
|
|
1245
|
+
out.transformers = obj.transformers;
|
|
1246
|
+
}
|
|
1222
1247
|
if (obj.resume) {
|
|
1223
1248
|
const parsed = SessionResumeHints.safeParse(obj.resume);
|
|
1224
1249
|
if (parsed.success) {
|
|
@@ -1385,6 +1410,8 @@ var SessionListEntry = z3.object({
|
|
|
1385
1410
|
// future "connect back to origin" callers would dial both.
|
|
1386
1411
|
importedFromMachine: z3.string().optional(),
|
|
1387
1412
|
importedFromUpstreamSessionId: z3.string().optional(),
|
|
1413
|
+
// Set when this session was spawned as a child by a transformer.
|
|
1414
|
+
parentSessionId: z3.string().optional(),
|
|
1388
1415
|
updatedAt: z3.string(),
|
|
1389
1416
|
attachedClients: z3.number().int().nonnegative(),
|
|
1390
1417
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
@@ -1518,6 +1545,65 @@ var PromptAmendedParams = z3.object({
|
|
|
1518
1545
|
originator: PromptOriginatorSchema,
|
|
1519
1546
|
amendedAt: z3.number()
|
|
1520
1547
|
});
|
|
1548
|
+
var StreamOpenParams = z3.object({
|
|
1549
|
+
sessionId: z3.string(),
|
|
1550
|
+
// 'memory' keeps the ring in RAM only — needed for the eventual MCP
|
|
1551
|
+
// tool surface. 'file' adds a temp file projection that the agent can
|
|
1552
|
+
// consume with shell tools (tail -f / head / grep) when MCP isn't
|
|
1553
|
+
// available. The temp file's path is returned in the response.
|
|
1554
|
+
mode: z3.enum(["memory", "file"]).optional(),
|
|
1555
|
+
// Ring capacity in bytes. Server clamps to a reasonable minimum and
|
|
1556
|
+
// its configured max; omitted falls back to the daemon default.
|
|
1557
|
+
capacityBytes: z3.number().int().positive().optional(),
|
|
1558
|
+
// File mode only. Soft cap in bytes; after this many bytes are
|
|
1559
|
+
// written to the file, further appends still land in the ring but
|
|
1560
|
+
// stop being mirrored to disk. The daemon emits one stream_truncated
|
|
1561
|
+
// session/update notification when the cap is first hit.
|
|
1562
|
+
fileCapBytes: z3.number().int().positive().optional()
|
|
1563
|
+
});
|
|
1564
|
+
var StreamOpenResult = z3.object({
|
|
1565
|
+
// Only present when mode === "file".
|
|
1566
|
+
filePath: z3.string().optional(),
|
|
1567
|
+
capacityBytes: z3.number().int().positive(),
|
|
1568
|
+
fileCapBytes: z3.number().int().positive().optional()
|
|
1569
|
+
});
|
|
1570
|
+
var StreamWriteParams = z3.object({
|
|
1571
|
+
sessionId: z3.string(),
|
|
1572
|
+
// Base64-encoded bytes. UTF-8 stdin gets re-encoded on the wire; the
|
|
1573
|
+
// ring is byte-exact so binary streams (audio, framed protocols) work
|
|
1574
|
+
// identically.
|
|
1575
|
+
chunk: z3.string(),
|
|
1576
|
+
// True on the final write. Pending long-poll reads / waits return with
|
|
1577
|
+
// eof:true once this is observed.
|
|
1578
|
+
eof: z3.boolean().optional()
|
|
1579
|
+
});
|
|
1580
|
+
var StreamWriteResult = z3.object({
|
|
1581
|
+
// Absolute writeCursor after this append landed.
|
|
1582
|
+
writeCursor: z3.number().int().nonnegative()
|
|
1583
|
+
});
|
|
1584
|
+
var StreamReadParams = z3.object({
|
|
1585
|
+
sessionId: z3.string(),
|
|
1586
|
+
cursor: z3.number().int().nonnegative(),
|
|
1587
|
+
// Cap on bytes returned. Server enforces a hard ceiling (STREAM_READ_MAX_BYTES,
|
|
1588
|
+
// currently 64 KiB) even when the caller asks for more.
|
|
1589
|
+
maxBytes: z3.number().int().positive().optional(),
|
|
1590
|
+
// Long-poll timeout in ms. 0 / omitted returns immediately with
|
|
1591
|
+
// whatever's available (possibly empty). Server cap 60s.
|
|
1592
|
+
waitMs: z3.number().int().nonnegative().optional()
|
|
1593
|
+
});
|
|
1594
|
+
var StreamReadResult = z3.object({
|
|
1595
|
+
// Base64-encoded bytes. Empty string when nothing new is available
|
|
1596
|
+
// and either waitMs was 0 or the long-poll expired without data.
|
|
1597
|
+
bytes: z3.string(),
|
|
1598
|
+
nextCursor: z3.number().int().nonnegative(),
|
|
1599
|
+
// Set when `cursor` pointed before the oldest still-resident byte —
|
|
1600
|
+
// value is the count of bytes that were evicted between the caller's
|
|
1601
|
+
// cursor and what we still have.
|
|
1602
|
+
gap: z3.number().int().nonnegative().optional(),
|
|
1603
|
+
// True when the producer has closed AND there are no more bytes
|
|
1604
|
+
// after nextCursor.
|
|
1605
|
+
eof: z3.boolean().optional()
|
|
1606
|
+
});
|
|
1521
1607
|
var AgentInstallProgressParams = z3.object({
|
|
1522
1608
|
agentId: z3.string(),
|
|
1523
1609
|
version: z3.string(),
|
|
@@ -1923,6 +2009,264 @@ import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
|
1923
2009
|
// src/core/session.ts
|
|
1924
2010
|
import { customAlphabet } from "nanoid";
|
|
1925
2011
|
|
|
2012
|
+
// src/core/stream-buffer.ts
|
|
2013
|
+
import * as fsp3 from "fs/promises";
|
|
2014
|
+
var DEFAULT_CAPACITY_BYTES = 16 * 1024 * 1024;
|
|
2015
|
+
var STREAM_READ_MAX_BYTES = 64 * 1024;
|
|
2016
|
+
var STREAM_WAIT_MAX_MS = 6e4;
|
|
2017
|
+
var SessionStreamBuffer = class {
|
|
2018
|
+
storage;
|
|
2019
|
+
capacityBytes;
|
|
2020
|
+
// Absolute monotonic byte offset of the next byte to be written. Also
|
|
2021
|
+
// the count of bytes ever appended. `writeCursor - capacityBytes`
|
|
2022
|
+
// (clamped at 0) is the oldest still-resident byte's cursor.
|
|
2023
|
+
writeCursor = 0;
|
|
2024
|
+
closed = false;
|
|
2025
|
+
waiters = [];
|
|
2026
|
+
filePath;
|
|
2027
|
+
fileCapBytes;
|
|
2028
|
+
fileBytesWritten = 0;
|
|
2029
|
+
fileCapReached = false;
|
|
2030
|
+
onFileCapReached;
|
|
2031
|
+
logWriteError;
|
|
2032
|
+
// Single-flight chain for file appends so concurrent stream_write
|
|
2033
|
+
// calls don't interleave their writes.
|
|
2034
|
+
fileWriteChain = Promise.resolve();
|
|
2035
|
+
constructor(opts = {}) {
|
|
2036
|
+
this.capacityBytes = opts.capacityBytes ?? DEFAULT_CAPACITY_BYTES;
|
|
2037
|
+
if (this.capacityBytes <= 0) {
|
|
2038
|
+
throw new Error("capacityBytes must be > 0");
|
|
2039
|
+
}
|
|
2040
|
+
this.storage = Buffer.alloc(this.capacityBytes);
|
|
2041
|
+
this.filePath = opts.filePath;
|
|
2042
|
+
this.fileCapBytes = opts.fileCapBytes ?? Number.POSITIVE_INFINITY;
|
|
2043
|
+
this.onFileCapReached = opts.onFileCapReached;
|
|
2044
|
+
this.logWriteError = opts.logWriteError;
|
|
2045
|
+
}
|
|
2046
|
+
get capacity() {
|
|
2047
|
+
return this.capacityBytes;
|
|
2048
|
+
}
|
|
2049
|
+
get writeCursorPos() {
|
|
2050
|
+
return this.writeCursor;
|
|
2051
|
+
}
|
|
2052
|
+
get oldestAvailable() {
|
|
2053
|
+
return Math.max(0, this.writeCursor - this.capacityBytes);
|
|
2054
|
+
}
|
|
2055
|
+
get isClosed() {
|
|
2056
|
+
return this.closed;
|
|
2057
|
+
}
|
|
2058
|
+
// Append-or-noop. Calls after close() are silently dropped (the
|
|
2059
|
+
// producer ought not to keep writing, but it's not worth throwing if
|
|
2060
|
+
// a chunk arrives late).
|
|
2061
|
+
append(chunk) {
|
|
2062
|
+
if (this.closed || chunk.length === 0) {
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
this.writeRing(chunk);
|
|
2066
|
+
this.writeCursor += chunk.length;
|
|
2067
|
+
if (this.filePath !== void 0) {
|
|
2068
|
+
this.scheduleFileWrite(chunk);
|
|
2069
|
+
}
|
|
2070
|
+
this.wakeWaiters("data");
|
|
2071
|
+
}
|
|
2072
|
+
close() {
|
|
2073
|
+
if (this.closed) {
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
this.closed = true;
|
|
2077
|
+
this.wakeWaiters("eof");
|
|
2078
|
+
}
|
|
2079
|
+
// Read up to `maxBytes` bytes starting at `cursor`. If `cursor` is
|
|
2080
|
+
// behind the oldest still-resident byte, gap-skip to the oldest and
|
|
2081
|
+
// report how many bytes were dropped. If `cursor` is at writeCursor
|
|
2082
|
+
// and the buffer is closed, return eof:true.
|
|
2083
|
+
read(cursor, maxBytes) {
|
|
2084
|
+
const cap = Math.max(0, Math.min(maxBytes, STREAM_READ_MAX_BYTES));
|
|
2085
|
+
if (cap === 0) {
|
|
2086
|
+
const tail = {
|
|
2087
|
+
bytes: Buffer.alloc(0),
|
|
2088
|
+
nextCursor: cursor
|
|
2089
|
+
};
|
|
2090
|
+
if (this.closed && cursor >= this.writeCursor) {
|
|
2091
|
+
tail.eof = true;
|
|
2092
|
+
}
|
|
2093
|
+
return tail;
|
|
2094
|
+
}
|
|
2095
|
+
let from = cursor;
|
|
2096
|
+
let gap = 0;
|
|
2097
|
+
const oldest = this.oldestAvailable;
|
|
2098
|
+
if (from < oldest) {
|
|
2099
|
+
gap = oldest - from;
|
|
2100
|
+
from = oldest;
|
|
2101
|
+
}
|
|
2102
|
+
const available = this.writeCursor - from;
|
|
2103
|
+
if (available <= 0) {
|
|
2104
|
+
const result2 = {
|
|
2105
|
+
bytes: Buffer.alloc(0),
|
|
2106
|
+
nextCursor: from
|
|
2107
|
+
};
|
|
2108
|
+
if (gap > 0) {
|
|
2109
|
+
result2.gap = gap;
|
|
2110
|
+
}
|
|
2111
|
+
if (this.closed) {
|
|
2112
|
+
result2.eof = true;
|
|
2113
|
+
}
|
|
2114
|
+
return result2;
|
|
2115
|
+
}
|
|
2116
|
+
const take = Math.min(available, cap);
|
|
2117
|
+
const bytes = this.sliceFromRing(from, take);
|
|
2118
|
+
const result = {
|
|
2119
|
+
bytes,
|
|
2120
|
+
nextCursor: from + take
|
|
2121
|
+
};
|
|
2122
|
+
if (gap > 0) {
|
|
2123
|
+
result.gap = gap;
|
|
2124
|
+
}
|
|
2125
|
+
if (this.closed && from + take >= this.writeCursor) {
|
|
2126
|
+
result.eof = true;
|
|
2127
|
+
}
|
|
2128
|
+
return result;
|
|
2129
|
+
}
|
|
2130
|
+
// Latest N bytes from the tail, capped at capacity / STREAM_READ_MAX_BYTES.
|
|
2131
|
+
// truncated:true when the requested span extends past the oldest
|
|
2132
|
+
// still-resident byte (i.e. there was more upstream that we don't have
|
|
2133
|
+
// anymore).
|
|
2134
|
+
tail(bytes) {
|
|
2135
|
+
const want = Math.max(0, Math.min(bytes, STREAM_READ_MAX_BYTES));
|
|
2136
|
+
const oldest = this.oldestAvailable;
|
|
2137
|
+
const startWant = this.writeCursor - want;
|
|
2138
|
+
const start = Math.max(oldest, startWant);
|
|
2139
|
+
const truncated = startWant < oldest;
|
|
2140
|
+
const slice = this.sliceFromRing(start, this.writeCursor - start);
|
|
2141
|
+
return {
|
|
2142
|
+
bytes: slice,
|
|
2143
|
+
startCursor: start,
|
|
2144
|
+
endCursor: this.writeCursor,
|
|
2145
|
+
truncated
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
// First N bytes since the stream began. Returns truncated:true when
|
|
2149
|
+
// the head has already been evicted (cursor 0 is no longer resident).
|
|
2150
|
+
head(bytes) {
|
|
2151
|
+
const want = Math.max(0, Math.min(bytes, STREAM_READ_MAX_BYTES));
|
|
2152
|
+
const oldest = this.oldestAvailable;
|
|
2153
|
+
const truncated = oldest > 0;
|
|
2154
|
+
const start = oldest;
|
|
2155
|
+
const end = Math.min(this.writeCursor, start + want);
|
|
2156
|
+
const slice = this.sliceFromRing(start, end - start);
|
|
2157
|
+
return {
|
|
2158
|
+
bytes: slice,
|
|
2159
|
+
startCursor: start,
|
|
2160
|
+
endCursor: end,
|
|
2161
|
+
truncated
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
// Long-poll until new bytes arrive past `cursor`, the buffer closes, or
|
|
2165
|
+
// the timeout expires. Resolves with "data" / "eof" / "timeout".
|
|
2166
|
+
waitForData(cursor, timeoutMs) {
|
|
2167
|
+
if (cursor < this.writeCursor) {
|
|
2168
|
+
return Promise.resolve("data");
|
|
2169
|
+
}
|
|
2170
|
+
if (this.closed) {
|
|
2171
|
+
return Promise.resolve("eof");
|
|
2172
|
+
}
|
|
2173
|
+
const cap = Math.max(0, Math.min(timeoutMs, STREAM_WAIT_MAX_MS));
|
|
2174
|
+
if (cap === 0) {
|
|
2175
|
+
return Promise.resolve("timeout");
|
|
2176
|
+
}
|
|
2177
|
+
return new Promise((resolve3) => {
|
|
2178
|
+
const waiter = {
|
|
2179
|
+
resolve: (outcome) => {
|
|
2180
|
+
if (waiter.timer !== void 0) {
|
|
2181
|
+
clearTimeout(waiter.timer);
|
|
2182
|
+
waiter.timer = void 0;
|
|
2183
|
+
}
|
|
2184
|
+
resolve3(outcome);
|
|
2185
|
+
},
|
|
2186
|
+
timer: setTimeout(() => {
|
|
2187
|
+
const idx = this.waiters.indexOf(waiter);
|
|
2188
|
+
if (idx >= 0) {
|
|
2189
|
+
this.waiters.splice(idx, 1);
|
|
2190
|
+
}
|
|
2191
|
+
waiter.timer = void 0;
|
|
2192
|
+
resolve3("timeout");
|
|
2193
|
+
}, cap)
|
|
2194
|
+
};
|
|
2195
|
+
this.waiters.push(waiter);
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
wakeWaiters(outcome) {
|
|
2199
|
+
if (this.waiters.length === 0) {
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const wake = this.waiters;
|
|
2203
|
+
this.waiters = [];
|
|
2204
|
+
for (const w of wake) {
|
|
2205
|
+
w.resolve(outcome);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
writeRing(chunk) {
|
|
2209
|
+
const len = chunk.length;
|
|
2210
|
+
if (len >= this.capacityBytes) {
|
|
2211
|
+
const tailStart = len - this.capacityBytes;
|
|
2212
|
+
chunk.copy(this.storage, 0, tailStart, len);
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
const offset = this.writeCursor % this.capacityBytes;
|
|
2216
|
+
const tailRoom = this.capacityBytes - offset;
|
|
2217
|
+
if (len <= tailRoom) {
|
|
2218
|
+
chunk.copy(this.storage, offset, 0, len);
|
|
2219
|
+
} else {
|
|
2220
|
+
chunk.copy(this.storage, offset, 0, tailRoom);
|
|
2221
|
+
chunk.copy(this.storage, 0, tailRoom, len);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
sliceFromRing(fromCursor, length) {
|
|
2225
|
+
if (length <= 0) {
|
|
2226
|
+
return Buffer.alloc(0);
|
|
2227
|
+
}
|
|
2228
|
+
const out = Buffer.alloc(length);
|
|
2229
|
+
const offset = fromCursor % this.capacityBytes;
|
|
2230
|
+
const tailLen = Math.min(length, this.capacityBytes - offset);
|
|
2231
|
+
this.storage.copy(out, 0, offset, offset + tailLen);
|
|
2232
|
+
if (tailLen < length) {
|
|
2233
|
+
this.storage.copy(out, tailLen, 0, length - tailLen);
|
|
2234
|
+
}
|
|
2235
|
+
return out;
|
|
2236
|
+
}
|
|
2237
|
+
scheduleFileWrite(chunk) {
|
|
2238
|
+
const path13 = this.filePath;
|
|
2239
|
+
if (path13 === void 0) {
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
if (this.fileCapReached) {
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
const remaining = this.fileCapBytes - this.fileBytesWritten;
|
|
2246
|
+
if (remaining <= 0) {
|
|
2247
|
+
this.fileCapReached = true;
|
|
2248
|
+
this.onFileCapReached?.();
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
const slice = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining);
|
|
2252
|
+
this.fileBytesWritten += slice.length;
|
|
2253
|
+
const willHitCap = this.fileBytesWritten >= this.fileCapBytes;
|
|
2254
|
+
this.fileWriteChain = this.fileWriteChain.then(() => fsp3.appendFile(path13, slice)).catch((err) => {
|
|
2255
|
+
this.logWriteError?.(err);
|
|
2256
|
+
});
|
|
2257
|
+
if (willHitCap && !this.fileCapReached) {
|
|
2258
|
+
this.fileCapReached = true;
|
|
2259
|
+
this.onFileCapReached?.();
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
// Wait for any pending file appends to flush. Used by tests and by
|
|
2263
|
+
// session close handlers that want to ensure the file is durable
|
|
2264
|
+
// before unlinking.
|
|
2265
|
+
async drainFileWrites() {
|
|
2266
|
+
await this.fileWriteChain.catch(() => void 0);
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
|
|
1926
2270
|
// src/core/hydra-commands.ts
|
|
1927
2271
|
var HYDRA_COMMANDS = [
|
|
1928
2272
|
{
|
|
@@ -1992,8 +2336,10 @@ async function deleteQueue(sessionId) {
|
|
|
1992
2336
|
}
|
|
1993
2337
|
|
|
1994
2338
|
// src/core/session.ts
|
|
2339
|
+
import * as fsp4 from "fs/promises";
|
|
1995
2340
|
var HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
1996
2341
|
var generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
2342
|
+
var generateChainToken = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
1997
2343
|
var HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
1998
2344
|
function generateMessageId() {
|
|
1999
2345
|
return `m_${generateHydraId()}`;
|
|
@@ -2002,6 +2348,7 @@ function stripHydraSessionPrefix(id) {
|
|
|
2002
2348
|
return id.startsWith(HYDRA_SESSION_PREFIX) ? id.slice(HYDRA_SESSION_PREFIX.length) : id;
|
|
2003
2349
|
}
|
|
2004
2350
|
var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
2351
|
+
var TRANSFORMER_CLAIM_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2005
2352
|
var RECENTLY_TERMINAL_LIMIT = 64;
|
|
2006
2353
|
var Session = class {
|
|
2007
2354
|
sessionId;
|
|
@@ -2014,7 +2361,9 @@ var Session = class {
|
|
|
2014
2361
|
agent;
|
|
2015
2362
|
upstreamSessionId;
|
|
2016
2363
|
agentMeta;
|
|
2364
|
+
agentCapabilities;
|
|
2017
2365
|
agentArgs;
|
|
2366
|
+
parentSessionId;
|
|
2018
2367
|
title;
|
|
2019
2368
|
// Snapshot state delivered to attaching clients via the attach
|
|
2020
2369
|
// response _meta rather than via history replay (which would be
|
|
@@ -2076,6 +2425,10 @@ var Session = class {
|
|
|
2076
2425
|
internalPromptCapture;
|
|
2077
2426
|
idleTimeoutMs;
|
|
2078
2427
|
idleTimer;
|
|
2428
|
+
// Separate timer that fires session.idle to the transformer chain after
|
|
2429
|
+
// a quiet period. Distinct from idleTimer, which drives session close.
|
|
2430
|
+
idleEventTimer;
|
|
2431
|
+
idleEventTimeoutMs;
|
|
2079
2432
|
// Time of the last recordable broadcast (or session creation, if
|
|
2080
2433
|
// none yet). Drives the inactivity-based idle close; deliberately
|
|
2081
2434
|
// does NOT include snapshot state pings (model/mode/title/commands)
|
|
@@ -2084,6 +2437,9 @@ var Session = class {
|
|
|
2084
2437
|
lastRecordedAt;
|
|
2085
2438
|
spawnReplacementAgent;
|
|
2086
2439
|
logger;
|
|
2440
|
+
transformChain;
|
|
2441
|
+
// Outstanding "processing" claims: token → claim waiting for respondsTo discharge.
|
|
2442
|
+
pendingClaims = /* @__PURE__ */ new Map();
|
|
2087
2443
|
agentChangeHandlers = [];
|
|
2088
2444
|
// Last available_commands_update we observed from the agent. Stored
|
|
2089
2445
|
// so we can re-broadcast a merged (hydra ∪ agent) list whenever
|
|
@@ -2110,6 +2466,7 @@ var Session = class {
|
|
|
2110
2466
|
modelHandlers = [];
|
|
2111
2467
|
modeHandlers = [];
|
|
2112
2468
|
usageHandlers = [];
|
|
2469
|
+
cumulativeCost = 0;
|
|
2113
2470
|
// Set by amendPrompt at the start of a cancel-and-resubmit dance.
|
|
2114
2471
|
// broadcastTurnComplete reads it to attach the _meta.amended marker
|
|
2115
2472
|
// to the cancelled turn's turn_complete notification, and to fire the
|
|
@@ -2124,6 +2481,11 @@ var Session = class {
|
|
|
2124
2481
|
// older entries fall out and resolve to target_not_found, which is
|
|
2125
2482
|
// the correct behavior.
|
|
2126
2483
|
recentlyTerminal = /* @__PURE__ */ new Map();
|
|
2484
|
+
// Optional ring buffer for piped stdin, populated by openStream() when
|
|
2485
|
+
// a cat --stream session attaches. Lifecycle follows the session — the
|
|
2486
|
+
// markClosed path closes the buffer and unlinks any file projection.
|
|
2487
|
+
streamBuffer;
|
|
2488
|
+
streamFilePath;
|
|
2127
2489
|
constructor(init) {
|
|
2128
2490
|
this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
|
|
2129
2491
|
this.cwd = init.cwd;
|
|
@@ -2131,11 +2493,14 @@ var Session = class {
|
|
|
2131
2493
|
this.agent = init.agent;
|
|
2132
2494
|
this.upstreamSessionId = init.upstreamSessionId;
|
|
2133
2495
|
this.agentMeta = init.agentMeta;
|
|
2496
|
+
this.agentCapabilities = init.agentCapabilities;
|
|
2134
2497
|
this.agentArgs = init.agentArgs;
|
|
2498
|
+
this.parentSessionId = init.parentSessionId;
|
|
2135
2499
|
this.title = init.title;
|
|
2136
2500
|
this.currentModel = init.currentModel;
|
|
2137
2501
|
this.currentMode = init.currentMode;
|
|
2138
2502
|
this.currentUsage = init.currentUsage;
|
|
2503
|
+
this.cumulativeCost = init.currentUsage?.cumulativeCost ?? 0;
|
|
2139
2504
|
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
2140
2505
|
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
2141
2506
|
}
|
|
@@ -2146,8 +2511,10 @@ var Session = class {
|
|
|
2146
2511
|
this.agentAdvertisedModels = [...init.agentModels];
|
|
2147
2512
|
}
|
|
2148
2513
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
2514
|
+
this.idleEventTimeoutMs = init.idleEventTimeoutMs ?? 3e4;
|
|
2149
2515
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
2150
2516
|
this.logger = init.logger;
|
|
2517
|
+
this.transformChain = init.transformChain ?? [];
|
|
2151
2518
|
if (init.firstPromptSeeded) {
|
|
2152
2519
|
this.firstPromptSeeded = true;
|
|
2153
2520
|
}
|
|
@@ -2159,6 +2526,7 @@ var Session = class {
|
|
|
2159
2526
|
this.lastRecordedAt = this.updatedAt;
|
|
2160
2527
|
this.wireAgent(this.agent);
|
|
2161
2528
|
this.scheduleIdleCheck();
|
|
2529
|
+
this.notifyChain("session.opened", {});
|
|
2162
2530
|
}
|
|
2163
2531
|
broadcastMergedCommands() {
|
|
2164
2532
|
const merged = [
|
|
@@ -2211,34 +2579,7 @@ var Session = class {
|
|
|
2211
2579
|
captureInternalChunk(this.internalPromptCapture, params);
|
|
2212
2580
|
return;
|
|
2213
2581
|
}
|
|
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);
|
|
2582
|
+
void this.runResponseChain(params);
|
|
2242
2583
|
});
|
|
2243
2584
|
agent.connection.onRequest("session/request_permission", async (params) => {
|
|
2244
2585
|
return this.handlePermissionRequest(params);
|
|
@@ -2250,6 +2591,105 @@ var Session = class {
|
|
|
2250
2591
|
this.markClosed({ deleteRecord: false });
|
|
2251
2592
|
});
|
|
2252
2593
|
}
|
|
2594
|
+
// Runs the response-side transformer chain, then the snapshot interceptors,
|
|
2595
|
+
// then recordAndBroadcast. All state mutation happens after the chain exits.
|
|
2596
|
+
// See forwardRequest for originatedBy / startIdx semantics.
|
|
2597
|
+
async runResponseChain(params, originatedBy = /* @__PURE__ */ new Set(), startIdx = 0) {
|
|
2598
|
+
let envelope = params;
|
|
2599
|
+
for (let i = startIdx; i < this.transformChain.length; i++) {
|
|
2600
|
+
const t = this.transformChain[i];
|
|
2601
|
+
if (originatedBy.has(t.name)) {
|
|
2602
|
+
continue;
|
|
2603
|
+
}
|
|
2604
|
+
if (!t.intercepts.has("response:session/update")) {
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
const token = `t_${generateChainToken()}`;
|
|
2608
|
+
let result;
|
|
2609
|
+
try {
|
|
2610
|
+
result = await t.connection.request("transformer/message", {
|
|
2611
|
+
token,
|
|
2612
|
+
phase: "response",
|
|
2613
|
+
method: "session/update",
|
|
2614
|
+
direction: "agent\u2192client",
|
|
2615
|
+
sessionId: this.sessionId,
|
|
2616
|
+
envelope
|
|
2617
|
+
});
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
this.logger?.warn(
|
|
2620
|
+
`transformer ${t.name} error on response:session/update: ${err.message}`
|
|
2621
|
+
);
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
const action = result?.action ?? "continue";
|
|
2625
|
+
if (action === "stop") {
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
if (action === "processing") {
|
|
2629
|
+
const claimIdx = i;
|
|
2630
|
+
const claimEnvelope = envelope;
|
|
2631
|
+
const claimOriginatedBy = new Set(originatedBy);
|
|
2632
|
+
await new Promise((resolve3) => {
|
|
2633
|
+
const timer = setTimeout(() => {
|
|
2634
|
+
if (this.pendingClaims.delete(token)) {
|
|
2635
|
+
this.broadcastQueueNotification(
|
|
2636
|
+
"hydra-acp/transformer_abandoned_request",
|
|
2637
|
+
{ sessionId: this.sessionId, token, transformerName: t.name }
|
|
2638
|
+
);
|
|
2639
|
+
void this.runResponseChain(
|
|
2640
|
+
claimEnvelope,
|
|
2641
|
+
/* @__PURE__ */ new Set([...claimOriginatedBy, t.name]),
|
|
2642
|
+
claimIdx + 1
|
|
2643
|
+
).then(resolve3);
|
|
2644
|
+
}
|
|
2645
|
+
}, TRANSFORMER_CLAIM_TIMEOUT_MS);
|
|
2646
|
+
if (typeof timer.unref === "function") {
|
|
2647
|
+
timer.unref();
|
|
2648
|
+
}
|
|
2649
|
+
this.pendingClaims.set(token, {
|
|
2650
|
+
resolve: () => resolve3(),
|
|
2651
|
+
timer,
|
|
2652
|
+
transformerName: t.name,
|
|
2653
|
+
method: "session/update",
|
|
2654
|
+
envelope: claimEnvelope,
|
|
2655
|
+
chainIdx: claimIdx,
|
|
2656
|
+
originatedBy: claimOriginatedBy,
|
|
2657
|
+
side: "response"
|
|
2658
|
+
});
|
|
2659
|
+
});
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
originatedBy.add(t.name);
|
|
2663
|
+
}
|
|
2664
|
+
const agentCmds = extractAdvertisedCommands(envelope);
|
|
2665
|
+
if (agentCmds !== null) {
|
|
2666
|
+
this.setAgentAdvertisedCommands(agentCmds);
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
const agentModes = extractAdvertisedModes(envelope);
|
|
2670
|
+
if (agentModes !== null) {
|
|
2671
|
+
this.setAgentAdvertisedModes(agentModes);
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
if (this.maybeApplyAgentModel(envelope)) {
|
|
2675
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
if (this.maybeApplyAgentMode(envelope)) {
|
|
2679
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
if (this.maybeApplyAgentConfigOption(envelope)) {
|
|
2683
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (this.maybeApplyAgentUsage(envelope)) {
|
|
2687
|
+
this.recordAndBroadcast("session/update", this.injectCumulativeCost(envelope));
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
this.maybeApplyAgentSessionInfo(envelope);
|
|
2691
|
+
this.recordAndBroadcast("session/update", envelope);
|
|
2692
|
+
}
|
|
2253
2693
|
onAgentChange(handler) {
|
|
2254
2694
|
this.agentChangeHandlers.push(handler);
|
|
2255
2695
|
}
|
|
@@ -2437,8 +2877,8 @@ var Session = class {
|
|
|
2437
2877
|
recordedAt
|
|
2438
2878
|
});
|
|
2439
2879
|
}
|
|
2440
|
-
if (this.currentUsage !== void 0) {
|
|
2441
|
-
const u = this.currentUsage;
|
|
2880
|
+
if (this.currentUsage !== void 0 || this.cumulativeCost > 0) {
|
|
2881
|
+
const u = this.currentUsage ?? {};
|
|
2442
2882
|
const update = {
|
|
2443
2883
|
sessionUpdate: "usage_update"
|
|
2444
2884
|
};
|
|
@@ -2450,8 +2890,8 @@ var Session = class {
|
|
|
2450
2890
|
}
|
|
2451
2891
|
if (typeof u.costAmount === "number" || typeof u.costCurrency === "string") {
|
|
2452
2892
|
const cost = {};
|
|
2453
|
-
if (typeof u.costAmount === "number") {
|
|
2454
|
-
cost.amount = u.costAmount;
|
|
2893
|
+
if (typeof u.costAmount === "number" || this.cumulativeCost) {
|
|
2894
|
+
cost.amount = this.cumulativeCost + (u.costAmount ?? 0);
|
|
2455
2895
|
}
|
|
2456
2896
|
if (typeof u.costCurrency === "string") {
|
|
2457
2897
|
cost.currency = u.costCurrency;
|
|
@@ -2990,8 +3430,142 @@ var Session = class {
|
|
|
2990
3430
|
sessionId: this.upstreamSessionId
|
|
2991
3431
|
});
|
|
2992
3432
|
}
|
|
2993
|
-
|
|
2994
|
-
|
|
3433
|
+
// Walk the request-side chain then forward to the agent.
|
|
3434
|
+
// originatedBy: transformer names already in the lineage — skipped for loop
|
|
3435
|
+
// prevention and to implement resume-routing on re-entry from emit_message.
|
|
3436
|
+
// startIdx: chain position to start from (0 for normal, emitterIdx+1 for re-entry).
|
|
3437
|
+
async forwardRequest(method, params, originatedBy = /* @__PURE__ */ new Set(), startIdx = 0) {
|
|
3438
|
+
let envelope = this.rewriteForAgent(params);
|
|
3439
|
+
for (let i = startIdx; i < this.transformChain.length; i++) {
|
|
3440
|
+
const t = this.transformChain[i];
|
|
3441
|
+
if (originatedBy.has(t.name)) {
|
|
3442
|
+
continue;
|
|
3443
|
+
}
|
|
3444
|
+
const intercept = `request:${method}`;
|
|
3445
|
+
if (!t.intercepts.has(intercept)) {
|
|
3446
|
+
continue;
|
|
3447
|
+
}
|
|
3448
|
+
const token = `t_${generateChainToken()}`;
|
|
3449
|
+
let result;
|
|
3450
|
+
try {
|
|
3451
|
+
result = await t.connection.request("transformer/message", {
|
|
3452
|
+
token,
|
|
3453
|
+
phase: "request",
|
|
3454
|
+
method,
|
|
3455
|
+
direction: "client\u2192agent",
|
|
3456
|
+
sessionId: this.sessionId,
|
|
3457
|
+
envelope
|
|
3458
|
+
});
|
|
3459
|
+
} catch (err) {
|
|
3460
|
+
this.logger?.warn(
|
|
3461
|
+
`transformer ${t.name} error on ${intercept}: ${err.message}`
|
|
3462
|
+
);
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
const action = result?.action ?? "continue";
|
|
3466
|
+
if (action === "stop") {
|
|
3467
|
+
return result?.payload ?? defaultStopPayload(method);
|
|
3468
|
+
}
|
|
3469
|
+
if (action === "processing") {
|
|
3470
|
+
const claimIdx = i;
|
|
3471
|
+
const claimEnvelope = envelope;
|
|
3472
|
+
const claimOriginatedBy = new Set(originatedBy);
|
|
3473
|
+
return new Promise((resolve3) => {
|
|
3474
|
+
const timer = setTimeout(() => {
|
|
3475
|
+
if (this.pendingClaims.delete(token)) {
|
|
3476
|
+
this.broadcastQueueNotification(
|
|
3477
|
+
"hydra-acp/transformer_abandoned_request",
|
|
3478
|
+
{ sessionId: this.sessionId, token, transformerName: t.name }
|
|
3479
|
+
);
|
|
3480
|
+
void this.forwardRequest(
|
|
3481
|
+
method,
|
|
3482
|
+
claimEnvelope,
|
|
3483
|
+
/* @__PURE__ */ new Set([...claimOriginatedBy, t.name]),
|
|
3484
|
+
claimIdx + 1
|
|
3485
|
+
).then(resolve3).catch(() => resolve3(defaultStopPayload(method)));
|
|
3486
|
+
}
|
|
3487
|
+
}, TRANSFORMER_CLAIM_TIMEOUT_MS);
|
|
3488
|
+
if (typeof timer.unref === "function") {
|
|
3489
|
+
timer.unref();
|
|
3490
|
+
}
|
|
3491
|
+
this.pendingClaims.set(token, {
|
|
3492
|
+
resolve: resolve3,
|
|
3493
|
+
timer,
|
|
3494
|
+
transformerName: t.name,
|
|
3495
|
+
method,
|
|
3496
|
+
envelope: claimEnvelope,
|
|
3497
|
+
chainIdx: claimIdx,
|
|
3498
|
+
originatedBy: claimOriginatedBy,
|
|
3499
|
+
side: "request"
|
|
3500
|
+
});
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
originatedBy.add(t.name);
|
|
3504
|
+
}
|
|
3505
|
+
return this.agent.connection.request(method, envelope);
|
|
3506
|
+
}
|
|
3507
|
+
// Called by the WS handler when emit_message carries respondsTo.
|
|
3508
|
+
// Discharges the outstanding claim so the original requester unblocks.
|
|
3509
|
+
dischargeClaim(token, result) {
|
|
3510
|
+
const claim = this.pendingClaims.get(token);
|
|
3511
|
+
if (!claim) {
|
|
3512
|
+
return false;
|
|
3513
|
+
}
|
|
3514
|
+
clearTimeout(claim.timer);
|
|
3515
|
+
this.pendingClaims.delete(token);
|
|
3516
|
+
claim.resolve(result);
|
|
3517
|
+
return true;
|
|
3518
|
+
}
|
|
3519
|
+
// Called by the WS handler on hydra-acp/keep_alive.
|
|
3520
|
+
// Resets the abandonment timer for an outstanding processing claim.
|
|
3521
|
+
keepAliveClaim(token, estimatedRemainingMs) {
|
|
3522
|
+
const claim = this.pendingClaims.get(token);
|
|
3523
|
+
if (!claim) {
|
|
3524
|
+
return false;
|
|
3525
|
+
}
|
|
3526
|
+
clearTimeout(claim.timer);
|
|
3527
|
+
const timeout = typeof estimatedRemainingMs === "number" && estimatedRemainingMs > 0 ? Math.min(estimatedRemainingMs * 1.5, 30 * 60 * 1e3) : TRANSFORMER_CLAIM_TIMEOUT_MS;
|
|
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: claim.transformerName }
|
|
3533
|
+
);
|
|
3534
|
+
if (claim.side === "response") {
|
|
3535
|
+
void this.runResponseChain(
|
|
3536
|
+
claim.envelope,
|
|
3537
|
+
/* @__PURE__ */ new Set([...claim.originatedBy, claim.transformerName]),
|
|
3538
|
+
claim.chainIdx + 1
|
|
3539
|
+
).then(() => claim.resolve(void 0));
|
|
3540
|
+
} else {
|
|
3541
|
+
void this.forwardRequest(
|
|
3542
|
+
claim.method,
|
|
3543
|
+
claim.envelope,
|
|
3544
|
+
/* @__PURE__ */ new Set([...claim.originatedBy, claim.transformerName]),
|
|
3545
|
+
claim.chainIdx + 1
|
|
3546
|
+
).then(claim.resolve).catch(() => claim.resolve(defaultStopPayload(claim.method)));
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
}, timeout);
|
|
3550
|
+
if (typeof timer.unref === "function") {
|
|
3551
|
+
timer.unref();
|
|
3552
|
+
}
|
|
3553
|
+
claim.timer = timer;
|
|
3554
|
+
return true;
|
|
3555
|
+
}
|
|
3556
|
+
// Called by the WS handler when a transformer emits via route:"chain".
|
|
3557
|
+
// Finds the emitter's position and re-enters the appropriate chain walk
|
|
3558
|
+
// from the next slot, with the emitter in originatedBy so it cannot see
|
|
3559
|
+
// its own re-emission.
|
|
3560
|
+
async emitToChain(emitterName, method, envelope) {
|
|
3561
|
+
const emitterIdx = this.transformChain.findIndex((t) => t.name === emitterName);
|
|
3562
|
+
const startIdx = emitterIdx >= 0 ? emitterIdx + 1 : 0;
|
|
3563
|
+
const originatedBy = /* @__PURE__ */ new Set([emitterName]);
|
|
3564
|
+
if (method === "session/update") {
|
|
3565
|
+
await this.runResponseChain(envelope, originatedBy, startIdx);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
await this.forwardRequest(method, envelope, originatedBy, startIdx);
|
|
2995
3569
|
}
|
|
2996
3570
|
rewriteForAgent(params) {
|
|
2997
3571
|
if (params && typeof params === "object" && !Array.isArray(params)) {
|
|
@@ -3232,6 +3806,65 @@ var Session = class {
|
|
|
3232
3806
|
}
|
|
3233
3807
|
return true;
|
|
3234
3808
|
}
|
|
3809
|
+
// Move currentUsage.costAmount into cumulativeCost and clear it so the
|
|
3810
|
+
// next agent life starts accumulating from $0. Fires usageHandlers so
|
|
3811
|
+
// meta.json is updated before the new agent starts emitting.
|
|
3812
|
+
accumulateAndResetCost() {
|
|
3813
|
+
const amount = this.currentUsage?.costAmount;
|
|
3814
|
+
if (!amount)
|
|
3815
|
+
return;
|
|
3816
|
+
this.cumulativeCost += amount;
|
|
3817
|
+
const next = {
|
|
3818
|
+
...this.currentUsage ?? {},
|
|
3819
|
+
cumulativeCost: this.cumulativeCost,
|
|
3820
|
+
costAmount: void 0
|
|
3821
|
+
};
|
|
3822
|
+
this.currentUsage = next;
|
|
3823
|
+
for (const handler of this.usageHandlers) {
|
|
3824
|
+
try {
|
|
3825
|
+
handler(next);
|
|
3826
|
+
} catch {
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
// Returns a modified envelope with cost.amount replaced by the running
|
|
3831
|
+
// total (cumulativeCost + raw agent amount). No-op if the envelope
|
|
3832
|
+
// doesn't carry a numeric cost.amount.
|
|
3833
|
+
injectCumulativeCost(envelope) {
|
|
3834
|
+
if (!this.cumulativeCost)
|
|
3835
|
+
return envelope;
|
|
3836
|
+
if (!envelope || typeof envelope !== "object")
|
|
3837
|
+
return envelope;
|
|
3838
|
+
const obj = envelope;
|
|
3839
|
+
if (!obj.update || typeof obj.update !== "object")
|
|
3840
|
+
return envelope;
|
|
3841
|
+
const update = obj.update;
|
|
3842
|
+
if (update.sessionUpdate !== "usage_update")
|
|
3843
|
+
return envelope;
|
|
3844
|
+
if (!update.cost || typeof update.cost !== "object")
|
|
3845
|
+
return envelope;
|
|
3846
|
+
const cost = update.cost;
|
|
3847
|
+
if (typeof cost.amount !== "number")
|
|
3848
|
+
return envelope;
|
|
3849
|
+
return {
|
|
3850
|
+
...obj,
|
|
3851
|
+
update: {
|
|
3852
|
+
...update,
|
|
3853
|
+
cost: { ...cost, amount: this.cumulativeCost + cost.amount }
|
|
3854
|
+
}
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
// Total cost across all agent lives for this hydra session. Used by
|
|
3858
|
+
// session/list so list rows show the accumulated figure.
|
|
3859
|
+
get totalUsage() {
|
|
3860
|
+
if (!this.currentUsage && !this.cumulativeCost)
|
|
3861
|
+
return void 0;
|
|
3862
|
+
const base = this.currentUsage ?? {};
|
|
3863
|
+
return {
|
|
3864
|
+
...base,
|
|
3865
|
+
costAmount: this.cumulativeCost + (this.currentUsage?.costAmount ?? 0)
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3235
3868
|
// Update the cached agent command list, fire persist handlers, and
|
|
3236
3869
|
// broadcast the merged list to attached clients. Idempotent on a
|
|
3237
3870
|
// structurally identical list so we don't churn meta.json on noisy
|
|
@@ -3463,12 +4096,14 @@ var Session = class {
|
|
|
3463
4096
|
cwd: this.cwd,
|
|
3464
4097
|
agentArgs: this.agentArgs
|
|
3465
4098
|
});
|
|
4099
|
+
this.accumulateAndResetCost();
|
|
3466
4100
|
this.wireAgent(fresh.agent);
|
|
3467
4101
|
const oldAgent = this.agent;
|
|
3468
4102
|
this.agent = fresh.agent;
|
|
3469
4103
|
this.agentId = newAgentId;
|
|
3470
4104
|
this.upstreamSessionId = fresh.upstreamSessionId;
|
|
3471
4105
|
this.agentMeta = fresh.agentMeta;
|
|
4106
|
+
this.agentCapabilities = fresh.agentCapabilities;
|
|
3472
4107
|
this.agentAdvertisedCommands = [];
|
|
3473
4108
|
this.broadcastMergedCommands();
|
|
3474
4109
|
if (this.agentAdvertisedModels.length > 0) {
|
|
@@ -3636,12 +4271,120 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3636
4271
|
}
|
|
3637
4272
|
});
|
|
3638
4273
|
}
|
|
4274
|
+
// stdin-stream lifecycle. Cat --stream calls openStream() once after
|
|
4275
|
+
// session/new, then forwards stdin chunks via streamWrite(). The agent
|
|
4276
|
+
// either consumes via the file path (when shell tools are available)
|
|
4277
|
+
// or — once the MCP surface lands — via tool calls that route through
|
|
4278
|
+
// streamRead() / streamTail() / streamHead() / streamWaitFor().
|
|
4279
|
+
hasStreamBuffer() {
|
|
4280
|
+
return this.streamBuffer !== void 0;
|
|
4281
|
+
}
|
|
4282
|
+
get streamPath() {
|
|
4283
|
+
return this.streamFilePath;
|
|
4284
|
+
}
|
|
4285
|
+
openStream(opts) {
|
|
4286
|
+
if (this.streamBuffer !== void 0) {
|
|
4287
|
+
throw new Error(
|
|
4288
|
+
`stream buffer already open for session ${this.sessionId}`
|
|
4289
|
+
);
|
|
4290
|
+
}
|
|
4291
|
+
const mode = opts.mode ?? "memory";
|
|
4292
|
+
const filePath = mode === "file" && opts.filePathFor !== void 0 ? opts.filePathFor(this.sessionId) : void 0;
|
|
4293
|
+
const bufferOpts = {};
|
|
4294
|
+
if (opts.capacityBytes !== void 0) {
|
|
4295
|
+
bufferOpts.capacityBytes = opts.capacityBytes;
|
|
4296
|
+
}
|
|
4297
|
+
if (filePath !== void 0) {
|
|
4298
|
+
bufferOpts.filePath = filePath;
|
|
4299
|
+
}
|
|
4300
|
+
if (opts.fileCapBytes !== void 0) {
|
|
4301
|
+
bufferOpts.fileCapBytes = opts.fileCapBytes;
|
|
4302
|
+
}
|
|
4303
|
+
bufferOpts.onFileCapReached = () => {
|
|
4304
|
+
this.recordAndBroadcast("session/update", {
|
|
4305
|
+
sessionId: this.upstreamSessionId,
|
|
4306
|
+
update: {
|
|
4307
|
+
sessionUpdate: "stream_truncated",
|
|
4308
|
+
...filePath !== void 0 ? { filePath } : {},
|
|
4309
|
+
fileCapBytes: opts.fileCapBytes
|
|
4310
|
+
}
|
|
4311
|
+
});
|
|
4312
|
+
};
|
|
4313
|
+
const buf = new SessionStreamBuffer(bufferOpts);
|
|
4314
|
+
this.streamBuffer = buf;
|
|
4315
|
+
this.streamFilePath = filePath;
|
|
4316
|
+
const result = {
|
|
4317
|
+
capacityBytes: buf.capacity
|
|
4318
|
+
};
|
|
4319
|
+
if (filePath !== void 0) {
|
|
4320
|
+
result.filePath = filePath;
|
|
4321
|
+
}
|
|
4322
|
+
if (opts.fileCapBytes !== void 0) {
|
|
4323
|
+
result.fileCapBytes = opts.fileCapBytes;
|
|
4324
|
+
}
|
|
4325
|
+
return result;
|
|
4326
|
+
}
|
|
4327
|
+
streamWrite(chunkB64, eof) {
|
|
4328
|
+
const buf = this.requireStreamBuffer();
|
|
4329
|
+
if (chunkB64.length > 0) {
|
|
4330
|
+
const chunk = Buffer.from(chunkB64, "base64");
|
|
4331
|
+
buf.append(chunk);
|
|
4332
|
+
}
|
|
4333
|
+
if (eof === true) {
|
|
4334
|
+
buf.close();
|
|
4335
|
+
}
|
|
4336
|
+
return { writeCursor: buf.writeCursorPos };
|
|
4337
|
+
}
|
|
4338
|
+
async streamRead(cursor, maxBytes, waitMs) {
|
|
4339
|
+
const buf = this.requireStreamBuffer();
|
|
4340
|
+
const cap = Math.max(
|
|
4341
|
+
0,
|
|
4342
|
+
Math.min(maxBytes ?? STREAM_READ_MAX_BYTES, STREAM_READ_MAX_BYTES)
|
|
4343
|
+
);
|
|
4344
|
+
let r = buf.read(cursor, cap);
|
|
4345
|
+
if (r.bytes.length === 0 && r.eof !== true && waitMs !== void 0 && waitMs > 0) {
|
|
4346
|
+
const outcome = await buf.waitForData(r.nextCursor, waitMs);
|
|
4347
|
+
if (outcome === "data" || outcome === "eof") {
|
|
4348
|
+
r = buf.read(r.nextCursor, cap);
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
const out = {
|
|
4352
|
+
bytes: r.bytes.toString("base64"),
|
|
4353
|
+
nextCursor: r.nextCursor
|
|
4354
|
+
};
|
|
4355
|
+
if (r.gap !== void 0) {
|
|
4356
|
+
out.gap = r.gap;
|
|
4357
|
+
}
|
|
4358
|
+
if (r.eof === true) {
|
|
4359
|
+
out.eof = true;
|
|
4360
|
+
}
|
|
4361
|
+
return out;
|
|
4362
|
+
}
|
|
4363
|
+
requireStreamBuffer() {
|
|
4364
|
+
if (this.streamBuffer === void 0) {
|
|
4365
|
+
const err = new Error(
|
|
4366
|
+
`session ${this.sessionId} has no stream buffer; call hydra-acp/stream_open first`
|
|
4367
|
+
);
|
|
4368
|
+
err.code = JsonRpcErrorCodes.StreamNotEnabled;
|
|
4369
|
+
throw err;
|
|
4370
|
+
}
|
|
4371
|
+
return this.streamBuffer;
|
|
4372
|
+
}
|
|
3639
4373
|
markClosed(opts) {
|
|
3640
4374
|
if (this.closed) {
|
|
3641
4375
|
return;
|
|
3642
4376
|
}
|
|
3643
4377
|
this.closed = true;
|
|
3644
4378
|
this.cancelIdleTimer();
|
|
4379
|
+
if (this.currentEntry?.kind === "user") {
|
|
4380
|
+
this.broadcastTurnComplete(
|
|
4381
|
+
this.currentEntry.clientId,
|
|
4382
|
+
{ stopReason: "interrupted" },
|
|
4383
|
+
this.currentEntry.messageId,
|
|
4384
|
+
this.currentEntry.wasAmend
|
|
4385
|
+
);
|
|
4386
|
+
this.currentEntry = void 0;
|
|
4387
|
+
}
|
|
3645
4388
|
const stranded = this.promptQueue;
|
|
3646
4389
|
this.promptQueue = [];
|
|
3647
4390
|
for (const entry of stranded) {
|
|
@@ -3654,12 +4397,23 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3654
4397
|
} catch {
|
|
3655
4398
|
}
|
|
3656
4399
|
}
|
|
4400
|
+
this.notifyChain("session.closed", {});
|
|
3657
4401
|
const sessionId = this.sessionId;
|
|
3658
4402
|
this.queueWriteChain = this.queueWriteChain.catch(() => void 0).then(() => deleteQueue(sessionId).catch(() => void 0));
|
|
3659
4403
|
for (const client of this.clients.values()) {
|
|
3660
4404
|
void client.connection.notify("hydra-acp/session_closed", { sessionId: this.sessionId }).catch(() => void 0);
|
|
3661
4405
|
}
|
|
3662
4406
|
this.clients.clear();
|
|
4407
|
+
if (this.streamBuffer !== void 0) {
|
|
4408
|
+
const buf = this.streamBuffer;
|
|
4409
|
+
const path13 = this.streamFilePath;
|
|
4410
|
+
this.streamBuffer = void 0;
|
|
4411
|
+
this.streamFilePath = void 0;
|
|
4412
|
+
buf.close();
|
|
4413
|
+
if (path13 !== void 0) {
|
|
4414
|
+
void buf.drainFileWrites().then(() => fsp4.unlink(path13).catch(() => void 0));
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
3663
4417
|
for (const handler of this.closeHandlers) {
|
|
3664
4418
|
handler(opts);
|
|
3665
4419
|
}
|
|
@@ -3722,6 +4476,44 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3722
4476
|
clearTimeout(this.idleTimer);
|
|
3723
4477
|
this.idleTimer = void 0;
|
|
3724
4478
|
}
|
|
4479
|
+
this.cancelIdleEventTimer();
|
|
4480
|
+
}
|
|
4481
|
+
// ── Lifecycle event timer ────────────────────────────────────────────────
|
|
4482
|
+
scheduleIdleEvent() {
|
|
4483
|
+
if (this.closed || this.idleEventTimeoutMs <= 0 || this.transformChain.length === 0) {
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
if (this.idleEventTimer) {
|
|
4487
|
+
clearTimeout(this.idleEventTimer);
|
|
4488
|
+
}
|
|
4489
|
+
this.idleEventTimer = setTimeout(() => {
|
|
4490
|
+
this.idleEventTimer = void 0;
|
|
4491
|
+
this.notifyChain("session.idle", {});
|
|
4492
|
+
}, this.idleEventTimeoutMs);
|
|
4493
|
+
if (typeof this.idleEventTimer.unref === "function") {
|
|
4494
|
+
this.idleEventTimer.unref();
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
cancelIdleEventTimer() {
|
|
4498
|
+
if (this.idleEventTimer) {
|
|
4499
|
+
clearTimeout(this.idleEventTimer);
|
|
4500
|
+
this.idleEventTimer = void 0;
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
// Send a lifecycle notification to every transformer in the chain that
|
|
4504
|
+
// declared the matching intercept. Fire-and-forget — no response expected.
|
|
4505
|
+
notifyChain(event, payload) {
|
|
4506
|
+
const intercept = `lifecycle:${event}`;
|
|
4507
|
+
for (const t of this.transformChain) {
|
|
4508
|
+
if (!t.intercepts.has(intercept)) {
|
|
4509
|
+
continue;
|
|
4510
|
+
}
|
|
4511
|
+
void t.connection.notify("transformer/session_event", {
|
|
4512
|
+
event,
|
|
4513
|
+
sessionId: this.sessionId,
|
|
4514
|
+
payload
|
|
4515
|
+
}).catch(() => void 0);
|
|
4516
|
+
}
|
|
3725
4517
|
}
|
|
3726
4518
|
rewriteForClient(params) {
|
|
3727
4519
|
if (params && typeof params === "object" && !Array.isArray(params)) {
|
|
@@ -3761,6 +4553,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3761
4553
|
}
|
|
3762
4554
|
}
|
|
3763
4555
|
this.scheduleIdleCheck();
|
|
4556
|
+
this.scheduleIdleEvent();
|
|
3764
4557
|
}
|
|
3765
4558
|
this.updatedAt = Date.now();
|
|
3766
4559
|
for (const client of this.clients.values()) {
|
|
@@ -3917,6 +4710,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3917
4710
|
return;
|
|
3918
4711
|
}
|
|
3919
4712
|
this.promptInFlight = true;
|
|
4713
|
+
await new Promise((r) => setImmediate(r));
|
|
3920
4714
|
try {
|
|
3921
4715
|
while (this.promptQueue.length > 0) {
|
|
3922
4716
|
const next = this.promptQueue.shift();
|
|
@@ -3936,7 +4730,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3936
4730
|
try {
|
|
3937
4731
|
const result = await this.runQueueEntry(next);
|
|
3938
4732
|
next.resolve(result);
|
|
3939
|
-
await Promise
|
|
4733
|
+
await new Promise((r) => setImmediate(r));
|
|
3940
4734
|
} catch (err) {
|
|
3941
4735
|
next.reject(err);
|
|
3942
4736
|
} finally {
|
|
@@ -3973,21 +4767,25 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3973
4767
|
}
|
|
3974
4768
|
);
|
|
3975
4769
|
} catch (err) {
|
|
4770
|
+
if (!this.closed) {
|
|
4771
|
+
this.broadcastTurnComplete(
|
|
4772
|
+
entry.clientId,
|
|
4773
|
+
{ stopReason: "error" },
|
|
4774
|
+
entry.messageId,
|
|
4775
|
+
entry.wasAmend
|
|
4776
|
+
);
|
|
4777
|
+
}
|
|
4778
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
4779
|
+
throw err;
|
|
4780
|
+
}
|
|
4781
|
+
if (!this.closed) {
|
|
3976
4782
|
this.broadcastTurnComplete(
|
|
3977
4783
|
entry.clientId,
|
|
3978
|
-
|
|
4784
|
+
response,
|
|
3979
4785
|
entry.messageId,
|
|
3980
4786
|
entry.wasAmend
|
|
3981
4787
|
);
|
|
3982
|
-
this.clearAmendIfMatches(entry.messageId);
|
|
3983
|
-
throw err;
|
|
3984
4788
|
}
|
|
3985
|
-
this.broadcastTurnComplete(
|
|
3986
|
-
entry.clientId,
|
|
3987
|
-
response,
|
|
3988
|
-
entry.messageId,
|
|
3989
|
-
entry.wasAmend
|
|
3990
|
-
);
|
|
3991
4789
|
this.clearAmendIfMatches(entry.messageId);
|
|
3992
4790
|
return response;
|
|
3993
4791
|
}
|
|
@@ -4252,6 +5050,12 @@ function extractPromptText(prompt) {
|
|
|
4252
5050
|
return "";
|
|
4253
5051
|
}).join("");
|
|
4254
5052
|
}
|
|
5053
|
+
function defaultStopPayload(method) {
|
|
5054
|
+
if (method === "session/prompt") {
|
|
5055
|
+
return { stopReason: "stopped" };
|
|
5056
|
+
}
|
|
5057
|
+
return {};
|
|
5058
|
+
}
|
|
4255
5059
|
function firstLine(text, max) {
|
|
4256
5060
|
for (const raw of text.split(/\r?\n/)) {
|
|
4257
5061
|
const line = raw.trim();
|
|
@@ -4292,7 +5096,8 @@ var PersistedUsage = z4.object({
|
|
|
4292
5096
|
used: z4.number().optional(),
|
|
4293
5097
|
size: z4.number().optional(),
|
|
4294
5098
|
costAmount: z4.number().optional(),
|
|
4295
|
-
costCurrency: z4.string().optional()
|
|
5099
|
+
costCurrency: z4.string().optional(),
|
|
5100
|
+
cumulativeCost: z4.number().optional()
|
|
4296
5101
|
});
|
|
4297
5102
|
var SessionRecord = z4.object({
|
|
4298
5103
|
version: z4.literal(1),
|
|
@@ -4341,6 +5146,9 @@ var SessionRecord = z4.object({
|
|
|
4341
5146
|
// it) so the local history.jsonl gets populated from the agent's
|
|
4342
5147
|
// memory. Cleared after that first resurrect completes.
|
|
4343
5148
|
pendingHistorySync: z4.boolean().optional(),
|
|
5149
|
+
// Set when this session was spawned as a child by a transformer via
|
|
5150
|
+
// hydra-acp/spawn_child_session. Points to the spawning session's id.
|
|
5151
|
+
parentSessionId: z4.string().optional(),
|
|
4344
5152
|
createdAt: z4.string(),
|
|
4345
5153
|
updatedAt: z4.string()
|
|
4346
5154
|
});
|
|
@@ -4462,6 +5270,7 @@ function recordFromMemorySession(args) {
|
|
|
4462
5270
|
agentModes: args.agentModes,
|
|
4463
5271
|
agentModels: args.agentModels,
|
|
4464
5272
|
pendingHistorySync: args.pendingHistorySync,
|
|
5273
|
+
parentSessionId: args.parentSessionId,
|
|
4465
5274
|
createdAt: args.createdAt ?? now,
|
|
4466
5275
|
updatedAt: args.updatedAt ?? now
|
|
4467
5276
|
};
|
|
@@ -4671,7 +5480,9 @@ var SessionManager = class {
|
|
|
4671
5480
|
this.sessionHistoryMaxEntries = options.sessionHistoryMaxEntries ?? 1e3;
|
|
4672
5481
|
this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
|
|
4673
5482
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
5483
|
+
this.idleEventTimeoutMs = options.idleEventTimeoutMs ?? 3e4;
|
|
4674
5484
|
this.defaultModels = options.defaultModels ?? {};
|
|
5485
|
+
this.defaultTransformers = options.defaultTransformers ?? [];
|
|
4675
5486
|
this.logger = options.logger;
|
|
4676
5487
|
this.npmRegistry = options.npmRegistry;
|
|
4677
5488
|
}
|
|
@@ -4683,6 +5494,8 @@ var SessionManager = class {
|
|
|
4683
5494
|
histories;
|
|
4684
5495
|
idleTimeoutMs;
|
|
4685
5496
|
defaultModels;
|
|
5497
|
+
defaultTransformers;
|
|
5498
|
+
idleEventTimeoutMs;
|
|
4686
5499
|
sessionHistoryMaxEntries;
|
|
4687
5500
|
// Serialize meta.json read-modify-write operations per session id so
|
|
4688
5501
|
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
@@ -4699,15 +5512,40 @@ var SessionManager = class {
|
|
|
4699
5512
|
model: params.model,
|
|
4700
5513
|
onInstallProgress: params.onInstallProgress
|
|
4701
5514
|
});
|
|
5515
|
+
if (params.transformChain && params.transformChain.length > 0) {
|
|
5516
|
+
let caps = { ...fresh.agentCapabilities ?? {} };
|
|
5517
|
+
for (const t of params.transformChain) {
|
|
5518
|
+
if (!t.intercepts.has("agent:initialize")) {
|
|
5519
|
+
continue;
|
|
5520
|
+
}
|
|
5521
|
+
try {
|
|
5522
|
+
const result = await t.connection.request("transformer/message", {
|
|
5523
|
+
token: `t_${generateRawSessionId()}`,
|
|
5524
|
+
phase: "response",
|
|
5525
|
+
method: "initialize",
|
|
5526
|
+
direction: "agent\u2192daemon",
|
|
5527
|
+
sessionId: "(pre-session)",
|
|
5528
|
+
envelope: caps
|
|
5529
|
+
});
|
|
5530
|
+
if (result.action === "stop" && result.payload) {
|
|
5531
|
+
caps = result.payload;
|
|
5532
|
+
}
|
|
5533
|
+
} catch {
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
fresh.agentCapabilities = caps;
|
|
5537
|
+
}
|
|
4702
5538
|
const session = new Session({
|
|
4703
5539
|
cwd: params.cwd,
|
|
4704
5540
|
agentId: params.agentId,
|
|
4705
5541
|
agent: fresh.agent,
|
|
4706
5542
|
upstreamSessionId: fresh.upstreamSessionId,
|
|
4707
5543
|
agentMeta: fresh.agentMeta,
|
|
5544
|
+
agentCapabilities: fresh.agentCapabilities,
|
|
4708
5545
|
title: params.title,
|
|
4709
5546
|
agentArgs: params.agentArgs,
|
|
4710
5547
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
5548
|
+
idleEventTimeoutMs: this.idleEventTimeoutMs,
|
|
4711
5549
|
logger: this.logger,
|
|
4712
5550
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
4713
5551
|
historyStore: this.histories,
|
|
@@ -4715,7 +5553,9 @@ var SessionManager = class {
|
|
|
4715
5553
|
currentModel: fresh.initialModel,
|
|
4716
5554
|
currentMode: fresh.initialMode,
|
|
4717
5555
|
agentModes: fresh.initialModes,
|
|
4718
|
-
agentModels: fresh.initialModels
|
|
5556
|
+
agentModels: fresh.initialModels,
|
|
5557
|
+
transformChain: params.transformChain,
|
|
5558
|
+
parentSessionId: params.parentSessionId
|
|
4719
5559
|
});
|
|
4720
5560
|
await this.attachManagerHooks(session);
|
|
4721
5561
|
return session;
|
|
@@ -4769,17 +5609,22 @@ var SessionManager = class {
|
|
|
4769
5609
|
cwd: params.cwd,
|
|
4770
5610
|
plan
|
|
4771
5611
|
});
|
|
5612
|
+
let agentCapabilities;
|
|
4772
5613
|
try {
|
|
4773
|
-
await agent.connection.request(
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
5614
|
+
const initResult = await agent.connection.request(
|
|
5615
|
+
"initialize",
|
|
5616
|
+
{
|
|
5617
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
5618
|
+
clientCapabilities: {},
|
|
5619
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
5620
|
+
}
|
|
5621
|
+
);
|
|
5622
|
+
agentCapabilities = initResult.agentCapabilities;
|
|
5623
|
+
} catch (err) {
|
|
5624
|
+
await agent.kill().catch(() => void 0);
|
|
5625
|
+
throw err;
|
|
5626
|
+
}
|
|
5627
|
+
let loadResult;
|
|
4783
5628
|
try {
|
|
4784
5629
|
loadResult = await agent.connection.request(
|
|
4785
5630
|
"session/load",
|
|
@@ -4811,6 +5656,7 @@ var SessionManager = class {
|
|
|
4811
5656
|
agent,
|
|
4812
5657
|
upstreamSessionId: params.upstreamSessionId,
|
|
4813
5658
|
agentMeta: loadResult?._meta,
|
|
5659
|
+
agentCapabilities,
|
|
4814
5660
|
title: params.title,
|
|
4815
5661
|
agentArgs: params.agentArgs,
|
|
4816
5662
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
@@ -4861,6 +5707,7 @@ var SessionManager = class {
|
|
|
4861
5707
|
agent: fresh.agent,
|
|
4862
5708
|
upstreamSessionId: fresh.upstreamSessionId,
|
|
4863
5709
|
agentMeta: fresh.agentMeta,
|
|
5710
|
+
agentCapabilities: fresh.agentCapabilities,
|
|
4864
5711
|
title: params.title,
|
|
4865
5712
|
agentArgs: params.agentArgs,
|
|
4866
5713
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
@@ -5042,11 +5889,15 @@ var SessionManager = class {
|
|
|
5042
5889
|
plan
|
|
5043
5890
|
});
|
|
5044
5891
|
try {
|
|
5045
|
-
await agent.connection.request(
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5892
|
+
const initResult = await agent.connection.request(
|
|
5893
|
+
"initialize",
|
|
5894
|
+
{
|
|
5895
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
5896
|
+
clientCapabilities: {},
|
|
5897
|
+
clientInfo: { name: "hydra", version: HYDRA_VERSION }
|
|
5898
|
+
}
|
|
5899
|
+
);
|
|
5900
|
+
const agentCapabilities = initResult.agentCapabilities;
|
|
5050
5901
|
const newResult = await agent.connection.request(
|
|
5051
5902
|
"session/new",
|
|
5052
5903
|
{
|
|
@@ -5088,6 +5939,7 @@ var SessionManager = class {
|
|
|
5088
5939
|
agent,
|
|
5089
5940
|
upstreamSessionId: sessionIdRaw,
|
|
5090
5941
|
agentMeta: newResult._meta,
|
|
5942
|
+
agentCapabilities,
|
|
5091
5943
|
initialModel,
|
|
5092
5944
|
initialModels: initialModels.length > 0 ? initialModels : void 0,
|
|
5093
5945
|
initialModes: initialModes.length > 0 ? initialModes : void 0,
|
|
@@ -5208,7 +6060,13 @@ var SessionManager = class {
|
|
|
5208
6060
|
agentArgs: record.agentArgs,
|
|
5209
6061
|
currentModel: record.currentModel,
|
|
5210
6062
|
currentMode: record.currentMode,
|
|
5211
|
-
currentUsage: persistedUsageToSnapshot(
|
|
6063
|
+
currentUsage: persistedUsageToSnapshot(
|
|
6064
|
+
record.currentUsage ? {
|
|
6065
|
+
...record.currentUsage,
|
|
6066
|
+
cumulativeCost: (record.currentUsage.cumulativeCost ?? 0) + (record.currentUsage.costAmount ?? 0),
|
|
6067
|
+
costAmount: void 0
|
|
6068
|
+
} : void 0
|
|
6069
|
+
),
|
|
5212
6070
|
agentCommands: record.agentCommands,
|
|
5213
6071
|
agentModes: record.agentModes,
|
|
5214
6072
|
agentModels: record.agentModels,
|
|
@@ -5309,7 +6167,8 @@ var SessionManager = class {
|
|
|
5309
6167
|
title: session.title,
|
|
5310
6168
|
agentId: session.agentId,
|
|
5311
6169
|
currentModel: session.currentModel,
|
|
5312
|
-
currentUsage: session.
|
|
6170
|
+
currentUsage: session.totalUsage,
|
|
6171
|
+
parentSessionId: session.parentSessionId,
|
|
5313
6172
|
updatedAt: used,
|
|
5314
6173
|
attachedClients: session.attachedCount,
|
|
5315
6174
|
status: "live",
|
|
@@ -5332,9 +6191,13 @@ var SessionManager = class {
|
|
|
5332
6191
|
title: r.title,
|
|
5333
6192
|
agentId: r.agentId,
|
|
5334
6193
|
currentModel: r.currentModel,
|
|
5335
|
-
currentUsage: r.currentUsage
|
|
6194
|
+
currentUsage: r.currentUsage ? {
|
|
6195
|
+
...r.currentUsage,
|
|
6196
|
+
costAmount: (r.currentUsage.cumulativeCost ?? 0) + (r.currentUsage.costAmount ?? 0) || void 0
|
|
6197
|
+
} : void 0,
|
|
5336
6198
|
importedFromMachine: r.importedFromMachine,
|
|
5337
6199
|
importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
|
|
6200
|
+
parentSessionId: r.parentSessionId,
|
|
5338
6201
|
updatedAt: used,
|
|
5339
6202
|
attachedClients: 0,
|
|
5340
6203
|
status: "cold",
|
|
@@ -5678,6 +6541,7 @@ function mergeForPersistence(session, existing) {
|
|
|
5678
6541
|
agentCommands,
|
|
5679
6542
|
agentModes,
|
|
5680
6543
|
agentModels,
|
|
6544
|
+
parentSessionId: session.parentSessionId ?? existing?.parentSessionId,
|
|
5681
6545
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
5682
6546
|
});
|
|
5683
6547
|
}
|
|
@@ -5698,6 +6562,9 @@ function usageSnapshotToPersisted(usage) {
|
|
|
5698
6562
|
if (usage.costCurrency !== void 0) {
|
|
5699
6563
|
out.costCurrency = usage.costCurrency;
|
|
5700
6564
|
}
|
|
6565
|
+
if (usage.cumulativeCost !== void 0) {
|
|
6566
|
+
out.cumulativeCost = usage.cumulativeCost;
|
|
6567
|
+
}
|
|
5701
6568
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
5702
6569
|
}
|
|
5703
6570
|
function persistedUsageToSnapshot(usage) {
|
|
@@ -5898,32 +6765,505 @@ async function historyMtimeIso(sessionId) {
|
|
|
5898
6765
|
}
|
|
5899
6766
|
}
|
|
5900
6767
|
|
|
5901
|
-
// src/core/extensions.ts
|
|
5902
|
-
import { spawn as spawn4 } from "child_process";
|
|
5903
|
-
import * as fs11 from "fs";
|
|
5904
|
-
import * as
|
|
5905
|
-
import * as path7 from "path";
|
|
5906
|
-
var RESTART_BASE_MS = 1e3;
|
|
5907
|
-
var RESTART_CAP_MS = 6e4;
|
|
5908
|
-
var STOP_GRACE_MS = 3e3;
|
|
5909
|
-
var ExtensionManager = class {
|
|
6768
|
+
// src/core/extensions.ts
|
|
6769
|
+
import { spawn as spawn4 } from "child_process";
|
|
6770
|
+
import * as fs11 from "fs";
|
|
6771
|
+
import * as fsp5 from "fs/promises";
|
|
6772
|
+
import * as path7 from "path";
|
|
6773
|
+
var RESTART_BASE_MS = 1e3;
|
|
6774
|
+
var RESTART_CAP_MS = 6e4;
|
|
6775
|
+
var STOP_GRACE_MS = 3e3;
|
|
6776
|
+
var ExtensionManager = class {
|
|
6777
|
+
entries = /* @__PURE__ */ new Map();
|
|
6778
|
+
stopping = false;
|
|
6779
|
+
context;
|
|
6780
|
+
tokenRegistry;
|
|
6781
|
+
constructor(extensions, context, options = {}) {
|
|
6782
|
+
this.context = context;
|
|
6783
|
+
this.tokenRegistry = options.tokenRegistry;
|
|
6784
|
+
for (const ext of extensions) {
|
|
6785
|
+
this.entries.set(ext.name, this.makeEntry(ext));
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
setContext(context) {
|
|
6789
|
+
this.context = context;
|
|
6790
|
+
}
|
|
6791
|
+
// Called by the WS handler after a process connects and calls initialize
|
|
6792
|
+
// with clientInfo.version. Stored on the entry and surfaced in list().
|
|
6793
|
+
reportVersion(name, version) {
|
|
6794
|
+
const entry = this.entries.get(name);
|
|
6795
|
+
if (entry) {
|
|
6796
|
+
entry.version = version;
|
|
6797
|
+
}
|
|
6798
|
+
}
|
|
6799
|
+
async start() {
|
|
6800
|
+
if (!this.context) {
|
|
6801
|
+
throw new Error("ExtensionManager: setContext must be called before start");
|
|
6802
|
+
}
|
|
6803
|
+
await fsp5.mkdir(paths.extensionsDir(), { recursive: true });
|
|
6804
|
+
await this.reapOrphans();
|
|
6805
|
+
for (const entry of this.entries.values()) {
|
|
6806
|
+
if (!entry.config.enabled) {
|
|
6807
|
+
continue;
|
|
6808
|
+
}
|
|
6809
|
+
this.spawn(entry, 0);
|
|
6810
|
+
}
|
|
6811
|
+
}
|
|
6812
|
+
async stop() {
|
|
6813
|
+
this.stopping = true;
|
|
6814
|
+
const tasks = [];
|
|
6815
|
+
for (const entry of this.entries.values()) {
|
|
6816
|
+
if (entry.restartTimer) {
|
|
6817
|
+
clearTimeout(entry.restartTimer);
|
|
6818
|
+
entry.restartTimer = void 0;
|
|
6819
|
+
}
|
|
6820
|
+
const child = entry.child;
|
|
6821
|
+
if (!child) {
|
|
6822
|
+
continue;
|
|
6823
|
+
}
|
|
6824
|
+
try {
|
|
6825
|
+
child.kill("SIGTERM");
|
|
6826
|
+
} catch {
|
|
6827
|
+
}
|
|
6828
|
+
tasks.push(
|
|
6829
|
+
new Promise((resolve3) => {
|
|
6830
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
6831
|
+
resolve3();
|
|
6832
|
+
return;
|
|
6833
|
+
}
|
|
6834
|
+
const timer = setTimeout(() => {
|
|
6835
|
+
try {
|
|
6836
|
+
child.kill("SIGKILL");
|
|
6837
|
+
} catch {
|
|
6838
|
+
}
|
|
6839
|
+
resolve3();
|
|
6840
|
+
}, STOP_GRACE_MS);
|
|
6841
|
+
child.on("exit", () => {
|
|
6842
|
+
clearTimeout(timer);
|
|
6843
|
+
resolve3();
|
|
6844
|
+
});
|
|
6845
|
+
})
|
|
6846
|
+
);
|
|
6847
|
+
}
|
|
6848
|
+
await Promise.allSettled(tasks);
|
|
6849
|
+
for (const entry of this.entries.values()) {
|
|
6850
|
+
try {
|
|
6851
|
+
entry.logStream?.end();
|
|
6852
|
+
} catch {
|
|
6853
|
+
}
|
|
6854
|
+
entry.child = void 0;
|
|
6855
|
+
entry.logStream = void 0;
|
|
6856
|
+
entry.pid = void 0;
|
|
6857
|
+
}
|
|
6858
|
+
}
|
|
6859
|
+
list() {
|
|
6860
|
+
return [...this.entries.values()].map((entry) => this.infoFor(entry));
|
|
6861
|
+
}
|
|
6862
|
+
get(name) {
|
|
6863
|
+
const entry = this.entries.get(name);
|
|
6864
|
+
return entry ? this.infoFor(entry) : void 0;
|
|
6865
|
+
}
|
|
6866
|
+
has(name) {
|
|
6867
|
+
return this.entries.has(name);
|
|
6868
|
+
}
|
|
6869
|
+
async startByName(name) {
|
|
6870
|
+
const entry = this.entries.get(name);
|
|
6871
|
+
if (!entry) {
|
|
6872
|
+
throw withCode2(new Error(`unknown extension: ${name}`), "NOT_FOUND");
|
|
6873
|
+
}
|
|
6874
|
+
if (entry.child) {
|
|
6875
|
+
throw withCode2(new Error(`extension ${name} already running`), "CONFLICT");
|
|
6876
|
+
}
|
|
6877
|
+
if (entry.restartTimer) {
|
|
6878
|
+
clearTimeout(entry.restartTimer);
|
|
6879
|
+
entry.restartTimer = void 0;
|
|
6880
|
+
}
|
|
6881
|
+
entry.manuallyStopped = false;
|
|
6882
|
+
entry.restartCount = 0;
|
|
6883
|
+
this.spawn(entry, 0);
|
|
6884
|
+
return this.infoFor(entry);
|
|
6885
|
+
}
|
|
6886
|
+
async stopByName(name) {
|
|
6887
|
+
const entry = this.entries.get(name);
|
|
6888
|
+
if (!entry) {
|
|
6889
|
+
throw withCode2(new Error(`unknown extension: ${name}`), "NOT_FOUND");
|
|
6890
|
+
}
|
|
6891
|
+
entry.manuallyStopped = true;
|
|
6892
|
+
if (entry.restartTimer) {
|
|
6893
|
+
clearTimeout(entry.restartTimer);
|
|
6894
|
+
entry.restartTimer = void 0;
|
|
6895
|
+
}
|
|
6896
|
+
const child = entry.child;
|
|
6897
|
+
if (!child) {
|
|
6898
|
+
return this.infoFor(entry);
|
|
6899
|
+
}
|
|
6900
|
+
await this.terminate(entry, child);
|
|
6901
|
+
return this.infoFor(entry);
|
|
6902
|
+
}
|
|
6903
|
+
async restartByName(name) {
|
|
6904
|
+
await this.stopByName(name);
|
|
6905
|
+
return this.startByName(name);
|
|
6906
|
+
}
|
|
6907
|
+
// Register a new extension and (if enabled) start it. Used by the
|
|
6908
|
+
// POST /v1/extensions endpoint so `hydra-acp extensions add` can take
|
|
6909
|
+
// effect without a daemon restart.
|
|
6910
|
+
register(config) {
|
|
6911
|
+
if (this.entries.has(config.name)) {
|
|
6912
|
+
throw withCode2(
|
|
6913
|
+
new Error(`extension ${config.name} already exists`),
|
|
6914
|
+
"CONFLICT"
|
|
6915
|
+
);
|
|
6916
|
+
}
|
|
6917
|
+
if (!this.context) {
|
|
6918
|
+
throw new Error("ExtensionManager: setContext must be called before register");
|
|
6919
|
+
}
|
|
6920
|
+
const entry = this.makeEntry(config);
|
|
6921
|
+
this.entries.set(config.name, entry);
|
|
6922
|
+
if (config.enabled) {
|
|
6923
|
+
this.spawn(entry, 0);
|
|
6924
|
+
}
|
|
6925
|
+
return this.infoFor(entry);
|
|
6926
|
+
}
|
|
6927
|
+
async unregister(name) {
|
|
6928
|
+
const entry = this.entries.get(name);
|
|
6929
|
+
if (!entry) {
|
|
6930
|
+
throw withCode2(new Error(`unknown extension: ${name}`), "NOT_FOUND");
|
|
6931
|
+
}
|
|
6932
|
+
entry.manuallyStopped = true;
|
|
6933
|
+
if (entry.restartTimer) {
|
|
6934
|
+
clearTimeout(entry.restartTimer);
|
|
6935
|
+
entry.restartTimer = void 0;
|
|
6936
|
+
}
|
|
6937
|
+
const child = entry.child;
|
|
6938
|
+
if (child) {
|
|
6939
|
+
await this.terminate(entry, child);
|
|
6940
|
+
}
|
|
6941
|
+
try {
|
|
6942
|
+
entry.logStream?.end();
|
|
6943
|
+
} catch {
|
|
6944
|
+
}
|
|
6945
|
+
this.entries.delete(name);
|
|
6946
|
+
}
|
|
6947
|
+
async terminate(entry, child) {
|
|
6948
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
6949
|
+
return;
|
|
6950
|
+
}
|
|
6951
|
+
const exited = new Promise((resolve3) => {
|
|
6952
|
+
entry.exitWaiters.push(resolve3);
|
|
6953
|
+
});
|
|
6954
|
+
try {
|
|
6955
|
+
child.kill("SIGTERM");
|
|
6956
|
+
} catch {
|
|
6957
|
+
}
|
|
6958
|
+
const killTimer = setTimeout(() => {
|
|
6959
|
+
try {
|
|
6960
|
+
child.kill("SIGKILL");
|
|
6961
|
+
} catch {
|
|
6962
|
+
}
|
|
6963
|
+
}, STOP_GRACE_MS);
|
|
6964
|
+
if (typeof killTimer.unref === "function") {
|
|
6965
|
+
killTimer.unref();
|
|
6966
|
+
}
|
|
6967
|
+
try {
|
|
6968
|
+
await exited;
|
|
6969
|
+
} finally {
|
|
6970
|
+
clearTimeout(killTimer);
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6973
|
+
infoFor(entry) {
|
|
6974
|
+
let status;
|
|
6975
|
+
if (entry.child) {
|
|
6976
|
+
status = "running";
|
|
6977
|
+
} else if (entry.restartTimer) {
|
|
6978
|
+
status = "restarting";
|
|
6979
|
+
} else if (!entry.config.enabled) {
|
|
6980
|
+
status = "disabled";
|
|
6981
|
+
} else {
|
|
6982
|
+
status = "stopped";
|
|
6983
|
+
}
|
|
6984
|
+
return {
|
|
6985
|
+
name: entry.config.name,
|
|
6986
|
+
status,
|
|
6987
|
+
pid: entry.pid,
|
|
6988
|
+
enabled: entry.config.enabled,
|
|
6989
|
+
restartCount: entry.restartCount,
|
|
6990
|
+
startedAt: entry.startedAt,
|
|
6991
|
+
lastExitCode: entry.lastExitCode,
|
|
6992
|
+
logPath: paths.extensionLogFile(entry.config.name),
|
|
6993
|
+
version: entry.version
|
|
6994
|
+
};
|
|
6995
|
+
}
|
|
6996
|
+
makeEntry(config) {
|
|
6997
|
+
return {
|
|
6998
|
+
config,
|
|
6999
|
+
child: void 0,
|
|
7000
|
+
logStream: void 0,
|
|
7001
|
+
restartTimer: void 0,
|
|
7002
|
+
pid: void 0,
|
|
7003
|
+
startedAt: void 0,
|
|
7004
|
+
restartCount: 0,
|
|
7005
|
+
lastExitCode: void 0,
|
|
7006
|
+
manuallyStopped: false,
|
|
7007
|
+
exitWaiters: [],
|
|
7008
|
+
version: void 0,
|
|
7009
|
+
processToken: void 0
|
|
7010
|
+
};
|
|
7011
|
+
}
|
|
7012
|
+
async reapOrphans() {
|
|
7013
|
+
let entries;
|
|
7014
|
+
try {
|
|
7015
|
+
entries = await fsp5.readdir(paths.extensionsDir());
|
|
7016
|
+
} catch (err) {
|
|
7017
|
+
const e = err;
|
|
7018
|
+
if (e.code === "ENOENT") {
|
|
7019
|
+
return;
|
|
7020
|
+
}
|
|
7021
|
+
throw err;
|
|
7022
|
+
}
|
|
7023
|
+
for (const entry of entries) {
|
|
7024
|
+
if (!entry.endsWith(".pid")) {
|
|
7025
|
+
continue;
|
|
7026
|
+
}
|
|
7027
|
+
const pidPath = path7.join(paths.extensionsDir(), entry);
|
|
7028
|
+
let pid;
|
|
7029
|
+
try {
|
|
7030
|
+
const raw = await fsp5.readFile(pidPath, "utf8");
|
|
7031
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
7032
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
7033
|
+
pid = parsed;
|
|
7034
|
+
}
|
|
7035
|
+
} catch {
|
|
7036
|
+
}
|
|
7037
|
+
if (typeof pid === "number" && isAlive(pid)) {
|
|
7038
|
+
try {
|
|
7039
|
+
process.kill(pid, "SIGTERM");
|
|
7040
|
+
} catch {
|
|
7041
|
+
}
|
|
7042
|
+
const deadline = Date.now() + STOP_GRACE_MS;
|
|
7043
|
+
while (Date.now() < deadline && isAlive(pid)) {
|
|
7044
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
7045
|
+
}
|
|
7046
|
+
if (isAlive(pid)) {
|
|
7047
|
+
try {
|
|
7048
|
+
process.kill(pid, "SIGKILL");
|
|
7049
|
+
} catch {
|
|
7050
|
+
}
|
|
7051
|
+
}
|
|
7052
|
+
}
|
|
7053
|
+
await fsp5.unlink(pidPath).catch(() => void 0);
|
|
7054
|
+
}
|
|
7055
|
+
}
|
|
7056
|
+
spawn(entry, attempt) {
|
|
7057
|
+
if (this.stopping || entry.manuallyStopped) {
|
|
7058
|
+
return;
|
|
7059
|
+
}
|
|
7060
|
+
const ctx = this.context;
|
|
7061
|
+
if (!ctx) {
|
|
7062
|
+
throw new Error("ExtensionManager.spawn called before setContext");
|
|
7063
|
+
}
|
|
7064
|
+
const ext = entry.config;
|
|
7065
|
+
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
7066
|
+
const logStream = fs11.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
7067
|
+
flags: "a"
|
|
7068
|
+
});
|
|
7069
|
+
logStream.write(
|
|
7070
|
+
`[hydra-acp] ${(/* @__PURE__ */ new Date()).toISOString()} starting extension ${ext.name} (attempt ${attempt + 1})
|
|
7071
|
+
`
|
|
7072
|
+
);
|
|
7073
|
+
const processToken = this.tokenRegistry?.mint(ext.name, "extension") ?? ctx.serviceToken;
|
|
7074
|
+
entry.processToken = processToken;
|
|
7075
|
+
entry.version = void 0;
|
|
7076
|
+
const env = {
|
|
7077
|
+
...process.env,
|
|
7078
|
+
HYDRA_ACP_DAEMON_URL: ctx.daemonUrl,
|
|
7079
|
+
HYDRA_ACP_DAEMON_HOST: ctx.daemonHost,
|
|
7080
|
+
HYDRA_ACP_DAEMON_PORT: String(ctx.daemonPort),
|
|
7081
|
+
HYDRA_ACP_TOKEN: processToken,
|
|
7082
|
+
HYDRA_ACP_WS_URL: ctx.daemonWsUrl,
|
|
7083
|
+
HYDRA_ACP_HOME: ctx.hydraHome,
|
|
7084
|
+
HYDRA_ACP_EXTENSION_NAME: ext.name,
|
|
7085
|
+
...ext.env
|
|
7086
|
+
};
|
|
7087
|
+
const [cmd, ...baseArgs] = command;
|
|
7088
|
+
if (cmd === void 0) {
|
|
7089
|
+
logStream.write(`[hydra-acp] extension ${ext.name} has empty command
|
|
7090
|
+
`);
|
|
7091
|
+
logStream.end();
|
|
7092
|
+
return;
|
|
7093
|
+
}
|
|
7094
|
+
const args = [...baseArgs, ...ext.args];
|
|
7095
|
+
let child;
|
|
7096
|
+
try {
|
|
7097
|
+
child = spawn4(cmd, args, {
|
|
7098
|
+
env,
|
|
7099
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7100
|
+
detached: false
|
|
7101
|
+
});
|
|
7102
|
+
} catch (err) {
|
|
7103
|
+
logStream.write(
|
|
7104
|
+
`[hydra-acp] failed to spawn ${ext.name}: ${err.message}
|
|
7105
|
+
`
|
|
7106
|
+
);
|
|
7107
|
+
logStream.end();
|
|
7108
|
+
this.scheduleRestart(entry, attempt);
|
|
7109
|
+
return;
|
|
7110
|
+
}
|
|
7111
|
+
if (child.stdout) {
|
|
7112
|
+
child.stdout.pipe(logStream, { end: false });
|
|
7113
|
+
}
|
|
7114
|
+
if (child.stderr) {
|
|
7115
|
+
child.stderr.pipe(logStream, { end: false });
|
|
7116
|
+
}
|
|
7117
|
+
if (typeof child.pid === "number") {
|
|
7118
|
+
try {
|
|
7119
|
+
fs11.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
7120
|
+
`, {
|
|
7121
|
+
encoding: "utf8",
|
|
7122
|
+
mode: 384
|
|
7123
|
+
});
|
|
7124
|
+
} catch (err) {
|
|
7125
|
+
logStream.write(
|
|
7126
|
+
`[hydra-acp] failed to write pid file for ${ext.name}: ${err.message}
|
|
7127
|
+
`
|
|
7128
|
+
);
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
entry.child = child;
|
|
7132
|
+
entry.logStream = logStream;
|
|
7133
|
+
entry.pid = typeof child.pid === "number" ? child.pid : void 0;
|
|
7134
|
+
entry.startedAt = Date.now();
|
|
7135
|
+
entry.lastExitCode = void 0;
|
|
7136
|
+
child.on("error", (err) => {
|
|
7137
|
+
logStream.write(
|
|
7138
|
+
`[hydra-acp] extension ${ext.name} error: ${err.message}
|
|
7139
|
+
`
|
|
7140
|
+
);
|
|
7141
|
+
});
|
|
7142
|
+
child.on("exit", (code, signal) => {
|
|
7143
|
+
try {
|
|
7144
|
+
fs11.unlinkSync(paths.extensionPidFile(ext.name));
|
|
7145
|
+
} catch {
|
|
7146
|
+
}
|
|
7147
|
+
logStream.write(
|
|
7148
|
+
`[hydra-acp] extension ${ext.name} exited code=${code ?? "null"} signal=${signal ?? "null"}
|
|
7149
|
+
`
|
|
7150
|
+
);
|
|
7151
|
+
entry.child = void 0;
|
|
7152
|
+
entry.pid = void 0;
|
|
7153
|
+
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
7154
|
+
if (entry.processToken) {
|
|
7155
|
+
this.tokenRegistry?.revoke(ext.name);
|
|
7156
|
+
entry.processToken = void 0;
|
|
7157
|
+
}
|
|
7158
|
+
const waiters = entry.exitWaiters.splice(0);
|
|
7159
|
+
for (const resolve3 of waiters) {
|
|
7160
|
+
resolve3();
|
|
7161
|
+
}
|
|
7162
|
+
if (this.stopping || entry.manuallyStopped) {
|
|
7163
|
+
try {
|
|
7164
|
+
logStream.end();
|
|
7165
|
+
} catch {
|
|
7166
|
+
}
|
|
7167
|
+
entry.logStream = void 0;
|
|
7168
|
+
return;
|
|
7169
|
+
}
|
|
7170
|
+
entry.restartCount += 1;
|
|
7171
|
+
this.scheduleRestart(entry, attempt + 1);
|
|
7172
|
+
});
|
|
7173
|
+
}
|
|
7174
|
+
scheduleRestart(entry, attempt) {
|
|
7175
|
+
if (this.stopping || entry.manuallyStopped) {
|
|
7176
|
+
return;
|
|
7177
|
+
}
|
|
7178
|
+
const delay = Math.min(
|
|
7179
|
+
RESTART_BASE_MS * 2 ** Math.min(attempt, 10),
|
|
7180
|
+
RESTART_CAP_MS
|
|
7181
|
+
);
|
|
7182
|
+
entry.restartTimer = setTimeout(() => {
|
|
7183
|
+
entry.restartTimer = void 0;
|
|
7184
|
+
this.spawn(entry, attempt);
|
|
7185
|
+
}, delay);
|
|
7186
|
+
if (typeof entry.restartTimer.unref === "function") {
|
|
7187
|
+
entry.restartTimer.unref();
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
};
|
|
7191
|
+
function isAlive(pid) {
|
|
7192
|
+
try {
|
|
7193
|
+
process.kill(pid, 0);
|
|
7194
|
+
return true;
|
|
7195
|
+
} catch {
|
|
7196
|
+
return false;
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
7199
|
+
function withCode2(err, code) {
|
|
7200
|
+
err.code = code;
|
|
7201
|
+
return err;
|
|
7202
|
+
}
|
|
7203
|
+
|
|
7204
|
+
// src/core/transformer-manager.ts
|
|
7205
|
+
import { spawn as spawn5 } from "child_process";
|
|
7206
|
+
import * as fs12 from "fs";
|
|
7207
|
+
import * as fsp6 from "fs/promises";
|
|
7208
|
+
import * as path8 from "path";
|
|
7209
|
+
var RESTART_BASE_MS2 = 1e3;
|
|
7210
|
+
var RESTART_CAP_MS2 = 6e4;
|
|
7211
|
+
var STOP_GRACE_MS2 = 3e3;
|
|
7212
|
+
var TransformerManager = class {
|
|
5910
7213
|
entries = /* @__PURE__ */ new Map();
|
|
7214
|
+
// Transformers that have completed transformer/initialize and are ready to
|
|
7215
|
+
// participate in chains. Keyed by transformer name.
|
|
7216
|
+
connected = /* @__PURE__ */ new Map();
|
|
5911
7217
|
stopping = false;
|
|
5912
7218
|
context;
|
|
5913
|
-
|
|
7219
|
+
tokenRegistry;
|
|
7220
|
+
constructor(transformers, context, options = {}) {
|
|
5914
7221
|
this.context = context;
|
|
5915
|
-
|
|
5916
|
-
|
|
7222
|
+
this.tokenRegistry = options.tokenRegistry;
|
|
7223
|
+
for (const t of transformers) {
|
|
7224
|
+
this.entries.set(t.name, this.makeEntry(t));
|
|
5917
7225
|
}
|
|
5918
7226
|
}
|
|
5919
7227
|
setContext(context) {
|
|
5920
7228
|
this.context = context;
|
|
5921
7229
|
}
|
|
7230
|
+
reportVersion(name, version) {
|
|
7231
|
+
const entry = this.entries.get(name);
|
|
7232
|
+
if (entry) {
|
|
7233
|
+
entry.version = version;
|
|
7234
|
+
}
|
|
7235
|
+
}
|
|
7236
|
+
// Called by the WS handler after transformer/initialize completes. The
|
|
7237
|
+
// transformer is now eligible to participate in session chains.
|
|
7238
|
+
registerConnection(name, connection, intercepts) {
|
|
7239
|
+
this.connected.set(name, {
|
|
7240
|
+
name,
|
|
7241
|
+
connection,
|
|
7242
|
+
intercepts: new Set(intercepts)
|
|
7243
|
+
});
|
|
7244
|
+
}
|
|
7245
|
+
// Called by the WS handler when the transformer's WS connection closes.
|
|
7246
|
+
deregisterConnection(name) {
|
|
7247
|
+
this.connected.delete(name);
|
|
7248
|
+
}
|
|
7249
|
+
// Resolve a list of transformer names to their live TransformerRef objects.
|
|
7250
|
+
// Names that are configured but not yet connected are silently skipped
|
|
7251
|
+
// (fail-open: session proceeds without that transformer rather than failing).
|
|
7252
|
+
resolveChain(names) {
|
|
7253
|
+
const out = [];
|
|
7254
|
+
for (const name of names) {
|
|
7255
|
+
const ref = this.connected.get(name);
|
|
7256
|
+
if (ref) {
|
|
7257
|
+
out.push(ref);
|
|
7258
|
+
}
|
|
7259
|
+
}
|
|
7260
|
+
return out;
|
|
7261
|
+
}
|
|
5922
7262
|
async start() {
|
|
5923
7263
|
if (!this.context) {
|
|
5924
|
-
throw new Error("
|
|
7264
|
+
throw new Error("TransformerManager: setContext must be called before start");
|
|
5925
7265
|
}
|
|
5926
|
-
await
|
|
7266
|
+
await fsp6.mkdir(paths.transformersDir(), { recursive: true });
|
|
5927
7267
|
await this.reapOrphans();
|
|
5928
7268
|
for (const entry of this.entries.values()) {
|
|
5929
7269
|
if (!entry.config.enabled) {
|
|
@@ -5960,7 +7300,7 @@ var ExtensionManager = class {
|
|
|
5960
7300
|
} catch {
|
|
5961
7301
|
}
|
|
5962
7302
|
resolve3();
|
|
5963
|
-
},
|
|
7303
|
+
}, STOP_GRACE_MS2);
|
|
5964
7304
|
child.on("exit", () => {
|
|
5965
7305
|
clearTimeout(timer);
|
|
5966
7306
|
resolve3();
|
|
@@ -5992,10 +7332,10 @@ var ExtensionManager = class {
|
|
|
5992
7332
|
async startByName(name) {
|
|
5993
7333
|
const entry = this.entries.get(name);
|
|
5994
7334
|
if (!entry) {
|
|
5995
|
-
throw
|
|
7335
|
+
throw withCode3(new Error(`unknown transformer: ${name}`), "NOT_FOUND");
|
|
5996
7336
|
}
|
|
5997
7337
|
if (entry.child) {
|
|
5998
|
-
throw
|
|
7338
|
+
throw withCode3(new Error(`transformer ${name} already running`), "CONFLICT");
|
|
5999
7339
|
}
|
|
6000
7340
|
if (entry.restartTimer) {
|
|
6001
7341
|
clearTimeout(entry.restartTimer);
|
|
@@ -6009,7 +7349,7 @@ var ExtensionManager = class {
|
|
|
6009
7349
|
async stopByName(name) {
|
|
6010
7350
|
const entry = this.entries.get(name);
|
|
6011
7351
|
if (!entry) {
|
|
6012
|
-
throw
|
|
7352
|
+
throw withCode3(new Error(`unknown transformer: ${name}`), "NOT_FOUND");
|
|
6013
7353
|
}
|
|
6014
7354
|
entry.manuallyStopped = true;
|
|
6015
7355
|
if (entry.restartTimer) {
|
|
@@ -6027,18 +7367,15 @@ var ExtensionManager = class {
|
|
|
6027
7367
|
await this.stopByName(name);
|
|
6028
7368
|
return this.startByName(name);
|
|
6029
7369
|
}
|
|
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
7370
|
register(config) {
|
|
6034
7371
|
if (this.entries.has(config.name)) {
|
|
6035
|
-
throw
|
|
6036
|
-
new Error(`
|
|
7372
|
+
throw withCode3(
|
|
7373
|
+
new Error(`transformer ${config.name} already exists`),
|
|
6037
7374
|
"CONFLICT"
|
|
6038
7375
|
);
|
|
6039
7376
|
}
|
|
6040
7377
|
if (!this.context) {
|
|
6041
|
-
throw new Error("
|
|
7378
|
+
throw new Error("TransformerManager: setContext must be called before register");
|
|
6042
7379
|
}
|
|
6043
7380
|
const entry = this.makeEntry(config);
|
|
6044
7381
|
this.entries.set(config.name, entry);
|
|
@@ -6050,7 +7387,7 @@ var ExtensionManager = class {
|
|
|
6050
7387
|
async unregister(name) {
|
|
6051
7388
|
const entry = this.entries.get(name);
|
|
6052
7389
|
if (!entry) {
|
|
6053
|
-
throw
|
|
7390
|
+
throw withCode3(new Error(`unknown transformer: ${name}`), "NOT_FOUND");
|
|
6054
7391
|
}
|
|
6055
7392
|
entry.manuallyStopped = true;
|
|
6056
7393
|
if (entry.restartTimer) {
|
|
@@ -6083,7 +7420,7 @@ var ExtensionManager = class {
|
|
|
6083
7420
|
child.kill("SIGKILL");
|
|
6084
7421
|
} catch {
|
|
6085
7422
|
}
|
|
6086
|
-
},
|
|
7423
|
+
}, STOP_GRACE_MS2);
|
|
6087
7424
|
if (typeof killTimer.unref === "function") {
|
|
6088
7425
|
killTimer.unref();
|
|
6089
7426
|
}
|
|
@@ -6112,7 +7449,8 @@ var ExtensionManager = class {
|
|
|
6112
7449
|
restartCount: entry.restartCount,
|
|
6113
7450
|
startedAt: entry.startedAt,
|
|
6114
7451
|
lastExitCode: entry.lastExitCode,
|
|
6115
|
-
logPath: paths.
|
|
7452
|
+
logPath: paths.transformerLogFile(entry.config.name),
|
|
7453
|
+
version: entry.version
|
|
6116
7454
|
};
|
|
6117
7455
|
}
|
|
6118
7456
|
makeEntry(config) {
|
|
@@ -6126,13 +7464,15 @@ var ExtensionManager = class {
|
|
|
6126
7464
|
restartCount: 0,
|
|
6127
7465
|
lastExitCode: void 0,
|
|
6128
7466
|
manuallyStopped: false,
|
|
6129
|
-
exitWaiters: []
|
|
7467
|
+
exitWaiters: [],
|
|
7468
|
+
version: void 0,
|
|
7469
|
+
processToken: void 0
|
|
6130
7470
|
};
|
|
6131
7471
|
}
|
|
6132
7472
|
async reapOrphans() {
|
|
6133
7473
|
let entries;
|
|
6134
7474
|
try {
|
|
6135
|
-
entries = await
|
|
7475
|
+
entries = await fsp6.readdir(paths.transformersDir());
|
|
6136
7476
|
} catch (err) {
|
|
6137
7477
|
const e = err;
|
|
6138
7478
|
if (e.code === "ENOENT") {
|
|
@@ -6144,33 +7484,33 @@ var ExtensionManager = class {
|
|
|
6144
7484
|
if (!entry.endsWith(".pid")) {
|
|
6145
7485
|
continue;
|
|
6146
7486
|
}
|
|
6147
|
-
const pidPath =
|
|
7487
|
+
const pidPath = path8.join(paths.transformersDir(), entry);
|
|
6148
7488
|
let pid;
|
|
6149
7489
|
try {
|
|
6150
|
-
const raw = await
|
|
7490
|
+
const raw = await fsp6.readFile(pidPath, "utf8");
|
|
6151
7491
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
6152
7492
|
if (Number.isInteger(parsed) && parsed > 0) {
|
|
6153
7493
|
pid = parsed;
|
|
6154
7494
|
}
|
|
6155
7495
|
} catch {
|
|
6156
7496
|
}
|
|
6157
|
-
if (typeof pid === "number" &&
|
|
7497
|
+
if (typeof pid === "number" && isAlive2(pid)) {
|
|
6158
7498
|
try {
|
|
6159
7499
|
process.kill(pid, "SIGTERM");
|
|
6160
7500
|
} catch {
|
|
6161
7501
|
}
|
|
6162
|
-
const deadline = Date.now() +
|
|
6163
|
-
while (Date.now() < deadline &&
|
|
7502
|
+
const deadline = Date.now() + STOP_GRACE_MS2;
|
|
7503
|
+
while (Date.now() < deadline && isAlive2(pid)) {
|
|
6164
7504
|
await new Promise((r) => setTimeout(r, 50));
|
|
6165
7505
|
}
|
|
6166
|
-
if (
|
|
7506
|
+
if (isAlive2(pid)) {
|
|
6167
7507
|
try {
|
|
6168
7508
|
process.kill(pid, "SIGKILL");
|
|
6169
7509
|
} catch {
|
|
6170
7510
|
}
|
|
6171
7511
|
}
|
|
6172
7512
|
}
|
|
6173
|
-
await
|
|
7513
|
+
await fsp6.unlink(pidPath).catch(() => void 0);
|
|
6174
7514
|
}
|
|
6175
7515
|
}
|
|
6176
7516
|
spawn(entry, attempt) {
|
|
@@ -6179,46 +7519,49 @@ var ExtensionManager = class {
|
|
|
6179
7519
|
}
|
|
6180
7520
|
const ctx = this.context;
|
|
6181
7521
|
if (!ctx) {
|
|
6182
|
-
throw new Error("
|
|
7522
|
+
throw new Error("TransformerManager.spawn called before setContext");
|
|
6183
7523
|
}
|
|
6184
|
-
const
|
|
6185
|
-
const command =
|
|
6186
|
-
const logStream =
|
|
7524
|
+
const t = entry.config;
|
|
7525
|
+
const command = t.command.length > 0 ? t.command : [t.name];
|
|
7526
|
+
const logStream = fs12.createWriteStream(paths.transformerLogFile(t.name), {
|
|
6187
7527
|
flags: "a"
|
|
6188
7528
|
});
|
|
6189
7529
|
logStream.write(
|
|
6190
|
-
`[hydra-acp] ${(/* @__PURE__ */ new Date()).toISOString()} starting
|
|
7530
|
+
`[hydra-acp] ${(/* @__PURE__ */ new Date()).toISOString()} starting transformer ${t.name} (attempt ${attempt + 1})
|
|
6191
7531
|
`
|
|
6192
7532
|
);
|
|
7533
|
+
const processToken = this.tokenRegistry?.mint(t.name, "transformer") ?? ctx.serviceToken;
|
|
7534
|
+
entry.processToken = processToken;
|
|
7535
|
+
entry.version = void 0;
|
|
6193
7536
|
const env = {
|
|
6194
7537
|
...process.env,
|
|
6195
7538
|
HYDRA_ACP_DAEMON_URL: ctx.daemonUrl,
|
|
6196
7539
|
HYDRA_ACP_DAEMON_HOST: ctx.daemonHost,
|
|
6197
7540
|
HYDRA_ACP_DAEMON_PORT: String(ctx.daemonPort),
|
|
6198
|
-
HYDRA_ACP_TOKEN:
|
|
7541
|
+
HYDRA_ACP_TOKEN: processToken,
|
|
6199
7542
|
HYDRA_ACP_WS_URL: ctx.daemonWsUrl,
|
|
6200
7543
|
HYDRA_ACP_HOME: ctx.hydraHome,
|
|
6201
|
-
|
|
6202
|
-
...
|
|
7544
|
+
HYDRA_ACP_TRANSFORMER_NAME: t.name,
|
|
7545
|
+
...t.env
|
|
6203
7546
|
};
|
|
6204
7547
|
const [cmd, ...baseArgs] = command;
|
|
6205
7548
|
if (cmd === void 0) {
|
|
6206
|
-
logStream.write(`[hydra-acp]
|
|
7549
|
+
logStream.write(`[hydra-acp] transformer ${t.name} has empty command
|
|
6207
7550
|
`);
|
|
6208
7551
|
logStream.end();
|
|
6209
7552
|
return;
|
|
6210
7553
|
}
|
|
6211
|
-
const args = [...baseArgs, ...
|
|
7554
|
+
const args = [...baseArgs, ...t.args];
|
|
6212
7555
|
let child;
|
|
6213
7556
|
try {
|
|
6214
|
-
child =
|
|
7557
|
+
child = spawn5(cmd, args, {
|
|
6215
7558
|
env,
|
|
6216
7559
|
stdio: ["ignore", "pipe", "pipe"],
|
|
6217
7560
|
detached: false
|
|
6218
7561
|
});
|
|
6219
7562
|
} catch (err) {
|
|
6220
7563
|
logStream.write(
|
|
6221
|
-
`[hydra-acp] failed to spawn ${
|
|
7564
|
+
`[hydra-acp] failed to spawn ${t.name}: ${err.message}
|
|
6222
7565
|
`
|
|
6223
7566
|
);
|
|
6224
7567
|
logStream.end();
|
|
@@ -6233,14 +7576,14 @@ var ExtensionManager = class {
|
|
|
6233
7576
|
}
|
|
6234
7577
|
if (typeof child.pid === "number") {
|
|
6235
7578
|
try {
|
|
6236
|
-
|
|
7579
|
+
fs12.writeFileSync(paths.transformerPidFile(t.name), `${child.pid}
|
|
6237
7580
|
`, {
|
|
6238
7581
|
encoding: "utf8",
|
|
6239
7582
|
mode: 384
|
|
6240
7583
|
});
|
|
6241
7584
|
} catch (err) {
|
|
6242
7585
|
logStream.write(
|
|
6243
|
-
`[hydra-acp] failed to write pid file for ${
|
|
7586
|
+
`[hydra-acp] failed to write pid file for ${t.name}: ${err.message}
|
|
6244
7587
|
`
|
|
6245
7588
|
);
|
|
6246
7589
|
}
|
|
@@ -6252,22 +7595,26 @@ var ExtensionManager = class {
|
|
|
6252
7595
|
entry.lastExitCode = void 0;
|
|
6253
7596
|
child.on("error", (err) => {
|
|
6254
7597
|
logStream.write(
|
|
6255
|
-
`[hydra-acp]
|
|
7598
|
+
`[hydra-acp] transformer ${t.name} error: ${err.message}
|
|
6256
7599
|
`
|
|
6257
7600
|
);
|
|
6258
7601
|
});
|
|
6259
7602
|
child.on("exit", (code, signal) => {
|
|
6260
7603
|
try {
|
|
6261
|
-
|
|
7604
|
+
fs12.unlinkSync(paths.transformerPidFile(t.name));
|
|
6262
7605
|
} catch {
|
|
6263
7606
|
}
|
|
6264
7607
|
logStream.write(
|
|
6265
|
-
`[hydra-acp]
|
|
7608
|
+
`[hydra-acp] transformer ${t.name} exited code=${code ?? "null"} signal=${signal ?? "null"}
|
|
6266
7609
|
`
|
|
6267
7610
|
);
|
|
6268
7611
|
entry.child = void 0;
|
|
6269
7612
|
entry.pid = void 0;
|
|
6270
7613
|
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
7614
|
+
if (entry.processToken) {
|
|
7615
|
+
this.tokenRegistry?.revoke(t.name);
|
|
7616
|
+
entry.processToken = void 0;
|
|
7617
|
+
}
|
|
6271
7618
|
const waiters = entry.exitWaiters.splice(0);
|
|
6272
7619
|
for (const resolve3 of waiters) {
|
|
6273
7620
|
resolve3();
|
|
@@ -6289,8 +7636,8 @@ var ExtensionManager = class {
|
|
|
6289
7636
|
return;
|
|
6290
7637
|
}
|
|
6291
7638
|
const delay = Math.min(
|
|
6292
|
-
|
|
6293
|
-
|
|
7639
|
+
RESTART_BASE_MS2 * 2 ** Math.min(attempt, 10),
|
|
7640
|
+
RESTART_CAP_MS2
|
|
6294
7641
|
);
|
|
6295
7642
|
entry.restartTimer = setTimeout(() => {
|
|
6296
7643
|
entry.restartTimer = void 0;
|
|
@@ -6301,7 +7648,7 @@ var ExtensionManager = class {
|
|
|
6301
7648
|
}
|
|
6302
7649
|
}
|
|
6303
7650
|
};
|
|
6304
|
-
function
|
|
7651
|
+
function isAlive2(pid) {
|
|
6305
7652
|
try {
|
|
6306
7653
|
process.kill(pid, 0);
|
|
6307
7654
|
return true;
|
|
@@ -6309,14 +7656,14 @@ function isAlive(pid) {
|
|
|
6309
7656
|
return false;
|
|
6310
7657
|
}
|
|
6311
7658
|
}
|
|
6312
|
-
function
|
|
7659
|
+
function withCode3(err, code) {
|
|
6313
7660
|
err.code = code;
|
|
6314
7661
|
return err;
|
|
6315
7662
|
}
|
|
6316
7663
|
|
|
6317
7664
|
// src/core/agent-prune.ts
|
|
6318
|
-
import * as
|
|
6319
|
-
import * as
|
|
7665
|
+
import * as fsp7 from "fs/promises";
|
|
7666
|
+
import * as path9 from "path";
|
|
6320
7667
|
var logSink3 = (msg) => {
|
|
6321
7668
|
process.stderr.write(msg + "\n");
|
|
6322
7669
|
};
|
|
@@ -6334,10 +7681,10 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
6334
7681
|
desiredByAgent.set(a.id, a.version ?? "current");
|
|
6335
7682
|
}
|
|
6336
7683
|
const activeByAgent = sessionManager.activeAgentVersions();
|
|
6337
|
-
const platformDir =
|
|
7684
|
+
const platformDir = path9.join(paths.agentsDir(), platformKey);
|
|
6338
7685
|
let agentEntries;
|
|
6339
7686
|
try {
|
|
6340
|
-
agentEntries = await
|
|
7687
|
+
agentEntries = await fsp7.readdir(platformDir, { withFileTypes: true });
|
|
6341
7688
|
} catch (err) {
|
|
6342
7689
|
const e = err;
|
|
6343
7690
|
if (e.code === "ENOENT") {
|
|
@@ -6356,10 +7703,10 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
6356
7703
|
continue;
|
|
6357
7704
|
}
|
|
6358
7705
|
const activeVersions = activeByAgent.get(agentId) ?? /* @__PURE__ */ new Set();
|
|
6359
|
-
const agentDir =
|
|
7706
|
+
const agentDir = path9.join(platformDir, agentId);
|
|
6360
7707
|
let versionEntries;
|
|
6361
7708
|
try {
|
|
6362
|
-
versionEntries = await
|
|
7709
|
+
versionEntries = await fsp7.readdir(agentDir, { withFileTypes: true });
|
|
6363
7710
|
} catch (err) {
|
|
6364
7711
|
logSink3(
|
|
6365
7712
|
`hydra-acp: prune: failed to read ${agentDir}: ${err.message}`
|
|
@@ -6380,9 +7727,9 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
6380
7727
|
if (version.includes(".partial-")) {
|
|
6381
7728
|
continue;
|
|
6382
7729
|
}
|
|
6383
|
-
const versionDir =
|
|
7730
|
+
const versionDir = path9.join(agentDir, version);
|
|
6384
7731
|
try {
|
|
6385
|
-
await
|
|
7732
|
+
await fsp7.rm(versionDir, { recursive: true, force: true });
|
|
6386
7733
|
logSink3(`hydra-acp: pruned stale ${agentId} ${version} (${versionDir})`);
|
|
6387
7734
|
} catch (err) {
|
|
6388
7735
|
logSink3(
|
|
@@ -6394,8 +7741,8 @@ async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
|
6394
7741
|
}
|
|
6395
7742
|
|
|
6396
7743
|
// src/core/session-tokens.ts
|
|
6397
|
-
import * as
|
|
6398
|
-
import * as
|
|
7744
|
+
import * as fs13 from "fs/promises";
|
|
7745
|
+
import * as path10 from "path";
|
|
6399
7746
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
6400
7747
|
var TOKEN_PREFIX = "hydra_session_";
|
|
6401
7748
|
var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
|
|
@@ -6403,7 +7750,7 @@ var ID_LENGTH = 12;
|
|
|
6403
7750
|
var TOKEN_BYTES = 32;
|
|
6404
7751
|
var WRITE_DEBOUNCE_MS = 50;
|
|
6405
7752
|
function tokensFilePath() {
|
|
6406
|
-
return
|
|
7753
|
+
return path10.join(paths.home(), "session-tokens.json");
|
|
6407
7754
|
}
|
|
6408
7755
|
function sha256Hex(input) {
|
|
6409
7756
|
return createHash("sha256").update(input).digest("hex");
|
|
@@ -6430,7 +7777,7 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
6430
7777
|
static async load() {
|
|
6431
7778
|
let records = [];
|
|
6432
7779
|
try {
|
|
6433
|
-
const raw = await
|
|
7780
|
+
const raw = await fs13.readFile(tokensFilePath(), "utf8");
|
|
6434
7781
|
const parsed = JSON.parse(raw);
|
|
6435
7782
|
if (parsed && Array.isArray(parsed.records)) {
|
|
6436
7783
|
records = parsed.records.filter(isRecord);
|
|
@@ -6558,8 +7905,8 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
6558
7905
|
const records = Array.from(this.records.values());
|
|
6559
7906
|
const payload = JSON.stringify({ records }, null, 2) + "\n";
|
|
6560
7907
|
this.writeInflight = (async () => {
|
|
6561
|
-
await
|
|
6562
|
-
await
|
|
7908
|
+
await fs13.mkdir(paths.home(), { recursive: true });
|
|
7909
|
+
await fs13.writeFile(tokensFilePath(), payload, {
|
|
6563
7910
|
encoding: "utf8",
|
|
6564
7911
|
mode: 384
|
|
6565
7912
|
});
|
|
@@ -6655,6 +8002,34 @@ function tokenFromUpgradeRequest(req) {
|
|
|
6655
8002
|
}
|
|
6656
8003
|
return void 0;
|
|
6657
8004
|
}
|
|
8005
|
+
var ProcessTokenRegistry = class {
|
|
8006
|
+
tokens = /* @__PURE__ */ new Map();
|
|
8007
|
+
mint(name, kind) {
|
|
8008
|
+
const token = generateServiceToken();
|
|
8009
|
+
this.tokens.set(token, { name, kind });
|
|
8010
|
+
return token;
|
|
8011
|
+
}
|
|
8012
|
+
// Revoke all tokens associated with the named process. Called when a
|
|
8013
|
+
// process exits or is unregistered so stale tokens can't be reused if a
|
|
8014
|
+
// new process happens to reconnect before a full daemon restart.
|
|
8015
|
+
revoke(name) {
|
|
8016
|
+
for (const [token, identity] of this.tokens) {
|
|
8017
|
+
if (identity.name === name) {
|
|
8018
|
+
this.tokens.delete(token);
|
|
8019
|
+
}
|
|
8020
|
+
}
|
|
8021
|
+
}
|
|
8022
|
+
resolve(token) {
|
|
8023
|
+
return this.tokens.get(token);
|
|
8024
|
+
}
|
|
8025
|
+
async validate(token) {
|
|
8026
|
+
const identity = this.tokens.get(token);
|
|
8027
|
+
if (!identity) {
|
|
8028
|
+
return void 0;
|
|
8029
|
+
}
|
|
8030
|
+
return `${identity.kind}:${identity.name}`;
|
|
8031
|
+
}
|
|
8032
|
+
};
|
|
6658
8033
|
function constantTimeEqual(a, b) {
|
|
6659
8034
|
if (a.length !== b.length) {
|
|
6660
8035
|
return false;
|
|
@@ -6739,7 +8114,13 @@ var Bundle = z5.object({
|
|
|
6739
8114
|
exportedAt: z5.string(),
|
|
6740
8115
|
exportedFrom: z5.object({
|
|
6741
8116
|
hydraVersion: z5.string(),
|
|
6742
|
-
machine: z5.string()
|
|
8117
|
+
machine: z5.string(),
|
|
8118
|
+
// Externally-reachable name (and optional ":port") for the exporting
|
|
8119
|
+
// daemon, sourced from config.daemon.publicHost (or daemon.host when
|
|
8120
|
+
// non-loopback). Carried so an importer can construct a hydra:// URL
|
|
8121
|
+
// that dials back to the origin — e.g. over Tailscale. Omitted when
|
|
8122
|
+
// the exporter has no routable address; never falls back to loopback.
|
|
8123
|
+
hydraHost: z5.string().optional()
|
|
6743
8124
|
}),
|
|
6744
8125
|
session: BundleSession,
|
|
6745
8126
|
history: z5.array(HistoryEntrySchema),
|
|
@@ -6751,7 +8132,8 @@ function encodeBundle(params) {
|
|
|
6751
8132
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6752
8133
|
exportedFrom: {
|
|
6753
8134
|
hydraVersion: params.hydraVersion,
|
|
6754
|
-
machine: params.machine
|
|
8135
|
+
machine: params.machine,
|
|
8136
|
+
...params.hydraHost !== void 0 && params.hydraHost.length > 0 ? { hydraHost: params.hydraHost } : {}
|
|
6755
8137
|
},
|
|
6756
8138
|
session: {
|
|
6757
8139
|
sessionId: params.record.sessionId,
|
|
@@ -6965,12 +8347,42 @@ function mapPromptReceived(u) {
|
|
|
6965
8347
|
}
|
|
6966
8348
|
return { kind: "user-text", text: promptText };
|
|
6967
8349
|
}
|
|
8350
|
+
function isExitPlanModeTool(name) {
|
|
8351
|
+
if (!name) {
|
|
8352
|
+
return false;
|
|
8353
|
+
}
|
|
8354
|
+
const normalised = name.toLowerCase().replace(/[_\s-]/g, "");
|
|
8355
|
+
return normalised === "exitplanmode";
|
|
8356
|
+
}
|
|
8357
|
+
function readExitPlanMarkdown(u) {
|
|
8358
|
+
const rawInput = u.rawInput;
|
|
8359
|
+
if (!rawInput || typeof rawInput !== "object" || Array.isArray(rawInput)) {
|
|
8360
|
+
return null;
|
|
8361
|
+
}
|
|
8362
|
+
const plan = rawInput.plan;
|
|
8363
|
+
if (typeof plan !== "string" || plan.length === 0) {
|
|
8364
|
+
return null;
|
|
8365
|
+
}
|
|
8366
|
+
return sanitizeWireText(plan);
|
|
8367
|
+
}
|
|
6968
8368
|
function mapToolCall(u) {
|
|
6969
8369
|
const toolCallId = readString(u, "toolCallId") ?? readString(u, "id");
|
|
6970
8370
|
if (!toolCallId) {
|
|
6971
8371
|
return null;
|
|
6972
8372
|
}
|
|
6973
8373
|
const rawTitle = readString(u, "title") ?? readString(u, "name") ?? readString(u, "label") ?? "tool call";
|
|
8374
|
+
const toolName = readString(u, "name") ?? readString(u, "title");
|
|
8375
|
+
if (isExitPlanModeTool(toolName)) {
|
|
8376
|
+
const plan = readExitPlanMarkdown(u);
|
|
8377
|
+
if (plan !== null) {
|
|
8378
|
+
const status2 = readString(u, "status");
|
|
8379
|
+
const event2 = { kind: "exit-plan-mode", toolCallId, plan };
|
|
8380
|
+
if (status2 !== void 0) {
|
|
8381
|
+
event2.status = status2;
|
|
8382
|
+
}
|
|
8383
|
+
return event2;
|
|
8384
|
+
}
|
|
8385
|
+
}
|
|
6974
8386
|
const title = sanitizeSingleLine(rawTitle);
|
|
6975
8387
|
const status = readString(u, "status");
|
|
6976
8388
|
const rawKind = readString(u, "kind");
|
|
@@ -6995,6 +8407,18 @@ function mapToolCallUpdate(u) {
|
|
|
6995
8407
|
if (!meaningful) {
|
|
6996
8408
|
return null;
|
|
6997
8409
|
}
|
|
8410
|
+
const toolName = readString(u, "name") ?? rawTitle;
|
|
8411
|
+
if (isExitPlanModeTool(toolName)) {
|
|
8412
|
+
const event2 = { kind: "exit-plan-mode", toolCallId };
|
|
8413
|
+
const plan = readExitPlanMarkdown(u);
|
|
8414
|
+
if (plan !== null) {
|
|
8415
|
+
event2.plan = plan;
|
|
8416
|
+
}
|
|
8417
|
+
if (status !== void 0) {
|
|
8418
|
+
event2.status = status;
|
|
8419
|
+
}
|
|
8420
|
+
return event2;
|
|
8421
|
+
}
|
|
6998
8422
|
const event = { kind: "tool-call-update", toolCallId };
|
|
6999
8423
|
if (title !== void 0) {
|
|
7000
8424
|
event.title = title;
|
|
@@ -7053,7 +8477,7 @@ function isUpstreamInterrupted(u, errorText) {
|
|
|
7053
8477
|
}
|
|
7054
8478
|
function mapPlan(u) {
|
|
7055
8479
|
const entries = u.entries;
|
|
7056
|
-
if (!Array.isArray(entries)) {
|
|
8480
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
7057
8481
|
return null;
|
|
7058
8482
|
}
|
|
7059
8483
|
const normalized = [];
|
|
@@ -7389,7 +8813,21 @@ function formatNumber(n) {
|
|
|
7389
8813
|
return n.toLocaleString("en-US");
|
|
7390
8814
|
}
|
|
7391
8815
|
|
|
8816
|
+
// src/core/remote-url.ts
|
|
8817
|
+
function isLoopbackHost(host) {
|
|
8818
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost" || host === "[::1]";
|
|
8819
|
+
}
|
|
8820
|
+
|
|
7392
8821
|
// src/daemon/routes/sessions.ts
|
|
8822
|
+
function resolveHydraHost(defaults) {
|
|
8823
|
+
if (defaults.publicHost && defaults.publicHost.length > 0) {
|
|
8824
|
+
return defaults.publicHost;
|
|
8825
|
+
}
|
|
8826
|
+
if (defaults.host && !isLoopbackHost(defaults.host)) {
|
|
8827
|
+
return defaults.port !== void 0 ? `${defaults.host}:${defaults.port}` : defaults.host;
|
|
8828
|
+
}
|
|
8829
|
+
return void 0;
|
|
8830
|
+
}
|
|
7393
8831
|
function registerSessionRoutes(app, manager, defaults) {
|
|
7394
8832
|
app.get("/v1/sessions", async (request) => {
|
|
7395
8833
|
const query = request.query;
|
|
@@ -7489,7 +8927,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7489
8927
|
history: exported.history,
|
|
7490
8928
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
7491
8929
|
hydraVersion: HYDRA_VERSION,
|
|
7492
|
-
machine: os3.hostname()
|
|
8930
|
+
machine: os3.hostname(),
|
|
8931
|
+
hydraHost: resolveHydraHost(defaults)
|
|
7493
8932
|
});
|
|
7494
8933
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7495
8934
|
reply.header(
|
|
@@ -7511,7 +8950,8 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7511
8950
|
history: exported.history,
|
|
7512
8951
|
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
7513
8952
|
hydraVersion: HYDRA_VERSION,
|
|
7514
|
-
machine: os3.hostname()
|
|
8953
|
+
machine: os3.hostname(),
|
|
8954
|
+
hydraHost: resolveHydraHost(defaults)
|
|
7515
8955
|
});
|
|
7516
8956
|
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
7517
8957
|
reply.code(200).send(bundleToMarkdown(bundle));
|
|
@@ -7824,6 +9264,122 @@ function parseRegisterBody(body) {
|
|
|
7824
9264
|
};
|
|
7825
9265
|
}
|
|
7826
9266
|
|
|
9267
|
+
// src/daemon/routes/transformers.ts
|
|
9268
|
+
var NAME_RE2 = /^[A-Za-z0-9._-]+$/;
|
|
9269
|
+
function registerTransformerRoutes(app, transformers) {
|
|
9270
|
+
app.get("/v1/transformers", async () => {
|
|
9271
|
+
return { transformers: transformers.list() };
|
|
9272
|
+
});
|
|
9273
|
+
app.get("/v1/transformers/:name", async (request, reply) => {
|
|
9274
|
+
const name = request.params.name;
|
|
9275
|
+
const info = transformers.get(name);
|
|
9276
|
+
if (!info) {
|
|
9277
|
+
reply.code(404).send({ error: `unknown transformer: ${name}` });
|
|
9278
|
+
return;
|
|
9279
|
+
}
|
|
9280
|
+
return info;
|
|
9281
|
+
});
|
|
9282
|
+
app.post("/v1/transformers", async (request, reply) => {
|
|
9283
|
+
const body = request.body ?? {};
|
|
9284
|
+
const parsed = parseRegisterBody2(body);
|
|
9285
|
+
if ("error" in parsed) {
|
|
9286
|
+
reply.code(400).send({ error: parsed.error });
|
|
9287
|
+
return;
|
|
9288
|
+
}
|
|
9289
|
+
try {
|
|
9290
|
+
const info = transformers.register(parsed.config);
|
|
9291
|
+
reply.code(201).send(info);
|
|
9292
|
+
} catch (err) {
|
|
9293
|
+
sendError2(reply, err);
|
|
9294
|
+
}
|
|
9295
|
+
});
|
|
9296
|
+
app.delete("/v1/transformers/:name", async (request, reply) => {
|
|
9297
|
+
const name = request.params.name;
|
|
9298
|
+
try {
|
|
9299
|
+
await transformers.unregister(name);
|
|
9300
|
+
reply.code(204).send();
|
|
9301
|
+
} catch (err) {
|
|
9302
|
+
sendError2(reply, err);
|
|
9303
|
+
}
|
|
9304
|
+
});
|
|
9305
|
+
app.post("/v1/transformers/:name/start", async (request, reply) => {
|
|
9306
|
+
const name = request.params.name;
|
|
9307
|
+
try {
|
|
9308
|
+
const info = await transformers.startByName(name);
|
|
9309
|
+
reply.code(200).send(info);
|
|
9310
|
+
} catch (err) {
|
|
9311
|
+
sendError2(reply, err);
|
|
9312
|
+
}
|
|
9313
|
+
});
|
|
9314
|
+
app.post("/v1/transformers/:name/stop", async (request, reply) => {
|
|
9315
|
+
const name = request.params.name;
|
|
9316
|
+
try {
|
|
9317
|
+
const info = await transformers.stopByName(name);
|
|
9318
|
+
reply.code(200).send(info);
|
|
9319
|
+
} catch (err) {
|
|
9320
|
+
sendError2(reply, err);
|
|
9321
|
+
}
|
|
9322
|
+
});
|
|
9323
|
+
app.post("/v1/transformers/:name/restart", async (request, reply) => {
|
|
9324
|
+
const name = request.params.name;
|
|
9325
|
+
try {
|
|
9326
|
+
const info = await transformers.restartByName(name);
|
|
9327
|
+
reply.code(200).send(info);
|
|
9328
|
+
} catch (err) {
|
|
9329
|
+
sendError2(reply, err);
|
|
9330
|
+
}
|
|
9331
|
+
});
|
|
9332
|
+
}
|
|
9333
|
+
function sendError2(reply, err) {
|
|
9334
|
+
const code = err.code;
|
|
9335
|
+
const message = err.message ?? "unknown error";
|
|
9336
|
+
if (code === "NOT_FOUND") {
|
|
9337
|
+
reply.code(404).send({ error: message });
|
|
9338
|
+
return;
|
|
9339
|
+
}
|
|
9340
|
+
if (code === "CONFLICT") {
|
|
9341
|
+
reply.code(409).send({ error: message });
|
|
9342
|
+
return;
|
|
9343
|
+
}
|
|
9344
|
+
reply.code(500).send({ error: message });
|
|
9345
|
+
}
|
|
9346
|
+
function parseRegisterBody2(body) {
|
|
9347
|
+
const name = body.name;
|
|
9348
|
+
if (typeof name !== "string" || !NAME_RE2.test(name)) {
|
|
9349
|
+
return { error: "name must match [A-Za-z0-9._-]+" };
|
|
9350
|
+
}
|
|
9351
|
+
const command = body.command;
|
|
9352
|
+
if (command !== void 0 && (!Array.isArray(command) || command.some((c) => typeof c !== "string"))) {
|
|
9353
|
+
return { error: "command must be string[]" };
|
|
9354
|
+
}
|
|
9355
|
+
const args = body.args;
|
|
9356
|
+
if (args !== void 0 && (!Array.isArray(args) || args.some((a) => typeof a !== "string"))) {
|
|
9357
|
+
return { error: "args must be string[]" };
|
|
9358
|
+
}
|
|
9359
|
+
const env = body.env;
|
|
9360
|
+
if (env !== void 0 && (typeof env !== "object" || env === null || Array.isArray(env))) {
|
|
9361
|
+
return { error: "env must be an object of string\u2192string" };
|
|
9362
|
+
}
|
|
9363
|
+
if (env && Object.values(env).some(
|
|
9364
|
+
(v) => typeof v !== "string"
|
|
9365
|
+
)) {
|
|
9366
|
+
return { error: "env values must be strings" };
|
|
9367
|
+
}
|
|
9368
|
+
const enabled = body.enabled;
|
|
9369
|
+
if (enabled !== void 0 && typeof enabled !== "boolean") {
|
|
9370
|
+
return { error: "enabled must be a boolean" };
|
|
9371
|
+
}
|
|
9372
|
+
return {
|
|
9373
|
+
config: {
|
|
9374
|
+
name,
|
|
9375
|
+
command: command ?? [],
|
|
9376
|
+
args: args ?? [],
|
|
9377
|
+
env: env ?? {},
|
|
9378
|
+
enabled: enabled === void 0 ? true : enabled
|
|
9379
|
+
}
|
|
9380
|
+
};
|
|
9381
|
+
}
|
|
9382
|
+
|
|
7827
9383
|
// src/daemon/routes/config.ts
|
|
7828
9384
|
function registerConfigRoutes(app, defaults) {
|
|
7829
9385
|
app.get("/v1/config", async () => {
|
|
@@ -7838,19 +9394,19 @@ function registerConfigRoutes(app, defaults) {
|
|
|
7838
9394
|
import { z as z6 } from "zod";
|
|
7839
9395
|
|
|
7840
9396
|
// src/core/password.ts
|
|
7841
|
-
import * as
|
|
7842
|
-
import * as
|
|
9397
|
+
import * as fs14 from "fs/promises";
|
|
9398
|
+
import * as path11 from "path";
|
|
7843
9399
|
import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
7844
9400
|
import { promisify } from "util";
|
|
7845
9401
|
var scryptAsync = promisify(scrypt);
|
|
7846
9402
|
function passwordHashPath() {
|
|
7847
|
-
return
|
|
9403
|
+
return path11.join(paths.home(), "password-hash");
|
|
7848
9404
|
}
|
|
7849
9405
|
var DEFAULT_N = 1 << 15;
|
|
7850
9406
|
var MAX_MEM = 128 * 1024 * 1024;
|
|
7851
9407
|
async function hasPassword() {
|
|
7852
9408
|
try {
|
|
7853
|
-
const text = await
|
|
9409
|
+
const text = await fs14.readFile(passwordHashPath(), "utf8");
|
|
7854
9410
|
return text.trim().length > 0;
|
|
7855
9411
|
} catch (err) {
|
|
7856
9412
|
const e = err;
|
|
@@ -7866,7 +9422,7 @@ async function verifyPassword(plaintext) {
|
|
|
7866
9422
|
}
|
|
7867
9423
|
let line;
|
|
7868
9424
|
try {
|
|
7869
|
-
line = (await
|
|
9425
|
+
line = (await fs14.readFile(passwordHashPath(), "utf8")).trim();
|
|
7870
9426
|
} catch (err) {
|
|
7871
9427
|
const e = err;
|
|
7872
9428
|
if (e.code === "ENOENT") {
|
|
@@ -8060,6 +9616,8 @@ function wsToMessageStream(ws) {
|
|
|
8060
9616
|
}
|
|
8061
9617
|
|
|
8062
9618
|
// src/daemon/acp-ws.ts
|
|
9619
|
+
import * as os4 from "os";
|
|
9620
|
+
import * as path12 from "path";
|
|
8063
9621
|
function registerAcpWsEndpoint(app, deps) {
|
|
8064
9622
|
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
8065
9623
|
const token = tokenFromUpgradeRequest({
|
|
@@ -8070,10 +9628,12 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8070
9628
|
socket.close(4401, "Unauthorized");
|
|
8071
9629
|
return;
|
|
8072
9630
|
}
|
|
9631
|
+
const processIdentity = deps.processRegistry?.resolve(token);
|
|
8073
9632
|
const stream = wsToMessageStream(socket);
|
|
8074
9633
|
const connection = new JsonRpcConnection(stream);
|
|
8075
9634
|
const state = {
|
|
8076
9635
|
clientId: `hydra_client_${nanoid2(12)}`,
|
|
9636
|
+
processIdentity,
|
|
8077
9637
|
attached: /* @__PURE__ */ new Map()
|
|
8078
9638
|
};
|
|
8079
9639
|
connection.onClose(() => {
|
|
@@ -8094,14 +9654,150 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8094
9654
|
}
|
|
8095
9655
|
};
|
|
8096
9656
|
connection.onRequest("initialize", async (raw) => {
|
|
8097
|
-
InitializeParams.parse(raw ?? {});
|
|
9657
|
+
const params = InitializeParams.parse(raw ?? {});
|
|
9658
|
+
const version = params.clientInfo?.version;
|
|
9659
|
+
if (version && processIdentity) {
|
|
9660
|
+
if (processIdentity.kind === "extension") {
|
|
9661
|
+
deps.onExtensionVersion?.(processIdentity.name, version);
|
|
9662
|
+
} else {
|
|
9663
|
+
deps.onTransformerVersion?.(processIdentity.name, version);
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
8098
9666
|
return buildInitializeResult();
|
|
8099
9667
|
});
|
|
9668
|
+
if (processIdentity?.kind === "transformer") {
|
|
9669
|
+
connection.onRequest("transformer/initialize", async (raw) => {
|
|
9670
|
+
const params = raw ?? {};
|
|
9671
|
+
const intercepts = Array.isArray(params.intercepts) ? params.intercepts.filter(
|
|
9672
|
+
(v) => typeof v === "string"
|
|
9673
|
+
) : [];
|
|
9674
|
+
if (deps.transformers) {
|
|
9675
|
+
deps.transformers.registerConnection(
|
|
9676
|
+
processIdentity.name,
|
|
9677
|
+
connection,
|
|
9678
|
+
intercepts
|
|
9679
|
+
);
|
|
9680
|
+
}
|
|
9681
|
+
return { ack: true };
|
|
9682
|
+
});
|
|
9683
|
+
connection.onClose(() => {
|
|
9684
|
+
deps.transformers?.deregisterConnection(processIdentity.name);
|
|
9685
|
+
});
|
|
9686
|
+
connection.onRequest("hydra-acp/emit_message", async (raw) => {
|
|
9687
|
+
const params = raw ?? {};
|
|
9688
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : void 0;
|
|
9689
|
+
const method = typeof params.method === "string" ? params.method : void 0;
|
|
9690
|
+
const envelope = params.envelope;
|
|
9691
|
+
const route = params.route;
|
|
9692
|
+
if (!sessionId || !method) {
|
|
9693
|
+
throw Object.assign(new Error("emit_message requires sessionId and method"), { code: -32602 });
|
|
9694
|
+
}
|
|
9695
|
+
const session = deps.manager.get(sessionId);
|
|
9696
|
+
if (!session) {
|
|
9697
|
+
throw Object.assign(new Error(`session ${sessionId} not found`), { code: JsonRpcErrorCodes.SessionNotFound });
|
|
9698
|
+
}
|
|
9699
|
+
const respondsTo = typeof params.respondsTo === "string" ? params.respondsTo : void 0;
|
|
9700
|
+
if (respondsTo) {
|
|
9701
|
+
session.dischargeClaim(respondsTo, envelope);
|
|
9702
|
+
return { ok: true };
|
|
9703
|
+
}
|
|
9704
|
+
if (route === "chain") {
|
|
9705
|
+
await session.emitToChain(processIdentity.name, method, envelope);
|
|
9706
|
+
return { ok: true };
|
|
9707
|
+
}
|
|
9708
|
+
if (route === "daemon") {
|
|
9709
|
+
await session.emitToChain(processIdentity.name, method, envelope);
|
|
9710
|
+
return { ok: true };
|
|
9711
|
+
}
|
|
9712
|
+
throw Object.assign(new Error(`unsupported route: ${JSON.stringify(route)}`), { code: -32602 });
|
|
9713
|
+
});
|
|
9714
|
+
connection.onRequest("hydra-acp/spawn_child_session", async (raw) => {
|
|
9715
|
+
const params = raw ?? {};
|
|
9716
|
+
const agentId = typeof params.agentId === "string" ? params.agentId : deps.defaultAgent;
|
|
9717
|
+
const cwd = typeof params.cwd === "string" ? params.cwd : void 0;
|
|
9718
|
+
const parentSessionId = typeof params.parentSessionId === "string" ? params.parentSessionId : void 0;
|
|
9719
|
+
if (!cwd) {
|
|
9720
|
+
throw Object.assign(new Error("spawn_child_session requires cwd"), { code: -32602 });
|
|
9721
|
+
}
|
|
9722
|
+
const child = await deps.manager.create({
|
|
9723
|
+
agentId,
|
|
9724
|
+
cwd,
|
|
9725
|
+
parentSessionId,
|
|
9726
|
+
transformChain: []
|
|
9727
|
+
// children start with no chain by default
|
|
9728
|
+
});
|
|
9729
|
+
return { childSessionId: child.sessionId };
|
|
9730
|
+
});
|
|
9731
|
+
connection.onRequest("hydra-acp/await_child", async (raw) => {
|
|
9732
|
+
const params = raw ?? {};
|
|
9733
|
+
const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
|
|
9734
|
+
const until = params.until === "idle" ? "idle" : "turn_complete";
|
|
9735
|
+
const timeoutMs = typeof params.timeoutMs === "number" ? Math.min(params.timeoutMs, 30 * 6e4) : 5 * 6e4;
|
|
9736
|
+
if (!childSessionId) {
|
|
9737
|
+
throw Object.assign(new Error("await_child requires childSessionId"), { code: -32602 });
|
|
9738
|
+
}
|
|
9739
|
+
const child = deps.manager.get(childSessionId);
|
|
9740
|
+
if (!child) {
|
|
9741
|
+
throw Object.assign(
|
|
9742
|
+
new Error(`child session ${childSessionId} not found`),
|
|
9743
|
+
{ code: JsonRpcErrorCodes.SessionNotFound }
|
|
9744
|
+
);
|
|
9745
|
+
}
|
|
9746
|
+
return new Promise((resolve3) => {
|
|
9747
|
+
const entries = [];
|
|
9748
|
+
let unsubscribe;
|
|
9749
|
+
const finish = () => {
|
|
9750
|
+
clearTimeout(timer);
|
|
9751
|
+
unsubscribe?.();
|
|
9752
|
+
resolve3({ entries });
|
|
9753
|
+
};
|
|
9754
|
+
unsubscribe = child.onBroadcast((entry) => {
|
|
9755
|
+
entries.push(entry);
|
|
9756
|
+
if (until === "turn_complete") {
|
|
9757
|
+
const upd = entry.params?.update;
|
|
9758
|
+
if (upd?.sessionUpdate === "turn_complete") {
|
|
9759
|
+
finish();
|
|
9760
|
+
}
|
|
9761
|
+
}
|
|
9762
|
+
});
|
|
9763
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
9764
|
+
if (typeof timer.unref === "function") {
|
|
9765
|
+
timer.unref();
|
|
9766
|
+
}
|
|
9767
|
+
child.onClose(() => finish());
|
|
9768
|
+
});
|
|
9769
|
+
});
|
|
9770
|
+
connection.onRequest("hydra-acp/close_child_session", async (raw) => {
|
|
9771
|
+
const params = raw ?? {};
|
|
9772
|
+
const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
|
|
9773
|
+
if (!childSessionId) {
|
|
9774
|
+
throw Object.assign(new Error("close_child_session requires childSessionId"), { code: -32602 });
|
|
9775
|
+
}
|
|
9776
|
+
const child = deps.manager.get(childSessionId);
|
|
9777
|
+
if (child) {
|
|
9778
|
+
await child.close({ deleteRecord: false });
|
|
9779
|
+
}
|
|
9780
|
+
return { ok: true };
|
|
9781
|
+
});
|
|
9782
|
+
connection.onRequest("hydra-acp/keep_alive", async (raw) => {
|
|
9783
|
+
const params = raw ?? {};
|
|
9784
|
+
const token2 = typeof params.token === "string" ? params.token : void 0;
|
|
9785
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : void 0;
|
|
9786
|
+
const estimatedRemainingMs = typeof params.estimatedRemainingMs === "number" ? params.estimatedRemainingMs : void 0;
|
|
9787
|
+
if (token2 && sessionId) {
|
|
9788
|
+
const session = deps.manager.get(sessionId);
|
|
9789
|
+
session?.keepAliveClaim(token2, estimatedRemainingMs);
|
|
9790
|
+
}
|
|
9791
|
+
return { ok: true };
|
|
9792
|
+
});
|
|
9793
|
+
}
|
|
8100
9794
|
connection.onRequest("session/new", async (raw) => {
|
|
8101
9795
|
const params = SessionNewParams.parse(raw);
|
|
8102
9796
|
const hydraMeta = extractHydraMeta(
|
|
8103
9797
|
raw?._meta
|
|
8104
9798
|
);
|
|
9799
|
+
const transformerNames = Array.isArray(hydraMeta.transformers) && hydraMeta.transformers.every((t) => typeof t === "string") ? hydraMeta.transformers : deps.manager.defaultTransformers ?? [];
|
|
9800
|
+
const transformChain = deps.transformers?.resolveChain(transformerNames) ?? [];
|
|
8105
9801
|
const session = await deps.manager.create({
|
|
8106
9802
|
cwd: params.cwd,
|
|
8107
9803
|
agentId: params.agentId ?? deps.defaultAgent,
|
|
@@ -8109,7 +9805,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8109
9805
|
title: hydraMeta.name,
|
|
8110
9806
|
agentArgs: hydraMeta.agentArgs,
|
|
8111
9807
|
model: hydraMeta.model,
|
|
8112
|
-
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
9808
|
+
onInstallProgress: makeInstallProgressForwarder(connection),
|
|
9809
|
+
transformChain
|
|
8113
9810
|
});
|
|
8114
9811
|
const client = bindClientToSession(connection, session, state);
|
|
8115
9812
|
const { entries: replay } = await session.attach(client, "full");
|
|
@@ -8141,6 +9838,14 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8141
9838
|
});
|
|
8142
9839
|
connection.onRequest("session/attach", async (raw) => {
|
|
8143
9840
|
const params = SessionAttachParams.parse(raw);
|
|
9841
|
+
const attachVersion = params.clientInfo?.version;
|
|
9842
|
+
if (attachVersion && processIdentity) {
|
|
9843
|
+
if (processIdentity.kind === "extension") {
|
|
9844
|
+
deps.onExtensionVersion?.(processIdentity.name, attachVersion);
|
|
9845
|
+
} else {
|
|
9846
|
+
deps.onTransformerVersion?.(processIdentity.name, attachVersion);
|
|
9847
|
+
}
|
|
9848
|
+
}
|
|
8144
9849
|
const hydraHints = extractHydraMeta(params._meta).resume;
|
|
8145
9850
|
const readonly = params.readonly === true;
|
|
8146
9851
|
app.log.info(
|
|
@@ -8386,6 +10091,51 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8386
10091
|
}
|
|
8387
10092
|
return session.amendPrompt(att.clientId, params);
|
|
8388
10093
|
});
|
|
10094
|
+
connection.onRequest("hydra-acp/stream_open", async (raw) => {
|
|
10095
|
+
const params = StreamOpenParams.parse(raw);
|
|
10096
|
+
denyIfReadonly(params.sessionId, "hydra-acp/stream_open");
|
|
10097
|
+
const session = deps.manager.get(params.sessionId);
|
|
10098
|
+
if (!session) {
|
|
10099
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
10100
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
10101
|
+
throw err;
|
|
10102
|
+
}
|
|
10103
|
+
const openOpts = {};
|
|
10104
|
+
if (params.mode !== void 0) {
|
|
10105
|
+
openOpts.mode = params.mode;
|
|
10106
|
+
}
|
|
10107
|
+
if (params.capacityBytes !== void 0) {
|
|
10108
|
+
openOpts.capacityBytes = params.capacityBytes;
|
|
10109
|
+
}
|
|
10110
|
+
if (params.fileCapBytes !== void 0) {
|
|
10111
|
+
openOpts.fileCapBytes = params.fileCapBytes;
|
|
10112
|
+
}
|
|
10113
|
+
if ((params.mode ?? "memory") === "file") {
|
|
10114
|
+
openOpts.filePathFor = (sid) => path12.join(os4.tmpdir(), `hydra-stdin-${sid}.log`);
|
|
10115
|
+
}
|
|
10116
|
+
return session.openStream(openOpts);
|
|
10117
|
+
});
|
|
10118
|
+
connection.onRequest("hydra-acp/stream_write", async (raw) => {
|
|
10119
|
+
const params = StreamWriteParams.parse(raw);
|
|
10120
|
+
denyIfReadonly(params.sessionId, "hydra-acp/stream_write");
|
|
10121
|
+
const session = deps.manager.get(params.sessionId);
|
|
10122
|
+
if (!session) {
|
|
10123
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
10124
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
10125
|
+
throw err;
|
|
10126
|
+
}
|
|
10127
|
+
return session.streamWrite(params.chunk, params.eof);
|
|
10128
|
+
});
|
|
10129
|
+
connection.onRequest("hydra-acp/stream_read", async (raw) => {
|
|
10130
|
+
const params = StreamReadParams.parse(raw);
|
|
10131
|
+
const session = deps.manager.get(params.sessionId);
|
|
10132
|
+
if (!session) {
|
|
10133
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
10134
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
10135
|
+
throw err;
|
|
10136
|
+
}
|
|
10137
|
+
return session.streamRead(params.cursor, params.maxBytes, params.waitMs);
|
|
10138
|
+
});
|
|
8389
10139
|
connection.onRequest("session/load", async (raw) => {
|
|
8390
10140
|
const rawObj = raw ?? {};
|
|
8391
10141
|
const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
|
|
@@ -8675,6 +10425,9 @@ function buildResponseMeta(session) {
|
|
|
8675
10425
|
if (session.turnStartedAt !== void 0) {
|
|
8676
10426
|
ours.turnStartedAt = session.turnStartedAt;
|
|
8677
10427
|
}
|
|
10428
|
+
if (session.agentCapabilities !== void 0) {
|
|
10429
|
+
ours.agentCapabilities = session.agentCapabilities;
|
|
10430
|
+
}
|
|
8678
10431
|
const queue = session.queueSnapshot();
|
|
8679
10432
|
if (queue.length > 0) {
|
|
8680
10433
|
ours.queue = queue;
|
|
@@ -8739,10 +10492,10 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
|
|
|
8739
10492
|
async function startDaemon(config, serviceToken) {
|
|
8740
10493
|
ensureLoopbackOrTls(config);
|
|
8741
10494
|
const httpsOptions = config.daemon.tls ? {
|
|
8742
|
-
key: await
|
|
8743
|
-
cert: await
|
|
10495
|
+
key: await fsp8.readFile(config.daemon.tls.key),
|
|
10496
|
+
cert: await fsp8.readFile(config.daemon.tls.cert)
|
|
8744
10497
|
} : void 0;
|
|
8745
|
-
await
|
|
10498
|
+
await fsp8.mkdir(paths.home(), { recursive: true });
|
|
8746
10499
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
8747
10500
|
config.daemon.logLevel
|
|
8748
10501
|
);
|
|
@@ -8767,9 +10520,11 @@ async function startDaemon(config, serviceToken) {
|
|
|
8767
10520
|
});
|
|
8768
10521
|
const sessionTokenStore = await SessionTokenStore.load();
|
|
8769
10522
|
const authRateLimiter = new AuthRateLimiter();
|
|
10523
|
+
const processRegistry = new ProcessTokenRegistry();
|
|
8770
10524
|
const validator = new CompositeTokenValidator([
|
|
8771
10525
|
new StaticTokenValidator(serviceToken),
|
|
8772
|
-
new SessionTokenValidator(sessionTokenStore)
|
|
10526
|
+
new SessionTokenValidator(sessionTokenStore),
|
|
10527
|
+
processRegistry
|
|
8773
10528
|
]);
|
|
8774
10529
|
const auth = bearerAuth({ validator });
|
|
8775
10530
|
app.addHook("onRequest", async (request, reply) => {
|
|
@@ -8806,18 +10561,28 @@ async function startDaemon(config, serviceToken) {
|
|
|
8806
10561
|
const manager = new SessionManager(registry, spawner, void 0, {
|
|
8807
10562
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
|
|
8808
10563
|
defaultModels: config.defaultModels,
|
|
10564
|
+
defaultTransformers: config.defaultTransformers,
|
|
8809
10565
|
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
|
|
8810
10566
|
logger: agentLogger,
|
|
8811
10567
|
npmRegistry: config.npmRegistry
|
|
8812
10568
|
});
|
|
8813
|
-
const extensions = new ExtensionManager(extensionList(config)
|
|
10569
|
+
const extensions = new ExtensionManager(extensionList(config), void 0, {
|
|
10570
|
+
tokenRegistry: processRegistry
|
|
10571
|
+
});
|
|
10572
|
+
const transformers = new TransformerManager(transformerList(config), void 0, {
|
|
10573
|
+
tokenRegistry: processRegistry
|
|
10574
|
+
});
|
|
8814
10575
|
registerHealthRoutes(app, HYDRA_VERSION);
|
|
8815
10576
|
registerSessionRoutes(app, manager, {
|
|
8816
10577
|
agentId: config.defaultAgent,
|
|
8817
|
-
cwd: config.defaultCwd
|
|
10578
|
+
cwd: config.defaultCwd,
|
|
10579
|
+
publicHost: config.daemon.publicHost,
|
|
10580
|
+
host: config.daemon.host,
|
|
10581
|
+
port: config.daemon.port
|
|
8818
10582
|
});
|
|
8819
10583
|
registerAgentRoutes(app, registry, manager, { npmRegistry: config.npmRegistry });
|
|
8820
10584
|
registerExtensionRoutes(app, extensions);
|
|
10585
|
+
registerTransformerRoutes(app, transformers);
|
|
8821
10586
|
registerConfigRoutes(app, {
|
|
8822
10587
|
defaultAgent: config.defaultAgent,
|
|
8823
10588
|
defaultCwd: config.defaultCwd
|
|
@@ -8829,13 +10594,17 @@ async function startDaemon(config, serviceToken) {
|
|
|
8829
10594
|
registerAcpWsEndpoint(app, {
|
|
8830
10595
|
validator,
|
|
8831
10596
|
manager,
|
|
8832
|
-
defaultAgent: config.defaultAgent
|
|
10597
|
+
defaultAgent: config.defaultAgent,
|
|
10598
|
+
processRegistry,
|
|
10599
|
+
onExtensionVersion: (name, version) => extensions.reportVersion(name, version),
|
|
10600
|
+
onTransformerVersion: (name, version) => transformers.reportVersion(name, version),
|
|
10601
|
+
transformers
|
|
8833
10602
|
});
|
|
8834
10603
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
8835
10604
|
const address = app.server.address();
|
|
8836
10605
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
8837
|
-
await
|
|
8838
|
-
await
|
|
10606
|
+
await fsp8.mkdir(paths.home(), { recursive: true });
|
|
10607
|
+
await fsp8.writeFile(
|
|
8839
10608
|
paths.pidFile(),
|
|
8840
10609
|
JSON.stringify({
|
|
8841
10610
|
pid: process.pid,
|
|
@@ -8847,15 +10616,18 @@ async function startDaemon(config, serviceToken) {
|
|
|
8847
10616
|
);
|
|
8848
10617
|
const scheme = config.daemon.tls ? "https" : "http";
|
|
8849
10618
|
const wsScheme = config.daemon.tls ? "wss" : "ws";
|
|
8850
|
-
|
|
10619
|
+
const processContext = {
|
|
8851
10620
|
daemonUrl: `${scheme}://${config.daemon.host}:${boundPort}`,
|
|
8852
10621
|
daemonHost: config.daemon.host,
|
|
8853
10622
|
daemonPort: boundPort,
|
|
8854
10623
|
serviceToken,
|
|
8855
10624
|
daemonWsUrl: `${wsScheme}://${config.daemon.host}:${boundPort}/acp`,
|
|
8856
10625
|
hydraHome: paths.home()
|
|
8857
|
-
}
|
|
10626
|
+
};
|
|
10627
|
+
extensions.setContext(processContext);
|
|
10628
|
+
transformers.setContext(processContext);
|
|
8858
10629
|
await extensions.start();
|
|
10630
|
+
await transformers.start();
|
|
8859
10631
|
void manager.resurrectPendingQueues().catch((err) => {
|
|
8860
10632
|
app.log.warn(
|
|
8861
10633
|
`queue replay scan failed: ${err.message}`
|
|
@@ -8865,6 +10637,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
8865
10637
|
clearInterval(sweepInterval);
|
|
8866
10638
|
await sessionTokenStore.flush();
|
|
8867
10639
|
await extensions.stop();
|
|
10640
|
+
await transformers.stop();
|
|
8868
10641
|
await manager.closeAll();
|
|
8869
10642
|
await manager.flushMetaWrites();
|
|
8870
10643
|
setBinaryInstallLogger(null);
|
|
@@ -8872,7 +10645,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
8872
10645
|
setAgentPruneLogger(null);
|
|
8873
10646
|
await app.close();
|
|
8874
10647
|
try {
|
|
8875
|
-
|
|
10648
|
+
fs15.unlinkSync(paths.pidFile());
|
|
8876
10649
|
} catch {
|
|
8877
10650
|
}
|
|
8878
10651
|
try {
|
|
@@ -8880,7 +10653,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
8880
10653
|
} catch {
|
|
8881
10654
|
}
|
|
8882
10655
|
};
|
|
8883
|
-
return { app, manager, registry, extensions, shutdown };
|
|
10656
|
+
return { app, manager, registry, extensions, transformers, shutdown };
|
|
8884
10657
|
}
|
|
8885
10658
|
async function buildLogStream(level) {
|
|
8886
10659
|
const fileStream = await createPinoRoll({
|