claude-mem 13.9.1 → 13.9.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "13.9.1",
3
+ "version": "13.9.3",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "author": {
6
6
  "name": "Alex Newman",
package/dist/index.js CHANGED
@@ -363,6 +363,29 @@ CREATE INDEX IF NOT EXISTS idx_observation_jobs_event ON observation_generation_
363
363
  CREATE INDEX IF NOT EXISTS idx_observation_jobs_source ON observation_generation_jobs(source_type, source_id);
364
364
  CREATE INDEX IF NOT EXISTS idx_observation_job_events_job_created ON observation_generation_job_events(generation_job_id, created_at);
365
365
  CREATE INDEX IF NOT EXISTS idx_audit_log_scope_created ON audit_log(project_id, team_id, created_at);
366
+
367
+ -- Usage metering: append-only per-team usage, aggregated for quotas + billing.
368
+ -- kind is open-ended ('request', 'tokens_in', 'tokens_out', 'observation', ...).
369
+ CREATE TABLE IF NOT EXISTS usage_events (
370
+ id TEXT PRIMARY KEY,
371
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
372
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
373
+ kind TEXT NOT NULL,
374
+ quantity BIGINT NOT NULL DEFAULT 1,
375
+ metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
376
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
377
+ );
378
+ CREATE INDEX IF NOT EXISTS idx_usage_events_team_created ON usage_events(team_id, created_at);
379
+ CREATE INDEX IF NOT EXISTS idx_usage_events_team_kind_created ON usage_events(team_id, kind, created_at);
380
+
381
+ -- Fixed-window rate-limit counters. subject_id is the api key id (per-key limit).
382
+ CREATE TABLE IF NOT EXISTS rate_limit_counters (
383
+ subject_id TEXT NOT NULL,
384
+ window_start TIMESTAMPTZ NOT NULL,
385
+ count BIGINT NOT NULL DEFAULT 0,
386
+ PRIMARY KEY (subject_id, window_start)
387
+ );
388
+ CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window ON rate_limit_counters(window_start);
366
389
  `;
367
390
 
368
391
  // src/storage/postgres/utils.ts
@@ -1586,6 +1609,77 @@ function mapTeamMemberRow(row) {
1586
1609
  };
1587
1610
  }
1588
1611
 
1612
+ // src/storage/postgres/usage.ts
1613
+ var PostgresUsageRepository = class {
1614
+ constructor(client) {
1615
+ this.client = client;
1616
+ }
1617
+ async record(input) {
1618
+ await this.client.query(
1619
+ `INSERT INTO usage_events (id, team_id, project_id, kind, quantity, metadata)
1620
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb)`,
1621
+ [
1622
+ newId(),
1623
+ input.teamId,
1624
+ input.projectId ?? null,
1625
+ input.kind,
1626
+ Math.max(0, Math.trunc(input.quantity ?? 1)),
1627
+ JSON.stringify(input.metadata ?? {})
1628
+ ]
1629
+ );
1630
+ }
1631
+ /** Total quantity of one `kind` for a team since `since` — the quota read. */
1632
+ async total(input) {
1633
+ const res = await this.client.query(
1634
+ `SELECT COALESCE(SUM(quantity), 0)::bigint AS total
1635
+ FROM usage_events
1636
+ WHERE team_id = $1 AND kind = $2 AND created_at >= $3`,
1637
+ [input.teamId, input.kind, input.since]
1638
+ );
1639
+ return Number(res.rows[0]?.total ?? 0);
1640
+ }
1641
+ /** Per-kind totals for a team since `since` — the /v1/usage read. */
1642
+ async summarize(input) {
1643
+ const res = await this.client.query(
1644
+ `SELECT kind, SUM(quantity)::bigint AS total
1645
+ FROM usage_events
1646
+ WHERE team_id = $1 AND created_at >= $2
1647
+ GROUP BY kind`,
1648
+ [input.teamId, input.since]
1649
+ );
1650
+ const out = {};
1651
+ for (const row of res.rows) out[row.kind] = Number(row.total);
1652
+ return out;
1653
+ }
1654
+ };
1655
+
1656
+ // src/storage/postgres/rate-limit.ts
1657
+ var PostgresRateLimitRepository = class {
1658
+ constructor(client) {
1659
+ this.client = client;
1660
+ }
1661
+ /**
1662
+ * Atomically increment the current window's counter for `subjectId` and
1663
+ * report whether it is still within `limit`.
1664
+ */
1665
+ async hit(input) {
1666
+ const res = await this.client.query(
1667
+ `INSERT INTO rate_limit_counters (subject_id, window_start, count)
1668
+ VALUES ($1, $2, 1)
1669
+ ON CONFLICT (subject_id, window_start)
1670
+ DO UPDATE SET count = rate_limit_counters.count + 1
1671
+ RETURNING count`,
1672
+ [input.subjectId, input.windowStart]
1673
+ );
1674
+ const count = Number(res.rows[0]?.count ?? 0);
1675
+ return { allowed: count <= input.limit, count, limit: input.limit, windowStart: input.windowStart };
1676
+ }
1677
+ /** Best-effort cleanup of expired windows. Call off the hot path. */
1678
+ async prune(before) {
1679
+ await this.client.query(`DELETE FROM rate_limit_counters WHERE window_start < $1`, [before]);
1680
+ }
1681
+ };
1682
+
1589
1683
  // src/storage/postgres/index.ts
1590
1684
  function createPostgresStorageRepositories(client) {
1591
1685
  return {
@@ -1597,7 +1691,9 @@ function createPostgresStorageRepositories(client) {
1597
1691
  observations: new PostgresObservationRepository(client),
1598
1692
  observationSources: new PostgresObservationSourcesRepository(client),
1599
1693
  observationGenerationJobs: new PostgresObservationGenerationJobRepository(client),
1600
- observationGenerationJobEvents: new PostgresObservationGenerationJobEventsRepository(client)
1694
+ observationGenerationJobEvents: new PostgresObservationGenerationJobEventsRepository(client),
1695
+ usage: new PostgresUsageRepository(client),
1696
+ rateLimits: new PostgresRateLimitRepository(client)
1601
1697
  };
1602
1698
  }
1603
1699
 
@@ -1994,10 +2090,6 @@ var SettingsDefaultsManager = class {
1994
2090
  // Default Gemini model (highest free tier RPM)
1995
2091
  CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: "true",
1996
2092
  // Rate limiting ON by default for free tier users
1997
- CLAUDE_MEM_GEMINI_MAX_CONTEXT_MESSAGES: "20",
1998
- // Max messages in Gemini context window
1999
- CLAUDE_MEM_GEMINI_MAX_TOKENS: "100000",
2000
- // Max estimated tokens (~100k safety limit)
2001
2093
  CLAUDE_MEM_OPENROUTER_API_KEY: "",
2002
2094
  // Empty by default, can be set via UI or env
2003
2095
  CLAUDE_MEM_OPENROUTER_MODEL: "xiaomi/mimo-v2-flash:free",
@@ -2008,10 +2100,6 @@ var SettingsDefaultsManager = class {
2008
2100
  // Optional: for OpenRouter analytics
2009
2101
  CLAUDE_MEM_OPENROUTER_APP_NAME: "claude-mem",
2010
2102
  // App name for OpenRouter analytics
2011
- CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES: "20",
2012
- // Max messages in context window
2013
- CLAUDE_MEM_OPENROUTER_MAX_TOKENS: "100000",
2014
- // Max estimated tokens (~100k safety limit)
2015
2103
  CLAUDE_MEM_DATA_DIR: join3(homedir2(), ".claude-mem"),
2016
2104
  CLAUDE_MEM_LOG_LEVEL: "INFO",
2017
2105
  CLAUDE_MEM_PYTHON_VERSION: "3.13",
@@ -6066,7 +6154,7 @@ async function processGeneratedResponse(input) {
6066
6154
  const observationsToWrite = parsed.observations ?? [];
6067
6155
  const skipped = parsed.summary?.skipped === true;
6068
6156
  const privateContentDetected = skipped || observationsToWrite.length === 0;
6069
- return await withPostgresTransaction(input.pool, async (client) => {
6157
+ const outcome = await withPostgresTransaction(input.pool, async (client) => {
6070
6158
  const obsRepo = new PostgresObservationRepository(client);
6071
6159
  const sourcesRepo = new PostgresObservationSourcesRepository(client);
6072
6160
  const jobsRepo = new PostgresObservationGenerationJobRepository(client);
@@ -6226,6 +6314,35 @@ async function processGeneratedResponse(input) {
6226
6314
  privateContentDetected
6227
6315
  };
6228
6316
  });
6317
+ if (outcome.kind === "completed" && process.env.CLAUDE_MEM_USAGE_METERING === "1") {
6318
+ try {
6319
+ const usageRepo = new PostgresUsageRepository(input.pool);
6320
+ if (input.tokensUsed && input.tokensUsed > 0) {
6321
+ await usageRepo.record({
6322
+ teamId: input.job.teamId,
6323
+ projectId: input.job.projectId,
6324
+ kind: "tokens",
6325
+ quantity: input.tokensUsed,
6326
+ metadata: { jobId: input.job.id, provider: input.providerLabel, model: input.modelId ?? null }
6327
+ });
6328
+ }
6329
+ if (outcome.observations.length > 0) {
6330
+ await usageRepo.record({
6331
+ teamId: input.job.teamId,
6332
+ projectId: input.job.projectId,
6333
+ kind: "observation",
6334
+ quantity: outcome.observations.length,
6335
+ metadata: { jobId: input.job.id }
6336
+ });
6337
+ }
6338
+ } catch (usageError) {
6339
+ logger.warn("SYSTEM", "usage metering record failed (post-commit)", {
6340
+ jobId: input.job.id,
6341
+ error: usageError instanceof Error ? usageError.message : String(usageError)
6342
+ });
6343
+ }
6344
+ }
6345
+ return outcome;
6229
6346
  }
6230
6347
  function renderObservationContent(observation) {
6231
6348
  const parts = [];