@joshuaswarren/openclaw-engram 9.2.6 → 9.2.7

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/dist/index.js CHANGED
@@ -6948,7 +6948,7 @@ var GraphDashboardServer = class {
6948
6948
 
6949
6949
  // src/access-http.ts
6950
6950
  import { createServer as createServer3 } from "http";
6951
- import { randomUUID, timingSafeEqual as timingSafeEqual2 } from "crypto";
6951
+ import { randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual2 } from "crypto";
6952
6952
  import { AsyncLocalStorage } from "async_hooks";
6953
6953
  import { existsSync } from "fs";
6954
6954
  import { readFile as readFile14 } from "fs/promises";
@@ -6957,6 +6957,7 @@ import { fileURLToPath, URL as URL3 } from "url";
6957
6957
 
6958
6958
  // src/access-mcp.ts
6959
6959
  import { readFile as readFile13 } from "fs/promises";
6960
+ import { randomUUID } from "crypto";
6960
6961
  var MCP_PROTOCOL_VERSION = "2024-11-05";
6961
6962
  async function getMcpServerVersion() {
6962
6963
  const envVersion = readEnvVar("OPENCLAW_ENGRAM_VERSION")?.trim() || readEnvVar("npm_package_version")?.trim();
@@ -7611,6 +7612,33 @@ var EngramMcpServer = class {
7611
7612
  flushTask = null;
7612
7613
  tools;
7613
7614
  authenticatedPrincipal;
7615
+ /**
7616
+ * MCP client info keyed by server-assigned session ID. On each `initialize`
7617
+ * handshake the server generates a UUID, stores the client's clientInfo
7618
+ * against it, and returns the ID as `Mcp-Session-Id` in the response
7619
+ * metadata. Subsequent requests from the same client include this header,
7620
+ * allowing per-session clientInfo lookup without cross-session leaks.
7621
+ */
7622
+ clientInfoBySession = /* @__PURE__ */ new Map();
7623
+ /**
7624
+ * Session IDs generated during initialize, keyed by caller-supplied correlation
7625
+ * ID (unique per HTTP request) to avoid collisions when multiple clients send
7626
+ * initialize with the same JSON-RPC id concurrently.
7627
+ */
7628
+ initSessionIds = /* @__PURE__ */ new Map();
7629
+ /** Get clientInfo for a specific MCP session. Returns undefined for non-MCP requests. */
7630
+ getClientInfo(sessionId) {
7631
+ if (sessionId) {
7632
+ return this.clientInfoBySession.get(sessionId);
7633
+ }
7634
+ return void 0;
7635
+ }
7636
+ /** Pop the session ID generated during an initialize handshake, keyed by correlation ID. */
7637
+ popInitSessionId(correlationId) {
7638
+ const sid = this.initSessionIds.get(correlationId);
7639
+ if (sid !== void 0) this.initSessionIds.delete(correlationId);
7640
+ return sid;
7641
+ }
7614
7642
  async handleRequest(request, options) {
7615
7643
  const id = request.id ?? null;
7616
7644
  const method = request.method ?? "";
@@ -7619,7 +7647,20 @@ var EngramMcpServer = class {
7619
7647
  return { jsonrpc: "2.0", id, result: {} };
7620
7648
  }
7621
7649
  if (method === "initialize") {
7650
+ const params = request.params ?? {};
7651
+ const rawClientInfo = params.clientInfo;
7652
+ const newSessionId = randomUUID();
7653
+ if (rawClientInfo && typeof rawClientInfo.name === "string") {
7654
+ const info = { name: rawClientInfo.name, version: rawClientInfo.version };
7655
+ this.clientInfoBySession.set(newSessionId, info);
7656
+ if (this.clientInfoBySession.size > 1e3) {
7657
+ const firstKey = this.clientInfoBySession.keys().next().value;
7658
+ if (firstKey) this.clientInfoBySession.delete(firstKey);
7659
+ }
7660
+ }
7622
7661
  const version = await getMcpServerVersion();
7662
+ const corrId = options?.correlationId;
7663
+ if (corrId) this.initSessionIds.set(corrId, newSessionId);
7623
7664
  return {
7624
7665
  jsonrpc: "2.0",
7625
7666
  id,
@@ -8240,123 +8281,99 @@ function validateRequest(schemaName, body) {
8240
8281
  return { success: false, error: formatZodError(result.error) };
8241
8282
  }
8242
8283
 
8284
+ // src/adapters/types.ts
8285
+ function headerValue(headers, key) {
8286
+ const raw = headers[key];
8287
+ const value = Array.isArray(raw) ? raw[0] : raw;
8288
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
8289
+ }
8290
+
8243
8291
  // src/adapters/claude-code.ts
8244
8292
  var ClaudeCodeAdapter = class {
8245
8293
  id = "claude-code";
8246
8294
  matches(context) {
8247
- const clientName = context.clientInfo?.name?.toLowerCase() ?? "";
8248
- if (clientName.includes("claude")) return true;
8249
- const sessionHeader = headerValue(context.headers, "x-claude-session-id");
8250
- if (sessionHeader) return true;
8295
+ if (context.clientInfo?.name === "claude-code") return true;
8296
+ const ua = headerValue(context.headers, "user-agent");
8297
+ if (ua && ua.toLowerCase().startsWith("claude-code/")) return true;
8298
+ const clientId = headerValue(context.headers, "x-engram-client-id");
8299
+ if (clientId?.toLowerCase() === "claude-code") return true;
8251
8300
  return false;
8252
8301
  }
8253
8302
  resolveIdentity(context) {
8254
- const sessionId = headerValue(context.headers, "x-claude-session-id");
8255
- const projectPath = headerValue(context.headers, "x-claude-project-path");
8256
- const namespace = projectPath ? slugify(projectPath) : "claude-code";
8257
- const principal = headerValue(context.headers, "x-engram-principal") || context.clientInfo?.name || "claude-code";
8303
+ const mcpSessionId = headerValue(context.headers, "mcp-session-id");
8304
+ const principal = headerValue(context.headers, "x-engram-principal") || "claude-code";
8305
+ const namespace = headerValue(context.headers, "x-engram-namespace") || "claude-code";
8258
8306
  return {
8259
8307
  namespace,
8260
8308
  principal,
8261
- sessionKey: sessionId ?? context.sessionKey,
8309
+ sessionKey: mcpSessionId ?? context.sessionKey,
8262
8310
  adapterId: this.id
8263
8311
  };
8264
8312
  }
8265
8313
  };
8266
- function headerValue(headers, key) {
8267
- const raw = headers[key];
8268
- const value = Array.isArray(raw) ? raw[0] : raw;
8269
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
8270
- }
8271
- function slugify(s) {
8272
- let slug = s.toLowerCase().replace(/[^a-z0-9]+/g, "-");
8273
- let start = 0;
8274
- while (start < slug.length && slug[start] === "-") start++;
8275
- let end = slug.length;
8276
- while (end > start && slug[end - 1] === "-") end--;
8277
- return slug.slice(start, end).slice(0, 80) || "claude-code";
8278
- }
8279
8314
 
8280
8315
  // src/adapters/codex.ts
8281
8316
  var CodexAdapter = class {
8282
8317
  id = "codex";
8283
8318
  matches(context) {
8319
+ if (context.clientInfo?.name === "codex-mcp-client") return true;
8284
8320
  const clientName = context.clientInfo?.name?.toLowerCase() ?? "";
8285
- if (clientName.includes("codex")) return true;
8286
- const agentHeader = headerValue2(context.headers, "x-codex-agent-name");
8287
- if (agentHeader) return true;
8321
+ if (clientName.includes("codex") && clientName !== "codex-mcp-client") return true;
8322
+ const clientId = headerValue(context.headers, "x-engram-client-id");
8323
+ if (clientId?.toLowerCase() === "codex") return true;
8288
8324
  return false;
8289
8325
  }
8290
8326
  resolveIdentity(context) {
8291
- const agentName = headerValue2(context.headers, "x-codex-agent-name");
8292
- const projectDir = headerValue2(context.headers, "x-codex-project-dir");
8293
- const namespace = projectDir ? slugify2(projectDir) : "codex";
8294
- const principal = headerValue2(context.headers, "x-engram-principal") || agentName || context.clientInfo?.name || "codex";
8327
+ const mcpSessionId = headerValue(context.headers, "mcp-session-id");
8328
+ const principal = headerValue(context.headers, "x-engram-principal") || "codex";
8329
+ const namespace = headerValue(context.headers, "x-engram-namespace") || "codex";
8295
8330
  return {
8296
8331
  namespace,
8297
8332
  principal,
8298
- sessionKey: context.sessionKey,
8333
+ sessionKey: mcpSessionId ?? context.sessionKey,
8299
8334
  adapterId: this.id
8300
8335
  };
8301
8336
  }
8302
8337
  };
8303
- function headerValue2(headers, key) {
8304
- const raw = headers[key];
8305
- const value = Array.isArray(raw) ? raw[0] : raw;
8306
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
8307
- }
8308
- function slugify2(s) {
8309
- let slug = s.toLowerCase().replace(/[^a-z0-9]+/g, "-");
8310
- let start = 0;
8311
- while (start < slug.length && slug[start] === "-") start++;
8312
- let end = slug.length;
8313
- while (end > start && slug[end - 1] === "-") end--;
8314
- return slug.slice(start, end).slice(0, 80) || "codex";
8315
- }
8316
8338
 
8317
8339
  // src/adapters/replit.ts
8318
8340
  var ReplitAdapter = class {
8319
8341
  id = "replit";
8320
8342
  matches(context) {
8321
- if (headerValue3(context.headers, "x-replit-project-id")) return true;
8322
- if (headerValue3(context.headers, "x-replit-user-id")) return true;
8343
+ const clientId = headerValue(context.headers, "x-engram-client-id");
8344
+ if (clientId?.toLowerCase() === "replit") return true;
8345
+ const clientName = context.clientInfo?.name?.toLowerCase() ?? "";
8346
+ if (clientName.includes("replit")) return true;
8323
8347
  return false;
8324
8348
  }
8325
8349
  resolveIdentity(context) {
8326
- const projectId = headerValue3(context.headers, "x-replit-project-id");
8327
- const userId = headerValue3(context.headers, "x-replit-user-id");
8328
- const namespace = projectId ? `replit-${sanitizeId(projectId)}` : "replit";
8329
- const principal = headerValue3(context.headers, "x-engram-principal") || (userId ? `replit-user-${sanitizeId(userId)}` : "replit-agent");
8350
+ const mcpSessionId = headerValue(context.headers, "mcp-session-id");
8351
+ const principal = headerValue(context.headers, "x-engram-principal") || "replit-agent";
8352
+ const namespace = headerValue(context.headers, "x-engram-namespace") || "replit";
8330
8353
  return {
8331
8354
  namespace,
8332
8355
  principal,
8333
- sessionKey: context.sessionKey,
8356
+ sessionKey: mcpSessionId ?? context.sessionKey,
8334
8357
  adapterId: this.id
8335
8358
  };
8336
8359
  }
8337
8360
  };
8338
- function headerValue3(headers, key) {
8339
- const raw = headers[key];
8340
- const value = Array.isArray(raw) ? raw[0] : raw;
8341
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
8342
- }
8343
- function sanitizeId(s) {
8344
- return s.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 64);
8345
- }
8346
8361
 
8347
8362
  // src/adapters/hermes.ts
8348
8363
  var HermesAdapter = class {
8349
8364
  id = "hermes";
8350
8365
  matches(context) {
8351
- if (headerValue4(context.headers, "x-hermes-session-id")) return true;
8352
- if (headerValue4(context.headers, "x-hermes-profile")) return true;
8366
+ if (headerValue(context.headers, "x-hermes-session-id")) return true;
8367
+ const clientId = headerValue(context.headers, "x-engram-client-id");
8368
+ if (clientId?.toLowerCase() === "hermes") return true;
8369
+ const clientName = context.clientInfo?.name?.toLowerCase() ?? "";
8370
+ if (clientName.includes("hermes")) return true;
8353
8371
  return false;
8354
8372
  }
8355
8373
  resolveIdentity(context) {
8356
- const sessionId = headerValue4(context.headers, "x-hermes-session-id");
8357
- const profile = headerValue4(context.headers, "x-hermes-profile");
8358
- const namespace = profile ? slugify3(profile) : "hermes";
8359
- const principal = headerValue4(context.headers, "x-engram-principal") || profile || "hermes-agent";
8374
+ const sessionId = headerValue(context.headers, "x-hermes-session-id");
8375
+ const principal = headerValue(context.headers, "x-engram-principal") || "hermes-agent";
8376
+ const namespace = headerValue(context.headers, "x-engram-namespace") || "hermes";
8360
8377
  return {
8361
8378
  namespace,
8362
8379
  principal,
@@ -8365,19 +8382,6 @@ var HermesAdapter = class {
8365
8382
  };
8366
8383
  }
8367
8384
  };
8368
- function headerValue4(headers, key) {
8369
- const raw = headers[key];
8370
- const value = Array.isArray(raw) ? raw[0] : raw;
8371
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
8372
- }
8373
- function slugify3(s) {
8374
- let slug = s.toLowerCase().replace(/[^a-z0-9]+/g, "-");
8375
- let start = 0;
8376
- while (start < slug.length && slug[start] === "-") start++;
8377
- let end = slug.length;
8378
- while (end > start && slug[end - 1] === "-") end--;
8379
- return slug.slice(start, end).slice(0, 80) || "hermes";
8380
- }
8381
8385
 
8382
8386
  // src/adapters/registry.ts
8383
8387
  var AdapterRegistry = class {
@@ -8494,7 +8498,7 @@ var EngramAccessHttpServer = class {
8494
8498
  }
8495
8499
  if (this.server) return this.status();
8496
8500
  const server = createServer3((req, res) => {
8497
- const correlationId = randomUUID();
8501
+ const correlationId = randomUUID2();
8498
8502
  correlationIdStore.run(correlationId, () => {
8499
8503
  void this.handle(req, res, correlationId).catch((err) => {
8500
8504
  log.debug(`engram access HTTP request failed [${correlationId}]: ${err}`);
@@ -8558,30 +8562,60 @@ var EngramAccessHttpServer = class {
8558
8562
  }
8559
8563
  /**
8560
8564
  * Resolve the adapter identity for the incoming request.
8565
+ * Includes MCP clientInfo from the last initialize handshake if available.
8561
8566
  * Returns null if no adapter matches or adapters are disabled.
8562
8567
  */
8563
8568
  resolveAdapterIdentity(req) {
8564
8569
  if (!this.adapterRegistry) return null;
8570
+ const sessionId = (() => {
8571
+ const raw = req.headers["mcp-session-id"];
8572
+ return typeof raw === "string" ? raw.trim() : void 0;
8573
+ })();
8565
8574
  return this.adapterRegistry.resolve({
8566
- headers: req.headers
8575
+ headers: req.headers,
8576
+ clientInfo: this.mcpServer.getClientInfo(sessionId)
8567
8577
  });
8568
8578
  }
8569
- resolveRequestPrincipal(req) {
8579
+ /** Cache for per-request identity resolution (avoids double adapter resolution) */
8580
+ identityCache = /* @__PURE__ */ new WeakMap();
8581
+ /** Resolve principal and namespace from request headers and adapter identity */
8582
+ resolveRequestIdentity(req) {
8583
+ const cached = this.identityCache.get(req);
8584
+ if (cached) return cached;
8585
+ let principal;
8586
+ let namespace;
8570
8587
  if (this.trustPrincipalHeader) {
8571
- const headerValue5 = req.headers["x-engram-principal"];
8572
- const raw = Array.isArray(headerValue5) ? headerValue5[0] : headerValue5;
8588
+ const headerVal = req.headers["x-engram-principal"];
8589
+ const raw = Array.isArray(headerVal) ? headerVal[0] : headerVal;
8573
8590
  if (typeof raw === "string") {
8574
8591
  const trimmed = raw.trim();
8575
8592
  if (trimmed.length > 0) {
8576
- return trimmed;
8593
+ principal = trimmed;
8577
8594
  }
8578
8595
  }
8579
8596
  }
8580
8597
  const adapterIdentity = this.resolveAdapterIdentity(req);
8581
8598
  if (adapterIdentity) {
8582
- return adapterIdentity.principal;
8599
+ if (!principal) {
8600
+ principal = adapterIdentity.principal;
8601
+ }
8602
+ namespace = adapterIdentity.namespace;
8583
8603
  }
8584
- return this.authenticatedPrincipal;
8604
+ if (!principal) {
8605
+ principal = this.authenticatedPrincipal;
8606
+ }
8607
+ const result = { principal, namespace };
8608
+ this.identityCache.set(req, result);
8609
+ return result;
8610
+ }
8611
+ resolveRequestPrincipal(req) {
8612
+ return this.resolveRequestIdentity(req).principal;
8613
+ }
8614
+ /** Resolve namespace: only use the explicit body value. Adapter-inferred namespace
8615
+ * is intentionally NOT used as a fallback for REST requests — omitting namespace
8616
+ * should default to the server's global namespace, not silently scope to an adapter. */
8617
+ resolveNamespace(_req, bodyNamespace) {
8618
+ return bodyNamespace || void 0;
8585
8619
  }
8586
8620
  async handle(req, res, correlationId) {
8587
8621
  const parsed = new URL3(req.url ?? "/", `http://${hostToUrlAuthority2(this.host)}`);
@@ -8621,7 +8655,7 @@ var EngramAccessHttpServer = class {
8621
8655
  const response = await this.service.recall({
8622
8656
  query: body.query ?? "",
8623
8657
  sessionKey: body.sessionKey,
8624
- namespace: body.namespace,
8658
+ namespace: this.resolveNamespace(req, body.namespace),
8625
8659
  topK: body.topK,
8626
8660
  mode: body.mode,
8627
8661
  includeDebug: body.includeDebug === true
@@ -8633,7 +8667,7 @@ var EngramAccessHttpServer = class {
8633
8667
  const body = await this.readValidatedBody(req, "recallExplain");
8634
8668
  const response = await this.service.recallExplain({
8635
8669
  sessionKey: body.sessionKey,
8636
- namespace: body.namespace
8670
+ namespace: this.resolveNamespace(req, body.namespace)
8637
8671
  });
8638
8672
  this.respondJson(res, 200, response);
8639
8673
  return;
@@ -8644,7 +8678,7 @@ var EngramAccessHttpServer = class {
8644
8678
  const response = await this.service.observe({
8645
8679
  sessionKey: body.sessionKey,
8646
8680
  messages: body.messages,
8647
- namespace: body.namespace,
8681
+ namespace: this.resolveNamespace(req, body.namespace),
8648
8682
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
8649
8683
  skipExtraction: body.skipExtraction === true
8650
8684
  });
@@ -8657,7 +8691,7 @@ var EngramAccessHttpServer = class {
8657
8691
  const response = await this.service.lcmSearch({
8658
8692
  query: body.query,
8659
8693
  sessionKey: body.sessionKey,
8660
- namespace: body.namespace,
8694
+ namespace: this.resolveNamespace(req, body.namespace),
8661
8695
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
8662
8696
  limit: body.limit
8663
8697
  });
@@ -8679,7 +8713,7 @@ var EngramAccessHttpServer = class {
8679
8713
  content: body.content,
8680
8714
  category: body.category,
8681
8715
  confidence: body.confidence,
8682
- namespace: body.namespace,
8716
+ namespace: this.resolveNamespace(req, body.namespace),
8683
8717
  tags: body.tags,
8684
8718
  entityRef: body.entityRef,
8685
8719
  ttl: body.ttl,
@@ -8707,7 +8741,7 @@ var EngramAccessHttpServer = class {
8707
8741
  content: body.content,
8708
8742
  category: body.category,
8709
8743
  confidence: body.confidence,
8710
- namespace: body.namespace,
8744
+ namespace: this.resolveNamespace(req, body.namespace),
8711
8745
  tags: body.tags,
8712
8746
  entityRef: body.entityRef,
8713
8747
  ttl: body.ttl,
@@ -8826,7 +8860,7 @@ var EngramAccessHttpServer = class {
8826
8860
  memoryId: body.memoryId,
8827
8861
  status: body.status,
8828
8862
  reasonCode: body.reasonCode,
8829
- namespace: body.namespace,
8863
+ namespace: this.resolveNamespace(req, body.namespace),
8830
8864
  authenticatedPrincipal: this.resolveRequestPrincipal(req)
8831
8865
  });
8832
8866
  if (this.shouldCountWriteRateLimit(response)) {
@@ -8848,7 +8882,7 @@ var EngramAccessHttpServer = class {
8848
8882
  recordedAt: body.recordedAt,
8849
8883
  summary: body.summary,
8850
8884
  dryRun,
8851
- namespace: body.namespace,
8885
+ namespace: this.resolveNamespace(req, body.namespace),
8852
8886
  authenticatedPrincipal: this.resolveRequestPrincipal(req)
8853
8887
  });
8854
8888
  if (this.shouldCountWriteRateLimit(response)) {
@@ -8867,7 +8901,7 @@ var EngramAccessHttpServer = class {
8867
8901
  scenario: body.scenario,
8868
8902
  recordedAt: body.recordedAt,
8869
8903
  dryRun,
8870
- namespace: body.namespace,
8904
+ namespace: this.resolveNamespace(req, body.namespace),
8871
8905
  authenticatedPrincipal: this.resolveRequestPrincipal(req)
8872
8906
  });
8873
8907
  if (this.shouldCountWriteRateLimit(response)) {
@@ -8885,8 +8919,15 @@ var EngramAccessHttpServer = class {
8885
8919
  if (isMcpWrite) {
8886
8920
  this.ensureWriteRateLimitAvailable();
8887
8921
  }
8922
+ const sessionId = (() => {
8923
+ const raw = req.headers["mcp-session-id"];
8924
+ return typeof raw === "string" ? raw.trim() : void 0;
8925
+ })();
8926
+ const mcpCorrelationId = correlationIdStore.getStore() ?? randomUUID2();
8888
8927
  const response = await this.mcpServer.handleRequest(request, {
8889
- principalOverride: this.resolveRequestPrincipal(req)
8928
+ principalOverride: this.resolveRequestPrincipal(req),
8929
+ sessionId,
8930
+ correlationId: mcpCorrelationId
8890
8931
  });
8891
8932
  if (isMcpWrite && response !== null) {
8892
8933
  const result = response.result;
@@ -8901,6 +8942,10 @@ var EngramAccessHttpServer = class {
8901
8942
  res.end();
8902
8943
  return;
8903
8944
  }
8945
+ const assignedSessionId = this.mcpServer.popInitSessionId(mcpCorrelationId);
8946
+ if (assignedSessionId) {
8947
+ res.setHeader("mcp-session-id", assignedSessionId);
8948
+ }
8904
8949
  this.respondJson(res, 200, response);
8905
8950
  }
8906
8951
  respondJson(res, status, payload) {