hippo-memory 1.57.0 → 1.58.0

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 (91) hide show
  1. package/README.md +11 -0
  2. package/dist/agent-memories/claude-code.js +1 -1
  3. package/dist/agent-memories/gemini.js +1 -1
  4. package/dist/api-errors.d.ts +27 -0
  5. package/dist/api-errors.js +37 -0
  6. package/dist/api.d.ts +5 -5
  7. package/dist/api.js +40 -47
  8. package/dist/audit.d.ts +4 -0
  9. package/dist/audit.js +11 -0
  10. package/dist/autolearn.d.ts +1 -1
  11. package/dist/autolearn.js +7 -5
  12. package/dist/capture-contract.d.ts +47 -0
  13. package/dist/capture-contract.js +49 -0
  14. package/dist/capture-error.js +2 -1
  15. package/dist/capture.d.ts +0 -13
  16. package/dist/capture.js +5 -66
  17. package/dist/cli/shared.js +10 -6
  18. package/dist/cli.js +100 -39
  19. package/dist/client.js +9 -0
  20. package/dist/codex-patch.js +1 -1
  21. package/dist/compaction-record.d.ts +1 -1
  22. package/dist/compaction-record.js +3 -2
  23. package/dist/config.d.ts +5 -0
  24. package/dist/config.js +17 -0
  25. package/dist/connectors/github/dlq.js +5 -2
  26. package/dist/connectors/github/octokit-client.js +4 -2
  27. package/dist/connectors/slack/dlq.js +6 -2
  28. package/dist/connectors/slack/web-client.js +7 -5
  29. package/dist/consolidate.d.ts +10 -0
  30. package/dist/consolidate.js +36 -34
  31. package/dist/customer-notes.js +14 -13
  32. package/dist/dag.js +3 -2
  33. package/dist/dashboard.js +1 -1
  34. package/dist/db.d.ts +12 -0
  35. package/dist/db.js +62 -1
  36. package/dist/decisions.js +9 -8
  37. package/dist/doctor.js +5 -0
  38. package/dist/embedding-provider.js +3 -3
  39. package/dist/embeddings.d.ts +4 -4
  40. package/dist/embeddings.js +72 -16
  41. package/dist/extract.js +3 -2
  42. package/dist/http-retry.d.ts +21 -0
  43. package/dist/http-retry.js +50 -0
  44. package/dist/http-util.d.ts +8 -0
  45. package/dist/http-util.js +10 -0
  46. package/dist/importers.d.ts +2 -0
  47. package/dist/importers.js +16 -5
  48. package/dist/incidents.js +11 -10
  49. package/dist/judgment.js +10 -17
  50. package/dist/log.d.ts +25 -0
  51. package/dist/log.js +48 -0
  52. package/dist/mcp/server.js +52 -24
  53. package/dist/mcp/tool-args.d.ts +21 -0
  54. package/dist/mcp/tool-args.js +80 -0
  55. package/dist/memory.js +3 -2
  56. package/dist/overlap-index.d.ts +7 -0
  57. package/dist/overlap-index.js +38 -0
  58. package/dist/pilot-arm.d.ts +9 -0
  59. package/dist/pilot-arm.js +47 -0
  60. package/dist/policies.js +12 -11
  61. package/dist/predictions.js +9 -8
  62. package/dist/processes.js +14 -13
  63. package/dist/project-briefs.js +16 -15
  64. package/dist/project-identity.d.ts +1 -1
  65. package/dist/project-identity.js +25 -1
  66. package/dist/raw-archive.js +7 -6
  67. package/dist/recall-scope.d.ts +2 -1
  68. package/dist/recall-scope.js +2 -1
  69. package/dist/refine-llm.js +3 -2
  70. package/dist/reject-flow.js +6 -9
  71. package/dist/rejection.d.ts +2 -1
  72. package/dist/rejection.js +2 -1
  73. package/dist/search.js +14 -2
  74. package/dist/secret-detect.d.ts +13 -1
  75. package/dist/secret-detect.js +33 -1
  76. package/dist/server.d.ts +3 -1
  77. package/dist/server.js +180 -409
  78. package/dist/session-digest.js +2 -1
  79. package/dist/shared.js +7 -6
  80. package/dist/skills.js +15 -14
  81. package/dist/store.js +4 -4
  82. package/dist/token-ledger.d.ts +4 -2
  83. package/dist/token-ledger.js +2 -2
  84. package/dist/version.d.ts +1 -1
  85. package/dist/version.js +1 -1
  86. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  87. package/extensions/openclaw-plugin/package.json +1 -1
  88. package/openclaw.plugin.json +1 -1
  89. package/package.json +1 -1
  90. package/dist/connectors/slack/ratelimit.d.ts +0 -9
  91. package/dist/connectors/slack/ratelimit.js +0 -18
