claude-mem 13.9.0 → 13.9.2

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
@@ -213,7 +213,6 @@ CREATE TABLE IF NOT EXISTS server_sessions (
213
213
  last_generated_at TIMESTAMPTZ,
214
214
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
215
215
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
216
- UNIQUE (project_id, external_session_id),
217
216
  FOREIGN KEY (project_id, team_id) REFERENCES projects(id, team_id) ON DELETE CASCADE
218
217
  );
219
218
 
@@ -318,6 +317,7 @@ CREATE TABLE IF NOT EXISTS observation_generation_job_events (
318
317
 
319
318
  CREATE INDEX IF NOT EXISTS idx_agent_events_project_session ON agent_events(project_id, server_session_id, occurred_at);
320
319
  ALTER TABLE server_sessions ADD COLUMN IF NOT EXISTS idempotency_key TEXT;
320
+ ALTER TABLE server_sessions DROP CONSTRAINT IF EXISTS server_sessions_project_id_external_session_id_key;
321
321
  -- #2560 \u2014 platform_source on agent_events (consistent with server_sessions and
322
322
  -- the plan-09 scoping): which platform produced the event (claude-code,
323
323
  -- opencode, cursor, ...). Idempotent so an existing DB upgrades in place.
@@ -334,10 +334,16 @@ ALTER TABLE observation_generation_jobs DROP CONSTRAINT IF EXISTS observation_ge
334
334
  CREATE UNIQUE INDEX IF NOT EXISTS idx_server_sessions_project_idempotency
335
335
  ON server_sessions(project_id, idempotency_key)
336
336
  WHERE idempotency_key IS NOT NULL;
337
- -- Supports the session linkage lookup on the /v1/events ingest path
338
- -- (WHERE content_session_id = $1 AND project_id = $2 ...).
339
- CREATE INDEX IF NOT EXISTS idx_server_sessions_content_session
340
- ON server_sessions(project_id, content_session_id)
337
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_server_sessions_external_session_legacy
338
+ ON server_sessions(project_id, external_session_id)
339
+ WHERE external_session_id IS NOT NULL AND platform_source IS NULL;
340
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_server_sessions_external_session_platform
341
+ ON server_sessions(project_id, platform_source, external_session_id)
342
+ WHERE external_session_id IS NOT NULL AND platform_source IS NOT NULL;
343
+ DROP INDEX IF EXISTS idx_server_sessions_content_session;
344
+ -- Supports platform-aware session linkage lookup on the /v1/events ingest path.
345
+ CREATE INDEX IF NOT EXISTS idx_server_sessions_content_session_platform
346
+ ON server_sessions(team_id, project_id, platform_source, content_session_id, started_at DESC)
341
347
  WHERE content_session_id IS NOT NULL;
342
348
  CREATE UNIQUE INDEX IF NOT EXISTS idx_observations_generation_key_scope
343
349
  ON observations(team_id, project_id, generation_key)
@@ -357,6 +363,29 @@ CREATE INDEX IF NOT EXISTS idx_observation_jobs_event ON observation_generation_
357
363
  CREATE INDEX IF NOT EXISTS idx_observation_jobs_source ON observation_generation_jobs(source_type, source_id);
358
364
  CREATE INDEX IF NOT EXISTS idx_observation_job_events_job_created ON observation_generation_job_events(generation_job_id, created_at);
359
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);
360
389
  `;
361
390
 
362
391
  // src/storage/postgres/utils.ts
@@ -430,6 +459,26 @@ function sortJson(value) {
430
459
  return value;
431
460
  }
432
461
 
462
+ // src/shared/platform-source.ts
463
+ var DEFAULT_PLATFORM_SOURCE = "claude";
464
+ function sanitizeRawSource(value) {
465
+ return value.trim().toLowerCase().replace(/\s+/g, "-");
466
+ }
467
+ function normalizePlatformSource(value) {
468
+ if (!value) return DEFAULT_PLATFORM_SOURCE;
469
+ const source = sanitizeRawSource(value);
470
+ if (!source) return DEFAULT_PLATFORM_SOURCE;
471
+ if (source === "transcript") return "codex";
472
+ if (source.includes("codex")) return "codex";
473
+ if (source.includes("cursor")) return "cursor";
474
+ if (source.includes("claude")) return "claude";
475
+ return source;
476
+ }
477
+ function normalizePlatformSourceOrNull(value) {
478
+ if (typeof value !== "string") return null;
479
+ return normalizePlatformSource(value);
480
+ }
481
+
433
482
  // src/storage/postgres/agent-events.ts
434
483
  var PostgresAgentEventsRepository = class {
435
484
  constructor(client) {
@@ -441,6 +490,7 @@ var PostgresAgentEventsRepository = class {
441
490
  await assertSessionOwnership(this.client, input.serverSessionId, input.projectId, input.teamId);
442
491
  }
443
492
  const idempotencyKey = buildAgentEventIdempotencyKey(input);
493
+ const platformSource = normalizePlatformSourceOrNull(input.platformSource);
444
494
  const row = await queryOne(
445
495
  this.client,
446
496
  `
@@ -463,7 +513,7 @@ var PostgresAgentEventsRepository = class {
463
513
  input.sourceEventId ?? null,
464
514
  idempotencyKey,
465
515
  input.eventType,
466
- input.platformSource ?? null,
516
+ platformSource,
467
517
  JSON.stringify(input.payload ?? {}),
468
518
  JSON.stringify(input.metadata ?? {}),
469
519
  new Date(input.occurredAt)
@@ -495,11 +545,14 @@ var PostgresAgentEventsRepository = class {
495
545
  }
496
546
  };
