claude-mem 13.9.0 → 13.9.1

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)
@@ -430,6 +436,26 @@ function sortJson(value) {
430
436
  return value;
431
437
  }
432
438
 
439
+ // src/shared/platform-source.ts
440
+ var DEFAULT_PLATFORM_SOURCE = "claude";
441
+ function sanitizeRawSource(value) {
442
+ return value.trim().toLowerCase().replace(/\s+/g, "-");
443
+ }
444
+ function normalizePlatformSource(value) {
445
+ if (!value) return DEFAULT_PLATFORM_SOURCE;
446
+ const source = sanitizeRawSource(value);
447
+ if (!source) return DEFAULT_PLATFORM_SOURCE;
448
+ if (source === "transcript") return "codex";
449
+ if (source.includes("codex")) return "codex";
450
+ if (source.includes("cursor")) return "cursor";
451
+ if (source.includes("claude")) return "claude";
452
+ return source;
453
+ }
454
+ function normalizePlatformSourceOrNull(value) {
455
+ if (typeof value !== "string") return null;
456
+ return normalizePlatformSource(value);
457
+ }
458
+
433
459
  // src/storage/postgres/agent-events.ts
434
460
  var PostgresAgentEventsRepository = class {
435
461
  constructor(client) {
@@ -441,6 +467,7 @@ var PostgresAgentEventsRepository = class {
441
467
  await assertSessionOwnership(this.client, input.serverSessionId, input.projectId, input.teamId);
442
468
  }
443
469
  const idempotencyKey = buildAgentEventIdempotencyKey(input);
470
+ const platformSource = normalizePlatformSourceOrNull(input.platformSource);
444
471
  const row = await queryOne(
445
472
  this.client,
446
473
  `
@@ -463,7 +490,7 @@ var PostgresAgentEventsRepository = class {
463
490
  input.sourceEventId ?? null,
464
491
  idempotencyKey,
465
492
  input.eventType,
466
- input.platformSource ?? null,
493
+ platformSource,
467
494
  JSON.stringify(input.payload ?? {}),
468
495
  JSON.stringify(input.metadata ?? {}),
469
496
  new Date(input.occurredAt)
@@ -495,11 +522,14 @@ var PostgresAgentEventsRepository = class {
495
522
  }
496
523
  };
497
524
  function buildAgentEventIdempotencyKey(input) {
525
+ const platformSource = normalizePlatformSourceOrNull(input.platformSource);
526
+ const platformScope = platformSource ? [platformSource] : [];
498
527
  if (input.sourceEventId) {
499
528
  return `agent_event:v1:${deterministicKey([
500
529
  input.teamId,
501
530
  input.projectId,
502
531
  input.sourceAdapter,
532
+ ...platformScope,
503
533
  input.sourceEventId
504
534
  ])}`;
505
535
  }
@@ -507,6 +537,7 @@ function buildAgentEventIdempotencyKey(input) {
507
537
  input.teamId,
508
538
  input.projectId,
509
539
  input.sourceAdapter,
540
+ ...platformScope,
510
541
  input.contentSessionId ?? input.serverSessionId ?? null,
511
542
  input.eventType,
512
543
  new Date(input.occurredAt).toISOString(),
@@ -968,16 +999,39 @@ var PostgresObservationRepository = class {
968
999
  return result.rows.map(mapObservationRow);
969
1000
  }
970
1001
  async search(input) {
1002
+ const platformSource = normalizePlatformSourceOrNull(input.platformSource);
971
1003
  const result = await this.client.query(
972
1004
  `
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
1005
+ SELECT observations.* FROM observations
1006
+ LEFT JOIN server_sessions
1007
+ ON server_sessions.id = observations.server_session_id
1008
+ AND server_sessions.project_id = observations.project_id
1009
+ AND server_sessions.team_id = observations.team_id
1010
+ WHERE observations.project_id = $1
1011
+ AND observations.team_id = $2
1012
+ AND observations.content_search @@ websearch_to_tsquery('english', $3)
1013
+ AND (
1014
+ $5::text IS NULL
1015
+ OR server_sessions.platform_source = $5
1016
+ OR (
1017
+ observations.server_session_id IS NULL
1018
+ AND EXISTS (
1019
+ SELECT 1
1020
+ FROM observation_sources
1021
+ INNER JOIN agent_events
1022
+ ON agent_events.id = observation_sources.agent_event_id
1023
+ AND agent_events.project_id = observations.project_id
1024
+ AND agent_events.team_id = observations.team_id
1025
+ WHERE observation_sources.observation_id = observations.id
1026
+ AND observation_sources.source_type = 'agent_event'
1027
+ AND agent_events.platform_source = $5
1028
+ )
1029
+ )
1030
+ )
1031
+ ORDER BY ts_rank(observations.content_search, websearch_to_tsquery('english', $3)) DESC, observations.updated_at DESC
978
1032
  LIMIT $4
979
1033
  `,
980
- [input.projectId, input.teamId, input.query, input.limit ?? 20]
1034
+ [input.projectId, input.teamId, input.query, input.limit ?? 20, platformSource]
981
1035
  );
982
1036
  return result.rows.map(mapObservationRow);
983
1037
  }
@@ -1190,7 +1244,11 @@ var PostgresServerSessionsRepository = class {
1190
1244
  async create(input) {
1191
1245
  await assertProjectOwnership(this.client, input.projectId, input.teamId);
1192
1246
  const id = input.id ?? newId();
1193
- const idempotencyKey = buildServerSessionIdempotencyKey(input);
1247
+ const platformSource = normalizePlatformSourceOrNull(input.platformSource);
1248
+ const idempotencyKey = buildServerSessionIdempotencyKey({
1249
+ ...input,
1250
+ platformSource
1251
+ });
1194
1252
  const row = await queryOne(
1195
1253
  this.client,
1196
1254
  `
@@ -1219,7 +1277,7 @@ var PostgresServerSessionsRepository = class {
1219
1277
  input.contentSessionId ?? null,
1220
1278
  input.agentId ?? null,
1221
1279
  input.agentType ?? null,
1222
- input.platformSource ?? null,
1280
+ platformSource,
1223
1281
  input.generationStatus ?? "idle",
1224
1282
  JSON.stringify(input.metadata ?? {})
1225
1283
  ]
@@ -1246,13 +1304,22 @@ var PostgresServerSessionsRepository = class {
1246
1304
  return result.rows.map(mapServerSessionRow);
1247
1305
  }
1248
1306
  async findByExternalIdForScope(input) {
1307
+ const hasPlatformScope = Object.prototype.hasOwnProperty.call(input, "platformSource");
1308
+ const platformSource = hasPlatformScope ? normalizePlatformSourceOrNull(input.platformSource) : null;
1249
1309
  const row = await queryOne(
1250
1310
  this.client,
1251
1311
  `
1252
1312
  SELECT * FROM server_sessions
1253
1313
  WHERE external_session_id = $1 AND project_id = $2 AND team_id = $3
1314
+ AND (
1315
+ $4::boolean = false
1316
+ OR ($5::text IS NULL AND platform_source IS NULL)
1317
+ OR platform_source = $5
1318
+ )
1319
+ ORDER BY started_at DESC
1320
+ LIMIT 1
1254
1321
  `,
1255
- [input.externalSessionId, input.projectId, input.teamId]
1322
+ [input.externalSessionId, input.projectId, input.teamId, hasPlatformScope, platformSource]
1256
1323
  );
1257
1324
  return row ? mapServerSessionRow(row) : null;
1258
1325
  }
@@ -1260,15 +1327,22 @@ var PostgresServerSessionsRepository = class {
1260
1327
  // /v1/events ingest linkage path, which needs nothing but the id, so we select
1261
1328
  // a single column to keep this hot-path query light.
1262
1329
  async findIdByContentSessionId(input) {
1330
+ const hasPlatformScope = Object.prototype.hasOwnProperty.call(input, "platformSource");
1331
+ const platformSource = hasPlatformScope ? normalizePlatformSourceOrNull(input.platformSource) : null;
1263
1332
  const row = await queryOne(
1264
1333
  this.client,
1265
1334
  `
1266
1335
  SELECT id FROM server_sessions
1267
1336
  WHERE content_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
+ )
1268
1342
  ORDER BY started_at DESC
1269
1343
  LIMIT 1
1270
1344
  `,
1271
- [input.contentSessionId, input.projectId, input.teamId]
1345
+ [input.contentSessionId, input.projectId, input.teamId, hasPlatformScope, platformSource]
1272
1346
  );
1273
1347
  return row ? row.id : null;
1274
1348
  }
@@ -1388,30 +1462,35 @@ function mapUnprocessedEventRow(row) {
1388
1462
  };
1389
1463
  }
1390
1464
  function buildServerSessionIdempotencyKey(input) {
1465
+ const platformSource = normalizePlatformSourceOrNull(input.platformSource);
1391
1466
  if (input.externalSessionId) {
1392
- return `server_session:v1:${deterministicKey([
1467
+ const parts = [
1393
1468
  input.teamId,
1394
1469
  input.projectId,
1395
- "external",
1396
- input.externalSessionId
1397
- ])}`;
1470
+ "external"
1471
+ ];
1472
+ if (platformSource) {
1473
+ parts.push(platformSource);
1474
+ }
1475
+ parts.push(input.externalSessionId);
1476
+ return `server_session:v1:${deterministicKey(parts)}`;
1398
1477
  }
1399
1478
  if (input.contentSessionId) {
1400
1479
  return `server_session:v1:${deterministicKey([
1401
1480
  input.teamId,
1402
1481
  input.projectId,
1403
1482
  "content",
1404
- input.platformSource ?? null,
1483
+ platformSource,
1405
1484
  input.agentId ?? null,
1406
1485
  input.contentSessionId
1407
1486
  ])}`;
1408
1487
  }
1409
- if (input.agentId && input.platformSource) {
1488
+ if (input.agentId && platformSource) {
1410
1489
  return `server_session:v1:${deterministicKey([
1411
1490
  input.teamId,
1412
1491
  input.projectId,
1413
1492
  "agent",
1414
- input.platformSource,
1493
+ platformSource,
1415
1494
  input.agentId,
1416
1495
  input.agentType ?? null
1417
1496
  ])}`;
@@ -1525,7 +1604,7 @@ function createPostgresStorageRepositories(client) {
1525
1604
  // src/services/sync/ChromaMcpManager.ts
1526
1605
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1527
1606
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1528
- import { execFile as execFile2, execSync } from "child_process";
1607
+ import { execFile as execFile2, execSync, spawn as spawn2 } from "child_process";
1529
1608
  import { promisify as promisify2 } from "util";
1530
1609
  import path2 from "path";
1531
1610
  import os from "os";
@@ -1991,6 +2070,7 @@ var SettingsDefaultsManager = class {
1991
2070
  CLAUDE_MEM_CHROMA_API_KEY: "",
1992
2071
  CLAUDE_MEM_CHROMA_TENANT: "default_tenant",
1993
2072
  CLAUDE_MEM_CHROMA_DATABASE: "default_database",
2073
+ CLAUDE_MEM_CHROMA_PREWARM_TIMEOUT_MS: "120000",
1994
2074
  CLAUDE_MEM_TELEGRAM_ENABLED: "true",
1995
2075
  CLAUDE_MEM_TELEGRAM_BOT_TOKEN: "",
1996
2076
  CLAUDE_MEM_TELEGRAM_CHAT_ID: "",
@@ -2822,19 +2902,70 @@ function validateWorkerPidFile(options = {}) {
2822
2902
  return "stale";
2823
2903
  }
2824
2904
 
2905
+ // src/shared/dependency-health.ts
2906
+ 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`.";
2907
+ var statuses = /* @__PURE__ */ new Map();
2908
+ function recordDependencyStatus(dependency, kind, message, remediation) {
2909
+ const status = {
2910
+ dependency,
2911
+ kind,
2912
+ message,
2913
+ ...remediation ? { remediation } : {},
2914
+ recordedAtMs: Date.now()
2915
+ };
2916
+ statuses.set(dependency, status);
2917
+ return status;
2918
+ }
2919
+ function recordUvxVectorSearchUnavailable(message) {
2920
+ return recordDependencyStatus("uvx", "vector_search_unavailable", message, UVX_VECTOR_SEARCH_REMEDIATION);
2921
+ }
2922
+ function clearDependencyStatus(dependency) {
2923
+ statuses.delete(dependency);
2924
+ }
2925
+
2926
+ // src/services/server/ErrorHandler.ts
2927
+ var AppError = class extends Error {
2928
+ constructor(message, statusCode = 500, code, details) {
2929
+ super(message);
2930
+ this.statusCode = statusCode;
2931
+ this.code = code;
2932
+ this.details = details;
2933
+ this.name = "AppError";
2934
+ }
2935
+ };
2936
+
2937
+ // src/services/worker/search/errors.ts
2938
+ var ChromaUnavailableError = class extends AppError {
2939
+ constructor(message, cause) {
2940
+ super(message, 503, "CHROMA_UNAVAILABLE", cause ? { cause: cause.message } : void 0);
2941
+ this.name = "ChromaUnavailableError";
2942
+ }
2943
+ };
2944
+
2825
2945
  // src/services/sync/ChromaMcpManager.ts
2826
2946
  var execFileAsync2 = promisify2(execFile2);
2827
2947
  var CHROMA_MCP_CLIENT_NAME = "claude-mem-chroma";
2828
2948
  var CHROMA_MCP_CLIENT_VERSION = "1.0.0";
2829
2949
  var MCP_CONNECTION_TIMEOUT_MS = 3e4;
2950
+ var DEFAULT_CHROMA_PREWARM_TIMEOUT_MS = 12e4;
2951
+ var CHROMA_PREWARM_TIMEOUT_SETTING = "CLAUDE_MEM_CHROMA_PREWARM_TIMEOUT_MS";
2952
+ var CHROMA_PREWARM_TIMEOUT_BOUNDS = { min: 1, max: 6e5 };
2953
+ var CHROMA_PREWARM_REAP_TIMEOUT_MS = 1e3;
2830
2954
  var RECONNECT_BACKOFF_MS = 1e4;
2831
2955
  var DEFAULT_CHROMA_DATA_DIR = paths.chroma();
2832
2956
  var CHROMA_SUPERVISOR_ID = "chroma-mcp";
2957
+ var CHROMA_OUTPUT_TAIL_MAX_CHARS = 2048;
2833
2958
  var CHROMA_MCP_PINNED_VERSION = "0.2.6";
2834
2959
  var CHROMA_MCP_DEP_OVERRIDES = [
2835
2960
  "onnxruntime>=1.20",
2836
2961
  "protobuf<7"
2837
2962
  ];
2963
+ var ChromaMcpConnectionCancelledError = class extends Error {
2964
+ constructor(message = "chroma-mcp connection cancelled during shutdown") {
2965
+ super(message);
2966
+ this.name = "ChromaMcpConnectionCancelledError";
2967
+ }
2968
+ };
2838
2969
  var ChromaMcpManager = class _ChromaMcpManager {
2839
2970
  static instance = null;
2840
2971
  client = null;
@@ -2842,6 +2973,10 @@ var ChromaMcpManager = class _ChromaMcpManager {
2842
2973
  connected = false;
2843
2974
  lastConnectionFailureTimestamp = 0;
2844
2975
  connecting = null;
2976
+ activePrewarmChild = null;
2977
+ connectionGeneration = 0;
2978
+ intentionallyClosingTransports = /* @__PURE__ */ new WeakSet();
2979
+ static uvxAvailabilityProbe = null;
2845
2980
  constructor() {
2846
2981
  }
2847
2982
  static getInstance() {
@@ -2856,7 +2991,7 @@ var ChromaMcpManager = class _ChromaMcpManager {
2856
2991
  }
2857
2992
  const timeSinceLastFailure = Date.now() - this.lastConnectionFailureTimestamp;
2858
2993
  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)`);
2994
+ throw new ChromaUnavailableError(`chroma-mcp connection in backoff (${Math.ceil((RECONNECT_BACKOFF_MS - timeSinceLastFailure) / 1e3)}s remaining)`);
2860
2995
  }
2861
2996
  if (this.connecting) {
2862
2997
  await this.connecting;
@@ -2866,6 +3001,10 @@ var ChromaMcpManager = class _ChromaMcpManager {
2866
3001
  try {
2867
3002
  await this.connecting;
2868
3003
  } catch (error) {
3004
+ if (error instanceof ChromaMcpConnectionCancelledError) {
3005
+ logger.debug("CHROMA_MCP", "Connection attempt cancelled during shutdown");
3006
+ throw error;
3007
+ }
2869
3008
  this.lastConnectionFailureTimestamp = Date.now();
2870
3009
  if (error instanceof Error) {
2871
3010
  logger.error("CHROMA_MCP", "Connection attempt failed", {}, error);
@@ -2878,12 +3017,23 @@ var ChromaMcpManager = class _ChromaMcpManager {
2878
3017
  }
2879
3018
  }
2880
3019
  async connectInternal() {
3020
+ const connectionGeneration = this.connectionGeneration;
2881
3021
  await this.disposeCurrentSubprocess();
3022
+ this.assertConnectionNotCancelled(connectionGeneration);
2882
3023
  const commandArgs = this.buildCommandArgs();
2883
- const spawnEnvironment = this.getSpawnEnv();
3024
+ const uvxPreflightEnv = _ChromaMcpManager.getUvxPreflightEnv();
2884
3025
  getSupervisor().assertCanSpawn("chroma mcp");
2885
3026
  const uvxSpawnCommand = _ChromaMcpManager.resolveUvxCommand();
2886
3027
  const uvxSpawnArgs = commandArgs;
3028
+ if (!_ChromaMcpManager.isUvxAvailable(uvxSpawnCommand, uvxPreflightEnv, process.platform)) {
3029
+ const message = `uvx executable not found for chroma-mcp (${uvxSpawnCommand})`;
3030
+ recordUvxVectorSearchUnavailable(message);
3031
+ throw new ChromaUnavailableError(message);
3032
+ }
3033
+ const spawnEnvironment = this.getSpawnEnv(uvxPreflightEnv);
3034
+ await this.prewarmChromaMcp(uvxSpawnCommand, uvxSpawnArgs, spawnEnvironment, connectionGeneration);
3035
+ this.assertConnectionNotCancelled(connectionGeneration);
3036
+ clearDependencyStatus("uvx");
2887
3037
  logger.info("CHROMA_MCP", "Connecting to chroma-mcp via MCP stdio", {
2888
3038
  command: uvxSpawnCommand,
2889
3039
  args: uvxSpawnArgs.join(" ")
@@ -2895,6 +3045,7 @@ var ChromaMcpManager = class _ChromaMcpManager {
2895
3045
  cwd: os.homedir(),
2896
3046
  stderr: "pipe"
2897
3047
  });
3048
+ const transportStderrTail = _ChromaMcpManager.captureOutputTail(this.transport.stderr);
2898
3049
  this.client = new Client(
2899
3050
  { name: CHROMA_MCP_CLIENT_NAME, version: CHROMA_MCP_CLIENT_VERSION },
2900
3051
  { capabilities: {} }
@@ -2909,10 +3060,18 @@ var ChromaMcpManager = class _ChromaMcpManager {
2909
3060
  });
2910
3061
  try {
2911
3062
  await Promise.race([mcpConnectionPromise, timeoutPromise]);
3063
+ this.assertConnectionNotCancelled(connectionGeneration);
2912
3064
  } catch (connectionError) {
2913
3065
  clearTimeout(timeoutId);
3066
+ if (connectionError instanceof ChromaMcpConnectionCancelledError || this.connectionGeneration !== connectionGeneration) {
3067
+ logger.debug("CHROMA_MCP", "MCP connection cancelled during shutdown");
3068
+ await this.disposeCurrentSubprocess();
3069
+ throw connectionError instanceof ChromaMcpConnectionCancelledError ? connectionError : new ChromaMcpConnectionCancelledError();
3070
+ }
3071
+ const stderrTail = transportStderrTail();
2914
3072
  logger.warn("CHROMA_MCP", "Connection failed, killing subprocess tree to prevent zombie", {
2915
- error: connectionError instanceof Error ? connectionError.message : String(connectionError)
3073
+ error: connectionError instanceof Error ? connectionError.message : String(connectionError),
3074
+ ...stderrTail ? { stderrTail } : {}
2916
3075
  });
2917
3076
  await this.disposeCurrentSubprocess();
2918
3077
  throw connectionError;
@@ -2928,6 +3087,10 @@ var ChromaMcpManager = class _ChromaMcpManager {
2928
3087
  logger.debug("CHROMA_MCP", "Ignoring stale onclose from previous transport");
2929
3088
  return;
2930
3089
  }
3090
+ if (this.connectionGeneration !== connectionGeneration || this.intentionallyClosingTransports.has(currentTransport)) {
3091
+ logger.debug("CHROMA_MCP", "Ignoring onclose from intentionally closed transport");
3092
+ return;
3093
+ }
2931
3094
  logger.warn("CHROMA_MCP", "chroma-mcp subprocess closed unexpectedly, applying reconnect backoff");
2932
3095
  this.connected = false;
2933
3096
  getSupervisor().unregisterProcess(CHROMA_SUPERVISOR_ID);
@@ -2944,11 +3107,16 @@ var ChromaMcpManager = class _ChromaMcpManager {
2944
3107
  }
2945
3108
  };
2946
3109
  }
3110
+ assertConnectionNotCancelled(connectionGeneration) {
3111
+ if (this.connectionGeneration !== connectionGeneration) {
3112
+ throw new ChromaMcpConnectionCancelledError();
3113
+ }
3114
+ }
2947
3115
  buildCommandArgs() {
2948
3116
  const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
2949
3117
  const chromaMode = settings.CLAUDE_MEM_CHROMA_MODE || "local";
2950
3118
  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]);
3119
+ const launcherPrefix = _ChromaMcpManager.buildLauncherPrefix(pythonVersion);
2952
3120
  if (chromaMode === "remote") {
2953
3121
  const chromaHost = settings.CLAUDE_MEM_CHROMA_HOST || "127.0.0.1";
2954
3122
  const chromaPort = settings.CLAUDE_MEM_CHROMA_PORT || "8000";
@@ -2957,10 +3125,7 @@ var ChromaMcpManager = class _ChromaMcpManager {
2957
3125
  const chromaDatabase = settings.CLAUDE_MEM_CHROMA_DATABASE || "default_database";
2958
3126
  const chromaApiKey = settings.CLAUDE_MEM_CHROMA_API_KEY || "";
2959
3127
  const args = [
2960
- "--python",
2961
- pythonVersion,
2962
- ...depOverrideFlags,
2963
- `chroma-mcp==${CHROMA_MCP_PINNED_VERSION}`,
3128
+ ...launcherPrefix,
2964
3129
  "--client-type",
2965
3130
  "http",
2966
3131
  "--host",
@@ -2981,16 +3146,160 @@ var ChromaMcpManager = class _ChromaMcpManager {
2981
3146
  return args;
2982
3147
  }
2983
3148
  return [
2984
- "--python",
2985
- pythonVersion,
2986
- ...depOverrideFlags,
2987
- `chroma-mcp==${CHROMA_MCP_PINNED_VERSION}`,
3149
+ ...launcherPrefix,
2988
3150
  "--client-type",
2989
3151
  "persistent",
2990
3152
  "--data-dir",
2991
3153
  DEFAULT_CHROMA_DATA_DIR.replace(/\\/g, "/")
2992
3154
  ];
2993
3155
  }
3156
+ static buildLauncherPrefix(pythonVersion) {
3157
+ const depOverrideFlags = CHROMA_MCP_DEP_OVERRIDES.flatMap((spec) => ["--with", spec]);
3158
+ return [
3159
+ "--python",
3160
+ pythonVersion,
3161
+ ...depOverrideFlags,
3162
+ "--from",
3163
+ `chroma-mcp==${CHROMA_MCP_PINNED_VERSION}`,
3164
+ "chroma-mcp"
3165
+ ];
3166
+ }
3167
+ static buildPrewarmCommandArgs(commandArgs) {
3168
+ const executableIndex = commandArgs.indexOf("chroma-mcp");
3169
+ const launcherPrefix = executableIndex >= 0 ? commandArgs.slice(0, executableIndex + 1) : commandArgs;
3170
+ return [...launcherPrefix, "--help"];
3171
+ }
3172
+ static parseBoundedTimeoutMs(rawValue) {
3173
+ if (!rawValue) {
3174
+ return null;
3175
+ }
3176
+ const parsed = Number.parseInt(rawValue, 10);
3177
+ if (Number.isFinite(parsed) && parsed >= CHROMA_PREWARM_TIMEOUT_BOUNDS.min && parsed <= CHROMA_PREWARM_TIMEOUT_BOUNDS.max) {
3178
+ return parsed;
3179
+ }
3180
+ return null;
3181
+ }
3182
+ static getChromaPrewarmTimeoutMs() {
3183
+ const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
3184
+ const envValue = process.env[CHROMA_PREWARM_TIMEOUT_SETTING];
3185
+ const settingsValue = settings[CHROMA_PREWARM_TIMEOUT_SETTING];
3186
+ const parsed = _ChromaMcpManager.parseBoundedTimeoutMs(envValue ?? settingsValue);
3187
+ if (parsed !== null) {
3188
+ return parsed;
3189
+ }
3190
+ if (envValue !== void 0 || settingsValue) {
3191
+ logger.warn("CHROMA_MCP", `Invalid ${CHROMA_PREWARM_TIMEOUT_SETTING}, using default`, {
3192
+ value: envValue ?? settingsValue,
3193
+ min: CHROMA_PREWARM_TIMEOUT_BOUNDS.min,
3194
+ max: CHROMA_PREWARM_TIMEOUT_BOUNDS.max
3195
+ });
3196
+ }
3197
+ return DEFAULT_CHROMA_PREWARM_TIMEOUT_MS;
3198
+ }
3199
+ static captureOutputTail(stream) {
3200
+ let tail = "";
3201
+ stream?.on("data", (chunk) => {
3202
+ const text = Buffer.isBuffer(chunk) ? chunk.toString() : chunk instanceof Uint8Array ? Buffer.from(chunk).toString() : String(chunk);
3203
+ tail = (tail + text).slice(-CHROMA_OUTPUT_TAIL_MAX_CHARS);
3204
+ });
3205
+ return () => tail.trim();
3206
+ }
3207
+ async prewarmChromaMcp(command, commandArgs, env, connectionGeneration) {
3208
+ this.assertConnectionNotCancelled(connectionGeneration);
3209
+ const args = _ChromaMcpManager.buildPrewarmCommandArgs(commandArgs);
3210
+ const timeoutMs = _ChromaMcpManager.getChromaPrewarmTimeoutMs();
3211
+ logger.info("CHROMA_MCP", "Prewarming chroma-mcp uvx environment", {
3212
+ command,
3213
+ args: args.join(" "),
3214
+ timeoutMs
3215
+ });
3216
+ const child = spawn2(command, args, {
3217
+ cwd: os.homedir(),
3218
+ env,
3219
+ shell: false,
3220
+ stdio: ["ignore", "pipe", "pipe"],
3221
+ windowsHide: process.platform === "win32"
3222
+ });
3223
+ this.activePrewarmChild = child;
3224
+ const stdoutTail = _ChromaMcpManager.captureOutputTail(child.stdout);
3225
+ const stderrTail = _ChromaMcpManager.captureOutputTail(child.stderr);
3226
+ let timeoutId = null;
3227
+ const exitPromise = new Promise((resolve, reject) => {
3228
+ child.once("error", (error) => {
3229
+ if (this.connectionGeneration !== connectionGeneration) {
3230
+ reject(new ChromaMcpConnectionCancelledError());
3231
+ return;
3232
+ }
3233
+ reject(error);
3234
+ });
3235
+ child.once("close", (code, signal) => {
3236
+ if (this.connectionGeneration !== connectionGeneration) {
3237
+ reject(new ChromaMcpConnectionCancelledError());
3238
+ return;
3239
+ }
3240
+ if (code === 0) {
3241
+ resolve();
3242
+ return;
3243
+ }
3244
+ reject(new Error(`chroma-mcp prewarm exited with code ${code ?? "null"}${signal ? ` signal ${signal}` : ""}`));
3245
+ });
3246
+ });
3247
+ const timeoutPromise = new Promise((_, reject) => {
3248
+ timeoutId = setTimeout(
3249
+ () => reject(new Error(`chroma-mcp prewarm timed out after ${timeoutMs}ms`)),
3250
+ timeoutMs
3251
+ );
3252
+ });
3253
+ try {
3254
+ await Promise.race([exitPromise, timeoutPromise]);
3255
+ this.assertConnectionNotCancelled(connectionGeneration);
3256
+ logger.debug("CHROMA_MCP", "chroma-mcp uvx prewarm completed");
3257
+ } catch (error) {
3258
+ if (error instanceof ChromaMcpConnectionCancelledError) {
3259
+ logger.debug("CHROMA_MCP", "chroma-mcp uvx prewarm cancelled during shutdown");
3260
+ throw error;
3261
+ }
3262
+ this.assertConnectionNotCancelled(connectionGeneration);
3263
+ const errorMessage = error instanceof Error ? error.message : String(error);
3264
+ const pid = child.pid;
3265
+ const stdout = stdoutTail();
3266
+ const stderr = stderrTail();
3267
+ logger.warn("CHROMA_MCP", "chroma-mcp uvx prewarm failed", {
3268
+ command,
3269
+ args: args.join(" "),
3270
+ timeoutMs,
3271
+ ...pid ? { pid } : {},
3272
+ error: errorMessage,
3273
+ ...stdout ? { stdoutTail: stdout } : {},
3274
+ ...stderr ? { stderrTail: stderr } : {}
3275
+ });
3276
+ if (pid) {
3277
+ try {
3278
+ await _ChromaMcpManager.killProcessTree(pid);
3279
+ } catch (killError) {
3280
+ logger.debug("CHROMA_MCP", "prewarm process tree kill finished (best-effort)", {
3281
+ pid,
3282
+ error: killError instanceof Error ? killError.message : String(killError)
3283
+ });
3284
+ }
3285
+ } else {
3286
+ try {
3287
+ child.kill("SIGKILL");
3288
+ } catch {
3289
+ }
3290
+ }
3291
+ const unavailableMessage = `chroma-mcp prewarm failed: ${errorMessage}`;
3292
+ recordUvxVectorSearchUnavailable(unavailableMessage);
3293
+ throw new ChromaUnavailableError(unavailableMessage, error instanceof Error ? error : void 0);
3294
+ } finally {
3295
+ if (timeoutId) {
3296
+ clearTimeout(timeoutId);
3297
+ }
3298
+ if (this.activePrewarmChild === child) {
3299
+ this.activePrewarmChild = null;
3300
+ }
3301
+ }
3302
+ }
2994
3303
  async callTool(toolName, toolArguments) {
2995
3304
  await this.ensureConnected();
2996
3305
  logger.debug("CHROMA_MCP", `Calling tool: ${toolName}`, {
@@ -3113,6 +3422,11 @@ var ChromaMcpManager = class _ChromaMcpManager {
3113
3422
  * subprocess (no-op in that case).
3114
3423
  */
3115
3424
  async disposeCurrentSubprocess() {
3425
+ await this.disposeActivePrewarm();
3426
+ const closingTransport = this.transport;
3427
+ if (closingTransport) {
3428
+ this.intentionallyClosingTransports.add(closingTransport);
3429
+ }
3116
3430
  const chromaProcess = this.transport?._process;
3117
3431
  const trackedPid = chromaProcess?.pid;
3118
3432
  if (trackedPid) {
@@ -3125,9 +3439,9 @@ var ChromaMcpManager = class _ChromaMcpManager {
3125
3439
  });
3126
3440
  }
3127
3441
  }
3128
- if (this.transport) {
3442
+ if (closingTransport) {
3129
3443
  try {
3130
- await this.transport.close();
3444
+ await closingTransport.close();
3131
3445
  } catch {
3132
3446
  }
3133
3447
  }
@@ -3144,6 +3458,51 @@ var ChromaMcpManager = class _ChromaMcpManager {
3144
3458
  this.transport = null;
3145
3459
  this.connected = false;
3146
3460
  }
3461
+ async disposeActivePrewarm() {
3462
+ const prewarmChild = this.activePrewarmChild;
3463
+ if (!prewarmChild) {
3464
+ return;
3465
+ }
3466
+ if (this.activePrewarmChild === prewarmChild) {
3467
+ this.activePrewarmChild = null;
3468
+ }
3469
+ const pid = prewarmChild.pid;
3470
+ if (pid) {
3471
+ try {
3472
+ await _ChromaMcpManager.killProcessTree(pid);
3473
+ } catch (error) {
3474
+ logger.warn("CHROMA_MCP", "failed to kill in-flight chroma-mcp prewarm tree (best-effort)", {
3475
+ pid,
3476
+ error: error instanceof Error ? error.message : String(error)
3477
+ });
3478
+ }
3479
+ }
3480
+ try {
3481
+ prewarmChild.kill("SIGKILL");
3482
+ } catch {
3483
+ }
3484
+ await _ChromaMcpManager.waitForChildClose(prewarmChild, CHROMA_PREWARM_REAP_TIMEOUT_MS);
3485
+ }
3486
+ static async waitForChildClose(child, timeoutMs) {
3487
+ if (child.exitCode !== null || child.signalCode !== null) {
3488
+ return;
3489
+ }
3490
+ await new Promise((resolve) => {
3491
+ let timeoutId = null;
3492
+ const finish = () => {
3493
+ if (timeoutId) {
3494
+ clearTimeout(timeoutId);
3495
+ timeoutId = null;
3496
+ }
3497
+ child.off("close", finish);
3498
+ child.off("exit", finish);
3499
+ resolve();
3500
+ };
3501
+ timeoutId = setTimeout(finish, timeoutMs);
3502
+ child.once("close", finish);
3503
+ child.once("exit", finish);
3504
+ });
3505
+ }
3147
3506
  /**
3148
3507
  * Gracefully stop the MCP connection and kill the chroma-mcp subprocess tree.
3149
3508
  *
@@ -3157,7 +3516,8 @@ var ChromaMcpManager = class _ChromaMcpManager {
3157
3516
  * pattern from shutdown.ts (Principle 5: OS-supervised teardown).
3158
3517
  */
3159
3518
  async stop() {
3160
- if (!this.client && !this.transport) {
3519
+ this.connectionGeneration += 1;
3520
+ if (!this.client && !this.transport && !this.activePrewarmChild) {
3161
3521
  logger.debug("CHROMA_MCP", "No active MCP connection to stop");
3162
3522
  this.connecting = null;
3163
3523
  return;
@@ -3389,6 +3749,39 @@ var ChromaMcpManager = class _ChromaMcpManager {
3389
3749
  }
3390
3750
  return "uvx.exe";
3391
3751
  }
3752
+ static isUvxAvailable(command, env, platform) {
3753
+ if (_ChromaMcpManager.uvxAvailabilityProbe) {
3754
+ return _ChromaMcpManager.uvxAvailabilityProbe(command, env, platform);
3755
+ }
3756
+ const executableNames = platform === "win32" && !command.toLowerCase().endsWith(".exe") ? [command, `${command}.exe`] : [command];
3757
+ if (command.includes("/") || command.includes("\\")) {
3758
+ return executableNames.some((candidate) => {
3759
+ try {
3760
+ return fs.existsSync(candidate) && fs.statSync(candidate).isFile();
3761
+ } catch {
3762
+ return false;
3763
+ }
3764
+ });
3765
+ }
3766
+ const sep2 = platform === "win32" ? ";" : ":";
3767
+ const pathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") ?? "PATH";
3768
+ const dirs = (env[pathKey] ?? "").split(sep2).filter(Boolean);
3769
+ for (const dir of dirs) {
3770
+ for (const name of executableNames) {
3771
+ const candidate = path2.join(dir, name);
3772
+ try {
3773
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
3774
+ return true;
3775
+ }
3776
+ } catch {
3777
+ }
3778
+ }
3779
+ }
3780
+ return false;
3781
+ }
3782
+ static setUvxAvailabilityProbeForTesting(probe) {
3783
+ _ChromaMcpManager.uvxAvailabilityProbe = probe;
3784
+ }
3392
3785
  static ensureUvOnPath(env) {
3393
3786
  const sep2 = process.platform === "win32" ? ";" : ":";
3394
3787
  const pathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") ?? "PATH";
@@ -3408,7 +3801,7 @@ var ChromaMcpManager = class _ChromaMcpManager {
3408
3801
  logger.debug("CHROMA_MCP", "Prepended uv bin dir(s) to chroma child PATH", { added: additions });
3409
3802
  }
3410
3803
  }
3411
- getSpawnEnv() {
3804
+ static getUvxPreflightEnv() {
3412
3805
  const baseEnv = {};
3413
3806
  for (const [key, value] of Object.entries(sanitizeEnv(process.env))) {
3414
3807
  if (value !== void 0) {
@@ -3417,6 +3810,10 @@ var ChromaMcpManager = class _ChromaMcpManager {
3417
3810
  }
3418
3811
  _ChromaMcpManager.ensureUvOnPath(baseEnv);
3419
3812
  if (!baseEnv.ANONYMIZED_TELEMETRY) baseEnv.ANONYMIZED_TELEMETRY = "false";
3813
+ return baseEnv;
3814
+ }
3815
+ getSpawnEnv(preflightEnv) {
3816
+ const baseEnv = preflightEnv ? { ...preflightEnv } : _ChromaMcpManager.getUvxPreflightEnv();
3420
3817
  const combinedCertPath = this.getCombinedCertPath();
3421
3818
  if (!combinedCertPath) {
3422
3819
  return baseEnv;
@@ -3584,6 +3981,7 @@ var ChromaSync = class _ChromaSync {
3584
3981
  memory_session_id: obs.memory_session_id,
3585
3982
  project: obs.project,
3586
3983
  merged_into_project: obs.merged_into_project ?? null,
3984
+ platform_source: obs.platform_source ? normalizePlatformSource(obs.platform_source) : normalizePlatformSource(void 0),
3587
3985
  created_at_epoch: obs.created_at_epoch,
3588
3986
  type: obs.type || "discovery",
3589
3987
  title: obs.title || "Untitled"
@@ -3631,6 +4029,7 @@ var ChromaSync = class _ChromaSync {
3631
4029
  memory_session_id: summary.memory_session_id,
3632
4030
  project: summary.project,
3633
4031
  merged_into_project: summary.merged_into_project ?? null,
4032
+ platform_source: summary.platform_source ? normalizePlatformSource(summary.platform_source) : normalizePlatformSource(void 0),
3634
4033
  created_at_epoch: summary.created_at_epoch,
3635
4034
  prompt_number: summary.prompt_number || 0
3636
4035
  };
@@ -3697,7 +4096,19 @@ var ChromaSync = class _ChromaSync {
3697
4096
  if (documents.length === 0) {
3698
4097
  return 0;
3699
4098
  }
3700
- await this.ensureCollectionExists();
4099
+ try {
4100
+ await this.ensureCollectionExists();
4101
+ } catch (error) {
4102
+ if (error instanceof ChromaUnavailableError) {
4103
+ logger.warn("CHROMA_SYNC", "Chroma unavailable before write; leaving documents unsynced", {
4104
+ collection: this.collectionName,
4105
+ requested: documents.length,
4106
+ error: error.message
4107
+ });
4108
+ return 0;
4109
+ }
4110
+ throw error;
4111
+ }
3701
4112
  const chromaMcp = ChromaMcpManager.getInstance();
3702
4113
  let written = 0;
3703
4114
  for (let i = 0; i < documents.length; i += this.BATCH_SIZE) {
@@ -3758,12 +4169,13 @@ var ChromaSync = class _ChromaSync {
3758
4169
  });
3759
4170
  return written;
3760
4171
  }
3761
- async syncObservation(observationId, memorySessionId, project, obs, promptNumber, createdAtEpoch) {
4172
+ async syncObservation(observationId, memorySessionId, project, obs, promptNumber, createdAtEpoch, platformSource) {
3762
4173
  const stored = {
3763
4174
  id: observationId,
3764
4175
  memory_session_id: memorySessionId,
3765
4176
  project,
3766
4177
  merged_into_project: null,
4178
+ platform_source: platformSource ? normalizePlatformSource(platformSource) : normalizePlatformSource(void 0),
3767
4179
  text: null,
3768
4180
  // Legacy field, not used
3769
4181
  type: obs.type,
@@ -3795,12 +4207,13 @@ var ChromaSync = class _ChromaSync {
3795
4207
  });
3796
4208
  }
3797
4209
  }
3798
- async syncSummary(summaryId, memorySessionId, project, summary, promptNumber, createdAtEpoch) {
4210
+ async syncSummary(summaryId, memorySessionId, project, summary, promptNumber, createdAtEpoch, platformSource) {
3799
4211
  const stored = {
3800
4212
  id: summaryId,
3801
4213
  memory_session_id: memorySessionId,
3802
4214
  project,
3803
4215
  merged_into_project: null,
4216
+ platform_source: platformSource ? normalizePlatformSource(platformSource) : normalizePlatformSource(void 0),
3804
4217
  request: summary.request,
3805
4218
  investigated: summary.investigated,
3806
4219
  learned: summary.learned,
@@ -3837,12 +4250,13 @@ var ChromaSync = class _ChromaSync {
3837
4250
  doc_type: "user_prompt",
3838
4251
  memory_session_id: prompt.memory_session_id,
3839
4252
  project: prompt.project,
4253
+ platform_source: prompt.platform_source,
3840
4254
  created_at_epoch: prompt.created_at_epoch,
3841
4255
  prompt_number: prompt.prompt_number
3842
4256
  }
3843
4257
  };
3844
4258
  }
3845
- async syncUserPrompt(promptId, memorySessionId, project, promptText, promptNumber, createdAtEpoch) {
4259
+ async syncUserPrompt(promptId, memorySessionId, project, promptText, promptNumber, createdAtEpoch, platformSource) {
3846
4260
  const stored = {
3847
4261
  id: promptId,
3848
4262
  content_session_id: "",
@@ -3851,7 +4265,8 @@ var ChromaSync = class _ChromaSync {
3851
4265
  prompt_text: promptText,
3852
4266
  created_at_epoch: createdAtEpoch,
3853
4267
  memory_session_id: memorySessionId,
3854
- project
4268
+ project,
4269
+ platform_source: normalizePlatformSource(platformSource)
3855
4270
  };
3856
4271
  const document = this.formatUserPromptDoc(stored);
3857
4272
  logger.info("CHROMA_SYNC", "Syncing user prompt", {
@@ -3966,9 +4381,13 @@ var ChromaSync = class _ChromaSync {
3966
4381
  }
3967
4382
  async backfillObservations(db, backfillProject, watermark) {
3968
4383
  const observations = db.db.prepare(`
3969
- SELECT * FROM observations
3970
- WHERE project = ? AND id > ?
3971
- ORDER BY id ASC
4384
+ SELECT
4385
+ o.*,
4386
+ COALESCE(NULLIF(s.platform_source, ''), 'claude') as platform_source
4387
+ FROM observations o
4388
+ LEFT JOIN sdk_sessions s ON s.memory_session_id = o.memory_session_id
4389
+ WHERE o.project = ? AND o.id > ?
4390
+ ORDER BY o.id ASC
3972
4391
  `).all(backfillProject, watermark);
3973
4392
  if (observations.length === 0) {
3974
4393
  return [];
@@ -4038,9 +4457,13 @@ var ChromaSync = class _ChromaSync {
4038
4457
  }
4039
4458
  async backfillSummaries(db, backfillProject, watermark) {
4040
4459
  const summaries = db.db.prepare(`
4041
- SELECT * FROM session_summaries
4042
- WHERE project = ? AND id > ?
4043
- ORDER BY id ASC
4460
+ SELECT
4461
+ ss.*,
4462
+ COALESCE(NULLIF(s.platform_source, ''), 'claude') as platform_source
4463
+ FROM session_summaries ss
4464
+ LEFT JOIN sdk_sessions s ON s.memory_session_id = ss.memory_session_id
4465
+ WHERE ss.project = ? AND ss.id > ?
4466
+ ORDER BY ss.id ASC
4044
4467
  `).all(backfillProject, watermark);
4045
4468
  if (summaries.length === 0) {
4046
4469
  return [];
@@ -4110,9 +4533,10 @@ var ChromaSync = class _ChromaSync {
4110
4533
  SELECT
4111
4534
  up.*,
4112
4535
  s.project,
4113
- s.memory_session_id
4536
+ s.memory_session_id,
4537
+ COALESCE(NULLIF(s.platform_source, ''), 'claude') as platform_source
4114
4538
  FROM user_prompts up
4115
- JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
4539
+ JOIN sdk_sessions s ON up.session_db_id = s.id
4116
4540
  WHERE s.project = ? AND up.id > ?
4117
4541
  ORDER BY up.id ASC
4118
4542
  `).all(backfillProject, watermark);
@@ -4122,7 +4546,7 @@ var ChromaSync = class _ChromaSync {
4122
4546
  const totalPromptCount = db.db.prepare(`
4123
4547
  SELECT COUNT(*) as count
4124
4548
  FROM user_prompts up
4125
- JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
4549
+ JOIN sdk_sessions s ON up.session_db_id = s.id
4126
4550
  WHERE s.project = ?
4127
4551
  `).get(backfillProject);
4128
4552
  logger.info("CHROMA_SYNC", "Backfilling user prompts", {
@@ -4866,35 +5290,36 @@ function classifyHttpProviderError(input) {
4866
5290
  const body = input.bodyText ?? "";
4867
5291
  const lower = body.toLowerCase();
4868
5292
  const retryAfterMs = input.headers ? parseRetryAfterMs(input.headers.get("retry-after")) : void 0;
5293
+ const cause = status === void 0 ? input.cause : new Error(`${providerLabel} HTTP error (status ${status})`);
4869
5294
  if (lower.includes("quota exceeded") || lower.includes("insufficient credits") || lower.includes("insufficient_quota") || lower.includes("resource_exhausted")) {
4870
5295
  return new ServerClassifiedProviderError(
4871
5296
  `${providerLabel} quota exhausted${status !== void 0 ? ` (status ${status})` : ""}`,
4872
- { kind: "quota_exhausted", cause: input.cause }
5297
+ { kind: "quota_exhausted", cause }
4873
5298
  );
4874
5299
  }
4875
5300
  if (status === 429) {
4876
5301
  return new ServerClassifiedProviderError(`${providerLabel} rate limit (429)`, {
4877
5302
  kind: "rate_limit",
4878
- cause: input.cause,
5303
+ cause,
4879
5304
  ...retryAfterMs !== void 0 ? { retryAfterMs } : {}
4880
5305
  });
4881
5306
  }
4882
5307
  if (status === 401 || status === 403) {
4883
5308
  return new ServerClassifiedProviderError(`${providerLabel} auth error (status ${status})`, {
4884
5309
  kind: "auth_invalid",
4885
- cause: input.cause
5310
+ cause
4886
5311
  });
4887
5312
  }
4888
5313
  if (status === 400 || status === 404) {
4889
5314
  return new ServerClassifiedProviderError(`${providerLabel} bad request (status ${status})`, {
4890
5315
  kind: "unrecoverable",
4891
- cause: input.cause
5316
+ cause
4892
5317
  });
4893
5318
  }
4894
5319
  if (status !== void 0 && status >= 500 && status < 600) {
4895
5320
  return new ServerClassifiedProviderError(`${providerLabel} upstream error (status ${status})`, {
4896
5321
  kind: "transient",
4897
- cause: input.cause
5322
+ cause
4898
5323
  });
4899
5324
  }
4900
5325
  if (status === void 0) {
@@ -4905,8 +5330,8 @@ function classifyHttpProviderError(input) {
4905
5330
  });
4906
5331
  }
4907
5332
  return new ServerClassifiedProviderError(
4908
- `${providerLabel} API error: ${status}${body ? ` - ${body.substring(0, 200)}` : ""}`,
4909
- { kind: "unrecoverable", cause: input.cause }
5333
+ `${providerLabel} API error (status ${status})`,
5334
+ { kind: "unrecoverable", cause }
4910
5335
  );
4911
5336
  }
4912
5337
 
@@ -5225,6 +5650,41 @@ async function safeReadBody(response) {
5225
5650
  // src/server/generation/providers/GeminiObservationProvider.ts
5226
5651
  var GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1/models";
5227
5652
  var DEFAULT_MODEL2 = "gemini-2.5-flash";
5653
+ function categorizeGeminiBadRequest(bodyText) {
5654
+ const lower = bodyText.toLowerCase();
5655
+ if (lower.includes("api key not valid") || lower.includes("api_key_invalid") || lower.includes("api key expired") || lower.includes("invalid api key")) {
5656
+ return "api_key";
5657
+ }
5658
+ 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"))) {
5659
+ return "role_sequence";
5660
+ }
5661
+ 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"))) {
5662
+ return "context_limit";
5663
+ }
5664
+ 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")) {
5665
+ return "model_unsupported";
5666
+ }
5667
+ return "unknown_bad_request";
5668
+ }
5669
+ function isQuotaBody(bodyText) {
5670
+ const lower = bodyText.toLowerCase();
5671
+ return lower.includes("quota exceeded") || lower.includes("insufficient credits") || lower.includes("insufficient_quota") || lower.includes("resource_exhausted");
5672
+ }
5673
+ function classifyGeminiServerError(input) {
5674
+ const status = input.status;
5675
+ const bodyText = input.bodyText ?? "";
5676
+ if (status === 400 && !isQuotaBody(bodyText)) {
5677
+ const category = categorizeGeminiBadRequest(bodyText);
5678
+ return new ServerClassifiedProviderError(`Gemini bad request: ${category}`, {
5679
+ kind: "unrecoverable",
5680
+ cause: new Error("Gemini HTTP error (status 400)")
5681
+ });
5682
+ }
5683
+ return classifyHttpProviderError({
5684
+ ...input,
5685
+ providerLabel: "Gemini"
5686
+ });
5687
+ }
5228
5688
  var GeminiObservationProvider = class {
5229
5689
  providerLabel = "gemini";
5230
5690
  apiKey;
@@ -5268,19 +5728,17 @@ var GeminiObservationProvider = class {
5268
5728
  signal
5269
5729
  });
5270
5730
  } catch (networkError) {
5271
- throw classifyHttpProviderError({
5272
- cause: networkError,
5273
- providerLabel: "Gemini"
5731
+ throw classifyGeminiServerError({
5732
+ cause: networkError
5274
5733
  });
5275
5734
  }
5276
5735
  if (!response.ok) {
5277
5736
  const bodyText = await safeReadBody2(response);
5278
- throw classifyHttpProviderError({
5737
+ throw classifyGeminiServerError({
5279
5738
  status: response.status,
5280
5739
  bodyText,
5281
5740
  headers: response.headers,
5282
- cause: new Error(`Gemini API error: ${response.status} - ${bodyText}`),
5283
- providerLabel: "Gemini"
5741
+ cause: new Error(`Gemini HTTP error (status ${response.status})`)
5284
5742
  });
5285
5743
  }
5286
5744
  let data;
@@ -5293,12 +5751,11 @@ var GeminiObservationProvider = class {
5293
5751
  });
5294
5752
  }
5295
5753
  if (data.error) {
5296
- throw classifyHttpProviderError({
5754
+ throw classifyGeminiServerError({
5297
5755
  status: response.status,
5298
5756
  bodyText: `${data.error.status ?? ""} ${data.error.message ?? ""}`,
5299
5757
  headers: response.headers,
5300
- cause: new Error(`Gemini API error: ${data.error.status} - ${data.error.message}`),
5301
- providerLabel: "Gemini"
5758
+ cause: new Error(`Gemini HTTP error (status ${response.status})`)
5302
5759
  });
5303
5760
  }
5304
5761
  const rawText = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";