@@ -2,7 +2,8 @@
2
2
  import { createHash } from 'crypto';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
- import { collectSessionTurns, errorMessage, isObjectLike, isStringValue, readTranscriptTail, splitSentences, } from './capture.js';
5
+ import { collectSessionTurns, errorMessage, readTranscriptTail, splitSentences, } from './capture.js';
6
+ import { isObjectLike, isStringValue } from './capture-contract.js';
6
7
  import { PATCH_SUCCESS_LINE, patchPaths, shellPatch } from './codex-patch.js';
7
8
  import { loadConfig } from './config.js';
8
9
  import { createMemory, Layer } from './memory.js';
package/dist/shared.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * Resolution: $HIPPO_HOME > $XDG_DATA_HOME/hippo > ~/.hippo/
5
5
  * Local .hippo/ stores are per-project.
6
6
  */
7
+ import { BadRequestError, NotFoundError } from './api-errors.js';
7
8
  import * as fs from 'fs';
8
9
  import * as path from 'path';
9
10
  import { generateId, COMPACTION_MEMORY_TAG } from './memory.js';
@@ -48,16 +49,16 @@ export function initGlobal() {
48
49
  export function promoteToGlobal(localRoot, id, opts) {
49
50
  const entry = readEntry(localRoot, id, opts?.tenantId);
50
51
  if (!entry)
51
- throw new Error(`Memory not found: ${id}`);
52
+ throw new NotFoundError(`Memory not found: ${id}`);
52
53
  // CD5: same veto as shareMemory; a promoted copy would have no quarantine record to review.
53
54
  if (isQuarantineScope(entry.scope)) {
54
- throw new Error(`Refusing to promote ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
55
+ throw new BadRequestError(`Refusing to promote ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
55
56
  }
56
57
  // v39 S4 producer veto: promote is a producer path to the global store
57
58
  // exactly like shareMemory - same hard rule (codex gating review P2).
58
59
  const promoteSecret = detectSecret(entry);
59
60
  if (promoteSecret.flagged) {
60
- throw new Error(`Refusing to promote ${id} to the global store: content matches secret material (${promoteSecret.reason}). ` +
61
+ throw new BadRequestError(`Refusing to promote ${id} to the global store: content matches secret material (${promoteSecret.reason}). ` +
61
62
  `Secrets stay in their owning project's store.`);
62
63
  }
63
64
  initGlobal();
@@ -273,18 +274,18 @@ export function shareMemory(localRoot, id, options = {}) {
273
274
  // when tenantId is provided.
274
275
  const entry = readEntry(localRoot, id, options.tenantId);
275
276
  if (!entry)
276
- throw new Error(`Memory not found: ${id}`);
277
+ throw new NotFoundError(`Memory not found: ${id}`);
277
278
  // v39 S4 producer veto: secrets never go to the global store, not even
278
279
  // with --force. Explicit and loud - a silent null would read as "low
279
280
  // transfer score" and invite retries.
280
281
  const secret = detectSecret(entry);
281
282
  if (secret.flagged) {
282
- throw new Error(`Refusing to share ${id} to the global store: content matches secret material (${secret.reason}). ` +
283
+ throw new BadRequestError(`Refusing to share ${id} to the global store: content matches secret material (${secret.reason}). ` +
283
284
  `Secrets stay in their owning project's store.`);
284
285
  }
285
286
  // CD5: a quarantined row is unreviewed input, not a lesson; sharing it would spread poison globally.
286
287
  if (isQuarantineScope(entry.scope)) {
287
- throw new Error(`Refusing to share ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
288
+ throw new BadRequestError(`Refusing to share ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
288
289
  }
289
290
  const score = transferScore(entry);
290
291
  if (score < 0.3 && !options.force)
package/dist/skills.js CHANGED
@@ -21,6 +21,7 @@
21
21
  * Lifecycle: active -> superseded (a newer version replaces it) or active ->
22
22
  * closed (retired). Export renders ACTIVE skills only.
23
23
  */
24
+ import { BadRequestError, ConflictError, NotFoundError } from './api-errors.js';
24
25
  import { openHippoDb, closeHippoDb } from './db.js';
25
26
  import { writeEntry } from './store.js';
26
27
  import { assertTenantId } from './tenant.js';
@@ -49,28 +50,28 @@ export const MAX_EXPORT_SKILLS = 1000;
49
50
  function validateSkillFields(skillName, instructions, trigger) {
50
51
  const name = (skillName ?? '').trim();
51
52
  if (name.length === 0)
52
- throw new Error('saveSkill: skillName is required');
53
+ throw new BadRequestError('saveSkill: skillName is required');
53
54
  if (/[\r\n]/.test(name))
54
- throw new Error('saveSkill: skillName must be a single line (no newlines)');
55
+ throw new BadRequestError('saveSkill: skillName must be a single line (no newlines)');
55
56
  if (name.length > MAX_SKILL_NAME_LEN) {
56
- throw new Error(`saveSkill: skillName exceeds the ${MAX_SKILL_NAME_LEN}-char cap`);
57
+ throw new BadRequestError(`saveSkill: skillName exceeds the ${MAX_SKILL_NAME_LEN}-char cap`);
57
58
  }
58
59
  if (!instructions || instructions.trim().length === 0) {
59
- throw new Error('saveSkill: instructions are required');
60
+ throw new BadRequestError('saveSkill: instructions are required');
60
61
  }
61
62
  if (instructions.length > MAX_SKILL_INSTRUCTIONS_LEN) {
62
- throw new Error(`saveSkill: instructions exceed the ${MAX_SKILL_INSTRUCTIONS_LEN}-char cap`);
63
+ throw new BadRequestError(`saveSkill: instructions exceed the ${MAX_SKILL_INSTRUCTIONS_LEN}-char cap`);
63
64
  }
64
65
  let triggerVal = null;
65
66
  if (trigger !== undefined && trigger !== null && trigger.trim().length > 0) {
66
67
  if (trigger.length > MAX_SKILL_TRIGGER_LEN) {
67
- throw new Error(`saveSkill: trigger exceeds the ${MAX_SKILL_TRIGGER_LEN}-char cap`);
68
+ throw new BadRequestError(`saveSkill: trigger exceeds the ${MAX_SKILL_TRIGGER_LEN}-char cap`);
68
69
  }
69
70
  // Single-line, like skill_name: a trigger is a short "when to apply" phrase,
70
71
  // and a newline would let it forge a heading inside the export **When:** line
71
72
  // (independent-review 2026-05-30). Reject rather than emit a multi-line trigger.
72
73
  if (/[\r\n]/.test(trigger)) {
73
- throw new Error('saveSkill: trigger must be a single line (no newlines)');
74
+ throw new BadRequestError('saveSkill: trigger must be a single line (no newlines)');
74
75
  }
75
76
  triggerVal = trigger;
76
77
  }
@@ -148,10 +149,10 @@ export function saveSkill(hippoRoot, tenantId, opts, actor = 'cli') {
148
149
  // SAFETY: row shape matches the `status, version` columns named in the SELECT below.
149
150
  const pred = db.prepare(`SELECT status, version FROM skills WHERE id = ? AND tenant_id = ?`).get(opts.supersedesSkillId, tenantId);
150
151
  if (!pred) {
151
- throw new Error(`saveSkill: skill ${opts.supersedesSkillId} to supersede not found for tenant ${tenantId}`);
152
+ throw new NotFoundError(`saveSkill: skill ${opts.supersedesSkillId} to supersede not found for tenant ${tenantId}`);
152
153
  }
153
154
  if (pred.status !== 'active') {
154
- throw new Error(`saveSkill: skill ${opts.supersedesSkillId} is not active (status='${pred.status}'); only active skills can be superseded.`);
155
+ throw new ConflictError(`saveSkill: skill ${opts.supersedesSkillId} is not active (status='${pred.status}'); only active skills can be superseded.`);
155
156
  }
156
157
  version = pred.version + 1;
157
158
  }
@@ -169,7 +170,7 @@ export function saveSkill(hippoRoot, tenantId, opts, actor = 'cli') {
169
170
  WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
170
171
  `).run(skillId, now, opts.supersedesSkillId, tenantId, skillId);
171
172
  if (sup.changes === 0) {
172
- throw new Error(`saveSkill: skill ${opts.supersedesSkillId} could not be superseded (no longer active or self-reference).`);
173
+ throw new ConflictError(`saveSkill: skill ${opts.supersedesSkillId} could not be superseded (no longer active or self-reference).`);
173
174
  }
174
175
  appendAuditEvent(db, {
175
176
  tenantId,
@@ -228,15 +229,15 @@ export function closeSkill(hippoRoot, tenantId, id, actor = 'cli') {
228
229
  // SAFETY: row shape matches the single `status` column named in the SELECT above.
229
230
  const existing = db.prepare(`SELECT status FROM skills WHERE id = ? AND tenant_id = ?`).get(id, tenantId);
230
231
  if (!existing) {
231
- throw new Error(`closeSkill: skill ${id} not found for tenant ${tenantId}`);
232
+ throw new NotFoundError(`closeSkill: skill ${id} not found for tenant ${tenantId}`);
232
233
  }
233
- throw new Error(`closeSkill: skill ${id} is not active (status='${existing.status}'); only active skills can be closed.`);
234
+ throw new ConflictError(`closeSkill: skill ${id} is not active (status='${existing.status}'); only active skills can be closed.`);
234
235
  }
235
236
  // SAFETY: row's shape matches the columns named in SKILL_COLS above.
236
237
  const row = db.prepare(`SELECT ${SKILL_COLS} FROM skills WHERE id = ? AND tenant_id = ?`)
237
238
  .get(id, tenantId);
238
239
  if (!row)
239
- throw new Error(`closeSkill: skill ${id} not found after UPDATE`);
240
+ throw new NotFoundError(`closeSkill: skill ${id} not found after UPDATE`);
240
241
  appendAuditEvent(db, {
241
242
  tenantId,
242
243
  actor,
@@ -282,7 +283,7 @@ export function loadSkills(hippoRoot, tenantId, opts = {}) {
282
283
  let rows;
283
284
  if (opts.status) {
284
285
  if (!VALID_SKILL_STATES.has(opts.status)) {
285
- throw new Error(`loadSkills: status must be one of ${Array.from(VALID_SKILL_STATES).join('|')}; got ${opts.status}`);
286
+ throw new BadRequestError(`loadSkills: status must be one of ${Array.from(VALID_SKILL_STATES).join('|')}; got ${opts.status}`);
286
287
  }
287
288
  // SAFETY: rows' shape matches the columns named in SKILL_COLS above.
288
289
  rows = db.prepare(`
package/dist/store.js CHANGED
@@ -15,7 +15,7 @@ import { RECALL_DEFAULT_DENY_SCOPES } from './recall-scope.js';
15
15
  import { assertTenantId } from './tenant.js';
16
16
  import { isRecallBoostAblated } from './ablation.js';
17
17
  import { rarestPromptTerms, RAREST_TERM_COUNT } from './prompt-recall.js';
18
- import { appendAuditEvent } from './audit.js';
18
+ import { appendAuditEvent, reportAuditWriteFailure } from './audit.js';
19
19
  import { resolveTenantId } from './tenant.js';
20
20
  import { redactSecretsStrict } from './secret-detect.js';
21
21
  import { deriveOriginProject, originFromSource, findHippoStoreDir, realpathOrResolve } from './project-identity.js';
@@ -42,9 +42,9 @@ function audit(db, op, targetId, metadata, actor = 'cli', tenantId) {
42
42
  metadata,
43
43
  });
44
44
  }
45
- catch {
46
- // Audit must never crash a mutation. Failures here mean the audit_log
47
- // table is broken; the mutation has already succeeded.
45
+ catch (error) {
46
+ // The mutation has already succeeded; a broken audit table must not undo it.
47
+ reportAuditWriteFailure(op, String(error), targetId);
48
48
  }
49
49
  }
50
50
  /**
@@ -7,8 +7,9 @@ import type { DatabaseSyncLike } from './db.js';
7
7
  * - `context`, `recall`: the CLI commands.
8
8
  * - `mcp_recall`, `mcp_context`: the MCP tools.
9
9
  * - `http_recall`, `http_context`, `http_assemble`: the HTTP API.
10
+ * - `pilot`: the session's pilot arm row (src/pilot-arm.ts); not a send, so it is in no surface list.
10
11
  */
11
- export type TokenSurface = 'hook' | 'hook_recall' | 'compact_resume' | 'context' | 'recall' | 'mcp_recall' | 'mcp_context' | 'http_recall' | 'http_context' | 'http_assemble';
12
+ export type TokenSurface = 'hook' | 'hook_recall' | 'compact_resume' | 'context' | 'recall' | 'mcp_recall' | 'mcp_context' | 'http_recall' | 'http_context' | 'http_assemble' | 'pilot';
12
13
  /** All surfaces, in report order. */
13
14
  export declare const TOKEN_SURFACES: readonly TokenSurface[];
14
15
  /** Surfaces whose re-reads are counted: only a hook payload tells a sub-agent's block from its parent's, as both carry one session id. */
@@ -19,8 +20,9 @@ export declare const REREAD_SURFACES: readonly TokenSurface[];
19
20
  * - `skip`: identical to the session's last injected block, so not sent again.
20
21
  * - `reset`: the host compacted its context, so the next block must be sent.
21
22
  * - `reread`: tokens later calls read again, booked at session end as one row per session, hook surface and UTC day of the calls.
23
+ * - `arm`: the pilot assignment; `block_hash` is `hippo` or `holdout`, `items` the holdout rate in basis points.
22
24
  */
23
- export type TokenEvent = 'inject' | 'skip' | 'reset' | 'reread';
25
+ export type TokenEvent = 'inject' | 'skip' | 'reset' | 'reread' | 'arm';
24
26
  /** Rows older than this are pruned on write. */
25
27
  export declare const TOKEN_LEDGER_RETENTION_DAYS = 90;
26
28
  /**
@@ -125,7 +125,7 @@ export function summarizeTokenUse(db, tenantId, sinceIso) {
125
125
  COUNT(DISTINCT CASE WHEN event = 'reread' THEN session_id END) AS reread_sessions,
126
126
  SUM(CASE WHEN event = 'inject' THEN tokens ELSE 0 END) AS tokens
127
127
  FROM token_ledger
128
- WHERE tenant_id = ? AND ts >= ? AND session_id IS NOT NULL`).get(...REREAD_SURFACES, tenantId, sinceIso);
128
+ WHERE tenant_id = ? AND ts >= ? AND session_id IS NOT NULL AND event <> 'arm'`).get(...REREAD_SURFACES, tenantId, sinceIso);
129
129
  const sessionCount = Number(perSession?.sessions ?? 0);
130
130
  const sessionTokens = Number(perSession?.tokens ?? 0);
131
131
  return {
@@ -197,7 +197,7 @@ export function tokensBySession(db, tenantId, sinceIso) {
197
197
  SUM(CASE WHEN event = 'skip' THEN tokens ELSE 0 END) AS skipped,
198
198
  SUM(CASE WHEN event = 'inject' THEN 1 ELSE 0 END) AS injections
199
199
  FROM token_ledger
200
- WHERE tenant_id = ? AND ts >= ? AND session_id IS NOT NULL
200
+ WHERE tenant_id = ? AND ts >= ? AND session_id IS NOT NULL AND event <> 'arm'
201
201
  GROUP BY session_id`).all(tenantId, sinceIso);
202
202
  return rows.map((r) => ({
203
203
  sessionId: r.session_id,
package/dist/version.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
17
17
  * any packager that drops .json files.
18
18
  */
19
- export declare const PACKAGE_VERSION = "1.57.0";
19
+ export declare const PACKAGE_VERSION = "1.58.0";
20
20
  /** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
21
21
  export declare function compareSemver(a: string, b: string): number;
22
22
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
17
17
  * any packager that drops .json files.
18
18
  */
19
- export const PACKAGE_VERSION = '1.57.0';
19
+ export const PACKAGE_VERSION = '1.58.0';
20
20
  /** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
21
21
  export function compareSemver(a, b) {
22
22
  const parse = (v) => {
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Memory for AI agents that learns what is wrong and ranks it down. Injects context at session start and captures errors.",
5
- "version": "1.57.0",
5
+ "version": "1.58.0",
6
6
 
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.57.0",
3
+ "version": "1.58.0",
4
4
  "type": "module",
5
5
  "description": "Hippo Memory plugin for OpenClaw - biologically-inspired agent memory",
6
6
  "main": "index.ts",
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Memory for AI agents that learns what is wrong and ranks it down. Injects context at session start and captures errors.",
5
- "version": "1.57.0",
5
+ "version": "1.58.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.57.0",
3
+ "version": "1.58.0",
4
4
  "description": "Memory for AI agents that learns what is wrong and ranks it down. MCP server, hooks for Claude Code, OpenCode and Codex, AGENTS.md instructions for Codex, Cursor, OpenClaw and Pi. SQLite, zero runtime deps.",
5
5
  "mcpName": "io.github.kitfunso/hippo-memory",
6
6
  "type": "module",
@@ -1,9 +0,0 @@
1
- export interface RetryOpts {
2
- url: string;
3
- init?: RequestInit;
4
- fetchImpl?: typeof fetch;
5
- sleep?: (ms: number) => Promise<void>;
6
- maxRetries?: number;
7
- }
8
- export declare function fetchWithRetry(opts: RetryOpts): Promise<Response>;
9
- //# sourceMappingURL=ratelimit.d.ts.map
@@ -1,18 +0,0 @@
1
- export async function fetchWithRetry(opts) {
2
- const f = opts.fetchImpl ?? fetch;
3
- const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4
- const max = opts.maxRetries ?? 3;
5
- let attempt = 0;
6
- while (true) {
7
- const r = await f(opts.url, opts.init);
8
- if (r.status !== 429)
9
- return r;
10
- if (attempt >= max)
11
- throw new Error(`rate-limited after ${attempt + 1} attempts: ${opts.url}`);
12
- const ra = r.headers.get('retry-after');
13
- const delaySec = ra ? Number(ra) : Math.pow(2, attempt);
14
- await sleep(Math.max(0, delaySec) * 1000);
15
- attempt++;
16
- }
17
- }
18
- //# sourceMappingURL=ratelimit.js.map