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/.codex-plugin/plugin.json +1 -1
- package/dist/index.js +527 -70
- package/dist/index.js.map +1 -1
- package/dist/npx-cli/index.js +321 -281
- package/dist/opencode-plugin/index.js +7 -7
- package/dist/sdk/index.d.ts +30 -14
- package/dist/sdk/index.js +527 -70
- package/dist/sdk/index.js.map +1 -1
- package/openclaw/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/plugin/scripts/context-generator.cjs +350 -167
- package/plugin/scripts/mcp-server.cjs +31 -31
- package/plugin/scripts/server-service.cjs +187 -147
- package/plugin/scripts/transcript-watcher.cjs +15 -15
- package/plugin/scripts/worker-service.cjs +714 -500
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
AND
|
|
977
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1467
|
+
const parts = [
|
|
1393
1468
|
input.teamId,
|
|
1394
1469
|
input.projectId,
|
|
1395
|
-
"external"
|
|
1396
|
-
|
|
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
|
-
|
|
1483
|
+
platformSource,
|
|
1405
1484
|
input.agentId ?? null,
|
|
1406
1485
|
input.contentSessionId
|
|
1407
1486
|
])}`;
|
|
1408
1487
|
}
|
|
1409
|
-
if (input.agentId &&
|
|
1488
|
+
if (input.agentId && platformSource) {
|
|
1410
1489
|
return `server_session:v1:${deterministicKey([
|
|
1411
1490
|
input.teamId,
|
|
1412
1491
|
input.projectId,
|
|
1413
1492
|
"agent",
|
|
1414
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
3442
|
+
if (closingTransport) {
|
|
3129
3443
|
try {
|
|
3130
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3970
|
-
|
|
3971
|
-
|
|
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
|
|
4042
|
-
|
|
4043
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4909
|
-
{ kind: "unrecoverable", 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
|
|
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
|
|
5737
|
+
throw classifyGeminiServerError({
|
|
5279
5738
|
status: response.status,
|
|
5280
5739
|
bodyText,
|
|
5281
5740
|
headers: response.headers,
|
|
5282
|
-
cause: new Error(`Gemini
|
|
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
|
|
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
|
|
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() ?? "";
|