497
547
  function buildAgentEventIdempotencyKey(input) {
548
+ const platformSource = normalizePlatformSourceOrNull(input.platformSource);
549
+ const platformScope = platformSource ? [platformSource] : [];
498
550
  if (input.sourceEventId) {
499
551
  return `agent_event:v1:${deterministicKey([
500
552
  input.teamId,
501
553
  input.projectId,
502
554
  input.sourceAdapter,
555
+ ...platformScope,
503
556
  input.sourceEventId
504
557
  ])}`;
505
558
  }
@@ -507,6 +560,7 @@ function buildAgentEventIdempotencyKey(input) {
507
560
  input.teamId,
508
561
  input.projectId,
509
562
  input.sourceAdapter,
563
+ ...platformScope,
510
564
  input.contentSessionId ?? input.serverSessionId ?? null,
511
565
  input.eventType,
512
566
  new Date(input.occurredAt).toISOString(),
@@ -968,16 +1022,39 @@ var PostgresObservationRepository = class {
968
1022
  return result.rows.map(mapObservationRow);
969
1023
  }
970
1024
  async search(input) {
1025
+ const platformSource = normalizePlatformSourceOrNull(input.platformSource);
971
1026
  const result = await this.client.query(
972
1027
  `
973
- SELECT * FROM observations
974
- WHERE project_id = $1
975
- AND team_id = $2
976
- AND content_search @@ websearch_to_tsquery('english', $3)
977
- ORDER BY ts_rank(content_search, websearch_to_tsquery('english', $3)) DESC, updated_at DESC
1028
+ SELECT observations.* FROM observations
1029
+ LEFT JOIN server_sessions
1030
+ ON server_sessions.id = observations.server_session_id
1031
+ AND server_sessions.project_id = observations.project_id
1032
+ AND server_sessions.team_id = observations.team_id
1033
+ WHERE observations.project_id = $1
1034
+ AND observations.team_id = $2
1035
+ AND observations.content_search @@ websearch_to_tsquery('english', $3)
1036
+ AND (
1037
+ $5::text IS NULL
1038
+ OR server_sessions.platform_source = $5
1039
+ OR (
1040
+ observations.server_session_id IS NULL
1041
+ AND EXISTS (
1042
+ SELECT 1
1043
+ FROM observation_sources
1044
+ INNER JOIN agent_events
1045
+ ON agent_events.id = observation_sources.agent_event_id
1046
+ AND agent_events.project_id = observations.project_id
1047
+ AND agent_events.team_id = observations.team_id
1048
+ WHERE observation_sources.observation_id = observations.id
1049
+ AND observation_sources.source_type = 'agent_event'
1050
+ AND agent_events.platform_source = $5
1051
+ )
1052
+ )
1053
+ )
1054
+ ORDER BY ts_rank(observations.content_search, websearch_to_tsquery('english', $3)) DESC, observations.updated_at DESC
978
1055
  LIMIT $4
979
1056
  `,
980
- [input.projectId, input.teamId, input.query, input.limit ?? 20]
1057
+ [input.projectId, input.teamId, input.query, input.limit ?? 20, platformSource]
981
1058
  );
982
1059
  return result.rows.map(mapObservationRow);
983
1060
  }
@@ -1190,7 +1267,11 @@ var PostgresServerSessionsRepository = class {
1190
1267
  async create(input) {
1191
1268
  await assertProjectOwnership(this.client, input.projectId, input.teamId);
1192
1269
  const id = input.id ?? newId();
1193
- const idempotencyKey = buildServerSessionIdempotencyKey(input);
1270
+ const platformSource = normalizePlatformSourceOrNull(input.platformSource);
1271
+ const idempotencyKey = buildServerSessionIdempotencyKey({
1272
+ ...input,
1273
+ platformSource
1274
+ });
1194
1275
  const row = await queryOne(
1195
1276
  this.client,
1196
1277
  `
@@ -1219,7 +1300,7 @@ var PostgresServerSessionsRepository = class {
1219
1300
  input.contentSessionId ?? null,
1220
1301
  input.agentId ?? null,
1221
1302
  input.agentType ?? null,
1222
- input.platformSource ?? null,
1303
+ platformSource,
1223
1304
  input.generationStatus ?? "idle",
1224
1305
  JSON.stringify(input.metadata ?? {})
1225
1306
  ]
@@ -1246,13 +1327,22 @@ var PostgresServerSessionsRepository = class {
1246
1327
  return result.rows.map(mapServerSessionRow);
1247
1328
  }
1248
1329
  async findByExternalIdForScope(input) {
1330
+ const hasPlatformScope = Object.prototype.hasOwnProperty.call(input, "platformSource");
1331
+ const platformSource = hasPlatformScope ? normalizePlatformSourceOrNull(input.platformSource) : null;
1249
1332
  const row = await queryOne(
1250
1333
  this.client,
1251
1334
  `
1252
1335
  SELECT * FROM server_sessions
1253
1336
  WHERE external_session_id = $1 AND project_id = $2 AND team_id = $3
1337
+ AND (
1338
+ $4::boolean = false
1339
+ OR ($5::text IS NULL AND platform_source IS NULL)
1340
+ OR platform_source = $5
1341
+ )
1342
+ ORDER BY started_at DESC
1343
+ LIMIT 1
1254
1344
  `,
1255
- [input.externalSessionId, input.projectId, input.teamId]
1345
+ [input.externalSessionId, input.projectId, input.teamId, hasPlatformScope, platformSource]
1256
1346
  );
1257
1347
  return row ? mapServerSessionRow(row) : null;
1258
1348
  }
@@ -1260,15 +1350,22 @@ var PostgresServerSessionsRepository = class {
1260
1350
  // /v1/events ingest linkage path, which needs nothing but the id, so we select
1261
1351
  // a single column to keep this hot-path query light.
1262
1352
  async findIdByContentSessionId(input) {
1353
+ const hasPlatformScope = Object.prototype.hasOwnProperty.call(input, "platformSource");
1354
+ const platformSource = hasPlatformScope ? normalizePlatformSourceOrNull(input.platformSource) : null;
1263
1355
  const row = await queryOne(
1264
1356
  this.client,
1265
1357
  `
1266
1358
  SELECT id FROM server_sessions
1267
1359
  WHERE content_session_id = $1 AND project_id = $2 AND team_id = $3
1360
+ AND (
1361
+ $4::boolean = false
1362
+ OR ($5::text IS NULL AND platform_source IS NULL)
1363
+ OR platform_source = $5
1364
+ )
1268
1365
  ORDER BY started_at DESC
1269
1366
  LIMIT 1
1270
1367
  `,
1271
- [input.contentSessionId, input.projectId, input.teamId]
1368
+ [input.contentSessionId, input.projectId, input.teamId, hasPlatformScope, platformSource]
1272
1369
  );
1273
1370
  return row ? row.id : null;
1274
1371
  }
@@ -1388,30 +1485,35 @@ function mapUnprocessedEventRow(row) {
1388
1485
  };
1389
1486
  }
1390
1487
  function buildServerSessionIdempotencyKey(input) {
1488
+ const platformSource = normalizePlatformSourceOrNull(input.platformSource);
1391
1489
  if (input.externalSessionId) {
1392
- return `server_session:v1:${deterministicKey([
1490
+ const parts = [
1393
1491
  input.teamId,
1394
1492
  input.projectId,
1395
- "external",
1396
- input.externalSessionId
1397
- ])}`;
1493
+ "external"
1494
+ ];
1495
+ if (platformSource) {
1496
+ parts.push(platformSource);
1497
+ }
1498
+ parts.push(input.externalSessionId);
1499
+ return `server_session:v1:${deterministicKey(parts)}`;
1398
1500
  }
1399
1501
  if (input.contentSessionId) {
1400
1502
  return `server_session:v1:${deterministicKey([
1401
1503
  input.teamId,
1402
1504
  input.projectId,
1403
1505
  "content",
1404
- input.platformSource ?? null,
1506
+ platformSource,
1405
1507
  input.agentId ?? null,
1406
1508
  input.contentSessionId
1407
1509
  ])}`;
1408
1510
  }
1409
- if (input.agentId && input.platformSource) {
1511
+ if (input.agentId && platformSource) {
1410
1512
  return `server_session:v1:${deterministicKey([
1411
1513
  input.teamId,
1412
1514
  input.projectId,
1413
1515
  "agent",
1414
- input.platformSource,
1516
+ platformSource,
1415
1517
  input.agentId,
1416
1518
  input.agentType ?? null
1417
1519
  ])}`;
@@ -1507,6 +1609,77 @@ function mapTeamMemberRow(row) {
1507
1609
  };
1508
1610
  }
1509
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
+
1510
1683
  // src/storage/postgres/index.ts
1511
1684
  function createPostgresStorageRepositories(client) {
1512
1685
  return {
@@ -1518,14 +1691,16 @@ function createPostgresStorageRepositories(client) {
1518
1691
  observations: new PostgresObservationRepository(client),
1519
1692
  observationSources: new PostgresObservationSourcesRepository(client),
1520
1693
  observationGenerationJobs: new PostgresObservationGenerationJobRepository(client),
1521
- observationGenerationJobEvents: new PostgresObservationGenerationJobEventsRepository(client)
1694
+ observationGenerationJobEvents: new PostgresObservationGenerationJobEventsRepository(client),
1695
+ usage: new PostgresUsageRepository(client),
1696
+ rateLimits: new PostgresRateLimitRepository(client)
1522
1697
  };
1523
1698
  }
1524
1699
 
1525
1700
  // src/services/sync/ChromaMcpManager.ts
1526
1701
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1527
1702
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1528
- import { execFile as execFile2, execSync } from "child_process";
1703
+ import { execFile as execFile2, execSync, spawn as spawn2 } from "child_process";
1529
1704
  import { promisify as promisify2 } from "util";
1530
1705
  import path2 from "path";
1531
1706
  import os from "os";
@@ -1915,10 +2090,6 @@ var SettingsDefaultsManager = class {
1915
2090
  // Default Gemini model (highest free tier RPM)
1916
2091
  CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: "true",
1917
2092
  // Rate limiting ON by default for free tier users
1918
- CLAUDE_MEM_GEMINI_MAX_CONTEXT_MESSAGES: "20",
1919
- // Max messages in Gemini context window
1920
- CLAUDE_MEM_GEMINI_MAX_TOKENS: "100000",
1921
- // Max estimated tokens (~100k safety limit)
1922
2093
  CLAUDE_MEM_OPENROUTER_API_KEY: "",
1923
2094
  // Empty by default, can be set via UI or env
1924
2095
  CLAUDE_MEM_OPENROUTER_MODEL: "xiaomi/mimo-v2-flash:free",
@@ -1929,10 +2100,6 @@ var SettingsDefaultsManager = class {
1929
2100
  // Optional: for OpenRouter analytics
1930
2101
  CLAUDE_MEM_OPENROUTER_APP_NAME: "claude-mem",
1931
2102
  // App name for OpenRouter analytics
1932
- CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES: "20",
1933
- // Max messages in context window
1934
- CLAUDE_MEM_OPENROUTER_MAX_TOKENS: "100000",
1935
- // Max estimated tokens (~100k safety limit)
1936
2103
  CLAUDE_MEM_DATA_DIR: join3(homedir2(), ".claude-mem"),
1937
2104
  CLAUDE_MEM_LOG_LEVEL: "INFO",
1938
2105
  CLAUDE_MEM_PYTHON_VERSION: "3.13",
@@ -1991,6 +2158,7 @@ var SettingsDefaultsManager = class {
1991
2158
  CLAUDE_MEM_CHROMA_API_KEY: "",
1992
2159
  CLAUDE_MEM_CHROMA_TENANT: "default_tenant",
1993
2160
  CLAUDE_MEM_CHROMA_DATABASE: "default_database",
2161
+ CLAUDE_MEM_CHROMA_PREWARM_TIMEOUT_MS: "120000",
1994
2162
  CLAUDE_MEM_TELEGRAM_ENABLED: "true",
1995
2163
  CLAUDE_MEM_TELEGRAM_BOT_TOKEN: "",
1996
2164
  CLAUDE_MEM_TELEGRAM_CHAT_ID: "",
@@ -2822,19 +2990,70 @@ function validateWorkerPidFile(options = {}) {
2822
2990
  return "stale";
2823
2991
  }
2824
2992
 
2993
+ // src/shared/dependency-health.ts
2994
+ var UVX_VECTOR_SEARCH_REMEDIATION = "Install uv/uvx and make uvx visible to the worker PATH, then restart claude-mem. Try `curl -LsSf https://astral.sh/uv/install.sh | sh` or `brew install uv`.";
2995
+ var statuses = /* @__PURE__ */ new Map();
2996
+ function recordDependencyStatus(dependency, kind, message, remediation) {
2997
+ const status = {
2998
+ dependency,
2999
+ kind,
3000
+ message,
3001
+ ...remediation ? { remediation } : {},
3002
+ recordedAtMs: Date.now()
3003
+ };
3004
+ statuses.set(dependency, status);
3005
+ return status;
3006
+ }
3007
+ function recordUvxVectorSearchUnavailable(message) {
3008
+ return recordDependencyStatus("uvx", "vector_search_unavailable", message, UVX_VECTOR_SEARCH_REMEDIATION);
3009
+ }
3010
+ function clearDependencyStatus(dependency) {
3011
+ statuses.delete(dependency);
3012
+ }
3013
+
3014
+ // src/services/server/ErrorHandler.ts
3015
+ var AppError = class extends Error {
3016
+ constructor(message, statusCode = 500, code, details) {
3017
+ super(message);
3018
+ this.statusCode = statusCode;
3019
+ this.code = code;
3020
+ this.details = details;
3021
+ this.name = "AppError";
3022
+ }
3023
+ };
3024
+
3025
+ // src/services/worker/search/errors.ts
3026
+ var ChromaUnavailableError = class extends AppError {
3027
+ constructor(message, cause) {
3028
+ super(message, 503, "CHROMA_UNAVAILABLE", cause ? { cause: cause.message } : void 0);
3029
+ this.name = "ChromaUnavailableError";
3030
+ }
3031
+ };
3032
+
2825
3033
  // src/services/sync/ChromaMcpManager.ts
2826
3034
  var execFileAsync2 = promisify2(execFile2);
2827
3035
  var CHROMA_MCP_CLIENT_NAME = "claude-mem-chroma";
2828
3036
  var CHROMA_MCP_CLIENT_VERSION = "1.0.0";
2829
3037
  var MCP_CONNECTION_TIMEOUT_MS = 3e4;
3038
+ var DEFAULT_CHROMA_PREWARM_TIMEOUT_MS = 12e4;
3039
+ var CHROMA_PREWARM_TIMEOUT_SETTING = "CLAUDE_MEM_CHROMA_PREWARM_TIMEOUT_MS";
3040
+ var CHROMA_PREWARM_TIMEOUT_BOUNDS = { min: 1, max: 6e5 };
3041
+ var CHROMA_PREWARM_REAP_TIMEOUT_MS = 1e3;
2830
3042
  var RECONNECT_BACKOFF_MS = 1e4;
2831
3043
  var DEFAULT_CHROMA_DATA_DIR = paths.chroma();
2832
3044
  var CHROMA_SUPERVISOR_ID = "chroma-mcp";
3045
+ var CHROMA_OUTPUT_TAIL_MAX_CHARS = 2048;
2833
3046
  var CHROMA_MCP_PINNED_VERSION = "0.2.6";
2834
3047
  var CHROMA_MCP_DEP_OVERRIDES = [
2835
3048
  "onnxruntime>=1.20",
2836
3049
  "protobuf<7"
2837
3050
  ];
3051
+ var ChromaMcpConnectionCancelledError = class extends Error {
3052
+ constructor(message = "chroma-mcp connection cancelled during shutdown") {
3053
+ super(message);
3054
+ this.name = "ChromaMcpConnectionCancelledError";
3055
+ }
3056
+ };
2838
3057
  var ChromaMcpManager = class _ChromaMcpManager {
2839
3058
  static instance = null;
2840
3059
  client = null;
@@ -2842,6 +3061,10 @@ var ChromaMcpManager = class _ChromaMcpManager {
2842
3061
  connected = false;
2843
3062
  lastConnectionFailureTimestamp = 0;
2844
3063
  connecting = null;
3064
+ activePrewarmChild = null;
3065
+ connectionGeneration = 0;
3066
+ intentionallyClosingTransports = /* @__PURE__ */ new WeakSet();
3067
+ static uvxAvailabilityProbe = null;
2845
3068
  constructor() {
2846
3069
  }
2847
3070
  static getInstance() {
@@ -2856,7 +3079,7 @@ var ChromaMcpManager = class _ChromaMcpManager {
2856
3079
  }
2857
3080
  const timeSinceLastFailure = Date.now() - this.lastConnectionFailureTimestamp;
2858
3081
  if (this.lastConnectionFailureTimestamp > 0 && timeSinceLastFailure < RECONNECT_BACKOFF_MS) {
2859
- throw new Error(`chroma-mcp connection in backoff (${Math.ceil((RECONNECT_BACKOFF_MS - timeSinceLastFailure) / 1e3)}s remaining)`);
3082
+ throw new ChromaUnavailableError(`chroma-mcp connection in backoff (${Math.ceil((RECONNECT_BACKOFF_MS - timeSinceLastFailure) / 1e3)}s remaining)`);
2860
3083
  }
2861
3084
  if (this.connecting) {
2862
3085
  await this.connecting;
@@ -2866,6 +3089,10 @@ var ChromaMcpManager = class _ChromaMcpManager {
2866
3089
  try {
2867
3090
  await this.connecting;
2868
3091
  } catch (error) {
3092
+ if (error instanceof ChromaMcpConnectionCancelledError) {
3093
+ logger.debug("CHROMA_MCP", "Connection attempt cancelled during shutdown");
3094
+ throw error;
3095
+ }
2869
3096
  this.lastConnectionFailureTimestamp = Date.now();
2870
3097
  if (error instanceof Error) {
2871
3098
  logger.error("CHROMA_MCP", "Connection attempt failed", {}, error);
@@ -2878,12 +3105,23 @@ var ChromaMcpManager = class _ChromaMcpManager {
2878
3105
  }
2879
3106
  }
2880
3107
  async connectInternal() {
3108
+ const connectionGeneration = this.connectionGeneration;
2881
3109
  await this.disposeCurrentSubprocess();
3110
+ this.assertConnectionNotCancelled(connectionGeneration);
2882
3111
  const commandArgs = this.buildCommandArgs();
2883
- const spawnEnvironment = this.getSpawnEnv();
3112
+ const uvxPreflightEnv = _ChromaMcpManager.getUvxPreflightEnv();
2884
3113
  getSupervisor().assertCanSpawn("chroma mcp");
2885
3114
  const uvxSpawnCommand = _ChromaMcpManager.resolveUvxCommand();
2886
3115
  const uvxSpawnArgs = commandArgs;
3116
+ if (!_ChromaMcpManager.isUvxAvailable(uvxSpawnCommand, uvxPreflightEnv, process.platform)) {
3117
+ const message = `uvx executable not found for chroma-mcp (${uvxSpawnCommand})`;
3118
+ recordUvxVectorSearchUnavailable(message);
3119
+ throw new ChromaUnavailableError(message);
3120
+ }
3121
+ const spawnEnvironment = this.getSpawnEnv(uvxPreflightEnv);
3122
+ await this.prewarmChromaMcp(uvxSpawnCommand, uvxSpawnArgs, spawnEnvironment, connectionGeneration);
3123
+ this.assertConnectionNotCancelled(connectionGeneration);
3124
+ clearDependencyStatus("uvx");
2887
3125
  logger.info("CHROMA_MCP", "Connecting to chroma-mcp via MCP stdio", {
2888
3126
  command: uvxSpawnCommand,
2889
3127
  args: uvxSpawnArgs.join(" ")
@@ -2895,6 +3133,7 @@ var ChromaMcpManager = class _ChromaMcpManager {
2895
3133
  cwd: os.homedir(),
2896
3134
  stderr: "pipe"
2897
3135
  });
3136
+ const transportStderrTail = _ChromaMcpManager.captureOutputTail(this.transport.stderr);
2898
3137
  this.client = new Client(
2899
3138
  { name: CHROMA_MCP_CLIENT_NAME, version: CHROMA_MCP_CLIENT_VERSION },
2900
3139
  { capabilities: {} }
@@ -2909,10 +3148,18 @@ var ChromaMcpManager = class _ChromaMcpManager {
2909
3148
  });
2910
3149
  try {
2911
3150
  await Promise.race([mcpConnectionPromise, timeoutPromise]);
3151
+ this.assertConnectionNotCancelled(connectionGeneration);
2912
3152
  } catch (connectionError) {
2913
3153
  clearTimeout(timeoutId);
3154
+ if (connectionError instanceof ChromaMcpConnectionCancelledError || this.connectionGeneration !== connectionGeneration) {
3155
+ logger.debug("CHROMA_MCP", "MCP connection cancelled during shutdown");
3156
+ await this.disposeCurrentSubprocess();
3157
+ throw connectionError instanceof ChromaMcpConnectionCancelledError ? connectionError : new ChromaMcpConnectionCancelledError();
3158
+ }
3159
+ const stderrTail = transportStderrTail();
2914
3160
  logger.warn("CHROMA_MCP", "Connection failed, killing subprocess tree to prevent zombie", {
2915
- error: connectionError instanceof Error ? connectionError.message : String(connectionError)
3161
+ error: connectionError instanceof Error ? connectionError.message : String(connectionError),
3162
+ ...stderrTail ? { stderrTail } : {}
2916
3163
  });
2917
3164
  await this.disposeCurrentSubprocess();
2918
3165
  throw connectionError;
@@ -2928,6 +3175,10 @@ var ChromaMcpManager = class _ChromaMcpManager {
2928
3175
  logger.debug("CHROMA_MCP", "Ignoring stale onclose from previous transport");
2929
3176
  return;
2930
3177
  }
3178
+ if (this.connectionGeneration !== connectionGeneration || this.intentionallyClosingTransports.has(currentTransport)) {
3179
+ logger.debug("CHROMA_MCP", "Ignoring onclose from intentionally closed transport");
3180
+ return;
3181
+ }
2931
3182
  logger.warn("CHROMA_MCP", "chroma-mcp subprocess closed unexpectedly, applying reconnect backoff");
2932
3183
  this.connected = false;
2933
3184
  getSupervisor().unregisterProcess(CHROMA_SUPERVISOR_ID);
@@ -2944,11 +3195,16 @@ var ChromaMcpManager = class _ChromaMcpManager {
2944
3195
  }
2945
3196
  };
2946
3197
  }
3198
+ assertConnectionNotCancelled(connectionGeneration) {
3199
+ if (this.connectionGeneration !== connectionGeneration) {
3200
+ throw new ChromaMcpConnectionCancelledError();
3201
+ }
3202
+ }
2947
3203
  buildCommandArgs() {
2948
3204
  const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
2949
3205
  const chromaMode = settings.CLAUDE_MEM_CHROMA_MODE || "local";
2950
3206
  const pythonVersion = process.env.CLAUDE_MEM_PYTHON_VERSION || settings.CLAUDE_MEM_PYTHON_VERSION || "3.13";
2951
- const depOverrideFlags = CHROMA_MCP_DEP_OVERRIDES.flatMap((spec) => ["--with", spec]);
3207
+ const launcherPrefix = _ChromaMcpManager.buildLauncherPrefix(pythonVersion);
2952
3208
  if (chromaMode === "remote") {
2953
3209
  const chromaHost = settings.CLAUDE_MEM_CHROMA_HOST || "127.0.0.1";
2954
3210
  const chromaPort = settings.CLAUDE_MEM_CHROMA_PORT || "8000";
@@ -2957,10 +3213,7 @@ var ChromaMcpManager = class _ChromaMcpManager {
2957
3213
  const chromaDatabase = settings.CLAUDE_MEM_CHROMA_DATABASE || "default_database";
2958
3214
  const chromaApiKey = settings.CLAUDE_MEM_CHROMA_API_KEY || "";
2959
3215
  const args = [
2960
- "--python",
2961
- pythonVersion,
2962
- ...depOverrideFlags,
2963
- `chroma-mcp==${CHROMA_MCP_PINNED_VERSION}`,
3216
+ ...launcherPrefix,
2964
3217
  "--client-type",
2965
3218
  "http",
2966
3219
  "--host",
@@ -2981,16 +3234,160 @@ var ChromaMcpManager = class _ChromaMcpManager {
2981
3234
  return args;
2982
3235
  }
2983
3236
  return [
2984
- "--python",
2985
- pythonVersion,
2986
- ...depOverrideFlags,
2987
- `chroma-mcp==${CHROMA_MCP_PINNED_VERSION}`,
3237
+ ...launcherPrefix,
2988
3238
  "--client-type",
2989
3239
  "persistent",
2990
3240
  "--data-dir",
2991
3241
  DEFAULT_CHROMA_DATA_DIR.replace(/\\/g, "/")
2992
3242
  ];
2993
3243
  }
3244
+ static buildLauncherPrefix(pythonVersion) {
3245
+ const depOverrideFlags = CHROMA_MCP_DEP_OVERRIDES.flatMap((spec) => ["--with", spec]);
3246
+ return [
3247
+ "--python",
3248
+ pythonVersion,
3249
+ ...depOverrideFlags,
3250
+ "--from",
3251
+ `chroma-mcp==${CHROMA_MCP_PINNED_VERSION}`,
3252
+ "chroma-mcp"
3253
+ ];
3254
+ }
3255
+ static buildPrewarmCommandArgs(commandArgs) {
3256
+ const executableIndex = commandArgs.indexOf("chroma-mcp");
3257
+ const launcherPrefix = executableIndex >= 0 ? commandArgs.slice(0, executableIndex + 1) : commandArgs;
3258
+ return [...launcherPrefix, "--help"];
3259
+ }
3260
+ static parseBoundedTimeoutMs(rawValue) {
3261
+ if (!rawValue) {
3262
+ return null;
3263
+ }
3264
+ const parsed = Number.parseInt(rawValue, 10);
3265
+ if (Number.isFinite(parsed) && parsed >= CHROMA_PREWARM_TIMEOUT_BOUNDS.min && parsed <= CHROMA_PREWARM_TIMEOUT_BOUNDS.max) {
3266
+ return parsed;
3267
+ }
3268
+ return null;
3269
+ }
3270
+ static getChromaPrewarmTimeoutMs() {
3271
+ const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
3272
+ const envValue = process.env[CHROMA_PREWARM_TIMEOUT_SETTING];
3273
+ const settingsValue = settings[CHROMA_PREWARM_TIMEOUT_SETTING];
3274
+ const parsed = _ChromaMcpManager.parseBoundedTimeoutMs(envValue ?? settingsValue);
3275
+ if (parsed !== null) {
3276
+ return parsed;
3277
+ }
3278
+ if (envValue !== void 0 || settingsValue) {
3279
+ logger.warn("CHROMA_MCP", `Invalid ${CHROMA_PREWARM_TIMEOUT_SETTING}, using default`, {
3280
+ value: envValue ?? settingsValue,
3281
+ min: CHROMA_PREWARM_TIMEOUT_BOUNDS.min,
3282
+ max: CHROMA_PREWARM_TIMEOUT_BOUNDS.max
3283
+ });
3284
+ }
3285
+ return DEFAULT_CHROMA_PREWARM_TIMEOUT_MS;
3286
+ }
3287
+ static captureOutputTail(stream) {
3288
+ let tail = "";
3289
+ stream?.on("data", (chunk) => {
3290
+ const text = Buffer.isBuffer(chunk) ? chunk.toString() : chunk instanceof Uint8Array ? Buffer.from(chunk).toString() : String(chunk);
3291
+ tail = (tail + text).slice(-CHROMA_OUTPUT_TAIL_MAX_CHARS);
3292
+ });
3293
+ return () => tail.trim();
3294
+ }
3295
+ async prewarmChromaMcp(command, commandArgs, env, connectionGeneration) {
3296
+ this.assertConnectionNotCancelled(connectionGeneration);
3297
+ const args = _ChromaMcpManager.buildPrewarmCommandArgs(commandArgs);
3298
+ const timeoutMs = _ChromaMcpManager.getChromaPrewarmTimeoutMs();
3299
+ logger.info("CHROMA_MCP", "Prewarming chroma-mcp uvx environment", {
3300
+ command,
3301
+ args: args.join(" "),
3302
+ timeoutMs
3303
+ });
3304
+ const child = spawn2(command, args, {
3305
+ cwd: os.homedir(),
3306
+ env,
3307
+ shell: false,
3308
+ stdio: ["ignore", "pipe", "pipe"],
3309
+ windowsHide: process.platform === "win32"
3310
+ });
3311
+ this.activePrewarmChild = child;
3312
+ const stdoutTail = _ChromaMcpManager.captureOutputTail(child.stdout);
3313
+ const stderrTail = _ChromaMcpManager.captureOutputTail(child.stderr);
3314
+ let timeoutId = null;
3315
+ const exitPromise = new Promise((resolve, reject) => {
3316
+ child.once("error", (error) => {
3317
+ if (this.connectionGeneration !== connectionGeneration) {
3318
+ reject(new ChromaMcpConnectionCancelledError());
3319
+ return;
3320
+ }
3321
+ reject(error);
3322
+ });
3323
+ child.once("close", (code, signal) => {
3324
+ if (this.connectionGeneration !== connectionGeneration) {
3325
+ reject(new ChromaMcpConnectionCancelledError());
3326
+ return;
3327
+ }
3328
+ if (code === 0) {
3329
+ resolve();
3330
+ return;
3331
+ }
3332
+ reject(new Error(`chroma-mcp prewarm exited with code ${code ?? "null"}${signal ? ` signal ${signal}` : ""}`));
3333
+ });
3334
+ });
3335
+ const timeoutPromise = new Promise((_, reject) => {
3336
+ timeoutId = setTimeout(
3337
+ () => reject(new Error(`chroma-mcp prewarm timed out after ${timeoutMs}ms`)),
3338
+ timeoutMs
3339
+ );
3340
+ });
3341
+ try {
3342
+ await Promise.race([exitPromise, timeoutPromise]);
3343
+ this.assertConnectionNotCancelled(connectionGeneration);
3344
+ logger.debug("CHROMA_MCP", "chroma-mcp uvx prewarm completed");
3345
+ } catch (error) {
3346
+ if (error instanceof ChromaMcpConnectionCancelledError) {
3347
+ logger.debug("CHROMA_MCP", "chroma-mcp uvx prewarm cancelled during shutdown");
3348
+ throw error;
3349
+ }
3350
+ this.assertConnectionNotCancelled(connectionGeneration);
3351
+ const errorMessage = error instanceof Error ? error.message : String(error);
3352
+ const pid = child.pid;
3353
+ const stdout = stdoutTail();
3354
+ const stderr = stderrTail();
3355
+ logger.warn("CHROMA_MCP", "chroma-mcp uvx prewarm failed", {
3356
+ command,
3357
+ args: args.join(" "),
3358
+ timeoutMs,
3359
+ ...pid ? { pid } : {},
3360
+ error: errorMessage,
3361
+ ...stdout ? { stdoutTail: stdout } : {},
3362
+ ...stderr ? { stderrTail: stderr } : {}
3363
+ });
3364
+ if (pid) {
3365
+ try {
3366
+ await _ChromaMcpManager.killProcessTree(pid);
3367
+ } catch (killError) {
3368
+ logger.debug("CHROMA_MCP", "prewarm process tree kill finished (best-effort)", {
3369
+ pid,
3370
+ error: killError instanceof Error ? killError.message : String(killError)
3371
+ });
3372
+ }
3373
+ } else {
3374
+ try {
3375
+ child.kill("SIGKILL");
3376
+ } catch {
3377
+ }
3378
+ }
3379
+ const unavailableMessage = `chroma-mcp prewarm failed: ${errorMessage}`;
3380
+ recordUvxVectorSearchUnavailable(unavailableMessage);
3381
+ throw new ChromaUnavailableError(unavailableMessage, error instanceof Error ? error : void 0);
3382
+ } finally {
3383
+ if (timeoutId) {
3384
+ clearTimeout(timeoutId);
3385
+ }
3386
+ if (this.activePrewarmChild === child) {
3387
+ this.activePrewarmChild = null;
3388
+ }
3389
+ }
3390
+ }
2994
3391
  async callTool(toolName, toolArguments) {
2995
3392
  await this.ensureConnected();
2996
3393
  logger.debug("CHROMA_MCP", `Calling tool: ${toolName}`, {
@@ -3113,6 +3510,11 @@ var ChromaMcpManager = class _ChromaMcpManager {
3113
3510
  * subprocess (no-op in that case).
3114
3511
  */
3115
3512
  async disposeCurrentSubprocess() {
3513
+ await this.disposeActivePrewarm();
3514
+ const closingTransport = this.transport;
3515
+ if (closingTransport) {
3516
+ this.intentionallyClosingTransports.add(closingTransport);
3517
+ }
3116
3518
  const chromaProcess = this.transport?._process;
3117
3519
  const trackedPid = chromaProcess?.pid;
3118
3520
  if (trackedPid) {
@@ -3125,9 +3527,9 @@ var ChromaMcpManager = class _ChromaMcpManager {
3125
3527
  });
3126
3528
  }
3127
3529
  }
3128
- if (this.transport) {
3530
+ if (closingTransport) {
3129
3531
  try {
3130
- await this.transport.close();
3532
+ await closingTransport.close();
3131
3533
  } catch {
3132
3534
  }
3133
3535
  }
@@ -3144,6 +3546,51 @@ var ChromaMcpManager = class _ChromaMcpManager {
3144
3546
  this.transport = null;
3145
3547
  this.connected = false;
3146
3548
  }
3549
+ async disposeActivePrewarm() {
3550
+ const prewarmChild = this.activePrewarmChild;
3551
+ if (!prewarmChild) {
3552
+ return;
3553
+ }
3554
+ if (this.activePrewarmChild === prewarmChild) {
3555
+ this.activePrewarmChild = null;
3556
+ }
3557
+ const pid = prewarmChild.pid;
3558
+ if (pid) {
3559
+ try {
3560
+ await _ChromaMcpManager.killProcessTree(pid);
3561
+ } catch (error) {
3562
+ logger.warn("CHROMA_MCP", "failed to kill in-flight chroma-mcp prewarm tree (best-effort)", {
3563
+ pid,
3564
+ error: error instanceof Error ? error.message : String(error)
3565
+ });
3566
+ }
3567
+ }
3568
+ try {
3569
+ prewarmChild.kill("SIGKILL");
3570
+ } catch {
3571
+ }
3572
+ await _ChromaMcpManager.waitForChildClose(prewarmChild, CHROMA_PREWARM_REAP_TIMEOUT_MS);
3573
+ }
3574
+ static async waitForChildClose(child, timeoutMs) {
3575
+ if (child.exitCode !== null || child.signalCode !== null) {
3576
+ return;
3577
+ }
3578
+ await new Promise((resolve) => {
3579
+ let timeoutId = null;
3580
+ const finish = () => {
3581
+ if (timeoutId) {
3582
+ clearTimeout(timeoutId);
3583
+ timeoutId = null;
3584
+ }
3585
+ child.off("close", finish);
3586
+ child.off("exit", finish);
3587
+ resolve();
3588
+ };
3589
+ timeoutId = setTimeout(finish, timeoutMs);
3590
+ child.once("close", finish);
3591
+ child.once("exit", finish);
3592
+ });
3593
+ }
3147
3594
  /**
3148
3595
  * Gracefully stop the MCP connection and kill the chroma-mcp subprocess tree.
3149
3596
  *
@@ -3157,7 +3604,8 @@ var ChromaMcpManager = class _ChromaMcpManager {
3157
3604
  * pattern from shutdown.ts (Principle 5: OS-supervised teardown).
3158
3605
  */
3159
3606
  async stop() {
3160
- if (!this.client && !this.transport) {
3607
+ this.connectionGeneration += 1;
3608
+ if (!this.client && !this.transport && !this.activePrewarmChild) {
3161
3609
  logger.debug("CHROMA_MCP", "No active MCP connection to stop");
3162
3610
  this.connecting = null;
3163
3611
  return;
@@ -3389,6 +3837,39 @@ var ChromaMcpManager = class _ChromaMcpManager {
3389
3837
  }
3390
3838
  return "uvx.exe";
3391
3839
  }
3840
+ static isUvxAvailable(command, env, platform) {
3841
+ if (_ChromaMcpManager.uvxAvailabilityProbe) {
3842
+ return _ChromaMcpManager.uvxAvailabilityProbe(command, env, platform);
3843
+ }
3844
+ const executableNames = platform === "win32" && !command.toLowerCase().endsWith(".exe") ? [command, `${command}.exe`] : [command];
3845
+ if (command.includes("/") || command.includes("\\")) {
3846
+ return executableNames.some((candidate) => {
3847
+ try {
3848
+ return fs.existsSync(candidate) && fs.statSync(candidate).isFile();
3849
+ } catch {
3850
+ return false;
3851
+ }
3852
+ });
3853
+ }
3854
+ const sep2 = platform === "win32" ? ";" : ":";
3855
+ const pathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") ?? "PATH";
3856
+ const dirs = (env[pathKey] ?? "").split(sep2).filter(Boolean);
3857
+ for (const dir of dirs) {
3858
+ for (const name of executableNames) {
3859
+ const candidate = path2.join(dir, name);
3860
+ try {
3861
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
3862
+ return true;
3863
+ }
3864
+ } catch {
3865
+ }
3866
+ }
3867
+ }
3868
+ return false;
3869
+ }
3870
+ static setUvxAvailabilityProbeForTesting(probe) {
3871
+ _ChromaMcpManager.uvxAvailabilityProbe = probe;
3872
+ }
3392
3873
  static ensureUvOnPath(env) {
3393
3874
  const sep2 = process.platform === "win32" ? ";" : ":";
3394
3875
  const pathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") ?? "PATH";
@@ -3408,7 +3889,7 @@ var ChromaMcpManager = class _ChromaMcpManager {
3408
3889
  logger.debug("CHROMA_MCP", "Prepended uv bin dir(s) to chroma child PATH", { added: additions });
3409
3890
  }
3410
3891
  }
3411
- getSpawnEnv() {
3892
+ static getUvxPreflightEnv() {
3412
3893
  const baseEnv = {};
3413
3894
  for (const [key, value] of Object.entries(sanitizeEnv(process.env))) {
3414
3895
  if (value !== void 0) {
@@ -3417,6 +3898,10 @@ var ChromaMcpManager = class _ChromaMcpManager {
3417
3898
  }
3418
3899
  _ChromaMcpManager.ensureUvOnPath(baseEnv);
3419
3900
  if (!baseEnv.ANONYMIZED_TELEMETRY) baseEnv.ANONYMIZED_TELEMETRY = "false";
3901
+ return baseEnv;
3902
+ }
3903
+ getSpawnEnv(preflightEnv) {
3904
+ const baseEnv = preflightEnv ? { ...preflightEnv } : _ChromaMcpManager.getUvxPreflightEnv();
3420
3905
  const combinedCertPath = this.getCombinedCertPath();
3421
3906
  if (!combinedCertPath) {
3422
3907
  return baseEnv;
@@ -3584,6 +4069,7 @@ var ChromaSync = class _ChromaSync {
3584
4069
  memory_session_id: obs.memory_session_id,
3585
4070
  project: obs.project,
3586
4071
  merged_into_project: obs.merged_into_project ?? null,
4072
+ platform_source: obs.platform_source ? normalizePlatformSource(obs.platform_source) : normalizePlatformSource(void 0),
3587
4073
  created_at_epoch: obs.created_at_epoch,
3588
4074
  type: obs.type || "discovery",
3589
4075
  title: obs.title || "Untitled"
@@ -3631,6 +4117,7 @@ var ChromaSync = class _ChromaSync {
3631
4117
  memory_session_id: summary.memory_session_id,
3632
4118
  project: summary.project,
3633
4119
  merged_into_project: summary.merged_into_project ?? null,
4120
+ platform_source: summary.platform_source ? normalizePlatformSource(summary.platform_source) : normalizePlatformSource(void 0),
3634
4121
  created_at_epoch: summary.created_at_epoch,
3635
4122
  prompt_number: summary.prompt_number || 0
3636
4123
  };
@@ -3697,7 +4184,19 @@ var ChromaSync = class _ChromaSync {
3697
4184
  if (documents.length === 0) {
3698
4185
  return 0;
3699
4186
  }
3700
- await this.ensureCollectionExists();
4187
+ try {
4188
+ await this.ensureCollectionExists();
4189
+ } catch (error) {
4190
+ if (error instanceof ChromaUnavailableError) {
4191
+ logger.warn("CHROMA_SYNC", "Chroma unavailable before write; leaving documents unsynced", {
4192
+ collection: this.collectionName,
4193
+ requested: documents.length,
4194
+ error: error.message
4195
+ });
4196
+ return 0;
4197
+ }
4198
+ throw error;
4199
+ }
3701
4200
  const chromaMcp = ChromaMcpManager.getInstance();
3702
4201
  let written = 0;
3703
4202
  for (let i = 0; i < documents.length; i += this.BATCH_SIZE) {
@@ -3758,12 +4257,13 @@ var ChromaSync = class _ChromaSync {
3758
4257
  });
3759
4258
  return written;
3760
4259
  }
3761
- async syncObservation(observationId, memorySessionId, project, obs, promptNumber, createdAtEpoch) {
4260
+ async syncObservation(observationId, memorySessionId, project, obs, promptNumber, createdAtEpoch, platformSource) {
3762
4261
  const stored = {
3763
4262
  id: observationId,
3764
4263
  memory_session_id: memorySessionId,
3765
4264
  project,
3766
4265
  merged_into_project: null,
4266
+ platform_source: platformSource ? normalizePlatformSource(platformSource) : normalizePlatformSource(void 0),
3767
4267
  text: null,
3768
4268
  // Legacy field, not used
3769
4269
  type: obs.type,
@@ -3795,12 +4295,13 @@ var ChromaSync = class _ChromaSync {
3795
4295
  });
3796
4296
  }
3797
4297
  }
3798
- async syncSummary(summaryId, memorySessionId, project, summary, promptNumber, createdAtEpoch) {
4298
+ async syncSummary(summaryId, memorySessionId, project, summary, promptNumber, createdAtEpoch, platformSource) {
3799
4299
  const stored = {
3800
4300
  id: summaryId,
3801
4301
  memory_session_id: memorySessionId,
3802
4302
  project,
3803
4303
  merged_into_project: null,
4304
+ platform_source: platformSource ? normalizePlatformSource(platformSource) : normalizePlatformSource(void 0),
3804
4305
  request: summary.request,
3805
4306
  investigated: summary.investigated,
3806
4307
  learned: summary.learned,
@@ -3837,12 +4338,13 @@ var ChromaSync = class _ChromaSync {
3837
4338
  doc_type: "user_prompt",
3838
4339
  memory_session_id: prompt.memory_session_id,
3839
4340
  project: prompt.project,
4341
+ platform_source: prompt.platform_source,
3840
4342
  created_at_epoch: prompt.created_at_epoch,
3841
4343
  prompt_number: prompt.prompt_number
3842
4344
  }
3843
4345
  };
3844
4346
  }
3845
- async syncUserPrompt(promptId, memorySessionId, project, promptText, promptNumber, createdAtEpoch) {
4347
+ async syncUserPrompt(promptId, memorySessionId, project, promptText, promptNumber, createdAtEpoch, platformSource) {
3846
4348
  const stored = {
3847
4349
  id: promptId,
3848
4350
  content_session_id: "",
@@ -3851,7 +4353,8 @@ var ChromaSync = class _ChromaSync {
3851
4353
  prompt_text: promptText,
3852
4354
  created_at_epoch: createdAtEpoch,
3853
4355
  memory_session_id: memorySessionId,
3854
- project
4356
+ project,
4357
+ platform_source: normalizePlatformSource(platformSource)
3855
4358
  };
3856
4359
  const document = this.formatUserPromptDoc(stored);
3857
4360
  logger.info("CHROMA_SYNC", "Syncing user prompt", {
@@ -3966,9 +4469,13 @@ var ChromaSync = class _ChromaSync {
3966
4469
  }
3967
4470
  async backfillObservations(db, backfillProject, watermark) {
3968
4471
  const observations = db.db.prepare(`
3969
- SELECT * FROM observations
3970
- WHERE project = ? AND id > ?
3971
- ORDER BY id ASC
4472
+ SELECT
4473
+ o.*,
4474
+ COALESCE(NULLIF(s.platform_source, ''), 'claude') as platform_source
4475
+ FROM observations o
4476
+ LEFT JOIN sdk_sessions s ON s.memory_session_id = o.memory_session_id
4477
+ WHERE o.project = ? AND o.id > ?
4478
+ ORDER BY o.id ASC
3972
4479
  `).all(backfillProject, watermark);
3973
4480
  if (observations.length === 0) {
3974
4481
  return [];
@@ -4038,9 +4545,13 @@ var ChromaSync = class _ChromaSync {
4038
4545
  }
4039
4546
  async backfillSummaries(db, backfillProject, watermark) {
4040
4547
  const summaries = db.db.prepare(`
4041
- SELECT * FROM session_summaries
4042
- WHERE project = ? AND id > ?
4043
- ORDER BY id ASC
4548
+ SELECT
4549
+ ss.*,
4550
+ COALESCE(NULLIF(s.platform_source, ''), 'claude') as platform_source
4551
+ FROM session_summaries ss
4552
+ LEFT JOIN sdk_sessions s ON s.memory_session_id = ss.memory_session_id
4553
+ WHERE ss.project = ? AND ss.id > ?
4554
+ ORDER BY ss.id ASC
4044
4555
  `).all(backfillProject, watermark);
4045
4556
  if (summaries.length === 0) {
4046
4557
  return [];
@@ -4110,9 +4621,10 @@ var ChromaSync = class _ChromaSync {
4110
4621
  SELECT
4111
4622
  up.*,
4112
4623
  s.project,
4113
- s.memory_session_id
4624
+ s.memory_session_id,
4625
+ COALESCE(NULLIF(s.platform_source, ''), 'claude') as platform_source
4114
4626
  FROM user_prompts up
4115
- JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
4627
+ JOIN sdk_sessions s ON up.session_db_id = s.id
4116
4628
  WHERE s.project = ? AND up.id > ?
4117
4629
  ORDER BY up.id ASC
4118
4630
  `).all(backfillProject, watermark);
@@ -4122,7 +4634,7 @@ var ChromaSync = class _ChromaSync {
4122
4634
  const totalPromptCount = db.db.prepare(`
4123
4635
  SELECT COUNT(*) as count
4124
4636
  FROM user_prompts up
4125
- JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
4637
+ JOIN sdk_sessions s ON up.session_db_id = s.id
4126
4638
  WHERE s.project = ?
4127
4639
  `).get(backfillProject);
4128
4640
  logger.info("CHROMA_SYNC", "Backfilling user prompts", {
@@ -4866,35 +5378,36 @@ function classifyHttpProviderError(input) {
4866
5378
  const body = input.bodyText ?? "";
4867
5379
  const lower = body.toLowerCase();
4868
5380
  const retryAfterMs = input.headers ? parseRetryAfterMs(input.headers.get("retry-after")) : void 0;
5381
+ const cause = status === void 0 ? input.cause : new Error(`${providerLabel} HTTP error (status ${status})`);
4869
5382
  if (lower.includes("quota exceeded") || lower.includes("insufficient credits") || lower.includes("insufficient_quota") || lower.includes("resource_exhausted")) {
4870
5383
  return new ServerClassifiedProviderError(
4871
5384
  `${providerLabel} quota exhausted${status !== void 0 ? ` (status ${status})` : ""}`,
4872
- { kind: "quota_exhausted", cause: input.cause }
5385
+ { kind: "quota_exhausted", cause }
4873
5386
  );
4874
5387
  }
4875
5388
  if (status === 429) {
4876
5389
  return new ServerClassifiedProviderError(`${providerLabel} rate limit (429)`, {
4877
5390
  kind: "rate_limit",
4878
- cause: input.cause,
5391
+ cause,
4879
5392
  ...retryAfterMs !== void 0 ? { retryAfterMs } : {}
4880
5393
  });
4881
5394
  }
4882
5395
  if (status === 401 || status === 403) {
4883
5396
  return new ServerClassifiedProviderError(`${providerLabel} auth error (status ${status})`, {
4884
5397
  kind: "auth_invalid",
4885
- cause: input.cause
5398
+ cause
4886
5399
  });
4887
5400
  }
4888
5401
  if (status === 400 || status === 404) {
4889
5402
  return new ServerClassifiedProviderError(`${providerLabel} bad request (status ${status})`, {
4890
5403
  kind: "unrecoverable",
4891
- cause: input.cause
5404
+ cause
4892
5405
  });
4893
5406
  }
4894
5407
  if (status !== void 0 && status >= 500 && status < 600) {
4895
5408
  return new ServerClassifiedProviderError(`${providerLabel} upstream error (status ${status})`, {
4896
5409
  kind: "transient",
4897
- cause: input.cause
5410
+ cause
4898
5411
  });
4899
5412
  }
4900
5413
  if (status === void 0) {
@@ -4905,8 +5418,8 @@ function classifyHttpProviderError(input) {
4905
5418
  });
4906
5419
  }
4907
5420
  return new ServerClassifiedProviderError(
4908
- `${providerLabel} API error: ${status}${body ? ` - ${body.substring(0, 200)}` : ""}`,
4909
- { kind: "unrecoverable", cause: input.cause }
5421
+ `${providerLabel} API error (status ${status})`,
5422
+ { kind: "unrecoverable", cause }
4910
5423
  );
4911
5424
  }
4912
5425
 
@@ -5225,6 +5738,41 @@ async function safeReadBody(response) {
5225
5738
  // src/server/generation/providers/GeminiObservationProvider.ts
5226
5739
  var GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1/models";
5227
5740
  var DEFAULT_MODEL2 = "gemini-2.5-flash";
5741
+ function categorizeGeminiBadRequest(bodyText) {
5742
+ const lower = bodyText.toLowerCase();
5743
+ if (lower.includes("api key not valid") || lower.includes("api_key_invalid") || lower.includes("api key expired") || lower.includes("invalid api key")) {
5744
+ return "api_key";
5745
+ }
5746
+ if (lower.includes("please ensure that multiturn requests alternate") || lower.includes("alternate between user and model") || lower.includes("first content should be with role") || lower.includes("contents") && lower.includes("role") && (lower.includes("user") || lower.includes("model"))) {
5747
+ return "role_sequence";
5748
+ }
5749
+ if (lower.includes("context limit") || lower.includes("context length") || lower.includes("too many tokens") || lower.includes("input is too long") || lower.includes("prompt is too long") || lower.includes("request payload size exceeds") || lower.includes("token") && (lower.includes("exceed") || lower.includes("maximum") || lower.includes("limit"))) {
5750
+ return "context_limit";
5751
+ }
5752
+ if (lower.includes("model not found") || lower.includes("model_unsupported") || lower.includes("unsupported model") || lower.includes("not supported for generatecontent") || lower.includes("not supported by this model") || lower.includes("model") && lower.includes("not supported") || lower.includes("models/") && lower.includes("not found")) {
5753
+ return "model_unsupported";
5754
+ }
5755
+ return "unknown_bad_request";
5756
+ }
5757
+ function isQuotaBody(bodyText) {
5758
+ const lower = bodyText.toLowerCase();
5759
+ return lower.includes("quota exceeded") || lower.includes("insufficient credits") || lower.includes("insufficient_quota") || lower.includes("resource_exhausted");
5760
+ }
5761
+ function classifyGeminiServerError(input) {
5762
+ const status = input.status;
5763
+ const bodyText = input.bodyText ?? "";
5764
+ if (status === 400 && !isQuotaBody(bodyText)) {
5765
+ const category = categorizeGeminiBadRequest(bodyText);
5766
+ return new ServerClassifiedProviderError(`Gemini bad request: ${category}`, {
5767
+ kind: "unrecoverable",
5768
+ cause: new Error("Gemini HTTP error (status 400)")
5769
+ });
5770
+ }
5771
+ return classifyHttpProviderError({
5772
+ ...input,
5773
+ providerLabel: "Gemini"
5774
+ });
5775
+ }
5228
5776
  var GeminiObservationProvider = class {
5229
5777
  providerLabel = "gemini";
5230
5778
  apiKey;
@@ -5268,19 +5816,17 @@ var GeminiObservationProvider = class {
5268
5816
  signal
5269
5817
  });
5270
5818
  } catch (networkError) {
5271
- throw classifyHttpProviderError({
5272
- cause: networkError,
5273
- providerLabel: "Gemini"
5819
+ throw classifyGeminiServerError({
5820
+ cause: networkError
5274
5821
  });
5275
5822
  }
5276
5823
  if (!response.ok) {
5277
5824
  const bodyText = await safeReadBody2(response);
5278
- throw classifyHttpProviderError({
5825
+ throw classifyGeminiServerError({
5279
5826
  status: response.status,
5280
5827
  bodyText,
5281
5828
  headers: response.headers,
5282
- cause: new Error(`Gemini API error: ${response.status} - ${bodyText}`),
5283
- providerLabel: "Gemini"
5829
+ cause: new Error(`Gemini HTTP error (status ${response.status})`)
5284
5830
  });
5285
5831
  }
5286
5832
  let data;
@@ -5293,12 +5839,11 @@ var GeminiObservationProvider = class {
5293
5839
  });
5294
5840
  }
5295
5841
  if (data.error) {
5296
- throw classifyHttpProviderError({
5842
+ throw classifyGeminiServerError({
5297
5843
  status: response.status,
5298
5844
  bodyText: `${data.error.status ?? ""} ${data.error.message ?? ""}`,
5299
5845
  headers: response.headers,
5300
- cause: new Error(`Gemini API error: ${data.error.status} - ${data.error.message}`),
5301
- providerLabel: "Gemini"
5846
+ cause: new Error(`Gemini HTTP error (status ${response.status})`)
5302
5847
  });
5303
5848
  }
5304
5849
  const rawText = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
@@ -5609,7 +6154,7 @@ async function processGeneratedResponse(input) {
5609
6154
  const observationsToWrite = parsed.observations ?? [];
5610
6155
  const skipped = parsed.summary?.skipped === true;
5611
6156
  const privateContentDetected = skipped || observationsToWrite.length === 0;
5612
- return await withPostgresTransaction(input.pool, async (client) => {
6157
+ const outcome = await withPostgresTransaction(input.pool, async (client) => {
5613
6158
  const obsRepo = new PostgresObservationRepository(client);
5614
6159
  const sourcesRepo = new PostgresObservationSourcesRepository(client);
5615
6160
  const jobsRepo = new PostgresObservationGenerationJobRepository(client);
@@ -5769,6 +6314,35 @@ async function processGeneratedResponse(input) {
5769
6314
  privateContentDetected
5770
6315
  };
5771
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;
5772
6346
  }
5773
6347
  function renderObservationContent(observation) {
5774
6348
  const parts = [];