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