memorix 1.9.4 → 1.9.6

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 (90) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/README.md +12 -6
  3. package/README.zh-CN.md +12 -6
  4. package/dist/cli/index.js +2533 -585
  5. package/dist/index.d.ts +15 -8
  6. package/dist/index.js +1201 -284
  7. package/dist/maintenance-runner.js +387 -216
  8. package/dist/media-audio-runner.js +300 -129
  9. package/dist/media-video-runner.js +300 -129
  10. package/dist/memcode-runtime/CHANGELOG.md +75 -1
  11. package/dist/sdk.d.ts +1 -0
  12. package/dist/sdk.js +1882 -998
  13. package/dist/types.d.ts +1 -1
  14. package/dist/vector-backfill-runner.js +300 -129
  15. package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
  16. package/docs/API_REFERENCE.md +39 -1
  17. package/docs/CONFIGURATION.md +58 -1
  18. package/docs/DEVELOPMENT.md +1 -1
  19. package/docs/DOCKER.md +23 -3
  20. package/docs/INTEGRATIONS.md +1 -1
  21. package/docs/KNOWN_ISSUES_AND_ROADMAP.md +61 -1
  22. package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
  23. package/docs/PERFORMANCE.md +46 -2
  24. package/docs/README.md +2 -1
  25. package/docs/SETUP.md +56 -1
  26. package/package.json +3 -2
  27. package/plugins/antigravity/memorix/plugin.json +1 -1
  28. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  29. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  30. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  31. package/plugins/copilot/memorix/plugin.json +1 -1
  32. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  33. package/plugins/hermes/memorix/plugin.yaml +1 -1
  34. package/plugins/omp/memorix/package.json +1 -1
  35. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  36. package/plugins/pi/memorix/package.json +1 -1
  37. package/server.json +2 -2
  38. package/src/cli/capability-map.ts +1 -0
  39. package/src/cli/command-guide.ts +15 -3
  40. package/src/cli/commands/agent-integrations.ts +57 -0
  41. package/src/cli/commands/background.ts +11 -2
  42. package/src/cli/commands/context.ts +2 -0
  43. package/src/cli/commands/continuity.ts +155 -0
  44. package/src/cli/commands/init.ts +5 -2
  45. package/src/cli/commands/serve-http.ts +435 -84
  46. package/src/cli/commands/setup.ts +77 -10
  47. package/src/cli/commands/sync-store.ts +47 -12
  48. package/src/cli/commands/sync.ts +26 -0
  49. package/src/cli/index.ts +3 -1
  50. package/src/cli/tui/ConfigureView.tsx +2 -0
  51. package/src/codegraph/auto-context.ts +17 -0
  52. package/src/config/legacy-loader.ts +2 -2
  53. package/src/config/resolved-config.ts +8 -5
  54. package/src/config/yaml-loader.ts +20 -10
  55. package/src/dashboard/server.ts +10 -12
  56. package/src/embedding/api-provider.ts +4 -2
  57. package/src/embedding/fastembed-provider.ts +47 -14
  58. package/src/embedding/transformers-provider.ts +29 -7
  59. package/src/evaluation/memory-quality-gate.ts +90 -0
  60. package/src/hooks/official-skills.ts +2 -0
  61. package/src/integrations/registry.ts +1 -1
  62. package/src/knowledge/context-assembly.ts +1 -0
  63. package/src/knowledge/context-receipt.ts +15 -0
  64. package/src/knowledge/task-continuity.ts +413 -0
  65. package/src/knowledge/workset.ts +77 -0
  66. package/src/llm/provider.ts +2 -1
  67. package/src/memory/graph.ts +10 -8
  68. package/src/memory/session.ts +1 -1
  69. package/src/orchestrate/coordinator.ts +60 -0
  70. package/src/orchestrate/effect-ledger.ts +160 -0
  71. package/src/orchestrate/pipeline-trace.ts +40 -6
  72. package/src/runtime/maintenance-jobs.ts +12 -6
  73. package/src/sdk.ts +24 -0
  74. package/src/server/mcp-binding-store.ts +14 -3
  75. package/src/server/modern-mcp-bridge.ts +153 -27
  76. package/src/server/tool-profile.ts +3 -2
  77. package/src/server.ts +131 -7
  78. package/src/store/file-lock.ts +28 -3
  79. package/src/store/graph-store.ts +178 -46
  80. package/src/store/persistence.ts +14 -9
  81. package/src/store/sqlite-db.ts +150 -7
  82. package/src/store/sqlite-store.ts +32 -27
  83. package/src/sync/engine.ts +9 -7
  84. package/src/sync/namespace.ts +23 -0
  85. package/src/sync/policy.ts +19 -4
  86. package/src/sync/store-port.ts +43 -13
  87. package/src/sync/types.ts +2 -0
  88. package/src/team/team-store.ts +62 -27
  89. package/src/types.ts +1 -1
  90. package/src/workspace/mcp-adapters/grok.ts +32 -0
@@ -22,6 +22,8 @@
22
22
  import { defineCommand } from 'citty';
23
23
  import type { IncomingMessage, ServerResponse } from 'node:http';
24
24
  import { getHeapStatistics } from 'node:v8';
25
+ import { monitorEventLoopDelay } from 'node:perf_hooks';
26
+ import { timingSafeEqual } from 'node:crypto';
25
27
  import type { ObservationStore } from '../../store/obs-store.js';
26
28
  import { resolveToolProfile } from '../../server/tool-profile.js';
27
29
  import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
@@ -39,6 +41,13 @@ import { mcpFileUriToPath } from '../mcp-root-path.js';
39
41
  */
40
42
  export const DEFAULT_SESSION_TIMEOUT_MS = 12 * 60 * 60 * 1000;
41
43
  export const EXPIRED_SESSION_TTL_MS = 10 * 60 * 1000;
