@joshuaswarren/openclaw-engram 9.0.68 → 9.0.70

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
@@ -3509,7 +3509,6 @@ Respond with valid JSON matching this schema:
3509
3509
  },
3510
3510
  { role: "user", content: conversation }
3511
3511
  ],
3512
- temperature: 0.3,
3513
3512
  ...buildChatCompletionTokenLimit(this.config.model, 4096, {
3514
3513
  assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
3515
3514
  })
@@ -3785,7 +3784,6 @@ Respond with valid JSON only, matching this schema:
3785
3784
  { role: "user", content: "Consolidate the new memories against existing ones." }
3786
3785
  ],
3787
3786
  ...this.config.reasoningEffort !== "none" ? { reasoning_effort: this.config.reasoningEffort } : {},
3788
- temperature: 0.3,
3789
3787
  ...buildChatCompletionTokenLimit(this.config.model, 4096, {
3790
3788
  assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
3791
3789
  })
@@ -4013,7 +4011,6 @@ Respond with valid JSON matching this schema:
4013
4011
  { role: "user", content: fullProfileContent }
4014
4012
  ],
4015
4013
  ...this.config.reasoningEffort !== "none" ? { reasoning_effort: this.config.reasoningEffort } : {},
4016
- temperature: 0.3,
4017
4014
  ...buildChatCompletionTokenLimit(this.config.model, 4096, {
4018
4015
  assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
4019
4016
  })
@@ -4209,7 +4206,6 @@ Respond with valid JSON matching this schema:
4209
4206
  { role: "user", content: fullIdentityContent }
4210
4207
  ],
4211
4208
  ...this.config.reasoningEffort !== "none" ? { reasoning_effort: this.config.reasoningEffort } : {},
4212
- temperature: 0.3,
4213
4209
  ...buildChatCompletionTokenLimit(this.config.model, 4096, {
4214
4210
  assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
4215
4211
  })
@@ -4407,7 +4403,6 @@ Respond with valid JSON matching this schema:
4407
4403
  { role: "system", content: systemPrompt },
4408
4404
  { role: "user", content: input }
4409
4405
  ],
4410
- temperature: 0.3,
4411
4406
  ...buildChatCompletionTokenLimit(this.config.model, 2048, {
4412
4407
  assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
4413
4408
  })
@@ -4511,7 +4506,6 @@ Respond with valid JSON matching this schema:
4511
4506
  { role: "system", content: systemPrompt },
4512
4507
  { role: "user", content: input }
4513
4508
  ],
4514
- temperature: 0.3,
4515
4509
  ...buildChatCompletionTokenLimit(this.config.model, 2048, {
4516
4510
  assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
4517
4511
  })
@@ -4611,7 +4605,6 @@ ${memoryList}` }
4611
4605
 
4612
4606
  ${memoryList}` }
4613
4607
  ],
4614
- temperature: 0.3,
4615
4608
  ...buildChatCompletionTokenLimit(this.config.model, 4096, {
4616
4609
  assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
4617
4610
  })
