@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.
Files changed (120) hide show
  1. package/bin/commands/minions.js +19 -0
  2. package/bin/minion.js +33 -4
  3. package/dist/server/cli/headless/claude-invoker-process.js +23 -7
  4. package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
  5. package/dist/server/cli/headless/haiku-assessments.js +2 -1
  6. package/dist/server/cli/headless/mcp-config.js +2 -1
  7. package/dist/server/engines/claude/claude-command.js +1 -0
  8. package/dist/server/engines/factory.js +2 -0
  9. package/dist/server/engines/hermes/HermesEngine.js +112 -219
  10. package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
  11. package/dist/server/index.js +10 -4
  12. package/dist/server/mcp/bouncer-integration.js +3 -2
  13. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
  14. package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
  15. package/dist/server/mcp/classifier/telemetry.js +0 -0
  16. package/dist/server/mcp/permission-channels.js +176 -0
  17. package/dist/server/mcp/security-analysis.js +9 -1
  18. package/dist/server/mcp/security-audit.js +22 -2
  19. package/dist/server/mcp/security-patterns.js +73 -1
  20. package/dist/server/mcp/server.js +80 -50
  21. package/dist/server/routes/internal.js +6 -1
  22. package/dist/server/routes/notifications.js +6 -18
  23. package/dist/server/server-setup.js +24 -48
  24. package/dist/server/services/analytics.js +4 -2
  25. package/dist/server/services/browser/host-mock.js +6 -1
  26. package/dist/server/services/browser/host.js +27 -3
  27. package/dist/server/services/browser/reasoning-agent.js +3 -1
  28. package/dist/server/services/chain/chain-engine.js +494 -0
  29. package/dist/server/services/chain/chain-store.js +542 -0
  30. package/dist/server/services/git/haiku.js +2 -2
  31. package/dist/server/services/plan/agents/check-injection.md +43 -2
  32. package/dist/server/services/plan/agents/review-code.md +62 -3
  33. package/dist/server/services/plan/agents/review-quality.md +52 -2
  34. package/dist/server/services/plan/board-export.js +4 -0
  35. package/dist/server/services/plan/composer-prompt.js +27 -8
  36. package/dist/server/services/plan/composer.js +10 -4
  37. package/dist/server/services/plan/config-installer.js +4 -83
  38. package/dist/server/services/plan/executor.js +155 -73
  39. package/dist/server/services/plan/issue-effort.js +9 -21
  40. package/dist/server/services/plan/issue-prompt-builder.js +72 -9
  41. package/dist/server/services/plan/parser-core.js +6 -1
  42. package/dist/server/services/plan/quality-delta.js +6 -24
  43. package/dist/server/services/plan/readiness-planner.js +12 -4
  44. package/dist/server/services/plan/record-file.js +52 -0
  45. package/dist/server/services/plan/review-approval.js +64 -0
  46. package/dist/server/services/plan/review-gate.js +186 -77
  47. package/dist/server/services/plan/review-outcome.js +146 -0
  48. package/dist/server/services/plan/review-report.js +28 -1
  49. package/dist/server/services/plan/review-target.js +57 -0
  50. package/dist/server/services/plan/state-reconciler.js +3 -3
  51. package/dist/server/services/plan/template-diff.js +2 -0
  52. package/dist/server/services/plan/template-instantiator.js +4 -0
  53. package/dist/server/services/plan/watcher.js +11 -1
  54. package/dist/server/services/platform-reconnect.js +7 -0
  55. package/dist/server/services/platform-token-lifecycle.js +2 -1
  56. package/dist/server/services/platform.js +293 -50
  57. package/dist/server/services/relay-http-client.js +107 -0
  58. package/dist/server/services/schedule/agent-schedule-store.js +69 -8
  59. package/dist/server/services/schedule/agent-scheduler.js +48 -7
  60. package/dist/server/services/schedule/daily-timing.js +52 -0
  61. package/dist/server/services/schedule/prompt-file.js +55 -0
  62. package/dist/server/services/schedule/run-bootstrap.js +20 -5
  63. package/dist/server/services/schedule/schedule-store.js +54 -4
  64. package/dist/server/services/schedule/schedule-wire.js +17 -0
  65. package/dist/server/services/schedule/scheduler.js +43 -9
  66. package/dist/server/services/sdk/agent-schedule.js +294 -0
  67. package/dist/server/services/sdk/agents.js +95 -13
  68. package/dist/server/services/sdk/app-tools.js +44 -7
  69. package/dist/server/services/sdk/atomic-write.js +75 -0
  70. package/dist/server/services/sdk/browser.js +5 -1
  71. package/dist/server/services/sdk/composer.js +2 -0
  72. package/dist/server/services/sdk/file-change-notify.js +60 -13
  73. package/dist/server/services/sdk/files-app.js +12 -7
  74. package/dist/server/services/sdk/files-search.js +14 -0
  75. package/dist/server/services/sdk/files-transfer.js +1 -1
  76. package/dist/server/services/sdk/files-watch.js +721 -0
  77. package/dist/server/services/sdk/files-workspace.js +37 -11
  78. package/dist/server/services/sdk/inference-claude.js +4 -2
  79. package/dist/server/services/sdk/inference.js +16 -2
  80. package/dist/server/services/sdk/pm-chain.js +0 -0
  81. package/dist/server/services/sdk/pm-schedule.js +72 -15
  82. package/dist/server/services/sdk/pm.js +22 -0
  83. package/dist/server/services/sdk/rate-limits.js +32 -0
  84. package/dist/server/services/sdk/registry.js +40 -2
  85. package/dist/server/services/sdk/terminal.js +1 -1
  86. package/dist/server/services/sentry.js +5 -1
  87. package/dist/server/services/terminal/pty-manager.js +26 -11
  88. package/dist/server/services/terminal/pty-utils.js +12 -1
  89. package/dist/server/services/terminal/terminal-modes.js +87 -0
  90. package/dist/server/services/timeline/assembler.js +2 -2
  91. package/dist/server/services/timeline/index.js +1 -1
  92. package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
  93. package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
  94. package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
  95. package/dist/server/services/websocket/browser-handlers.js +5 -2
  96. package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
  97. package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
  98. package/dist/server/services/websocket/file-transfer-http.js +12 -6
  99. package/dist/server/services/websocket/handler.js +185 -33
  100. package/dist/server/services/websocket/msg-id-tracker.js +94 -19
  101. package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
  102. package/dist/server/services/websocket/plan-handlers.js +10 -5
  103. package/dist/server/services/websocket/plan-helpers.js +23 -0
  104. package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
  105. package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
  106. package/dist/server/services/websocket/schedule-handlers.js +8 -2
  107. package/dist/server/services/websocket/sdk-agent-host.js +5 -1
  108. package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
  109. package/dist/server/services/websocket/sdk-browser-host.js +11 -4
  110. package/dist/server/services/websocket/sdk-handlers.js +6 -1
  111. package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
  112. package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
  113. package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
  114. package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
  115. package/dist/server/services/websocket/settings-handlers.js +6 -18
  116. package/dist/server/services/websocket/tab-broadcast.js +5 -1
  117. package/dist/server/services/websocket/terminal-handlers.js +51 -6
  118. package/dist/server/services/websocket/types.js +6 -5
  119. package/dist/server/services/websocket/viewer-interest.js +30 -0
  120. 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
- function delay(ms) {
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
- resolvePort;
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
- apiKey = '';
76
- baseUrl = '';
41
+ command = '';
77
42
  gateway = null;
78
- gatewayStartError = null;
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.resolvePort = options.resolvePort ?? (() => this.claimPort());
98
- this.generateApiKey = options.generateApiKey ?? (() => randomUUID());
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 port = await this.resolvePort();
141
- this.baseUrl = `http://127.0.0.1:${port}`;
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
- return this.spawn(command, ['gateway', 'run'], {
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
- catch (err) {
166
- throw this.gatewayStartFailure(err instanceof Error ? err : new Error(String(err)), cwd);
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.killGateway();
228
- this.releasePort();
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(`${this.baseUrl}/v1/responses`, {
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 ${this.apiKey}`,
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);