44
+ export const DEFAULT_HTTP_BODY_LIMIT_BYTES = 10 * 1024 * 1024;
45
+ export const MAX_HTTP_BODY_LIMIT_BYTES = 64 * 1024 * 1024;
46
+ export const MIN_HTTP_BODY_LIMIT_BYTES = 64 * 1024;
47
+ export const MAX_PROJECT_CACHE_ENTRIES = 256;
48
+ export const MAX_STORE_CACHE_ENTRIES = 32;
49
+ export const DEFAULT_MAX_HTTP_SESSIONS = 64;
50
+ export const MAX_HTTP_SESSIONS_LIMIT = 512;
42
51
  /** Pinned SDK protocol accepted internally while the HTTP boundary speaks the current stateless contract. */
43
52
  export const MCP_SDK_COMPAT_PROTOCOL_VERSION = '2025-11-25';
44
53
 
@@ -56,6 +65,109 @@ export function parseSessionTimeoutMs(raw: string | undefined): number {
56
65
  return Math.floor(parsed);
57
66
  }
58
67
 
68
+ export function parseHttpBodyLimit(raw: string | undefined): number {
69
+ const value = raw?.trim();
70
+ if (!value) return DEFAULT_HTTP_BODY_LIMIT_BYTES;
71
+ const parsed = Number(value);
72
+ if (!Number.isSafeInteger(parsed) || parsed < MIN_HTTP_BODY_LIMIT_BYTES) {
73
+ return DEFAULT_HTTP_BODY_LIMIT_BYTES;
74
+ }
75
+ return Math.min(parsed, MAX_HTTP_BODY_LIMIT_BYTES);
76
+ }
77
+
78
+ export function parseMaxHttpSessions(raw: string | undefined): number {
79
+ const value = raw?.trim();
80
+ if (!value) return DEFAULT_MAX_HTTP_SESSIONS;
81
+ const parsed = Number(value);
82
+ if (!Number.isSafeInteger(parsed) || parsed < 1) return DEFAULT_MAX_HTTP_SESSIONS;
83
+ return Math.min(parsed, MAX_HTTP_SESSIONS_LIMIT);
84
+ }
85
+
86
+ export function isLoopbackHost(host: string): boolean {
87
+ const normalized = host.trim().toLowerCase();
88
+ return normalized === 'localhost'
89
+ || normalized === '127.0.0.1'
90
+ || normalized === '::1'
91
+ || normalized === '[::1]';
92
+ }
93
+
94
+ export function validateHttpBindSecurity(
95
+ host: string,
96
+ env: NodeJS.ProcessEnv = process.env,
97
+ ): { requiresAuth: boolean; explicitlyUnauthenticated: boolean } {
98
+ const hasToken = Boolean(env.MEMORIX_HTTP_AUTH_TOKEN?.trim());
99
+ const explicitlyUnauthenticated = env.MEMORIX_HTTP_ALLOW_UNAUTHENTICATED_BIND === '1';
100
+ if (!isLoopbackHost(host) && !hasToken && !explicitlyUnauthenticated) {
101
+ throw new Error(
102
+ `[memorix] Refusing unauthenticated non-loopback HTTP bind on ${host}. `
103
+ + 'Set MEMORIX_HTTP_AUTH_TOKEN or explicitly set MEMORIX_HTTP_ALLOW_UNAUTHENTICATED_BIND=1.',
104
+ );
105
+ }
106
+ return { requiresAuth: hasToken, explicitlyUnauthenticated };
107
+ }
108
+
109
+ function isAuthorizedHttpRequest(req: IncomingMessage, token: string): boolean {
110
+ const header = req.headers.authorization;
111
+ if (!header) return false;
112
+ const match = /^Bearer\s+(.+)$/iu.exec(header);
113
+ if (!match) return false;
114
+ const provided = Buffer.from(match[1], 'utf8');
115
+ const expected = Buffer.from(token, 'utf8');
116
+ return provided.length === expected.length && timingSafeEqual(provided, expected);
117
+ }
118
+
119
+ export class HttpPayloadTooLargeError extends Error {
120
+ readonly statusCode = 413;
121
+
122
+ constructor(limitBytes: number) {
123
+ super(`Request body exceeds the ${limitBytes} byte limit.`);
124
+ this.name = 'HttpPayloadTooLargeError';
125
+ }
126
+ }
127
+
128
+ export function parseJsonBody(req: IncomingMessage, limitBytes: number): Promise<unknown> {
129
+ return new Promise((resolve, reject) => {
130
+ const contentLength = Number(req.headers['content-length']);
131
+ if (Number.isSafeInteger(contentLength) && contentLength > limitBytes) {
132
+ req.resume();
133
+ reject(new HttpPayloadTooLargeError(limitBytes));
134
+ return;
135
+ }
136
+
137
+ const chunks: Buffer[] = [];
138
+ let size = 0;
139
+ let settled = false;
140
+ const fail = (error: unknown) => {
141
+ if (settled) return;
142
+ settled = true;
143
+ reject(error);
144
+ };
145
+
146
+ req.on('data', (chunk: Buffer) => {
147
+ if (settled) return;
148
+ size += chunk.length;
149
+ if (size > limitBytes) {
150
+ req.removeAllListeners('data');
151
+ req.resume();
152
+ fail(new HttpPayloadTooLargeError(limitBytes));
153
+ return;
154
+ }
155
+ chunks.push(chunk);
156
+ });
157
+ req.on('end', () => {
158
+ if (settled) return;
159
+ settled = true;
160
+ try {
161
+ const body = Buffer.concat(chunks).toString('utf-8');
162
+ resolve(body ? JSON.parse(body) : undefined);
163
+ } catch (error) {
164
+ reject(error);
165
+ }
166
+ });
167
+ req.on('error', fail);
168
+ });
169
+ }
170
+
59
171
  export interface ServeHttpConfiguredDefaults {
60
172
  port?: number;
61
173
  transport?: 'stdio' | 'http';
@@ -132,7 +244,8 @@ export default defineCommand({
132
244
  // node:fs from being evaluated before their mock factories initialize.
133
245
  const { McpBindingStore } = await import('../../server/mcp-binding-store.js');
134
246
  const { MemoryFeedbackStore } = await import('../../memory/feedback.js');
135
- const { createModernMcpBridge } = await import('../../server/modern-mcp-bridge.js');
247
+ const { createModernMcpRuntime, ModernMcpRuntimePool, withBusinessRuntimeQueue } = await import('../../server/modern-mcp-bridge.js');
248
+ const { acquireDatabase, evictIdleDatabases, getDatabaseStats } = await import('../../store/sqlite-db.js');
136
249
  const { createMcpHandler } = await import('@modelcontextprotocol/server');
137
250
  const { toNodeHandler } = await import('@modelcontextprotocol/node');
138
251
 
@@ -146,6 +259,14 @@ export default defineCommand({
146
259
  if (resolved.transportNotice) console.error(resolved.transportNotice);
147
260
  const port = resolved.port;
148
261
  const host = args.host || '127.0.0.1';
262
+ const bindSecurity = validateHttpBindSecurity(host);
263
+ const httpAuthToken = process.env.MEMORIX_HTTP_AUTH_TOKEN?.trim() || '';
264
+ if (bindSecurity.explicitlyUnauthenticated && !isLoopbackHost(host)) {
265
+ console.error(`[memorix] WARNING: HTTP control plane is unauthenticated on ${host}; keep it behind a private network or authenticated proxy.`);
266
+ }
267
+ if (bindSecurity.requiresAuth) {
268
+ console.error('[memorix] HTTP control plane authentication is enabled (Bearer token).');
269
+ }
149
270
  const toolProfile = resolveToolProfile({ explicit: args.mode, envValue: process.env.MEMORIX_MODE, fallback: 'team' });
150
271
 
151
272
  // Priority: explicit --cwd arg > MEMORIX_PROJECT_ROOT env > process.cwd().
@@ -176,31 +297,80 @@ export default defineCommand({
176
297
  console.error(`[memorix] HTTP transport starting on ${host}:${port}`);
177
298
  console.error(`[memorix] Project root: ${projectRoot}`);
178
299
  const sessionTimeoutMs = parseSessionTimeoutMs(process.env.MEMORIX_SESSION_TIMEOUT_MS);
300
+ const httpBodyLimitBytes = parseHttpBodyLimit(process.env.MEMORIX_HTTP_MAX_BODY_BYTES);
301
+ const maxHttpSessions = parseMaxHttpSessions(process.env.MEMORIX_MAX_HTTP_SESSIONS);
302
+ const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
303
+ eventLoopDelay.enable();
179
304
  console.error(
180
305
  sessionTimeoutMs === 0
181
306
  ? '[memorix] HTTP session idle timeout: disabled'
182
307
  : `[memorix] HTTP session idle timeout: ${Math.round(sessionTimeoutMs / 60000)}min (${sessionTimeoutMs}ms)`,
183
308
  );
309
+ console.error(`[memorix] HTTP session limit: ${maxHttpSessions}`);
184
310
 
185
311
  // Per-project TeamStore cache (Option B) — each dataDir gets its own TeamStore instance.
186
312
  // This ensures true isolation between projects in HTTP control-plane mode.
187
313
  const teamStoreCache = new Map<string, Awaited<ReturnType<typeof initTeamStore>>>();
314
+ const teamStoreLeases = new Map<string, { release: () => void }>();
188
315
  const { initTeamStore } = await import('../../team/team-store.js');
189
316
 
317
+ function cacheSet<K, V>(
318
+ cache: Map<K, V>,
319
+ key: K,
320
+ value: V,
321
+ limit: number,
322
+ onEvict?: (evictedKey: K, evictedValue: V) => void,
323
+ ): void {
324
+ cache.delete(key);
325
+ cache.set(key, value);
326
+ while (cache.size > limit) {
327
+ const oldest = cache.keys().next().value as K | undefined;
328
+ if (oldest === undefined) break;
329
+ const oldValue = cache.get(oldest);
330
+ cache.delete(oldest);
331
+ if (oldValue !== undefined) onEvict?.(oldest, oldValue);
332
+ }
333
+ }
334
+
190
335
  async function getTeamStore(dataDir: string) {
191
336
  const existing = teamStoreCache.get(dataDir);
192
- if (existing) return existing;
193
- const store = await initTeamStore(dataDir);
194
- teamStoreCache.set(dataDir, store);
195
- return store;
337
+ if (existing) {
338
+ cacheSet(teamStoreCache, dataDir, existing, MAX_STORE_CACHE_ENTRIES);
339
+ return existing;
340
+ }
341
+ const lease = acquireDatabase(dataDir);
342
+ try {
343
+ const store = await initTeamStore(dataDir);
344
+ teamStoreLeases.set(dataDir, lease);
345
+ cacheSet(teamStoreCache, dataDir, store, MAX_STORE_CACHE_ENTRIES, (evictedDataDir) => {
346
+ teamStoreLeases.get(evictedDataDir)?.release();
347
+ teamStoreLeases.delete(evictedDataDir);
348
+ evictIdleDatabases(MAX_STORE_CACHE_ENTRIES);
349
+ });
350
+ return store;
351
+ } catch (error) {
352
+ lease.release();
353
+ throw error;
354
+ }
196
355
  }
197
356
 
198
357
  // TeamStore is resolved per MCP session/project. Sharing one mutable
199
358
  // singleton here made project A's coordination state visible to project B
200
359
  // after a roots switch. The cache still reuses the same project's SQLite
201
360
  // handle without crossing project data directories.
361
+ const controlPlaneBaseDir = persistMod.getBaseDataDir();
362
+ const controlPlaneBaseLease = acquireDatabase(controlPlaneBaseDir);
202
363
  const statelessBindingStore = new McpBindingStore();
203
- await statelessBindingStore.init(persistMod.getBaseDataDir());
364
+ await statelessBindingStore.init(controlPlaneBaseDir);
365
+ const modernRuntimePool = new ModernMcpRuntimePool(MAX_STORE_CACHE_ENTRIES);
366
+
367
+ async function resolveModernDataDir(root: string, persisted?: { dataDir?: string }): Promise<string> {
368
+ if (persisted?.dataDir) return persisted.dataDir;
369
+ const detected = detectorMod.detectProject(root);
370
+ return detected
371
+ ? persistMod.getProjectDataDir(detected.id)
372
+ : controlPlaneBaseDir;
373
+ }
204
374
 
205
375
  // Modern MCP (2026-07-28) is stateless at the protocol layer. The official
206
376
  // v2 handler owns discovery, per-request envelopes, result metadata,
@@ -211,32 +381,64 @@ export default defineCommand({
211
381
  const persisted = handleId ? statelessBindingStore.touch(handleId) : undefined;
212
382
  const requestedRoot = requestInfo?.headers.get('mcp-project-root') ?? undefined;
213
383
  const root = persisted?.projectRoot ?? requestedRoot ?? projectRoot;
214
- return createModernMcpBridge({
215
- projectRoot: root,
216
- allowUntrackedFallback: false,
217
- deferProjectInitUntilBound: true,
218
- deferProjectRuntimeInit: true,
219
- dashboardMode: 'control-plane',
220
- dashboardPort: port,
221
- toolProfile,
222
- });
384
+ const dataDir = await resolveModernDataDir(root, persisted);
385
+ const runtimeKey = JSON.stringify([dataDir, root]);
386
+ // The SDK closes the per-request bridge after the response. The pool
387
+ // decrements the runtime reference there; the business runtime remains
388
+ // available for the next request for this project.
389
+ return modernRuntimePool.createBridge(runtimeKey, () => withBusinessRuntimeQueue(async () => {
390
+ const lease = acquireDatabase(dataDir);
391
+ try {
392
+ const runtime = await createModernMcpRuntime({
393
+ projectRoot: root,
394
+ allowUntrackedFallback: false,
395
+ deferProjectInitUntilBound: true,
396
+ deferProjectRuntimeInit: true,
397
+ dashboardMode: 'control-plane',
398
+ dashboardPort: port,
399
+ toolProfile,
400
+ });
401
+ const close = runtime.close.bind(runtime);
402
+ return {
403
+ ...runtime,
404
+ close: async () => {
405
+ try {
406
+ await close();
407
+ } finally {
408
+ lease.release();
409
+ }
410
+ },
411
+ };
412
+ } catch (error) {
413
+ lease.release();
414
+ throw error;
415
+ }
416
+ }));
223
417
  }, {
224
418
  // Legacy requests are routed to the existing sessionful implementation
225
419
  // below; this handler is strict modern-only by construction.
226
420
  legacy: 'reject',
227
421
  responseMode: 'auto',
422
+ onerror: error => {
423
+ const message = error instanceof Error ? error.message : String(error);
424
+ const stack = error instanceof Error ? error.stack : undefined;
425
+ console.error(`[memorix] modern MCP request failed: ${message}${stack ? `\n${stack}` : ''}`);
426
+ },
228
427
  });
229
428
  const modernNodeHandler = toNodeHandler(modernMcpHandler);
230
429
 
231
430
  type SessionState = {
232
431
  transport: InstanceType<typeof StreamableHTTPServerTransport>;
233
432
  server: Awaited<ReturnType<typeof createMemorixServer>>['server'];
433
+ activateProjectRuntime: Awaited<ReturnType<typeof createMemorixServer>>['activateProjectRuntime'];
234
434
  switchProject: Awaited<ReturnType<typeof createMemorixServer>>['switchProject'];
235
435
  binding: import('../../server/request-context.js').ProjectBindingController;
436
+ leases: Array<{ release: () => void }>;
236
437
  };
237
438
 
238
439
  // Session map: sessionId → transport + per-session server state
239
440
  const sessions = new Map<string, SessionState>();
441
+ let pendingSessionInitializations = 0;
240
442
 
241
443
  // Session activity tracking (for GC timeout)
242
444
  const sessionLastActivity = new Map<string, number>();
@@ -283,25 +485,6 @@ export default defineCommand({
283
485
  res.end(message);
284
486
  }
285
487
 
286
- /**
287
- * Parse JSON body from IncomingMessage
288
- */
289
- function parseBody(req: IncomingMessage): Promise<unknown> {
290
- return new Promise((resolve, reject) => {
291
- const chunks: Buffer[] = [];
292
- req.on('data', (chunk: Buffer) => chunks.push(chunk));
293
- req.on('end', () => {
294
- try {
295
- const body = Buffer.concat(chunks).toString('utf-8');
296
- resolve(body ? JSON.parse(body) : undefined);
297
- } catch (err) {
298
- reject(err);
299
- }
300
- });
301
- req.on('error', reject);
302
- });
303
- }
304
-
305
488
  /**
306
489
  * Patch ServerResponse for MCP compatibility with strict HTTP clients (rmcp/reqwest).
307
490
  *
@@ -442,7 +625,7 @@ export default defineCommand({
442
625
  // No Access-Control-Allow-Origin at all for disallowed origins —
443
626
  // browser will block the response (fail-closed).
444
627
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
445
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, Mcp-Project-Handle, Mcp-Project-Root, Mcp-Stateless, Last-Event-Id');
628
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Mcp-Session-Id, Mcp-Project-Handle, Mcp-Project-Root, Mcp-Stateless, Last-Event-Id');
446
629
  res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id, Mcp-Project-Handle, Mcp-Protocol-Version, Mcp-Method, Mcp-Name');
447
630
  res.setHeader('Mcp-Protocol-Version', MCP_SDK_COMPAT_PROTOCOL_VERSION);
448
631
  res.setHeader('Mcp-Method', req.method || 'UNKNOWN');
@@ -454,7 +637,7 @@ export default defineCommand({
454
637
  */
455
638
  async function handlePost(req: IncomingMessage, res: ServerResponse) {
456
639
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
457
- const body = await parseBody(req);
640
+ const body = await parseJsonBody(req, httpBodyLimitBytes);
458
641
 
459
642
  // Keep the pre-2026 startup probe compatible. Older MCP clients sent a
460
643
  // claim-less server/discover before initialize and expect a plain JSON
@@ -525,7 +708,11 @@ export default defineCommand({
525
708
  // Existing session — route to its transport
526
709
  sessionLastActivity.set(sessionId, Date.now());
527
710
  patchMcpResponse(res);
528
- await sessions.get(sessionId)!.transport.handleRequest(req, res, body);
711
+ const session = sessions.get(sessionId)!;
712
+ await withBusinessRuntimeQueue(async () => {
713
+ await session.activateProjectRuntime();
714
+ await session.transport.handleRequest(req, res, body);
715
+ });
529
716
  return;
530
717
  }
531
718
 
@@ -535,12 +722,34 @@ export default defineCommand({
535
722
  }
536
723
 
537
724
  if (!sessionId && isInitializeRequest(body)) {
725
+ if (sessions.size + pendingSessionInitializations >= maxHttpSessions) {
726
+ res.writeHead(429, {
727
+ 'Content-Type': 'application/json',
728
+ 'Retry-After': '5',
729
+ });
730
+ res.end(JSON.stringify({
731
+ jsonrpc: '2.0',
732
+ error: { code: -32029, message: `HTTP session limit reached (${maxHttpSessions}). Retry after an existing session closes.` },
733
+ id: (body as { id?: string | number | null }).id ?? null,
734
+ }));
735
+ return;
736
+ }
737
+ pendingSessionInitializations++;
738
+ let pendingReservationReleased = false;
739
+ const releasePendingReservation = () => {
740
+ if (pendingReservationReleased) return;
741
+ pendingReservationReleased = true;
742
+ pendingSessionInitializations = Math.max(0, pendingSessionInitializations - 1);
743
+ };
744
+
538
745
  // New session — create transport + server
539
746
  let createdState: SessionState | null = null;
747
+ const sessionLeases: Array<{ release: () => void }> = [acquireDatabase(defaultDataDir)];
540
748
  const transport = new StreamableHTTPServerTransport({
541
749
  sessionIdGenerator: () => randomUUID(),
542
750
  enableJsonResponse: true,
543
751
  onsessioninitialized: (sid: string) => {
752
+ releasePendingReservation();
544
753
  if (createdState) sessions.set(sid, createdState);
545
754
  sessionLastActivity.set(sid, Date.now());
546
755
  // Defer log — only emit if session survives past SESSION_LOG_DELAY_MS
@@ -553,6 +762,7 @@ export default defineCommand({
553
762
  });
554
763
 
555
764
  transport.onclose = () => {
765
+ releasePendingReservation();
556
766
  const sid = transport.sessionId;
557
767
  if (sid) {
558
768
  // If the deferred init log hasn't fired yet, this was a short-lived probe
@@ -569,30 +779,53 @@ export default defineCommand({
569
779
  sessionLastActivity.delete(sid);
570
780
  }
571
781
  handleTransportClose();
782
+ for (const lease of sessionLeases) lease.release();
783
+ evictIdleDatabases(MAX_STORE_CACHE_ENTRIES);
572
784
  };
573
785
 
574
786
  // Legacy Mcp-Session-Id only routes requests. Project ownership lives
575
787
  // in this transport-neutral binding object, ready for stateless MCP.
576
788
  const binding = createProjectBindingController(projectRoot);
577
789
  // Create a fresh MCP server for this session (with shared team state)
578
- const { server, switchProject, handleTransportClose } = await createMemorixServer(
579
- projectRoot,
580
- undefined,
581
- undefined,
582
- {
583
- allowUntrackedFallback: false,
584
- deferProjectInitUntilBound: true,
585
- // Same as stdio: initialize/tools/list must not hydrate 40k+
586
- // observations on the HTTP thread. /health and the LaunchAgent
587
- // watchdog die if createMemorixServer blocks here.
588
- deferProjectRuntimeInit: true,
589
- dashboardMode: 'control-plane',
590
- dashboardPort: port,
591
- toolProfile,
592
- projectBinding: binding,
593
- },
594
- );
595
- createdState = { transport, server, switchProject, binding };
790
+ let server: Awaited<ReturnType<typeof createMemorixServer>>['server'];
791
+ let activateProjectRuntime: Awaited<ReturnType<typeof createMemorixServer>>['activateProjectRuntime'];
792
+ let handleTransportClose: Awaited<ReturnType<typeof createMemorixServer>>['handleTransportClose'];
793
+ let getProjectDataDirForSession: Awaited<ReturnType<typeof createMemorixServer>>['getProjectDataDir'];
794
+ let switchProject: Awaited<ReturnType<typeof createMemorixServer>>['switchProject'];
795
+ try {
796
+ ({ server, activateProjectRuntime, switchProject, handleTransportClose, getProjectDataDir: getProjectDataDirForSession } = await createMemorixServer(
797
+ projectRoot,
798
+ undefined,
799
+ undefined,
800
+ {
801
+ allowUntrackedFallback: false,
802
+ deferProjectInitUntilBound: true,
803
+ // Same as stdio: initialize/tools/list must not hydrate 40k+
804
+ // observations on the HTTP thread. /health and the LaunchAgent
805
+ // watchdog die if createMemorixServer blocks here.
806
+ deferProjectRuntimeInit: true,
807
+ dashboardMode: 'control-plane',
808
+ dashboardPort: port,
809
+ toolProfile,
810
+ projectBinding: binding,
811
+ },
812
+ ));
813
+ } catch (error) {
814
+ releasePendingReservation();
815
+ for (const lease of sessionLeases) lease.release();
816
+ throw error;
817
+ }
818
+ const switchProjectWithLease = async (newCwd: string, source?: import('../../server/request-context.js').ProjectBindingSource): Promise<boolean> => {
819
+ const before = getProjectDataDirForSession();
820
+ const changed = await switchProject(newCwd, source);
821
+ const after = getProjectDataDirForSession();
822
+ if (changed && after !== before) {
823
+ sessionLeases[0].release();
824
+ sessionLeases[0] = acquireDatabase(after);
825
+ }
826
+ return changed;
827
+ };
828
+ createdState = { transport, server, activateProjectRuntime, switchProject: switchProjectWithLease, binding, leases: sessionLeases };
596
829
  await server.connect(transport);
597
830
 
598
831
  const tryRootsSwitch = async () => {
@@ -639,7 +872,16 @@ export default defineCommand({
639
872
  } catch { /* optional */ }
640
873
 
641
874
  patchMcpResponse(res);
642
- await transport.handleRequest(req, res, body);
875
+ try {
876
+ await withBusinessRuntimeQueue(async () => {
877
+ await activateProjectRuntime();
878
+ await transport.handleRequest(req, res, body);
879
+ });
880
+ } finally {
881
+ // A malformed probe can finish without creating a session; do not
882
+ // let it consume an admission slot forever.
883
+ if (!transport.sessionId) releasePendingReservation();
884
+ }
643
885
  // Do NOT proactively call listRoots() after session establishment —
644
886
  // this violates MCP SEP-2260 (server-initiated request must be associated
645
887
  // with a client request). Codex and other strict clients treat standalone
@@ -675,6 +917,8 @@ export default defineCommand({
675
917
  return;
676
918
  }
677
919
 
920
+ // GET may keep an SSE stream open; it does not enter the business tool
921
+ // layer and must not occupy the singleton runtime gate.
678
922
  await sessions.get(sessionId)!.transport.handleRequest(req, res);
679
923
  }
680
924
 
@@ -693,7 +937,7 @@ export default defineCommand({
693
937
  return;
694
938
  }
695
939
 
696
- await sessions.get(sessionId)!.transport.handleRequest(req, res);
940
+ await withBusinessRuntimeQueue(() => sessions.get(sessionId)!.transport.handleRequest(req, res));
697
941
  }
698
942
 
699
943
  // Create HTTP server
@@ -725,8 +969,9 @@ export default defineCommand({
725
969
 
726
970
  // Cache resolved project data dirs to avoid repeated fs lookups
727
971
  const projectDataDirCache = new Map<string, string>();
728
- projectDataDirCache.set(defaultProject.id, defaultDataDir);
972
+ cacheSet(projectDataDirCache, defaultProject.id, defaultDataDir, MAX_PROJECT_CACHE_ENTRIES);
729
973
  const dashboardObservationStores = new Map<string, ObservationStore>();
974
+ const dashboardStoreLeases = new Map<string, { release: () => void }>();
730
975
 
731
976
  /** Resolve ?project= query param to { projectId, projectName, dataDir } */
732
977
  async function resolveRequestProject(url: URL): Promise<{
@@ -742,7 +987,7 @@ export default defineCommand({
742
987
  let dataDir = projectDataDirCache.get(requestedId);
743
988
  if (!dataDir) {
744
989
  dataDir = await getProjectDataDir(requestedId);
745
- projectDataDirCache.set(requestedId, dataDir);
990
+ cacheSet(projectDataDirCache, requestedId, dataDir, MAX_PROJECT_CACHE_ENTRIES);
746
991
  }
747
992
  const name = requestedId.split('/').pop() || requestedId;
748
993
  return { projectId: requestedId, projectName: name, dataDir };
@@ -750,11 +995,26 @@ export default defineCommand({
750
995
 
751
996
  async function getDashboardObservationStore(dataDir: string): Promise<ObservationStore> {
752
997
  const cached = dashboardObservationStores.get(dataDir);
753
- if (cached) return cached;
998
+ if (cached) {
999
+ cacheSet(dashboardObservationStores, dataDir, cached, MAX_STORE_CACHE_ENTRIES);
1000
+ return cached;
1001
+ }
754
1002
  const { createObservationStore } = await import('../../store/obs-store.js');
755
- const store = await createObservationStore(dataDir);
756
- dashboardObservationStores.set(dataDir, store);
757
- return store;
1003
+ const lease = acquireDatabase(dataDir);
1004
+ try {
1005
+ const store = await createObservationStore(dataDir);
1006
+ dashboardStoreLeases.set(dataDir, lease);
1007
+ cacheSet(dashboardObservationStores, dataDir, store, MAX_STORE_CACHE_ENTRIES, (evictedDataDir, evictedStore) => {
1008
+ try { evictedStore.close(); } catch { /* best-effort */ }
1009
+ dashboardStoreLeases.get(evictedDataDir)?.release();
1010
+ dashboardStoreLeases.delete(evictedDataDir);
1011
+ evictIdleDatabases(MAX_STORE_CACHE_ENTRIES);
1012
+ });
1013
+ return store;
1014
+ } catch (error) {
1015
+ lease.release();
1016
+ throw error;
1017
+ }
758
1018
  }
759
1019
 
760
1020
  async function loadDashboardObservations(dataDir: string) {
@@ -861,7 +1121,7 @@ export default defineCommand({
861
1121
  sendJson({ error: 'Maintenance actions require POST.' }, 405);
862
1122
  return;
863
1123
  }
864
- const bodyValue = await parseBody(req);
1124
+ const bodyValue = await parseJsonBody(req, Math.min(httpBodyLimitBytes, 1024 * 1024));
865
1125
  const body = bodyValue && typeof bodyValue === 'object' && !Array.isArray(bodyValue)
866
1126
  ? bodyValue as Record<string, unknown>
867
1127
  : {};
@@ -1023,9 +1283,9 @@ export default defineCommand({
1023
1283
 
1024
1284
  if (apiPath === '/stats') {
1025
1285
  const { projectId: statsProjectId, dataDir: statsDataDir } = await resolveRequestProject(url);
1026
- const { initGraphStore, getGraphStore } = await import('../../store/graph-store.js');
1027
- await initGraphStore(statsDataDir);
1028
- const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
1286
+ const { initGraphStore } = await import('../../store/graph-store.js');
1287
+ const gStore = await initGraphStore(statsDataDir, statsProjectId);
1288
+ const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
1029
1289
 
1030
1290
  const observations = await loadDashboardProjectObservations(statsDataDir, statsProjectId, 'active');
1031
1291
  const feedback = await loadProjectFeedback(statsDataDir, statsProjectId);
@@ -1210,9 +1470,9 @@ export default defineCommand({
1210
1470
 
1211
1471
  if (apiPath === '/graph') {
1212
1472
  const { projectId: graphProjectId, dataDir: graphDataDir } = await resolveRequestProject(url);
1213
- const { initGraphStore, getGraphStore } = await import('../../store/graph-store.js');
1214
- await initGraphStore(graphDataDir);
1215
- const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
1473
+ const { initGraphStore } = await import('../../store/graph-store.js');
1474
+ const gStore = await initGraphStore(graphDataDir, graphProjectId);
1475
+ const fullGraph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
1216
1476
 
1217
1477
  const allObs = await loadDashboardProjectObservations(graphDataDir, graphProjectId, 'active');
1218
1478
  const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
@@ -1298,15 +1558,15 @@ export default defineCommand({
1298
1558
  const { projectId: kgProjectId, dataDir: kgDataDir } = await resolveRequestProject(url);
1299
1559
  const { generateKnowledgeGraph } = await import('../../wiki/knowledge-graph.js');
1300
1560
  const { initMiniSkillStore, getMiniSkillStore } = await import('../../store/mini-skill-store.js');
1301
- const { initGraphStore, getGraphStore } = await import('../../store/graph-store.js');
1561
+ const { initGraphStore } = await import('../../store/graph-store.js');
1302
1562
 
1303
1563
  await initMiniSkillStore(kgDataDir);
1304
- await initGraphStore(kgDataDir);
1564
+ const gStore = await initGraphStore(kgDataDir, kgProjectId);
1305
1565
 
1306
1566
  const allObs = await loadDashboardProjectObservations(kgDataDir, kgProjectId, 'active');
1307
1567
  const skills = await getMiniSkillStore().loadByProject(kgProjectId);
1308
1568
 
1309
- const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
1569
+ const fullGraph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
1310
1570
  const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
1311
1571
 
1312
1572
  const graph = generateKnowledgeGraph({
@@ -1634,9 +1894,8 @@ export default defineCommand({
1634
1894
  await store.remove(obsId);
1635
1895
  // Sync: clean up graph entity references
1636
1896
  try {
1637
- const { initGraphStore, getGraphStore } = await import('../../store/graph-store.js');
1638
- await initGraphStore(delDataDir);
1639
- const gStore = getGraphStore();
1897
+ const { initGraphStore } = await import('../../store/graph-store.js');
1898
+ const gStore = await initGraphStore(delDataDir, delProjectId);
1640
1899
  const prefix = `[#${obsId}] `;
1641
1900
  const deletions: { entityName: string; observations: string[] }[] = [];
1642
1901
  for (const entity of gStore.loadEntities()) {
@@ -1653,9 +1912,9 @@ export default defineCommand({
1653
1912
  // GET /api/export — export observations as JSON
1654
1913
  if (apiPath === '/export') {
1655
1914
  const { projectId: expProjectId, projectName: expProjectName, dataDir: expDataDir } = await resolveRequestProject(url);
1656
- const { initGraphStore, getGraphStore } = await import('../../store/graph-store.js');
1657
- await initGraphStore(expDataDir);
1658
- const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
1915
+ const { initGraphStore } = await import('../../store/graph-store.js');
1916
+ const gStore = await initGraphStore(expDataDir, expProjectId);
1917
+ const fullGraph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
1659
1918
  const observations = await loadDashboardProjectObservations(expDataDir, expProjectId, 'active');
1660
1919
  const expStore = await getDashboardObservationStore(expDataDir);
1661
1920
  const nextId = await expStore.loadIdCounter();
@@ -1694,7 +1953,8 @@ export default defineCommand({
1694
1953
  }, 503);
1695
1954
  return;
1696
1955
  }
1697
- sendJson({ error: err instanceof Error ? err.message : 'Unknown error' }, 500);
1956
+ const status = err instanceof HttpPayloadTooLargeError ? err.statusCode : 500;
1957
+ sendJson({ error: err instanceof Error ? err.message : 'Unknown error' }, status);
1698
1958
  }
1699
1959
  }
1700
1960
 
@@ -1711,6 +1971,15 @@ export default defineCommand({
1711
1971
 
1712
1972
  const url = new URL(req.url || '/', `http://localhost:${port}`);
1713
1973
 
1974
+ if (bindSecurity.requiresAuth && url.pathname !== '/health' && !isAuthorizedHttpRequest(req, httpAuthToken)) {
1975
+ res.writeHead(401, {
1976
+ 'Content-Type': 'application/json',
1977
+ 'WWW-Authenticate': 'Bearer',
1978
+ });
1979
+ res.end(JSON.stringify({ error: 'Authentication required' }));
1980
+ return;
1981
+ }
1982
+
1714
1983
  // Lightweight health check — responds immediately, no heavy init required.
1715
1984
  // This is the readiness signal for background start / status.
1716
1985
  if (url.pathname === '/health') {
@@ -1736,6 +2005,18 @@ export default defineCommand({
1736
2005
  arrayBuffers: memory.arrayBuffers,
1737
2006
  heapLimit: heap.heap_size_limit,
1738
2007
  },
2008
+ sqlite: getDatabaseStats(),
2009
+ sessions: {
2010
+ active: sessions.size,
2011
+ pending: pendingSessionInitializations,
2012
+ limit: maxHttpSessions,
2013
+ },
2014
+ modernRuntime: modernRuntimePool.stats(),
2015
+ eventLoop: {
2016
+ meanMs: Number.isFinite(eventLoopDelay.mean) ? eventLoopDelay.mean / 1e6 : null,
2017
+ p95Ms: eventLoopDelay.percentile(95) / 1e6,
2018
+ maxMs: eventLoopDelay.max / 1e6,
2019
+ },
1739
2020
  embedding: getEmbeddingRuntimeHealth(),
1740
2021
  }));
1741
2022
  return;
@@ -1792,10 +2073,15 @@ export default defineCommand({
1792
2073
  } catch (err) {
1793
2074
  console.error('[memorix] HTTP handler error:', err);
1794
2075
  if (!res.headersSent) {
1795
- res.writeHead(500, { 'Content-Type': 'application/json' });
2076
+ const status = err instanceof HttpPayloadTooLargeError ? err.statusCode : 500;
2077
+ const message = err instanceof Error ? err.message : 'Internal server error';
2078
+ res.writeHead(status, { 'Content-Type': 'application/json' });
1796
2079
  res.end(JSON.stringify({
1797
2080
  jsonrpc: '2.0',
1798
- error: { code: -32603, message: 'Internal server error' },
2081
+ error: {
2082
+ code: status === 413 ? -32013 : -32603,
2083
+ message: status === 413 ? message : 'Internal server error',
2084
+ },
1799
2085
  id: null,
1800
2086
  }));
1801
2087
  }
@@ -1819,7 +2105,13 @@ export default defineCommand({
1819
2105
  httpServer.keepAliveTimeout = 60_000;
1820
2106
  httpServer.headersTimeout = 65_000;
1821
2107
 
1822
- httpServer.listen(port, host, () => {
2108
+ const listenPromise = new Promise<void>((resolve, reject) => {
2109
+ const onError = (error: Error) => {
2110
+ if (typeof (httpServer as any).off === 'function') httpServer.off('listening', onListening);
2111
+ reject(error);
2112
+ };
2113
+ const onListening = () => {
2114
+ if (typeof (httpServer as any).off === 'function') httpServer.off('error', onError);
1823
2115
  httpReadyAt = new Date().toISOString();
1824
2116
  // Write readiness file — background start polls this for out-of-band readiness detection
1825
2117
  try {
@@ -1841,7 +2133,36 @@ export default defineCommand({
1841
2133
  // Fire-and-forget: background update check after server is fully started.
1842
2134
  // Default is notify-only. All output goes to stderr only. Failures never affect the server.
1843
2135
  import('../update-checker.js').then(m => m.checkForUpdates()).catch(() => {});
2136
+ resolve();
2137
+ };
2138
+ if (typeof (httpServer as any).once === 'function') {
2139
+ httpServer.once('error', onError);
2140
+ httpServer.once('listening', onListening);
2141
+ httpServer.listen(port, host);
2142
+ } else {
2143
+ // Lightweight test doubles may only implement listen(callback).
2144
+ httpServer.listen(port, host, onListening);
2145
+ }
1844
2146
  });
2147
+ try {
2148
+ await listenPromise;
2149
+ } catch (error) {
2150
+ console.error(`[memorix] HTTP server failed to listen on ${host}:${port}: ${error instanceof Error ? error.message : String(error)}`);
2151
+ for (const store of dashboardObservationStores.values()) {
2152
+ try { store.close(); } catch { /* best-effort */ }
2153
+ }
2154
+ dashboardObservationStores.clear();
2155
+ for (const lease of dashboardStoreLeases.values()) lease.release();
2156
+ dashboardStoreLeases.clear();
2157
+ for (const lease of teamStoreLeases.values()) lease.release();
2158
+ teamStoreLeases.clear();
2159
+ controlPlaneBaseLease.release();
2160
+ await modernRuntimePool.close();
2161
+ eventLoopDelay.disable();
2162
+ const { closeAllDatabases } = await import('../../store/sqlite-db.js');
2163
+ closeAllDatabases();
2164
+ throw error;
2165
+ }
1845
2166
 
1846
2167
  // Session timeout GC — close sessions idle past the configured threshold.
1847
2168
  // MEMORIX_SESSION_TIMEOUT_MS lets operators work around HTTP clients that do
@@ -1851,6 +2172,9 @@ export default defineCommand({
1851
2172
  maybeProbeSummary(); // Flush suppressed probe count periodically
1852
2173
  const now = Date.now();
1853
2174
  forgetExpiredSessions(now);
2175
+ statelessBindingStore.cleanupExpired(now);
2176
+ evictIdleDatabases(MAX_STORE_CACHE_ENTRIES);
2177
+ void modernRuntimePool.evictIdle();
1854
2178
  if (SESSION_TIMEOUT_MS === 0) return;
1855
2179
  for (const [sid, state] of sessions) {
1856
2180
  const lastActive = sessionLastActivity.get(sid) ?? 0;
@@ -1866,7 +2190,10 @@ export default defineCommand({
1866
2190
  gcInterval.unref(); // Don't prevent process exit
1867
2191
 
1868
2192
  // Graceful shutdown
2193
+ let shutdownStarted = false;
1869
2194
  const shutdown = async () => {
2195
+ if (shutdownStarted) return;
2196
+ shutdownStarted = true;
1870
2197
  if (suppressedProbeCount > 0) {
1871
2198
  console.error(`[memorix] ${suppressedProbeCount} probe connection(s) suppressed during this session`);
1872
2199
  }
@@ -1883,6 +2210,10 @@ export default defineCommand({
1883
2210
  try { store.close(); } catch { /* best-effort */ }
1884
2211
  }
1885
2212
  dashboardObservationStores.clear();
2213
+ for (const lease of dashboardStoreLeases.values()) lease.release();
2214
+ dashboardStoreLeases.clear();
2215
+ for (const lease of teamStoreLeases.values()) lease.release();
2216
+ teamStoreLeases.clear();
1886
2217
  for (const [sid, state] of sessions) {
1887
2218
  try {
1888
2219
  await state.transport.close();
@@ -1891,7 +2222,20 @@ export default defineCommand({
1891
2222
  console.error(`[memorix] Error closing session ${sid}:`, err);
1892
2223
  }
1893
2224
  }
1894
- httpServer.close();
2225
+ await modernRuntimePool.close();
2226
+ clearInterval(gcInterval);
2227
+ clearInterval(heartbeatInterval);
2228
+ eventLoopDelay.disable();
2229
+ await new Promise<void>((resolve) => {
2230
+ if (!httpServer.listening) {
2231
+ resolve();
2232
+ return;
2233
+ }
2234
+ httpServer.close(() => resolve());
2235
+ });
2236
+ const { closeAllDatabases } = await import('../../store/sqlite-db.js');
2237
+ controlPlaneBaseLease.release();
2238
+ closeAllDatabases();
1895
2239
  process.exit(0);
1896
2240
  };
1897
2241
 
@@ -1943,6 +2287,13 @@ export default defineCommand({
1943
2287
  /** @internal */
1944
2288
  export const _testing = {
1945
2289
  DEFAULT_SESSION_TIMEOUT_MS,
2290
+ DEFAULT_HTTP_BODY_LIMIT_BYTES,
2291
+ MAX_HTTP_BODY_LIMIT_BYTES,
2292
+ MIN_HTTP_BODY_LIMIT_BYTES,
1946
2293
  parseSessionTimeoutMs,
2294
+ parseMaxHttpSessions,
2295
+ parseHttpBodyLimit,
2296
+ isLoopbackHost,
2297
+ validateHttpBindSecurity,
1947
2298
  parseTcpPortOrReport,
1948
2299
  };