@@ -34222,786 +34215,812 @@ var EngramAccessService = class {
34222
34215
  import { createServer as createServer3 } from "http";
34223
34216
  import { timingSafeEqual as timingSafeEqual2 } from "crypto";
34224
34217
  import { existsSync as existsSync5 } from "fs";
34225
- import { readFile as readFile38 } from "fs/promises";
34218
+ import { readFile as readFile39 } from "fs/promises";
34226
34219
  import path64 from "path";
34227
34220
  import { fileURLToPath as fileURLToPath2, URL as URL3 } from "url";
34228
- function resolveDefaultAdminConsolePublicDir() {
34229
- const candidates = [
34230
- fileURLToPath2(new URL3("../admin-console/public", import.meta.url)),
34231
- fileURLToPath2(new URL3("./admin-console/public", import.meta.url))
34232
- ];
34233
- return candidates.find((candidate) => existsSync5(candidate)) ?? candidates[0];
34234
- }
34235
- var defaultAdminConsolePublicDir = resolveDefaultAdminConsolePublicDir();
34236
- var WRITE_RATE_LIMIT_WINDOW_MS = 6e4;
34237
- var WRITE_RATE_LIMIT_MAX_REQUESTS = 30;
34238
- var HttpError = class extends Error {
34239
- constructor(status, message) {
34240
- super(message);
34241
- this.status = status;
34242
- }
34243
- };
34244
- function hostToUrlAuthority2(host) {
34245
- if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) {
34246
- return `[${host}]`;
34221
+
34222
+ // src/access-mcp.ts
34223
+ import { readFile as readFile38 } from "fs/promises";
34224
+ var MCP_PROTOCOL_VERSION = "2024-11-05";
34225
+ async function getMcpServerVersion() {
34226
+ const envVersion = process.env.OPENCLAW_ENGRAM_VERSION?.trim() || process.env.npm_package_version?.trim();
34227
+ if (envVersion) return envVersion;
34228
+ try {
34229
+ const pkgPath = new URL("../package.json", import.meta.url);
34230
+ const raw = await readFile38(pkgPath, "utf-8");
34231
+ const parsed = JSON.parse(raw);
34232
+ return parsed.version?.trim() || "unknown";
34233
+ } catch {
34234
+ return "unknown";
34247
34235
  }
34248
- return host;
34249
34236
  }
34250
- var EngramAccessHttpServer = class {
34251
- service;
34252
- host;
34253
- requestedPort;
34254
- authToken;
34255
- authenticatedPrincipal;
34256
- maxBodyBytes;
34257
- adminConsoleEnabled;
34258
- adminConsolePublicDir;
34259
- writeRequestTimestamps = [];
34260
- server = null;
34261
- boundPort = 0;
34262
- constructor(options) {
34263
- this.service = options.service;
34264
- this.host = options.host?.trim() || "127.0.0.1";
34265
- this.requestedPort = Number.isFinite(options.port) ? Math.max(0, Math.floor(options.port ?? 0)) : 0;
34266
- this.authToken = options.authToken?.trim() || void 0;
34267
- this.authenticatedPrincipal = options.principal?.trim() || void 0;
34268
- this.maxBodyBytes = Number.isFinite(options.maxBodyBytes) ? Math.max(1, Math.floor(options.maxBodyBytes ?? 131072)) : 131072;
34269
- this.adminConsoleEnabled = options.adminConsoleEnabled !== false;
34270
- this.adminConsolePublicDir = options.adminConsolePublicDir ?? defaultAdminConsolePublicDir;
34271
- }
34272
- async start() {
34273
- if (!this.authToken) {
34274
- throw new Error("engram access HTTP requires authToken");
34275
- }
34276
- if (this.server) return this.status();
34277
- const server = createServer3((req, res) => {
34278
- void this.handle(req, res).catch((err) => {
34279
- log.debug(`engram access HTTP request failed: ${err}`);
34280
- if (err instanceof HttpError) {
34281
- this.respondJson(res, err.status, { error: err.message });
34282
- return;
34237
+ var EngramMcpServer = class {
34238
+ constructor(service, options = {}) {
34239
+ this.service = service;
34240
+ this.authenticatedPrincipal = options.principal?.trim() || process.env.OPENCLAW_ENGRAM_ACCESS_PRINCIPAL?.trim() || void 0;
34241
+ this.tools = [
34242
+ {
34243
+ name: "engram.recall",
34244
+ description: "Recall Engram context for a query.",
34245
+ inputSchema: {
34246
+ type: "object",
34247
+ properties: {
34248
+ query: { type: "string" },
34249
+ sessionKey: { type: "string" },
34250
+ namespace: { type: "string" },
34251
+ topK: { type: "number" },
34252
+ mode: { type: "string", enum: ["auto", "no_recall", "minimal", "full", "graph_mode"] },
34253
+ includeDebug: { type: "boolean" }
34254
+ },
34255
+ required: ["query"],
34256
+ additionalProperties: false
34283
34257
  }
34284
- if (err instanceof EngramAccessInputError) {
34285
- this.respondJson(res, 400, { error: err.message });
34286
- return;
34258
+ },
34259
+ {
34260
+ name: "engram.recall_explain",
34261
+ description: "Return the last recall snapshot for a session or the most recent one.",
34262
+ inputSchema: {
34263
+ type: "object",
34264
+ properties: {
34265
+ sessionKey: { type: "string" },
34266
+ namespace: { type: "string" }
34267
+ },
34268
+ additionalProperties: false
34287
34269
  }
34288
- if (res.headersSent) {
34289
- res.destroy(err);
34290
- return;
34270
+ },
34271
+ {
34272
+ name: "engram.memory_get",
34273
+ description: "Fetch one Engram memory by id.",
34274
+ inputSchema: {
34275
+ type: "object",
34276
+ properties: {
34277
+ memoryId: { type: "string" },
34278
+ namespace: { type: "string" }
34279
+ },
34280
+ required: ["memoryId"],
34281
+ additionalProperties: false
34291
34282
  }
34292
- this.respondJson(res, 500, { error: "internal_error" });
34293
- });
34294
- });
34295
- try {
34296
- await new Promise((resolve, reject) => {
34297
- const onError = (err) => {
34298
- server.off("listening", onListening);
34299
- reject(err);
34300
- };
34301
- const onListening = () => {
34302
- server.off("error", onError);
34303
- resolve();
34304
- };
34305
- server.once("error", onError);
34306
- server.once("listening", onListening);
34307
- server.listen(this.requestedPort, this.host);
34308
- });
34309
- } catch (err) {
34310
- server.close();
34311
- throw err;
34312
- }
34313
- this.server = server;
34314
- const address = server.address();
34315
- this.boundPort = typeof address === "object" && address ? address.port : this.requestedPort;
34316
- return this.status();
34317
- }
34318
- async stop() {
34319
- if (!this.server) return;
34320
- const server = this.server;
34321
- this.server = null;
34322
- this.boundPort = 0;
34323
- await new Promise((resolve, reject) => {
34324
- server.close((err) => err ? reject(err) : resolve());
34325
- });
34326
- }
34327
- status() {
34328
- return {
34329
- running: this.server !== null,
34330
- host: this.host,
34331
- port: this.boundPort,
34332
- maxBodyBytes: this.maxBodyBytes
34333
- };
34283
+ },
34284
+ {
34285
+ name: "engram.memory_timeline",
34286
+ description: "Fetch one Engram memory timeline by id.",
34287
+ inputSchema: {
34288
+ type: "object",
34289
+ properties: {
34290
+ memoryId: { type: "string" },
34291
+ namespace: { type: "string" },
34292
+ limit: { type: "number" }
34293
+ },
34294
+ required: ["memoryId"],
34295
+ additionalProperties: false
34296
+ }
34297
+ },
34298
+ {
34299
+ name: "engram.memory_store",
34300
+ description: "Store an explicit Engram memory through the access layer.",
34301
+ inputSchema: {
34302
+ type: "object",
34303
+ properties: {
34304
+ schemaVersion: { type: "number" },
34305
+ idempotencyKey: { type: "string" },
34306
+ dryRun: { type: "boolean" },
34307
+ sessionKey: { type: "string" },
34308
+ content: { type: "string" },
34309
+ category: { type: "string" },
34310
+ confidence: { type: "number" },
34311
+ namespace: { type: "string" },
34312
+ tags: { type: "array", items: { type: "string" } },
34313
+ entityRef: { type: "string" },
34314
+ ttl: { type: "string" },
34315
+ sourceReason: { type: "string" }
34316
+ },
34317
+ required: ["content"],
34318
+ additionalProperties: false
34319
+ }
34320
+ },
34321
+ {
34322
+ name: "engram.suggestion_submit",
34323
+ description: "Queue a suggested Engram memory for review.",
34324
+ inputSchema: {
34325
+ type: "object",
34326
+ properties: {
34327
+ schemaVersion: { type: "number" },
34328
+ idempotencyKey: { type: "string" },
34329
+ dryRun: { type: "boolean" },
34330
+ sessionKey: { type: "string" },
34331
+ content: { type: "string" },
34332
+ category: { type: "string" },
34333
+ confidence: { type: "number" },
34334
+ namespace: { type: "string" },
34335
+ tags: { type: "array", items: { type: "string" } },
34336
+ entityRef: { type: "string" },
34337
+ ttl: { type: "string" },
34338
+ sourceReason: { type: "string" }
34339
+ },
34340
+ required: ["content"],
34341
+ additionalProperties: false
34342
+ }
34343
+ },
34344
+ {
34345
+ name: "engram.entity_get",
34346
+ description: "Fetch one Engram entity by name.",
34347
+ inputSchema: {
34348
+ type: "object",
34349
+ properties: {
34350
+ name: { type: "string" },
34351
+ namespace: { type: "string" }
34352
+ },
34353
+ required: ["name"],
34354
+ additionalProperties: false
34355
+ }
34356
+ },
34357
+ {
34358
+ name: "engram.review_queue_list",
34359
+ description: "Fetch the latest Engram review queue artifact bundle.",
34360
+ inputSchema: {
34361
+ type: "object",
34362
+ properties: {
34363
+ runId: { type: "string" },
34364
+ namespace: { type: "string" }
34365
+ },
34366
+ additionalProperties: false
34367
+ }
34368
+ }
34369
+ ];
34334
34370
  }
34335
- async handle(req, res) {
34336
- const parsed = new URL3(req.url ?? "/", `http://${hostToUrlAuthority2(this.host)}`);
34337
- const pathname = parsed.pathname;
34338
- if (this.adminConsoleEnabled && await this.handleAdminConsole(req, res, pathname)) {
34339
- return;
34340
- }
34341
- if (!this.isAuthorized(req)) {
34342
- res.writeHead(401, {
34343
- "content-type": "application/json; charset=utf-8",
34344
- "www-authenticate": "Bearer"
34345
- });
34346
- res.end(JSON.stringify({ error: "unauthorized" }));
34347
- return;
34348
- }
34349
- if (req.method === "GET" && pathname === "/engram/v1/health") {
34350
- this.respondJson(res, 200, await this.service.health());
34351
- return;
34352
- }
34353
- if (req.method === "POST" && pathname === "/engram/v1/recall") {
34354
- const body = await this.readJsonBody(req);
34355
- const response = await this.service.recall({
34356
- query: typeof body.query === "string" ? body.query : "",
34357
- sessionKey: typeof body.sessionKey === "string" ? body.sessionKey : void 0,
34358
- namespace: typeof body.namespace === "string" ? body.namespace : void 0,
34359
- topK: typeof body.topK === "number" ? body.topK : void 0,
34360
- mode: typeof body.mode === "string" ? body.mode : void 0,
34361
- includeDebug: body.includeDebug === true
34362
- });
34363
- this.respondJson(res, 200, response);
34364
- return;
34365
- }
34366
- if (req.method === "POST" && pathname === "/engram/v1/recall/explain") {
34367
- const body = await this.readJsonBody(req);
34368
- const response = await this.service.recallExplain({
34369
- sessionKey: typeof body.sessionKey === "string" ? body.sessionKey : void 0,
34370
- namespace: typeof body.namespace === "string" ? body.namespace : void 0
34371
- });
34372
- this.respondJson(res, 200, response);
34373
- return;
34371
+ buffer = Buffer.alloc(0);
34372
+ flushTask = null;
34373
+ tools;
34374
+ authenticatedPrincipal;
34375
+ async handleRequest(request) {
34376
+ const id = request.id ?? null;
34377
+ const method = request.method ?? "";
34378
+ if (method === "notifications/initialized") return null;
34379
+ if (method === "ping") {
34380
+ return { jsonrpc: "2.0", id, result: {} };
34374
34381
  }
34375
- if (req.method === "POST" && pathname === "/engram/v1/memories") {
34376
- const body = await this.readJsonBody(req);
34377
- const request = {
34378
- schemaVersion: typeof body.schemaVersion === "number" ? body.schemaVersion : void 0,
34379
- idempotencyKey: typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0,
34380
- dryRun: body.dryRun === true,
34381
- sessionKey: typeof body.sessionKey === "string" ? body.sessionKey : void 0,
34382
- authenticatedPrincipal: this.authenticatedPrincipal,
34383
- content: typeof body.content === "string" ? body.content : "",
34384
- category: typeof body.category === "string" ? body.category : void 0,
34385
- confidence: typeof body.confidence === "number" ? body.confidence : void 0,
34386
- namespace: typeof body.namespace === "string" ? body.namespace : void 0,
34387
- tags: Array.isArray(body.tags) ? body.tags.filter((tag) => typeof tag === "string") : void 0,
34388
- entityRef: typeof body.entityRef === "string" ? body.entityRef : void 0,
34389
- ttl: typeof body.ttl === "string" ? body.ttl : void 0,
34390
- sourceReason: typeof body.sourceReason === "string" ? body.sourceReason : void 0
34382
+ if (method === "initialize") {
34383
+ const version = await getMcpServerVersion();
34384
+ return {
34385
+ jsonrpc: "2.0",
34386
+ id,
34387
+ result: {
34388
+ protocolVersion: MCP_PROTOCOL_VERSION,
34389
+ capabilities: {
34390
+ tools: {}
34391
+ },
34392
+ serverInfo: {
34393
+ name: "openclaw-engram",
34394
+ version
34395
+ }
34396
+ }
34391
34397
  };
34392
- const idempotencyStatus = await this.service.peekMemoryStoreIdempotency(request);
34393
- if (idempotencyStatus === "miss" && request.dryRun !== true) {
34394
- this.ensureWriteRateLimitAvailable();
34395
- }
34396
- const response = await this.service.memoryStore(request);
34397
- if (this.shouldCountWriteRateLimit(response)) {
34398
- this.recordWriteRateLimitHit();
34399
- }
34400
- this.respondJson(res, this.writeResponseStatus(response), response);
34401
- return;
34402
34398
  }
34403
- if (req.method === "POST" && pathname === "/engram/v1/suggestions") {
34404
- const body = await this.readJsonBody(req);
34405
- const request = {
34406
- schemaVersion: typeof body.schemaVersion === "number" ? body.schemaVersion : void 0,
34407
- idempotencyKey: typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0,
34408
- dryRun: body.dryRun === true,
34409
- sessionKey: typeof body.sessionKey === "string" ? body.sessionKey : void 0,
34410
- authenticatedPrincipal: this.authenticatedPrincipal,
34411
- content: typeof body.content === "string" ? body.content : "",
34412
- category: typeof body.category === "string" ? body.category : void 0,
34413
- confidence: typeof body.confidence === "number" ? body.confidence : void 0,
34414
- namespace: typeof body.namespace === "string" ? body.namespace : void 0,
34415
- tags: Array.isArray(body.tags) ? body.tags.filter((tag) => typeof tag === "string") : void 0,
34416
- entityRef: typeof body.entityRef === "string" ? body.entityRef : void 0,
34417
- ttl: typeof body.ttl === "string" ? body.ttl : void 0,
34418
- sourceReason: typeof body.sourceReason === "string" ? body.sourceReason : void 0
34399
+ if (method === "tools/list") {
34400
+ return {
34401
+ jsonrpc: "2.0",
34402
+ id,
34403
+ result: {
34404
+ tools: this.tools
34405
+ }
34419
34406
  };
34420
- const idempotencyStatus = await this.service.peekSuggestionSubmitIdempotency(request);
34421
- if (idempotencyStatus === "miss" && request.dryRun !== true) {
34422
- this.ensureWriteRateLimitAvailable();
34423
- }
34424
- const response = await this.service.suggestionSubmit(request);
34425
- if (this.shouldCountWriteRateLimit(response)) {
34426
- this.recordWriteRateLimitHit();
34427
- }
34428
- this.respondJson(res, this.writeResponseStatus(response), response);
34429
- return;
34430
- }
34431
- if (req.method === "GET" && pathname === "/engram/v1/memories") {
34432
- const limitRaw = parseInt(parsed.searchParams.get("limit") ?? "50", 10);
34433
- const offsetRaw = parseInt(parsed.searchParams.get("offset") ?? "0", 10);
34434
- const sortParam = parsed.searchParams.get("sort") ?? void 0;
34435
- const sort = sortParam === "updated_desc" || sortParam === "updated_asc" || sortParam === "created_desc" || sortParam === "created_asc" ? sortParam : void 0;
34436
- const response = await this.service.memoryBrowse({
34437
- query: parsed.searchParams.get("q") ?? void 0,
34438
- status: parsed.searchParams.get("status") ?? void 0,
34439
- category: parsed.searchParams.get("category") ?? void 0,
34440
- namespace: parsed.searchParams.get("namespace") ?? void 0,
34441
- sort,
34442
- limit: Number.isFinite(limitRaw) ? limitRaw : 50,
34443
- offset: Number.isFinite(offsetRaw) ? offsetRaw : 0
34444
- });
34445
- this.respondJson(res, 200, response);
34446
- return;
34447
- }
34448
- const memoryMatch = pathname.match(/^\/engram\/v1\/memories\/([^/]+)$/);
34449
- if (req.method === "GET" && memoryMatch) {
34450
- const memoryId = decodeURIComponent(memoryMatch[1] ?? "");
34451
- const namespace = parsed.searchParams.get("namespace") ?? void 0;
34452
- const response = await this.service.memoryGet(memoryId, namespace);
34453
- this.respondJson(res, response.found ? 200 : 404, response);
34454
- return;
34455
- }
34456
- const timelineMatch = pathname.match(/^\/engram\/v1\/memories\/([^/]+)\/timeline$/);
34457
- if (req.method === "GET" && timelineMatch) {
34458
- const memoryId = decodeURIComponent(timelineMatch[1] ?? "");
34459
- const namespace = parsed.searchParams.get("namespace") ?? void 0;
34460
- const limitRaw = parseInt(parsed.searchParams.get("limit") ?? "200", 10);
34461
- const limit = Number.isFinite(limitRaw) ? limitRaw : 200;
34462
- const response = await this.service.memoryTimeline(memoryId, namespace, limit);
34463
- this.respondJson(res, response.found ? 200 : 404, response);
34464
- return;
34465
- }
34466
- if (req.method === "GET" && pathname === "/engram/v1/entities") {
34467
- const limitRaw = parseInt(parsed.searchParams.get("limit") ?? "50", 10);
34468
- const offsetRaw = parseInt(parsed.searchParams.get("offset") ?? "0", 10);
34469
- const response = await this.service.entityList({
34470
- namespace: parsed.searchParams.get("namespace") ?? void 0,
34471
- query: parsed.searchParams.get("q") ?? void 0,
34472
- limit: Number.isFinite(limitRaw) ? limitRaw : 50,
34473
- offset: Number.isFinite(offsetRaw) ? offsetRaw : 0
34474
- });
34475
- this.respondJson(res, 200, response);
34476
- return;
34477
34407
  }
34478
- const entityMatch = pathname.match(/^\/engram\/v1\/entities\/([^/]+)$/);
34479
- if (req.method === "GET" && entityMatch) {
34480
- const entityName = decodeURIComponent(entityMatch[1] ?? "");
34481
- const namespace = parsed.searchParams.get("namespace") ?? void 0;
34482
- const response = await this.service.entityGet(entityName, namespace);
34483
- this.respondJson(res, response.found ? 200 : 404, response);
34484
- return;
34485
- }
34486
- if (req.method === "GET" && pathname === "/engram/v1/review-queue") {
34487
- const response = await this.service.reviewQueue(
34488
- parsed.searchParams.get("runId") ?? void 0,
34489
- parsed.searchParams.get("namespace") ?? void 0
34490
- );
34491
- this.respondJson(res, 200, response);
34492
- return;
34493
- }
34494
- if (req.method === "GET" && pathname === "/engram/v1/maintenance") {
34495
- this.respondJson(res, 200, await this.service.maintenance(parsed.searchParams.get("namespace") ?? void 0));
34496
- return;
34497
- }
34498
- if (req.method === "GET" && pathname === "/engram/v1/quality") {
34499
- this.respondJson(res, 200, await this.service.quality(parsed.searchParams.get("namespace") ?? void 0));
34500
- return;
34501
- }
34502
- if (req.method === "POST" && pathname === "/engram/v1/review-disposition") {
34503
- const body = await this.readJsonBody(req);
34504
- const status = typeof body.status === "string" ? body.status : "";
34505
- if (status !== "active" && status !== "pending_review" && status !== "quarantined" && status !== "rejected" && status !== "superseded" && status !== "archived") {
34506
- throw new HttpError(400, "invalid_review_status");
34507
- }
34508
- this.ensureWriteRateLimitAvailable();
34509
- const response = await this.service.reviewDisposition({
34510
- memoryId: typeof body.memoryId === "string" ? body.memoryId : "",
34511
- status,
34512
- reasonCode: typeof body.reasonCode === "string" ? body.reasonCode : "",
34513
- namespace: typeof body.namespace === "string" ? body.namespace : void 0,
34514
- authenticatedPrincipal: this.authenticatedPrincipal
34515
- });
34516
- if (this.shouldCountWriteRateLimit(response)) {
34517
- this.recordWriteRateLimitHit();
34408
+ if (method === "tools/call") {
34409
+ const params = request.params ?? {};
34410
+ const name = typeof params.name === "string" ? params.name : "";
34411
+ const argumentsObject = params.arguments && typeof params.arguments === "object" && !Array.isArray(params.arguments) ? params.arguments : {};
34412
+ try {
34413
+ const result = await this.callTool(name, argumentsObject);
34414
+ return {
34415
+ jsonrpc: "2.0",
34416
+ id,
34417
+ result: {
34418
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
34419
+ structuredContent: result,
34420
+ isError: false
34421
+ }
34422
+ };
34423
+ } catch (err) {
34424
+ const message = err instanceof Error ? err.message : String(err);
34425
+ return {
34426
+ jsonrpc: "2.0",
34427
+ id,
34428
+ result: {
34429
+ content: [{ type: "text", text: message }],
34430
+ isError: true
34431
+ }
34432
+ };
34518
34433
  }
34519
- this.respondJson(res, 200, response);
34520
- return;
34521
34434
  }
34522
- this.respondJson(res, 404, { error: "not_found" });
34435
+ return {
34436
+ jsonrpc: "2.0",
34437
+ id,
34438
+ error: {
34439
+ code: -32601,
34440
+ message: `Method not found: ${method}`
34441
+ }
34442
+ };
34523
34443
  }
34524
- respondJson(res, status, payload) {
34525
- const body = JSON.stringify(payload, null, 2);
34526
- res.statusCode = status;
34527
- res.setHeader("content-type", "application/json; charset=utf-8");
34528
- res.setHeader("content-length", String(Buffer.byteLength(body)));
34529
- res.end(body);
34444
+ async runStdio(input, output) {
34445
+ input.on("data", (chunk) => {
34446
+ this.buffer = Buffer.concat([this.buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
34447
+ this.scheduleFlush(output);
34448
+ });
34449
+ await new Promise((resolve, reject) => {
34450
+ input.on("end", resolve);
34451
+ input.on("error", reject);
34452
+ });
34453
+ while (this.flushTask) {
34454
+ await this.flushTask;
34455
+ }
34530
34456
  }
34531
- async handleAdminConsole(req, res, pathname) {
34532
- if (req.method !== "GET") return false;
34533
- if (pathname === "/engram/ui" || pathname === "/engram/ui/") {
34534
- await this.respondStatic(res, path64.join(this.adminConsolePublicDir, "index.html"), "text/html; charset=utf-8");
34535
- return true;
34536
- }
34537
- if (pathname === "/engram/ui/app.js") {
34538
- await this.respondStatic(res, path64.join(this.adminConsolePublicDir, "app.js"), "application/javascript; charset=utf-8");
34539
- return true;
34540
- }
34541
- return false;
34542
- }
34543
- async respondStatic(res, filePath, contentType) {
34544
- try {
34545
- const body = await readFile38(filePath, "utf-8");
34546
- res.statusCode = 200;
34547
- res.setHeader("content-type", contentType);
34548
- res.setHeader("content-length", String(Buffer.byteLength(body)));
34549
- res.end(body);
34550
- } catch {
34551
- this.respondJson(res, 404, { error: "not_found" });
34552
- }
34457
+ scheduleFlush(output) {
34458
+ if (this.flushTask) return;
34459
+ const task = this.flushBuffer(output).catch((err) => {
34460
+ this.writeMessage(output, {
34461
+ jsonrpc: "2.0",
34462
+ id: null,
34463
+ error: {
34464
+ code: -32700,
34465
+ message: err instanceof Error ? err.message : String(err)
34466
+ }
34467
+ });
34468
+ }).finally(() => {
34469
+ if (this.flushTask === task) {
34470
+ this.flushTask = null;
34471
+ }
34472
+ if (this.buffer.length > 0) {
34473
+ this.scheduleFlush(output);
34474
+ }
34475
+ });
34476
+ this.flushTask = task;
34553
34477
  }
34554
- async readJsonBody(req) {
34555
- const chunks = [];
34556
- let total = 0;
34557
- for await (const chunk of req) {
34558
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
34559
- total += buffer.length;
34560
- if (total > this.maxBodyBytes) {
34561
- throw new HttpError(413, "request_body_too_large");
34478
+ async flushBuffer(output) {
34479
+ while (true) {
34480
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
34481
+ if (headerEnd < 0) return;
34482
+ const headerText = this.buffer.slice(0, headerEnd).toString("utf-8");
34483
+ const headers = headerText.split("\r\n");
34484
+ const contentLengthHeader = headers.find((line) => line.toLowerCase().startsWith("content-length:"));
34485
+ if (!contentLengthHeader) {
34486
+ this.buffer = Buffer.alloc(0);
34487
+ return;
34488
+ }
34489
+ const contentLength = parseInt(contentLengthHeader.split(":")[1]?.trim() ?? "0", 10);
34490
+ if (!Number.isFinite(contentLength) || contentLength < 0) {
34491
+ this.buffer = Buffer.alloc(0);
34492
+ return;
34493
+ }
34494
+ const messageStart = headerEnd + 4;
34495
+ const messageEnd = messageStart + contentLength;
34496
+ if (this.buffer.length < messageEnd) return;
34497
+ const body = this.buffer.slice(messageStart, messageEnd).toString("utf-8");
34498
+ this.buffer = this.buffer.slice(messageEnd);
34499
+ let parsed;
34500
+ try {
34501
+ parsed = JSON.parse(body);
34502
+ } catch {
34503
+ this.writeMessage(output, {
34504
+ jsonrpc: "2.0",
34505
+ id: null,
34506
+ error: {
34507
+ code: -32700,
34508
+ message: "Parse error"
34509
+ }
34510
+ });
34511
+ continue;
34512
+ }
34513
+ const response = await this.handleRequest(parsed);
34514
+ if (response) {
34515
+ this.writeMessage(output, response);
34562
34516
  }
34563
- chunks.push(buffer);
34564
- }
34565
- if (chunks.length === 0) return {};
34566
- const raw = Buffer.concat(chunks).toString("utf-8").trim();
34567
- if (raw.length === 0) return {};
34568
- let parsed;
34569
- try {
34570
- parsed = JSON.parse(raw);
34571
- } catch {
34572
- throw new HttpError(400, "invalid_json");
34573
- }
34574
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
34575
- throw new HttpError(400, "invalid_json_object");
34576
34517
  }
34577
- return parsed;
34578
- }
34579
- isAuthorized(req) {
34580
- if (!this.authToken) return false;
34581
- const raw = req.headers.authorization;
34582
- if (!raw) return false;
34583
- const separator = raw.indexOf(" ");
34584
- if (separator <= 0) return false;
34585
- const scheme = raw.slice(0, separator).toLowerCase();
34586
- if (scheme !== "bearer") return false;
34587
- const token = raw.slice(separator + 1).trim();
34588
- return this.timingSafeStringEqual(token, this.authToken);
34589
- }
34590
- timingSafeStringEqual(a, b) {
34591
- const left = this.encodeSecret(a);
34592
- const right = this.encodeSecret(b);
34593
- if (!left || !right) return false;
34594
- return timingSafeEqual2(left, right);
34595
- }
34596
- encodeSecret(value) {
34597
- const encoded = Buffer.from(value, "utf-8");
34598
- if (encoded.length > 1024) return null;
34599
- const out = Buffer.alloc(2 + 1024);
34600
- out.writeUInt16BE(encoded.length, 0);
34601
- encoded.copy(out, 2);
34602
- return out;
34603
34518
  }
34604
- writeResponseStatus(response) {
34605
- if (response.dryRun === true) return 200;
34606
- if (response.status === "stored" || response.status === "queued_for_review") return 201;
34607
- return 200;
34519
+ writeMessage(output, payload) {
34520
+ const body = JSON.stringify(payload);
34521
+ const message = `Content-Length: ${Buffer.byteLength(body, "utf-8")}\r
34522
+ \r
34523
+ ${body}`;
34524
+ output.write(message);
34608
34525
  }
34609
- ensureWriteRateLimitAvailable() {
34610
- const now = Date.now();
34611
- while (this.writeRequestTimestamps.length > 0 && now - (this.writeRequestTimestamps[0] ?? 0) > WRITE_RATE_LIMIT_WINDOW_MS) {
34612
- this.writeRequestTimestamps.shift();
34613
- }
34614
- if (this.writeRequestTimestamps.length >= WRITE_RATE_LIMIT_MAX_REQUESTS) {
34615
- throw new HttpError(429, "write_rate_limited");
34526
+ async callTool(name, args) {
34527
+ switch (name) {
34528
+ case "engram.recall":
34529
+ return this.service.recall({
34530
+ query: typeof args.query === "string" ? args.query : "",
34531
+ sessionKey: typeof args.sessionKey === "string" ? args.sessionKey : void 0,
34532
+ namespace: typeof args.namespace === "string" ? args.namespace : void 0,
34533
+ topK: typeof args.topK === "number" && Number.isFinite(args.topK) ? args.topK : void 0,
34534
+ mode: typeof args.mode === "string" ? args.mode : void 0,
34535
+ includeDebug: args.includeDebug === true
34536
+ });
34537
+ case "engram.recall_explain":
34538
+ return this.service.recallExplain({
34539
+ sessionKey: typeof args.sessionKey === "string" ? args.sessionKey : void 0,
34540
+ namespace: typeof args.namespace === "string" ? args.namespace : void 0
34541
+ });
34542
+ case "engram.memory_get":
34543
+ return this.service.memoryGet(
34544
+ typeof args.memoryId === "string" ? args.memoryId : "",
34545
+ typeof args.namespace === "string" ? args.namespace : void 0
34546
+ );
34547
+ case "engram.memory_timeline": {
34548
+ const limit = typeof args.limit === "number" && Number.isFinite(args.limit) ? args.limit : 200;
34549
+ return this.service.memoryTimeline(
34550
+ typeof args.memoryId === "string" ? args.memoryId : "",
34551
+ typeof args.namespace === "string" ? args.namespace : void 0,
34552
+ limit
34553
+ );
34554
+ }
34555
+ case "engram.memory_store":
34556
+ return this.service.memoryStore({
34557
+ schemaVersion: typeof args.schemaVersion === "number" ? args.schemaVersion : void 0,
34558
+ idempotencyKey: typeof args.idempotencyKey === "string" ? args.idempotencyKey : void 0,
34559
+ dryRun: args.dryRun === true,
34560
+ sessionKey: typeof args.sessionKey === "string" ? args.sessionKey : void 0,
34561
+ authenticatedPrincipal: this.authenticatedPrincipal,
34562
+ content: typeof args.content === "string" ? args.content : "",
34563
+ category: typeof args.category === "string" ? args.category : void 0,
34564
+ confidence: typeof args.confidence === "number" ? args.confidence : void 0,
34565
+ namespace: typeof args.namespace === "string" ? args.namespace : void 0,
34566
+ tags: Array.isArray(args.tags) ? args.tags.filter((tag) => typeof tag === "string") : void 0,
34567
+ entityRef: typeof args.entityRef === "string" ? args.entityRef : void 0,
34568
+ ttl: typeof args.ttl === "string" ? args.ttl : void 0,
34569
+ sourceReason: typeof args.sourceReason === "string" ? args.sourceReason : void 0
34570
+ });
34571
+ case "engram.suggestion_submit":
34572
+ return this.service.suggestionSubmit({
34573
+ schemaVersion: typeof args.schemaVersion === "number" ? args.schemaVersion : void 0,
34574
+ idempotencyKey: typeof args.idempotencyKey === "string" ? args.idempotencyKey : void 0,
34575
+ dryRun: args.dryRun === true,
34576
+ sessionKey: typeof args.sessionKey === "string" ? args.sessionKey : void 0,
34577
+ authenticatedPrincipal: this.authenticatedPrincipal,
34578
+ content: typeof args.content === "string" ? args.content : "",
34579
+ category: typeof args.category === "string" ? args.category : void 0,
34580
+ confidence: typeof args.confidence === "number" ? args.confidence : void 0,
34581
+ namespace: typeof args.namespace === "string" ? args.namespace : void 0,
34582
+ tags: Array.isArray(args.tags) ? args.tags.filter((tag) => typeof tag === "string") : void 0,
34583
+ entityRef: typeof args.entityRef === "string" ? args.entityRef : void 0,
34584
+ ttl: typeof args.ttl === "string" ? args.ttl : void 0,
34585
+ sourceReason: typeof args.sourceReason === "string" ? args.sourceReason : void 0
34586
+ });
34587
+ case "engram.entity_get":
34588
+ return this.service.entityGet(
34589
+ typeof args.name === "string" ? args.name : "",
34590
+ typeof args.namespace === "string" ? args.namespace : void 0
34591
+ );
34592
+ case "engram.review_queue_list":
34593
+ return this.service.reviewQueue(
34594
+ typeof args.runId === "string" ? args.runId : void 0,
34595
+ typeof args.namespace === "string" ? args.namespace : void 0
34596
+ );
34597
+ default:
34598
+ throw new Error(`unknown tool: ${name}`);
34616
34599
  }
34617
34600
  }
34618
- recordWriteRateLimitHit() {
34619
- this.writeRequestTimestamps.push(Date.now());
34620
- }
34621
- shouldCountWriteRateLimit(response) {
34622
- return response.dryRun !== true && response.idempotencyReplay !== true;
34623
- }
34624
34601
  };
34625
34602
 
34626
- // src/access-mcp.ts
34627
- import { readFile as readFile39 } from "fs/promises";
34628
- var MCP_PROTOCOL_VERSION = "2024-11-05";
34629
- async function getMcpServerVersion() {
34630
- const envVersion = process.env.OPENCLAW_ENGRAM_VERSION?.trim() || process.env.npm_package_version?.trim();
34631
- if (envVersion) return envVersion;
34632
- try {
34633
- const pkgPath = new URL("../package.json", import.meta.url);
34634
- const raw = await readFile39(pkgPath, "utf-8");
34635
- const parsed = JSON.parse(raw);
34636
- return parsed.version?.trim() || "unknown";
34637
- } catch {
34638
- return "unknown";
34603
+ // src/access-http.ts
34604
+ function resolveDefaultAdminConsolePublicDir() {
34605
+ const candidates = [
34606
+ fileURLToPath2(new URL3("../admin-console/public", import.meta.url)),
34607
+ fileURLToPath2(new URL3("./admin-console/public", import.meta.url))
34608
+ ];
34609
+ return candidates.find((candidate) => existsSync5(candidate)) ?? candidates[0];
34610
+ }
34611
+ var defaultAdminConsolePublicDir = resolveDefaultAdminConsolePublicDir();
34612
+ var WRITE_RATE_LIMIT_WINDOW_MS = 6e4;
34613
+ var WRITE_RATE_LIMIT_MAX_REQUESTS = 30;
34614
+ var HttpError = class extends Error {
34615
+ constructor(status, message) {
34616
+ super(message);
34617
+ this.status = status;
34618
+ }
34619
+ };
34620
+ function hostToUrlAuthority2(host) {
34621
+ if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) {
34622
+ return `[${host}]`;
34639
34623
  }
34624
+ return host;
34640
34625
  }
34641
- var EngramMcpServer = class {
34642
- constructor(service, options = {}) {
34643
- this.service = service;
34644
- this.authenticatedPrincipal = options.principal?.trim() || process.env.OPENCLAW_ENGRAM_ACCESS_PRINCIPAL?.trim() || void 0;
34645
- this.tools = [
34646
- {
34647
- name: "engram.recall",
34648
- description: "Recall Engram context for a query.",
34649
- inputSchema: {
34650
- type: "object",
34651
- properties: {
34652
- query: { type: "string" },
34653
- sessionKey: { type: "string" },
34654
- namespace: { type: "string" },
34655
- topK: { type: "number" },
34656
- mode: { type: "string", enum: ["auto", "no_recall", "minimal", "full", "graph_mode"] },
34657
- includeDebug: { type: "boolean" }
34658
- },
34659
- required: ["query"],
34660
- additionalProperties: false
34661
- }
34662
- },
34663
- {
34664
- name: "engram.recall_explain",
34665
- description: "Return the last recall snapshot for a session or the most recent one.",
34666
- inputSchema: {
34667
- type: "object",
34668
- properties: {
34669
- sessionKey: { type: "string" },
34670
- namespace: { type: "string" }
34671
- },
34672
- additionalProperties: false
34673
- }
34674
- },
34675
- {
34676
- name: "engram.memory_get",
34677
- description: "Fetch one Engram memory by id.",
34678
- inputSchema: {
34679
- type: "object",
34680
- properties: {
34681
- memoryId: { type: "string" },
34682
- namespace: { type: "string" }
34683
- },
34684
- required: ["memoryId"],
34685
- additionalProperties: false
34686
- }
34687
- },
34688
- {
34689
- name: "engram.memory_timeline",
34690
- description: "Fetch one Engram memory timeline by id.",
34691
- inputSchema: {
34692
- type: "object",
34693
- properties: {
34694
- memoryId: { type: "string" },
34695
- namespace: { type: "string" },
34696
- limit: { type: "number" }
34697
- },
34698
- required: ["memoryId"],
34699
- additionalProperties: false
34700
- }
34701
- },
34702
- {
34703
- name: "engram.memory_store",
34704
- description: "Store an explicit Engram memory through the access layer.",
34705
- inputSchema: {
34706
- type: "object",
34707
- properties: {
34708
- schemaVersion: { type: "number" },
34709
- idempotencyKey: { type: "string" },
34710
- dryRun: { type: "boolean" },
34711
- sessionKey: { type: "string" },
34712
- content: { type: "string" },
34713
- category: { type: "string" },
34714
- confidence: { type: "number" },
34715
- namespace: { type: "string" },
34716
- tags: { type: "array", items: { type: "string" } },
34717
- entityRef: { type: "string" },
34718
- ttl: { type: "string" },
34719
- sourceReason: { type: "string" }
34720
- },
34721
- required: ["content"],
34722
- additionalProperties: false
34723
- }
34724
- },
34725
- {
34726
- name: "engram.suggestion_submit",
34727
- description: "Queue a suggested Engram memory for review.",
34728
- inputSchema: {
34729
- type: "object",
34730
- properties: {
34731
- schemaVersion: { type: "number" },
34732
- idempotencyKey: { type: "string" },
34733
- dryRun: { type: "boolean" },
34734
- sessionKey: { type: "string" },
34735
- content: { type: "string" },
34736
- category: { type: "string" },
34737
- confidence: { type: "number" },
34738
- namespace: { type: "string" },
34739
- tags: { type: "array", items: { type: "string" } },
34740
- entityRef: { type: "string" },
34741
- ttl: { type: "string" },
34742
- sourceReason: { type: "string" }
34743
- },
34744
- required: ["content"],
34745
- additionalProperties: false
34626
+ var EngramAccessHttpServer = class {
34627
+ service;
34628
+ host;
34629
+ requestedPort;
34630
+ authToken;
34631
+ authenticatedPrincipal;
34632
+ maxBodyBytes;
34633
+ adminConsoleEnabled;
34634
+ adminConsolePublicDir;
34635
+ writeRequestTimestamps = [];
34636
+ mcpServer;
34637
+ server = null;
34638
+ boundPort = 0;
34639
+ constructor(options) {
34640
+ this.service = options.service;
34641
+ this.host = options.host?.trim() || "127.0.0.1";
34642
+ this.requestedPort = Number.isFinite(options.port) ? Math.max(0, Math.floor(options.port ?? 0)) : 0;
34643
+ this.authToken = options.authToken?.trim() || void 0;
34644
+ this.authenticatedPrincipal = options.principal?.trim() || void 0;
34645
+ this.maxBodyBytes = Number.isFinite(options.maxBodyBytes) ? Math.max(1, Math.floor(options.maxBodyBytes ?? 131072)) : 131072;
34646
+ this.adminConsoleEnabled = options.adminConsoleEnabled !== false;
34647
+ this.adminConsolePublicDir = options.adminConsolePublicDir ?? defaultAdminConsolePublicDir;
34648
+ this.mcpServer = new EngramMcpServer(this.service, { principal: options.principal });
34649
+ }
34650
+ async start() {
34651
+ if (!this.authToken) {
34652
+ throw new Error("engram access HTTP requires authToken");
34653
+ }
34654
+ if (this.server) return this.status();
34655
+ const server = createServer3((req, res) => {
34656
+ void this.handle(req, res).catch((err) => {
34657
+ log.debug(`engram access HTTP request failed: ${err}`);
34658
+ if (err instanceof HttpError) {
34659
+ this.respondJson(res, err.status, { error: err.message });
34660
+ return;
34746
34661
  }
34747
- },
34748
- {
34749
- name: "engram.entity_get",
34750
- description: "Fetch one Engram entity by name.",
34751
- inputSchema: {
34752
- type: "object",
34753
- properties: {
34754
- name: { type: "string" },
34755
- namespace: { type: "string" }
34756
- },
34757
- required: ["name"],
34758
- additionalProperties: false
34662
+ if (err instanceof EngramAccessInputError) {
34663
+ this.respondJson(res, 400, { error: err.message });
34664
+ return;
34759
34665
  }
34760
- },
34761
- {
34762
- name: "engram.review_queue_list",
34763
- description: "Fetch the latest Engram review queue artifact bundle.",
34764
- inputSchema: {
34765
- type: "object",
34766
- properties: {
34767
- runId: { type: "string" },
34768
- namespace: { type: "string" }
34769
- },
34770
- additionalProperties: false
34666
+ if (res.headersSent) {
34667
+ res.destroy(err);
34668
+ return;
34771
34669
  }
34772
- }
34773
- ];
34670
+ this.respondJson(res, 500, { error: "internal_error" });
34671
+ });
34672
+ });
34673
+ try {
34674
+ await new Promise((resolve, reject) => {
34675
+ const onError = (err) => {
34676
+ server.off("listening", onListening);
34677
+ reject(err);
34678
+ };
34679
+ const onListening = () => {
34680
+ server.off("error", onError);
34681
+ resolve();
34682
+ };
34683
+ server.once("error", onError);
34684
+ server.once("listening", onListening);
34685
+ server.listen(this.requestedPort, this.host);
34686
+ });
34687
+ } catch (err) {
34688
+ server.close();
34689
+ throw err;
34690
+ }
34691
+ this.server = server;
34692
+ const address = server.address();
34693
+ this.boundPort = typeof address === "object" && address ? address.port : this.requestedPort;
34694
+ return this.status();
34774
34695
  }
34775
- buffer = Buffer.alloc(0);
34776
- flushTask = null;
34777
- tools;
34778
- authenticatedPrincipal;
34779
- async handleRequest(request) {
34780
- const id = request.id ?? null;
34781
- const method = request.method ?? "";
34782
- if (method === "notifications/initialized") return null;
34783
- if (method === "ping") {
34784
- return { jsonrpc: "2.0", id, result: {} };
34696
+ async stop() {
34697
+ if (!this.server) return;
34698
+ const server = this.server;
34699
+ this.server = null;
34700
+ this.boundPort = 0;
34701
+ await new Promise((resolve, reject) => {
34702
+ server.close((err) => err ? reject(err) : resolve());
34703
+ });
34704
+ }
34705
+ status() {
34706
+ return {
34707
+ running: this.server !== null,
34708
+ host: this.host,
34709
+ port: this.boundPort,
34710
+ maxBodyBytes: this.maxBodyBytes
34711
+ };
34712
+ }
34713
+ async handle(req, res) {
34714
+ const parsed = new URL3(req.url ?? "/", `http://${hostToUrlAuthority2(this.host)}`);
34715
+ const pathname = parsed.pathname;
34716
+ if (this.adminConsoleEnabled && await this.handleAdminConsole(req, res, pathname)) {
34717
+ return;
34785
34718
  }
34786
- if (method === "initialize") {
34787
- const version = await getMcpServerVersion();
34788
- return {
34789
- jsonrpc: "2.0",
34790
- id,
34791
- result: {
34792
- protocolVersion: MCP_PROTOCOL_VERSION,
34793
- capabilities: {
34794
- tools: {}
34795
- },
34796
- serverInfo: {
34797
- name: "openclaw-engram",
34798
- version
34799
- }
34800
- }
34719
+ if (!this.isAuthorized(req)) {
34720
+ res.writeHead(401, {
34721
+ "content-type": "application/json; charset=utf-8",
34722
+ "www-authenticate": "Bearer"
34723
+ });
34724
+ res.end(JSON.stringify({ error: "unauthorized" }));
34725
+ return;
34726
+ }
34727
+ if (req.method === "POST" && pathname === "/mcp") {
34728
+ await this.handleMcpRequest(req, res);
34729
+ return;
34730
+ }
34731
+ if (req.method === "GET" && pathname === "/engram/v1/health") {
34732
+ this.respondJson(res, 200, await this.service.health());
34733
+ return;
34734
+ }
34735
+ if (req.method === "POST" && pathname === "/engram/v1/recall") {
34736
+ const body = await this.readJsonBody(req);
34737
+ const response = await this.service.recall({
34738
+ query: typeof body.query === "string" ? body.query : "",
34739
+ sessionKey: typeof body.sessionKey === "string" ? body.sessionKey : void 0,
34740
+ namespace: typeof body.namespace === "string" ? body.namespace : void 0,
34741
+ topK: typeof body.topK === "number" ? body.topK : void 0,
34742
+ mode: typeof body.mode === "string" ? body.mode : void 0,
34743
+ includeDebug: body.includeDebug === true
34744
+ });
34745
+ this.respondJson(res, 200, response);
34746
+ return;
34747
+ }
34748
+ if (req.method === "POST" && pathname === "/engram/v1/recall/explain") {
34749
+ const body = await this.readJsonBody(req);
34750
+ const response = await this.service.recallExplain({
34751
+ sessionKey: typeof body.sessionKey === "string" ? body.sessionKey : void 0,
34752
+ namespace: typeof body.namespace === "string" ? body.namespace : void 0
34753
+ });
34754
+ this.respondJson(res, 200, response);
34755
+ return;
34756
+ }
34757
+ if (req.method === "POST" && pathname === "/engram/v1/memories") {
34758
+ const body = await this.readJsonBody(req);
34759
+ const request = {
34760
+ schemaVersion: typeof body.schemaVersion === "number" ? body.schemaVersion : void 0,
34761
+ idempotencyKey: typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0,
34762
+ dryRun: body.dryRun === true,
34763
+ sessionKey: typeof body.sessionKey === "string" ? body.sessionKey : void 0,
34764
+ authenticatedPrincipal: this.authenticatedPrincipal,
34765
+ content: typeof body.content === "string" ? body.content : "",
34766
+ category: typeof body.category === "string" ? body.category : void 0,
34767
+ confidence: typeof body.confidence === "number" ? body.confidence : void 0,
34768
+ namespace: typeof body.namespace === "string" ? body.namespace : void 0,
34769
+ tags: Array.isArray(body.tags) ? body.tags.filter((tag) => typeof tag === "string") : void 0,
34770
+ entityRef: typeof body.entityRef === "string" ? body.entityRef : void 0,
34771
+ ttl: typeof body.ttl === "string" ? body.ttl : void 0,
34772
+ sourceReason: typeof body.sourceReason === "string" ? body.sourceReason : void 0
34801
34773
  };
34774
+ const idempotencyStatus = await this.service.peekMemoryStoreIdempotency(request);
34775
+ if (idempotencyStatus === "miss" && request.dryRun !== true) {
34776
+ this.ensureWriteRateLimitAvailable();
34777
+ }
34778
+ const response = await this.service.memoryStore(request);
34779
+ if (this.shouldCountWriteRateLimit(response)) {
34780
+ this.recordWriteRateLimitHit();
34781
+ }
34782
+ this.respondJson(res, this.writeResponseStatus(response), response);
34783
+ return;
34802
34784
  }
34803
- if (method === "tools/list") {
34804
- return {
34805
- jsonrpc: "2.0",
34806
- id,
34807
- result: {
34808
- tools: this.tools
34809
- }
34785
+ if (req.method === "POST" && pathname === "/engram/v1/suggestions") {
34786
+ const body = await this.readJsonBody(req);
34787
+ const request = {
34788
+ schemaVersion: typeof body.schemaVersion === "number" ? body.schemaVersion : void 0,
34789
+ idempotencyKey: typeof body.idempotencyKey === "string" ? body.idempotencyKey : void 0,
34790
+ dryRun: body.dryRun === true,
34791
+ sessionKey: typeof body.sessionKey === "string" ? body.sessionKey : void 0,
34792
+ authenticatedPrincipal: this.authenticatedPrincipal,
34793
+ content: typeof body.content === "string" ? body.content : "",
34794
+ category: typeof body.category === "string" ? body.category : void 0,
34795
+ confidence: typeof body.confidence === "number" ? body.confidence : void 0,
34796
+ namespace: typeof body.namespace === "string" ? body.namespace : void 0,
34797
+ tags: Array.isArray(body.tags) ? body.tags.filter((tag) => typeof tag === "string") : void 0,
34798
+ entityRef: typeof body.entityRef === "string" ? body.entityRef : void 0,
34799
+ ttl: typeof body.ttl === "string" ? body.ttl : void 0,
34800
+ sourceReason: typeof body.sourceReason === "string" ? body.sourceReason : void 0
34810
34801
  };
34802
+ const idempotencyStatus = await this.service.peekSuggestionSubmitIdempotency(request);
34803
+ if (idempotencyStatus === "miss" && request.dryRun !== true) {
34804
+ this.ensureWriteRateLimitAvailable();
34805
+ }
34806
+ const response = await this.service.suggestionSubmit(request);
34807
+ if (this.shouldCountWriteRateLimit(response)) {
34808
+ this.recordWriteRateLimitHit();
34809
+ }
34810
+ this.respondJson(res, this.writeResponseStatus(response), response);
34811
+ return;
34811
34812
  }
34812
- if (method === "tools/call") {
34813
- const params = request.params ?? {};
34814
- const name = typeof params.name === "string" ? params.name : "";
34815
- const argumentsObject = params.arguments && typeof params.arguments === "object" && !Array.isArray(params.arguments) ? params.arguments : {};
34816
- try {
34817
- const result = await this.callTool(name, argumentsObject);
34818
- return {
34819
- jsonrpc: "2.0",
34820
- id,
34821
- result: {
34822
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
34823
- structuredContent: result,
34824
- isError: false
34825
- }
34826
- };
34827
- } catch (err) {
34828
- const message = err instanceof Error ? err.message : String(err);
34829
- return {
34830
- jsonrpc: "2.0",
34831
- id,
34832
- result: {
34833
- content: [{ type: "text", text: message }],
34834
- isError: true
34835
- }
34836
- };
34813
+ if (req.method === "GET" && pathname === "/engram/v1/memories") {
34814
+ const limitRaw = parseInt(parsed.searchParams.get("limit") ?? "50", 10);
34815
+ const offsetRaw = parseInt(parsed.searchParams.get("offset") ?? "0", 10);
34816
+ const sortParam = parsed.searchParams.get("sort") ?? void 0;
34817
+ const sort = sortParam === "updated_desc" || sortParam === "updated_asc" || sortParam === "created_desc" || sortParam === "created_asc" ? sortParam : void 0;
34818
+ const response = await this.service.memoryBrowse({
34819
+ query: parsed.searchParams.get("q") ?? void 0,
34820
+ status: parsed.searchParams.get("status") ?? void 0,
34821
+ category: parsed.searchParams.get("category") ?? void 0,
34822
+ namespace: parsed.searchParams.get("namespace") ?? void 0,
34823
+ sort,
34824
+ limit: Number.isFinite(limitRaw) ? limitRaw : 50,
34825
+ offset: Number.isFinite(offsetRaw) ? offsetRaw : 0
34826
+ });
34827
+ this.respondJson(res, 200, response);
34828
+ return;
34829
+ }
34830
+ const memoryMatch = pathname.match(/^\/engram\/v1\/memories\/([^/]+)$/);
34831
+ if (req.method === "GET" && memoryMatch) {
34832
+ const memoryId = decodeURIComponent(memoryMatch[1] ?? "");
34833
+ const namespace = parsed.searchParams.get("namespace") ?? void 0;
34834
+ const response = await this.service.memoryGet(memoryId, namespace);
34835
+ this.respondJson(res, response.found ? 200 : 404, response);
34836
+ return;
34837
+ }
34838
+ const timelineMatch = pathname.match(/^\/engram\/v1\/memories\/([^/]+)\/timeline$/);
34839
+ if (req.method === "GET" && timelineMatch) {
34840
+ const memoryId = decodeURIComponent(timelineMatch[1] ?? "");
34841
+ const namespace = parsed.searchParams.get("namespace") ?? void 0;
34842
+ const limitRaw = parseInt(parsed.searchParams.get("limit") ?? "200", 10);
34843
+ const limit = Number.isFinite(limitRaw) ? limitRaw : 200;
34844
+ const response = await this.service.memoryTimeline(memoryId, namespace, limit);
34845
+ this.respondJson(res, response.found ? 200 : 404, response);
34846
+ return;
34847
+ }
34848
+ if (req.method === "GET" && pathname === "/engram/v1/entities") {
34849
+ const limitRaw = parseInt(parsed.searchParams.get("limit") ?? "50", 10);
34850
+ const offsetRaw = parseInt(parsed.searchParams.get("offset") ?? "0", 10);
34851
+ const response = await this.service.entityList({
34852
+ namespace: parsed.searchParams.get("namespace") ?? void 0,
34853
+ query: parsed.searchParams.get("q") ?? void 0,
34854
+ limit: Number.isFinite(limitRaw) ? limitRaw : 50,
34855
+ offset: Number.isFinite(offsetRaw) ? offsetRaw : 0
34856
+ });
34857
+ this.respondJson(res, 200, response);
34858
+ return;
34859
+ }
34860
+ const entityMatch = pathname.match(/^\/engram\/v1\/entities\/([^/]+)$/);
34861
+ if (req.method === "GET" && entityMatch) {
34862
+ const entityName = decodeURIComponent(entityMatch[1] ?? "");
34863
+ const namespace = parsed.searchParams.get("namespace") ?? void 0;
34864
+ const response = await this.service.entityGet(entityName, namespace);
34865
+ this.respondJson(res, response.found ? 200 : 404, response);
34866
+ return;
34867
+ }
34868
+ if (req.method === "GET" && pathname === "/engram/v1/review-queue") {
34869
+ const response = await this.service.reviewQueue(
34870
+ parsed.searchParams.get("runId") ?? void 0,
34871
+ parsed.searchParams.get("namespace") ?? void 0
34872
+ );
34873
+ this.respondJson(res, 200, response);
34874
+ return;
34875
+ }
34876
+ if (req.method === "GET" && pathname === "/engram/v1/maintenance") {
34877
+ this.respondJson(res, 200, await this.service.maintenance(parsed.searchParams.get("namespace") ?? void 0));
34878
+ return;
34879
+ }
34880
+ if (req.method === "GET" && pathname === "/engram/v1/quality") {
34881
+ this.respondJson(res, 200, await this.service.quality(parsed.searchParams.get("namespace") ?? void 0));
34882
+ return;
34883
+ }
34884
+ if (req.method === "POST" && pathname === "/engram/v1/review-disposition") {
34885
+ const body = await this.readJsonBody(req);
34886
+ const status = typeof body.status === "string" ? body.status : "";
34887
+ if (status !== "active" && status !== "pending_review" && status !== "quarantined" && status !== "rejected" && status !== "superseded" && status !== "archived") {
34888
+ throw new HttpError(400, "invalid_review_status");
34889
+ }
34890
+ this.ensureWriteRateLimitAvailable();
34891
+ const response = await this.service.reviewDisposition({
34892
+ memoryId: typeof body.memoryId === "string" ? body.memoryId : "",
34893
+ status,
34894
+ reasonCode: typeof body.reasonCode === "string" ? body.reasonCode : "",
34895
+ namespace: typeof body.namespace === "string" ? body.namespace : void 0,
34896
+ authenticatedPrincipal: this.authenticatedPrincipal
34897
+ });
34898
+ if (this.shouldCountWriteRateLimit(response)) {
34899
+ this.recordWriteRateLimitHit();
34837
34900
  }
34901
+ this.respondJson(res, 200, response);
34902
+ return;
34838
34903
  }
34839
- return {
34840
- jsonrpc: "2.0",
34841
- id,
34842
- error: {
34843
- code: -32601,
34844
- message: `Method not found: ${method}`
34904
+ this.respondJson(res, 404, { error: "not_found" });
34905
+ }
34906
+ async handleMcpRequest(req, res) {
34907
+ const body = await this.readJsonBody(req);
34908
+ const request = body;
34909
+ if (request.method === "tools/call") {
34910
+ const toolName = typeof request.params?.name === "string" ? request.params.name : "";
34911
+ if (toolName === "engram.memory_store" || toolName === "engram.suggestion_submit") {
34912
+ this.ensureWriteRateLimitAvailable();
34913
+ this.recordWriteRateLimitHit();
34845
34914
  }
34846
- };
34915
+ }
34916
+ const response = await this.mcpServer.handleRequest(request);
34917
+ if (response === null) {
34918
+ res.statusCode = 202;
34919
+ res.end();
34920
+ return;
34921
+ }
34922
+ this.respondJson(res, 200, response);
34847
34923
  }
34848
- async runStdio(input, output) {
34849
- input.on("data", (chunk) => {
34850
- this.buffer = Buffer.concat([this.buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
34851
- this.scheduleFlush(output);
34852
- });
34853
- await new Promise((resolve, reject) => {
34854
- input.on("end", resolve);
34855
- input.on("error", reject);
34856
- });
34857
- while (this.flushTask) {
34858
- await this.flushTask;
34924
+ respondJson(res, status, payload) {
34925
+ const body = JSON.stringify(payload, null, 2);
34926
+ res.statusCode = status;
34927
+ res.setHeader("content-type", "application/json; charset=utf-8");
34928
+ res.setHeader("content-length", String(Buffer.byteLength(body)));
34929
+ res.end(body);
34930
+ }
34931
+ async handleAdminConsole(req, res, pathname) {
34932
+ if (req.method !== "GET") return false;
34933
+ if (pathname === "/engram/ui" || pathname === "/engram/ui/") {
34934
+ await this.respondStatic(res, path64.join(this.adminConsolePublicDir, "index.html"), "text/html; charset=utf-8");
34935
+ return true;
34936
+ }
34937
+ if (pathname === "/engram/ui/app.js") {
34938
+ await this.respondStatic(res, path64.join(this.adminConsolePublicDir, "app.js"), "application/javascript; charset=utf-8");
34939
+ return true;
34859
34940
  }
34941
+ return false;
34860
34942
  }
34861
- scheduleFlush(output) {
34862
- if (this.flushTask) return;
34863
- const task = this.flushBuffer(output).catch((err) => {
34864
- this.writeMessage(output, {
34865
- jsonrpc: "2.0",
34866
- id: null,
34867
- error: {
34868
- code: -32700,
34869
- message: err instanceof Error ? err.message : String(err)
34870
- }
34871
- });
34872
- }).finally(() => {
34873
- if (this.flushTask === task) {
34874
- this.flushTask = null;
34875
- }
34876
- if (this.buffer.length > 0) {
34877
- this.scheduleFlush(output);
34878
- }
34879
- });
34880
- this.flushTask = task;
34943
+ async respondStatic(res, filePath, contentType) {
34944
+ try {
34945
+ const body = await readFile39(filePath, "utf-8");
34946
+ res.statusCode = 200;
34947
+ res.setHeader("content-type", contentType);
34948
+ res.setHeader("content-length", String(Buffer.byteLength(body)));
34949
+ res.end(body);
34950
+ } catch {
34951
+ this.respondJson(res, 404, { error: "not_found" });
34952
+ }
34881
34953
  }
34882
- async flushBuffer(output) {
34883
- while (true) {
34884
- const headerEnd = this.buffer.indexOf("\r\n\r\n");
34885
- if (headerEnd < 0) return;
34886
- const headerText = this.buffer.slice(0, headerEnd).toString("utf-8");
34887
- const headers = headerText.split("\r\n");
34888
- const contentLengthHeader = headers.find((line) => line.toLowerCase().startsWith("content-length:"));
34889
- if (!contentLengthHeader) {
34890
- this.buffer = Buffer.alloc(0);
34891
- return;
34892
- }
34893
- const contentLength = parseInt(contentLengthHeader.split(":")[1]?.trim() ?? "0", 10);
34894
- if (!Number.isFinite(contentLength) || contentLength < 0) {
34895
- this.buffer = Buffer.alloc(0);
34896
- return;
34897
- }
34898
- const messageStart = headerEnd + 4;
34899
- const messageEnd = messageStart + contentLength;
34900
- if (this.buffer.length < messageEnd) return;
34901
- const body = this.buffer.slice(messageStart, messageEnd).toString("utf-8");
34902
- this.buffer = this.buffer.slice(messageEnd);
34903
- let parsed;
34904
- try {
34905
- parsed = JSON.parse(body);
34906
- } catch {
34907
- this.writeMessage(output, {
34908
- jsonrpc: "2.0",
34909
- id: null,
34910
- error: {
34911
- code: -32700,
34912
- message: "Parse error"
34913
- }
34914
- });
34915
- continue;
34916
- }
34917
- const response = await this.handleRequest(parsed);
34918
- if (response) {
34919
- this.writeMessage(output, response);
34954
+ async readJsonBody(req) {
34955
+ const chunks = [];
34956
+ let total = 0;
34957
+ for await (const chunk of req) {
34958
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
34959
+ total += buffer.length;
34960
+ if (total > this.maxBodyBytes) {
34961
+ throw new HttpError(413, "request_body_too_large");
34920
34962
  }
34963
+ chunks.push(buffer);
34964
+ }
34965
+ if (chunks.length === 0) return {};
34966
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
34967
+ if (raw.length === 0) return {};
34968
+ let parsed;
34969
+ try {
34970
+ parsed = JSON.parse(raw);
34971
+ } catch {
34972
+ throw new HttpError(400, "invalid_json");
34973
+ }
34974
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
34975
+ throw new HttpError(400, "invalid_json_object");
34921
34976
  }
34977
+ return parsed;
34922
34978
  }
34923
- writeMessage(output, payload) {
34924
- const body = JSON.stringify(payload);
34925
- const message = `Content-Length: ${Buffer.byteLength(body, "utf-8")}\r
34926
- \r
34927
- ${body}`;
34928
- output.write(message);
34979
+ isAuthorized(req) {
34980
+ if (!this.authToken) return false;
34981
+ const raw = req.headers.authorization;
34982
+ if (!raw) return false;
34983
+ const separator = raw.indexOf(" ");
34984
+ if (separator <= 0) return false;
34985
+ const scheme = raw.slice(0, separator).toLowerCase();
34986
+ if (scheme !== "bearer") return false;
34987
+ const token = raw.slice(separator + 1).trim();
34988
+ return this.timingSafeStringEqual(token, this.authToken);
34929
34989
  }
34930
- async callTool(name, args) {
34931
- switch (name) {
34932
- case "engram.recall":
34933
- return this.service.recall({
34934
- query: typeof args.query === "string" ? args.query : "",
34935
- sessionKey: typeof args.sessionKey === "string" ? args.sessionKey : void 0,
34936
- namespace: typeof args.namespace === "string" ? args.namespace : void 0,
34937
- topK: typeof args.topK === "number" && Number.isFinite(args.topK) ? args.topK : void 0,
34938
- mode: typeof args.mode === "string" ? args.mode : void 0,
34939
- includeDebug: args.includeDebug === true
34940
- });
34941
- case "engram.recall_explain":
34942
- return this.service.recallExplain({
34943
- sessionKey: typeof args.sessionKey === "string" ? args.sessionKey : void 0,
34944
- namespace: typeof args.namespace === "string" ? args.namespace : void 0
34945
- });
34946
- case "engram.memory_get":
34947
- return this.service.memoryGet(
34948
- typeof args.memoryId === "string" ? args.memoryId : "",
34949
- typeof args.namespace === "string" ? args.namespace : void 0
34950
- );
34951
- case "engram.memory_timeline": {
34952
- const limit = typeof args.limit === "number" && Number.isFinite(args.limit) ? args.limit : 200;
34953
- return this.service.memoryTimeline(
34954
- typeof args.memoryId === "string" ? args.memoryId : "",
34955
- typeof args.namespace === "string" ? args.namespace : void 0,
34956
- limit
34957
- );
34958
- }
34959
- case "engram.memory_store":
34960
- return this.service.memoryStore({
34961
- schemaVersion: typeof args.schemaVersion === "number" ? args.schemaVersion : void 0,
34962
- idempotencyKey: typeof args.idempotencyKey === "string" ? args.idempotencyKey : void 0,
34963
- dryRun: args.dryRun === true,
34964
- sessionKey: typeof args.sessionKey === "string" ? args.sessionKey : void 0,
34965
- authenticatedPrincipal: this.authenticatedPrincipal,
34966
- content: typeof args.content === "string" ? args.content : "",
34967
- category: typeof args.category === "string" ? args.category : void 0,
34968
- confidence: typeof args.confidence === "number" ? args.confidence : void 0,
34969
- namespace: typeof args.namespace === "string" ? args.namespace : void 0,
34970
- tags: Array.isArray(args.tags) ? args.tags.filter((tag) => typeof tag === "string") : void 0,
34971
- entityRef: typeof args.entityRef === "string" ? args.entityRef : void 0,
34972
- ttl: typeof args.ttl === "string" ? args.ttl : void 0,
34973
- sourceReason: typeof args.sourceReason === "string" ? args.sourceReason : void 0
34974
- });
34975
- case "engram.suggestion_submit":
34976
- return this.service.suggestionSubmit({
34977
- schemaVersion: typeof args.schemaVersion === "number" ? args.schemaVersion : void 0,
34978
- idempotencyKey: typeof args.idempotencyKey === "string" ? args.idempotencyKey : void 0,
34979
- dryRun: args.dryRun === true,
34980
- sessionKey: typeof args.sessionKey === "string" ? args.sessionKey : void 0,
34981
- authenticatedPrincipal: this.authenticatedPrincipal,
34982
- content: typeof args.content === "string" ? args.content : "",
34983
- category: typeof args.category === "string" ? args.category : void 0,
34984
- confidence: typeof args.confidence === "number" ? args.confidence : void 0,
34985
- namespace: typeof args.namespace === "string" ? args.namespace : void 0,
34986
- tags: Array.isArray(args.tags) ? args.tags.filter((tag) => typeof tag === "string") : void 0,
34987
- entityRef: typeof args.entityRef === "string" ? args.entityRef : void 0,
34988
- ttl: typeof args.ttl === "string" ? args.ttl : void 0,
34989
- sourceReason: typeof args.sourceReason === "string" ? args.sourceReason : void 0
34990
- });
34991
- case "engram.entity_get":
34992
- return this.service.entityGet(
34993
- typeof args.name === "string" ? args.name : "",
34994
- typeof args.namespace === "string" ? args.namespace : void 0
34995
- );
34996
- case "engram.review_queue_list":
34997
- return this.service.reviewQueue(
34998
- typeof args.runId === "string" ? args.runId : void 0,
34999
- typeof args.namespace === "string" ? args.namespace : void 0
35000
- );
35001
- default:
35002
- throw new Error(`unknown tool: ${name}`);
34990
+ timingSafeStringEqual(a, b) {
34991
+ const left = this.encodeSecret(a);
34992
+ const right = this.encodeSecret(b);
34993
+ if (!left || !right) return false;
34994
+ return timingSafeEqual2(left, right);
34995
+ }
34996
+ encodeSecret(value) {
34997
+ const encoded = Buffer.from(value, "utf-8");
34998
+ if (encoded.length > 1024) return null;
34999
+ const out = Buffer.alloc(2 + 1024);
35000
+ out.writeUInt16BE(encoded.length, 0);
35001
+ encoded.copy(out, 2);
35002
+ return out;
35003
+ }
35004
+ writeResponseStatus(response) {
35005
+ if (response.dryRun === true) return 200;
35006
+ if (response.status === "stored" || response.status === "queued_for_review") return 201;
35007
+ return 200;
35008
+ }
35009
+ ensureWriteRateLimitAvailable() {
35010
+ const now = Date.now();
35011
+ while (this.writeRequestTimestamps.length > 0 && now - (this.writeRequestTimestamps[0] ?? 0) > WRITE_RATE_LIMIT_WINDOW_MS) {
35012
+ this.writeRequestTimestamps.shift();
35013
+ }
35014
+ if (this.writeRequestTimestamps.length >= WRITE_RATE_LIMIT_MAX_REQUESTS) {
35015
+ throw new HttpError(429, "write_rate_limited");
35003
35016
  }
35004
35017
  }
35018
+ recordWriteRateLimitHit() {
35019
+ this.writeRequestTimestamps.push(Date.now());
35020
+ }
35021
+ shouldCountWriteRateLimit(response) {
35022
+ return response.dryRun !== true && response.idempotencyReplay !== true;
35023
+ }
35005
35024
  };
35006
35025
 
35007
35026
  // src/compat/checks.ts