@minionry/minion 0.7.36 → 0.7.39
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/bin/commands/minions.js +19 -0
- package/bin/minion.js +33 -4
- package/dist/server/cli/headless/claude-invoker-process.js +23 -7
- package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
- package/dist/server/cli/headless/haiku-assessments.js +2 -1
- package/dist/server/cli/headless/mcp-config.js +2 -1
- package/dist/server/engines/claude/claude-command.js +1 -0
- package/dist/server/engines/factory.js +2 -0
- package/dist/server/engines/hermes/HermesEngine.js +112 -219
- package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
- package/dist/server/index.js +10 -4
- package/dist/server/mcp/bouncer-integration.js +3 -2
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
- package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
- package/dist/server/mcp/classifier/telemetry.js +0 -0
- package/dist/server/mcp/permission-channels.js +176 -0
- package/dist/server/mcp/security-analysis.js +9 -1
- package/dist/server/mcp/security-audit.js +22 -2
- package/dist/server/mcp/security-patterns.js +73 -1
- package/dist/server/mcp/server.js +80 -50
- package/dist/server/routes/internal.js +6 -1
- package/dist/server/routes/notifications.js +6 -18
- package/dist/server/server-setup.js +24 -48
- package/dist/server/services/analytics.js +4 -2
- package/dist/server/services/browser/host-mock.js +6 -1
- package/dist/server/services/browser/host.js +27 -3
- package/dist/server/services/browser/reasoning-agent.js +3 -1
- package/dist/server/services/chain/chain-engine.js +494 -0
- package/dist/server/services/chain/chain-store.js +542 -0
- package/dist/server/services/git/haiku.js +2 -2
- package/dist/server/services/plan/agents/check-injection.md +43 -2
- package/dist/server/services/plan/agents/review-code.md +62 -3
- package/dist/server/services/plan/agents/review-quality.md +52 -2
- package/dist/server/services/plan/board-export.js +4 -0
- package/dist/server/services/plan/composer-prompt.js +27 -8
- package/dist/server/services/plan/composer.js +10 -4
- package/dist/server/services/plan/config-installer.js +4 -83
- package/dist/server/services/plan/executor.js +155 -73
- package/dist/server/services/plan/issue-effort.js +9 -21
- package/dist/server/services/plan/issue-prompt-builder.js +72 -9
- package/dist/server/services/plan/parser-core.js +6 -1
- package/dist/server/services/plan/quality-delta.js +6 -24
- package/dist/server/services/plan/readiness-planner.js +12 -4
- package/dist/server/services/plan/record-file.js +52 -0
- package/dist/server/services/plan/review-approval.js +64 -0
- package/dist/server/services/plan/review-gate.js +186 -77
- package/dist/server/services/plan/review-outcome.js +146 -0
- package/dist/server/services/plan/review-report.js +28 -1
- package/dist/server/services/plan/review-target.js +57 -0
- package/dist/server/services/plan/state-reconciler.js +3 -3
- package/dist/server/services/plan/template-diff.js +2 -0
- package/dist/server/services/plan/template-instantiator.js +4 -0
- package/dist/server/services/plan/watcher.js +11 -1
- package/dist/server/services/platform-reconnect.js +7 -0
- package/dist/server/services/platform-token-lifecycle.js +2 -1
- package/dist/server/services/platform.js +293 -50
- package/dist/server/services/relay-http-client.js +107 -0
- package/dist/server/services/schedule/agent-schedule-store.js +69 -8
- package/dist/server/services/schedule/agent-scheduler.js +48 -7
- package/dist/server/services/schedule/daily-timing.js +52 -0
- package/dist/server/services/schedule/prompt-file.js +55 -0
- package/dist/server/services/schedule/run-bootstrap.js +20 -5
- package/dist/server/services/schedule/schedule-store.js +54 -4
- package/dist/server/services/schedule/schedule-wire.js +17 -0
- package/dist/server/services/schedule/scheduler.js +43 -9
- package/dist/server/services/sdk/agent-schedule.js +294 -0
- package/dist/server/services/sdk/agents.js +95 -13
- package/dist/server/services/sdk/app-tools.js +44 -7
- package/dist/server/services/sdk/atomic-write.js +75 -0
- package/dist/server/services/sdk/browser.js +5 -1
- package/dist/server/services/sdk/composer.js +2 -0
- package/dist/server/services/sdk/file-change-notify.js +60 -13
- package/dist/server/services/sdk/files-app.js +12 -7
- package/dist/server/services/sdk/files-search.js +14 -0
- package/dist/server/services/sdk/files-transfer.js +1 -1
- package/dist/server/services/sdk/files-watch.js +721 -0
- package/dist/server/services/sdk/files-workspace.js +37 -11
- package/dist/server/services/sdk/inference-claude.js +4 -2
- package/dist/server/services/sdk/inference.js +16 -2
- package/dist/server/services/sdk/pm-chain.js +0 -0
- package/dist/server/services/sdk/pm-schedule.js +72 -15
- package/dist/server/services/sdk/pm.js +22 -0
- package/dist/server/services/sdk/rate-limits.js +32 -0
- package/dist/server/services/sdk/registry.js +40 -2
- package/dist/server/services/sdk/terminal.js +1 -1
- package/dist/server/services/sentry.js +5 -1
- package/dist/server/services/terminal/pty-manager.js +26 -11
- package/dist/server/services/terminal/pty-utils.js +12 -1
- package/dist/server/services/terminal/terminal-modes.js +87 -0
- package/dist/server/services/timeline/assembler.js +2 -2
- package/dist/server/services/timeline/index.js +1 -1
- package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
- package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
- package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
- package/dist/server/services/websocket/browser-handlers.js +5 -2
- package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
- package/dist/server/services/websocket/file-transfer-http.js +12 -6
- package/dist/server/services/websocket/handler.js +185 -33
- package/dist/server/services/websocket/msg-id-tracker.js +94 -19
- package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
- package/dist/server/services/websocket/plan-handlers.js +10 -5
- package/dist/server/services/websocket/plan-helpers.js +23 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
- package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
- package/dist/server/services/websocket/schedule-handlers.js +8 -2
- package/dist/server/services/websocket/sdk-agent-host.js +5 -1
- package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
- package/dist/server/services/websocket/sdk-browser-host.js +11 -4
- package/dist/server/services/websocket/sdk-handlers.js +6 -1
- package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
- package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
- package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
- package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
- package/dist/server/services/websocket/settings-handlers.js +6 -18
- package/dist/server/services/websocket/tab-broadcast.js +5 -1
- package/dist/server/services/websocket/terminal-handlers.js +51 -6
- package/dist/server/services/websocket/types.js +6 -5
- package/dist/server/services/websocket/viewer-interest.js +30 -0
- package/package.json +6 -6
|
@@ -1,28 +1,17 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
-
import { spawn as nodeSpawn } from 'node:child_process';
|
|
4
3
|
import { randomUUID } from 'node:crypto';
|
|
5
4
|
import { mkdirSync } from 'node:fs';
|
|
6
5
|
import { hlog } from '../../cli/headless/headless-logger.js';
|
|
7
6
|
import { BROWSER_SYSTEM_PROMPT } from '../../cli/headless/mcp-config.js';
|
|
8
|
-
import { killProcessGroup } from '../../cli/headless/runner.js';
|
|
9
|
-
import { isDirectory } from '../../services/pathUtils.js';
|
|
10
|
-
import { findAvailablePort } from '../../utils/port-manager.js';
|
|
11
7
|
import { EngineEventStream } from '../engine-event-stream.js';
|
|
12
8
|
import { HERMES_READ_ONLY_REFUSAL, READ_ONLY_UNSUPPORTED, sessionIsReadOnly } from '../read-only.js';
|
|
13
9
|
import { AppRunUnsupportedError, EngineStartError, } from '../types.js';
|
|
14
10
|
import { HERMES_CONNECT_MESSAGE, } from './hermes-auth.js';
|
|
15
11
|
import { hermesAppRunBaseEnv } from './hermes-gateway-home.js';
|
|
12
|
+
import { HermesGatewayRegistry, userHermesHome, } from './hermes-gateway-registry.js';
|
|
16
13
|
import { HermesEventMapper } from './hermes-invoker.js';
|
|
17
14
|
import { hermesAvailability, resolveHermesCommand } from './hermes-provision.js';
|
|
18
|
-
function asRecord(value) {
|
|
19
|
-
return typeof value === 'object' && value !== null
|
|
20
|
-
? value
|
|
21
|
-
: undefined;
|
|
22
|
-
}
|
|
23
|
-
function asString(value) {
|
|
24
|
-
return typeof value === 'string' ? value : undefined;
|
|
25
|
-
}
|
|
26
15
|
const HERMES_THINKING_MODEL_PATTERN = /(^|\/)(hermes-4|deephermes)/i;
|
|
27
16
|
export function resolveHermesReasoningEffort(model, effortLevel) {
|
|
28
17
|
if (!model || !HERMES_THINKING_MODEL_PATTERN.test(model))
|
|
@@ -31,36 +20,13 @@ export function resolveHermesReasoningEffort(model, effortLevel) {
|
|
|
31
20
|
return undefined;
|
|
32
21
|
return effortLevel === 'ultracode' ? 'ultra' : effortLevel;
|
|
33
22
|
}
|
|
34
|
-
|
|
35
|
-
return new Promise((resolve) => {
|
|
36
|
-
setTimeout(resolve, ms).unref?.();
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
const GATEWAY_BASE_PORT = 8642;
|
|
40
|
-
const claimedGatewayPorts = new Set();
|
|
41
|
-
async function claimGatewayPort() {
|
|
42
|
-
let from = GATEWAY_BASE_PORT;
|
|
43
|
-
for (;;) {
|
|
44
|
-
const port = await findAvailablePort(from);
|
|
45
|
-
if (!claimedGatewayPorts.has(port)) {
|
|
46
|
-
claimedGatewayPorts.add(port);
|
|
47
|
-
return port;
|
|
48
|
-
}
|
|
49
|
-
from = port + 1;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const STARTUP_OUTPUT_LINES = 5;
|
|
53
|
-
const STARTUP_OUTPUT_LINE_CHARS = 300;
|
|
23
|
+
export const HERMES_APP_ENDPOINT_GONE_MESSAGE = "This app's model endpoint is no longer registered with your minion, so the message was not sent. The app has to register its endpoint again before this conversation can continue.";
|
|
54
24
|
export class HermesEngine extends EngineEventStream {
|
|
55
25
|
engineId = 'hermes';
|
|
56
26
|
resolveCommand;
|
|
57
|
-
spawn;
|
|
58
27
|
fetchImpl;
|
|
59
|
-
|
|
60
|
-
generateApiKey;
|
|
28
|
+
gateways;
|
|
61
29
|
generateConversationId;
|
|
62
|
-
healthTimeoutMs;
|
|
63
|
-
healthPollIntervalMs;
|
|
64
30
|
auth;
|
|
65
31
|
provisionGatewayHome;
|
|
66
32
|
checkAppRunAvailability;
|
|
@@ -72,12 +38,9 @@ export class HermesEngine extends EngineEventStream {
|
|
|
72
38
|
sessionOptions = null;
|
|
73
39
|
conversationId = 'pending';
|
|
74
40
|
previousResponseId;
|
|
75
|
-
|
|
76
|
-
baseUrl = '';
|
|
41
|
+
command = '';
|
|
77
42
|
gateway = null;
|
|
78
|
-
|
|
79
|
-
claimedPort = null;
|
|
80
|
-
startupOutput = [];
|
|
43
|
+
acquiring = null;
|
|
81
44
|
currentAbort = null;
|
|
82
45
|
currentRunSettled = null;
|
|
83
46
|
cancelling = false;
|
|
@@ -91,14 +54,19 @@ export class HermesEngine extends EngineEventStream {
|
|
|
91
54
|
constructor(options = {}) {
|
|
92
55
|
super();
|
|
93
56
|
this.resolveCommand = options.resolveCommand ?? (() => resolveHermesCommand());
|
|
94
|
-
this.spawn = options.spawn ?? nodeSpawn;
|
|
95
57
|
this.fetchImpl =
|
|
96
58
|
options.fetch ?? ((url, init) => globalThis.fetch(url, init));
|
|
97
|
-
this.
|
|
98
|
-
|
|
59
|
+
this.gateways =
|
|
60
|
+
options.gateways ??
|
|
61
|
+
new HermesGatewayRegistry({
|
|
62
|
+
spawn: options.spawn,
|
|
63
|
+
fetch: this.fetchImpl,
|
|
64
|
+
resolvePort: options.resolvePort,
|
|
65
|
+
generateApiKey: options.generateApiKey,
|
|
66
|
+
healthTimeoutMs: options.healthTimeoutMs,
|
|
67
|
+
healthPollIntervalMs: options.healthPollIntervalMs,
|
|
68
|
+
});
|
|
99
69
|
this.generateConversationId = options.generateConversationId ?? (() => randomUUID());
|
|
100
|
-
this.healthTimeoutMs = options.healthTimeoutMs ?? 30_000;
|
|
101
|
-
this.healthPollIntervalMs = options.healthPollIntervalMs ?? 250;
|
|
102
70
|
this.auth = options.auth;
|
|
103
71
|
this.provisionGatewayHome = options.provisionGatewayHome;
|
|
104
72
|
this.checkAppRunAvailability = options.checkAppRunAvailability ?? (() => hermesAvailability());
|
|
@@ -116,11 +84,9 @@ export class HermesEngine extends EngineEventStream {
|
|
|
116
84
|
this.sessionOptions = options;
|
|
117
85
|
this.conversationId = options.resumeSessionId ?? this.generateConversationId();
|
|
118
86
|
this.previousResponseId = undefined;
|
|
119
|
-
this.apiKey = this.generateApiKey();
|
|
120
87
|
this.mapper = new HermesEventMapper({ sessionId: this.conversationId });
|
|
121
|
-
let command;
|
|
122
88
|
try {
|
|
123
|
-
command = await this.resolveCommand();
|
|
89
|
+
this.command = await this.resolveCommand();
|
|
124
90
|
}
|
|
125
91
|
catch (err) {
|
|
126
92
|
throw new EngineStartError('HERMES_NOT_INSTALLED', err instanceof Error ? err.message : String(err));
|
|
@@ -130,42 +96,63 @@ export class HermesEngine extends EngineEventStream {
|
|
|
130
96
|
return;
|
|
131
97
|
if (this.auth && !appRun) {
|
|
132
98
|
this.providerEnv = this.auth.providerEnv();
|
|
133
|
-
this.connection = await this.auth.connectionStatus(command);
|
|
99
|
+
this.connection = await this.auth.connectionStatus(this.command);
|
|
134
100
|
if (this.disposed)
|
|
135
101
|
return;
|
|
136
102
|
}
|
|
137
103
|
this.provisionSessionHome(options);
|
|
138
104
|
this.nativeResume = !options.resumeSessionId || !this.gatewayHome || this.gatewayHome.persistent;
|
|
139
105
|
this.resolveBrowserMcpActive(options);
|
|
140
|
-
const
|
|
141
|
-
this.
|
|
142
|
-
const cwd = appRun?.cwd ?? options.workingDir;
|
|
143
|
-
const child = this.spawnGateway(command, cwd, port, options);
|
|
144
|
-
this.gateway = child;
|
|
145
|
-
this.wireGateway(child);
|
|
146
|
-
await this.waitForHealthy(cwd);
|
|
147
|
-
}
|
|
148
|
-
spawnGateway(command, cwd, port, options) {
|
|
106
|
+
const acquiring = this.acquireGateway(options);
|
|
107
|
+
this.acquiring = acquiring;
|
|
149
108
|
try {
|
|
150
|
-
|
|
151
|
-
cwd,
|
|
152
|
-
env: {
|
|
153
|
-
...this.inheritedSpawnEnv(options),
|
|
154
|
-
...this.providerEnv,
|
|
155
|
-
...(this.gatewayHome?.env ?? {}),
|
|
156
|
-
API_SERVER_ENABLED: 'true',
|
|
157
|
-
API_SERVER_KEY: this.apiKey,
|
|
158
|
-
API_SERVER_PORT: String(port),
|
|
159
|
-
API_SERVER_HOST: '127.0.0.1',
|
|
160
|
-
},
|
|
161
|
-
detached: true,
|
|
162
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
163
|
-
});
|
|
109
|
+
await acquiring;
|
|
164
110
|
}
|
|
165
|
-
|
|
166
|
-
|
|
111
|
+
finally {
|
|
112
|
+
this.acquiring = null;
|
|
167
113
|
}
|
|
168
114
|
}
|
|
115
|
+
async acquireGateway(options) {
|
|
116
|
+
const lease = await this.gateways.acquire(this.gatewayLaunch(options), (failure) => this.onGatewayLost(failure));
|
|
117
|
+
if (this.disposed) {
|
|
118
|
+
await lease.release();
|
|
119
|
+
throw new Error('HermesEngine: engine disposed during startup');
|
|
120
|
+
}
|
|
121
|
+
if (!lease.endpoint)
|
|
122
|
+
throw new Error('HermesEngine: gateway failed to start — it exited as this session attached');
|
|
123
|
+
this.gateway = lease;
|
|
124
|
+
this.healthy = true;
|
|
125
|
+
}
|
|
126
|
+
gatewayLaunch(options) {
|
|
127
|
+
const homeEnv = this.gatewayHome?.env ?? {};
|
|
128
|
+
const env = {
|
|
129
|
+
...this.inheritedSpawnEnv(options),
|
|
130
|
+
...this.providerEnv,
|
|
131
|
+
...homeEnv,
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
home: this.gatewayHome?.home ?? userHermesHome(env),
|
|
135
|
+
owned: this.gatewayHome !== null,
|
|
136
|
+
command: this.command,
|
|
137
|
+
cwd: options.appRun?.cwd ?? options.workingDir,
|
|
138
|
+
env,
|
|
139
|
+
secrets: [...Object.values(this.providerEnv), ...Object.values(homeEnv)],
|
|
140
|
+
quietOnceServing: !!options.appRun,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
onGatewayLost(failure) {
|
|
144
|
+
this.gateway = null;
|
|
145
|
+
if (!this.healthy || this.disposed)
|
|
146
|
+
return;
|
|
147
|
+
this.emit({
|
|
148
|
+
kind: 'engine.error',
|
|
149
|
+
sessionId: this.sessionIdForEvent(),
|
|
150
|
+
timestamp: Date.now(),
|
|
151
|
+
code: failure.code,
|
|
152
|
+
message: failure.message,
|
|
153
|
+
fatal: true,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
169
156
|
async sendPrompt(prompt, _attachments) {
|
|
170
157
|
if (!this.sessionOptions) {
|
|
171
158
|
throw new Error('HermesEngine: sendPrompt called before startSession');
|
|
@@ -224,12 +211,14 @@ export class HermesEngine extends EngineEventStream {
|
|
|
224
211
|
return;
|
|
225
212
|
this.disposed = true;
|
|
226
213
|
this.currentAbort?.abort();
|
|
227
|
-
this.
|
|
228
|
-
this.
|
|
214
|
+
const released = this.gateway?.release();
|
|
215
|
+
this.gateway = null;
|
|
229
216
|
if (this.currentRunSettled) {
|
|
230
217
|
await this.currentRunSettled;
|
|
231
218
|
}
|
|
232
219
|
this.closeIterator();
|
|
220
|
+
await released;
|
|
221
|
+
await this.acquiring?.catch(() => { });
|
|
233
222
|
this.gatewayHome?.cleanup();
|
|
234
223
|
this.gatewayHome = null;
|
|
235
224
|
}
|
|
@@ -291,107 +280,9 @@ export class HermesEngine extends EngineEventStream {
|
|
|
291
280
|
.filter(Boolean)
|
|
292
281
|
.join('\n\n');
|
|
293
282
|
}
|
|
294
|
-
wireGateway(child) {
|
|
295
|
-
this.drainGatewayOutput(child.stdout, 'stdout');
|
|
296
|
-
this.drainGatewayOutput(child.stderr, 'stderr');
|
|
297
|
-
child.on('error', (err) => {
|
|
298
|
-
this.gatewayStartError = err;
|
|
299
|
-
if (this.gateway === child && this.sessionOptions && !this.disposed) {
|
|
300
|
-
this.gateway = null;
|
|
301
|
-
}
|
|
302
|
-
if (this.healthy && !this.disposed) {
|
|
303
|
-
this.emit({
|
|
304
|
-
kind: 'engine.error',
|
|
305
|
-
sessionId: this.sessionIdForEvent(),
|
|
306
|
-
timestamp: Date.now(),
|
|
307
|
-
code: 'HERMES_GATEWAY_ERROR',
|
|
308
|
-
message: err.message,
|
|
309
|
-
fatal: true,
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
child.on('exit', (code, signal) => {
|
|
314
|
-
if (this.gateway === child)
|
|
315
|
-
this.gateway = null;
|
|
316
|
-
if (this.disposed)
|
|
317
|
-
return;
|
|
318
|
-
this.gatewayStartError ??= new Error(`hermes gateway exited (${signal ? `signal ${signal}` : `code ${code}`})`);
|
|
319
|
-
if (this.healthy) {
|
|
320
|
-
this.emit({
|
|
321
|
-
kind: 'engine.error',
|
|
322
|
-
sessionId: this.sessionIdForEvent(),
|
|
323
|
-
timestamp: Date.now(),
|
|
324
|
-
code: 'HERMES_GATEWAY_EXIT',
|
|
325
|
-
message: this.gatewayStartError.message,
|
|
326
|
-
fatal: true,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
drainGatewayOutput(stream, label) {
|
|
332
|
-
if (!stream)
|
|
333
|
-
return;
|
|
334
|
-
stream.on('data', (chunk) => {
|
|
335
|
-
if (this.disposed)
|
|
336
|
-
return;
|
|
337
|
-
if (this.sessionOptions?.appRun && this.healthy)
|
|
338
|
-
return;
|
|
339
|
-
const text = chunk.toString('utf8').trim();
|
|
340
|
-
if (!text)
|
|
341
|
-
return;
|
|
342
|
-
hlog(`[hermes gateway ${label}] ${text.slice(0, 2000)}`);
|
|
343
|
-
if (!this.healthy)
|
|
344
|
-
this.keepStartupOutput(text);
|
|
345
|
-
});
|
|
346
|
-
stream.on('error', () => { });
|
|
347
|
-
}
|
|
348
|
-
keepStartupOutput(text) {
|
|
349
|
-
for (const line of text.split('\n')) {
|
|
350
|
-
const trimmed = line.trim();
|
|
351
|
-
if (trimmed)
|
|
352
|
-
this.startupOutput.push(trimmed.slice(0, STARTUP_OUTPUT_LINE_CHARS));
|
|
353
|
-
}
|
|
354
|
-
this.startupOutput.splice(0, Math.max(0, this.startupOutput.length - STARTUP_OUTPUT_LINES));
|
|
355
|
-
}
|
|
356
|
-
gatewayStartFailure(err, cwd) {
|
|
357
|
-
const errno = err.code;
|
|
358
|
-
if ((errno === 'ENOENT' || errno === 'ENOTDIR') && !isDirectory(cwd)) {
|
|
359
|
-
return new EngineStartError('WORKING_DIRECTORY_NOT_FOUND', `Could not start Hermes: its working directory ${cwd} is not a directory — it may have been moved, renamed, or deleted.`);
|
|
360
|
-
}
|
|
361
|
-
return new Error(`HermesEngine: gateway failed to start — ${err.message}${this.startupOutputSuffix()}`);
|
|
362
|
-
}
|
|
363
|
-
async waitForHealthy(cwd) {
|
|
364
|
-
const deadline = Date.now() + this.healthTimeoutMs;
|
|
365
|
-
while (Date.now() < deadline) {
|
|
366
|
-
if (this.disposed) {
|
|
367
|
-
throw new Error('HermesEngine: engine disposed during startup');
|
|
368
|
-
}
|
|
369
|
-
if (this.gatewayStartError) {
|
|
370
|
-
const err = this.gatewayStartError;
|
|
371
|
-
this.killGateway();
|
|
372
|
-
throw this.gatewayStartFailure(err, cwd);
|
|
373
|
-
}
|
|
374
|
-
try {
|
|
375
|
-
const res = await this.fetchImpl(`${this.baseUrl}/health`, {
|
|
376
|
-
method: 'GET',
|
|
377
|
-
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
378
|
-
});
|
|
379
|
-
if (res.ok) {
|
|
380
|
-
const body = asRecord(await res.json());
|
|
381
|
-
if (asString(body?.status) === 'ok') {
|
|
382
|
-
this.healthy = true;
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
}
|
|
389
|
-
await delay(this.healthPollIntervalMs);
|
|
390
|
-
}
|
|
391
|
-
this.killGateway();
|
|
392
|
-
throw new Error(`HermesEngine: gateway did not become healthy within ${this.healthTimeoutMs}ms${this.startupOutputSuffix()}`);
|
|
393
|
-
}
|
|
394
283
|
async runTurn(prompt, signal) {
|
|
284
|
+
if (!(await this.refreshAppRunGateway(signal)))
|
|
285
|
+
return;
|
|
395
286
|
const resumeId = this.previousResponseId;
|
|
396
287
|
const degrade = await this.attemptTurn(prompt, signal, resumeId, resumeId !== undefined);
|
|
397
288
|
if (degrade && !this.cancelling && !signal.aborted) {
|
|
@@ -400,6 +291,43 @@ export class HermesEngine extends EngineEventStream {
|
|
|
400
291
|
await this.attemptTurn(prompt, signal, undefined, false);
|
|
401
292
|
}
|
|
402
293
|
}
|
|
294
|
+
async refreshAppRunGateway(signal) {
|
|
295
|
+
const options = this.sessionOptions;
|
|
296
|
+
const lease = this.gateway;
|
|
297
|
+
if (!options?.appRun || !options.model || !this.provisionGatewayHome || !lease)
|
|
298
|
+
return true;
|
|
299
|
+
let home;
|
|
300
|
+
try {
|
|
301
|
+
home = this.provisionGatewayHome(options.model, undefined, options.appRun);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
return this.settleUnreadyTurn('HERMES_GATEWAY_ERROR', errMessage(err), signal);
|
|
305
|
+
}
|
|
306
|
+
if (!home)
|
|
307
|
+
return this.settleUnreadyTurn('HERMES_APP_ENDPOINT_GONE', HERMES_APP_ENDPOINT_GONE_MESSAGE, signal);
|
|
308
|
+
this.gatewayHome = home;
|
|
309
|
+
try {
|
|
310
|
+
await lease.reconcile(this.gatewayLaunch(options));
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
return this.settleUnreadyTurn('HERMES_GATEWAY_ERROR', errMessage(err), signal);
|
|
314
|
+
}
|
|
315
|
+
if (this.cancelling || signal.aborted) {
|
|
316
|
+
this.emitAbortedIdle();
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
settleUnreadyTurn(code, message, signal) {
|
|
322
|
+
if (this.cancelling || signal.aborted) {
|
|
323
|
+
this.emitAbortedIdle();
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
this.emitTurnError(code, message);
|
|
327
|
+
this.emitTurnIdle('error');
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
403
331
|
async attemptTurn(prompt, signal, previousResponseId, canDegrade) {
|
|
404
332
|
const outcome = await this.postTurn(prompt, signal, previousResponseId);
|
|
405
333
|
if (outcome.kind === 'aborted') {
|
|
@@ -453,13 +381,17 @@ export class HermesEngine extends EngineEventStream {
|
|
|
453
381
|
const reasoningEffort = resolveHermesReasoningEffort(model, this.sessionOptions?.effortLevel);
|
|
454
382
|
if (reasoningEffort)
|
|
455
383
|
body.model_options = { reasoning: { effort: reasoningEffort } };
|
|
384
|
+
const endpoint = this.gateway?.endpoint;
|
|
385
|
+
if (!endpoint) {
|
|
386
|
+
return { kind: 'error', code: 'HERMES_GATEWAY_EXIT', detail: 'The Hermes gateway for this session is not running.' };
|
|
387
|
+
}
|
|
456
388
|
let response;
|
|
457
389
|
try {
|
|
458
|
-
response = await this.fetchImpl(`${
|
|
390
|
+
response = await this.fetchImpl(`${endpoint.baseUrl}/v1/responses`, {
|
|
459
391
|
method: 'POST',
|
|
460
392
|
headers: {
|
|
461
393
|
'Content-Type': 'application/json',
|
|
462
|
-
Authorization: `Bearer ${
|
|
394
|
+
Authorization: `Bearer ${endpoint.apiKey}`,
|
|
463
395
|
'X-Hermes-Session-Key': this.conversationId,
|
|
464
396
|
},
|
|
465
397
|
body: JSON.stringify(body),
|
|
@@ -583,45 +515,6 @@ export class HermesEngine extends EngineEventStream {
|
|
|
583
515
|
raw,
|
|
584
516
|
});
|
|
585
517
|
}
|
|
586
|
-
async claimPort() {
|
|
587
|
-
const port = await claimGatewayPort();
|
|
588
|
-
if (this.disposed) {
|
|
589
|
-
claimedGatewayPorts.delete(port);
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
this.claimedPort = port;
|
|
593
|
-
}
|
|
594
|
-
return port;
|
|
595
|
-
}
|
|
596
|
-
releasePort() {
|
|
597
|
-
if (this.claimedPort === null)
|
|
598
|
-
return;
|
|
599
|
-
claimedGatewayPorts.delete(this.claimedPort);
|
|
600
|
-
this.claimedPort = null;
|
|
601
|
-
}
|
|
602
|
-
startupOutputSuffix() {
|
|
603
|
-
if (this.startupOutput.length === 0)
|
|
604
|
-
return '';
|
|
605
|
-
const secrets = [
|
|
606
|
-
this.apiKey,
|
|
607
|
-
...Object.values(this.providerEnv),
|
|
608
|
-
...Object.values(this.gatewayHome?.env ?? {}),
|
|
609
|
-
].filter((value) => value.length >= 8);
|
|
610
|
-
const lines = this.startupOutput.map((line) => secrets.reduce((masked, secret) => masked.split(secret).join('[redacted]'), line));
|
|
611
|
-
return `\nLast gateway output:\n${lines.join('\n')}`;
|
|
612
|
-
}
|
|
613
|
-
killGateway() {
|
|
614
|
-
const child = this.gateway;
|
|
615
|
-
if (!child || child.pid === undefined)
|
|
616
|
-
return;
|
|
617
|
-
const pid = child.pid;
|
|
618
|
-
killProcessGroup(pid, 'SIGTERM');
|
|
619
|
-
setTimeout(() => {
|
|
620
|
-
if (!child.killed) {
|
|
621
|
-
killProcessGroup(pid, 'SIGKILL');
|
|
622
|
-
}
|
|
623
|
-
}, 5000).unref?.();
|
|
624
|
-
}
|
|
625
518
|
}
|
|
626
519
|
function errMessage(err) {
|
|
627
520
|
return err instanceof Error ? err.message : String(err);
|