@ramarivera/coding-agent-langfuse 0.1.45 → 0.1.46

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.
Files changed (2) hide show
  1. package/dist/backfill.js +75 -12
  2. package/package.json +1 -1
package/dist/backfill.js CHANGED
@@ -7,7 +7,7 @@ import { dirname, join } from "node:path";
7
7
  const allAgents = ["claude", "codex", "grok", "opencode", "pi"];
8
8
  const importStateIdentityVersion = "v9-cost-details";
9
9
  const importStateIdentityVersions = {
10
- claude: "v12-cost-details",
10
+ claude: "v13-claude-message-snapshot-dedupe",
11
11
  codex: "v11-codex-token-accounting-nonbillable",
12
12
  grok: "v12-cost-details",
13
13
  opencode: "v11-cost-details",
@@ -23,6 +23,7 @@ const langfuseIdIdentityVersions = {
23
23
  };
24
24
  const importPayloadVersion = "v10-cost-details";
25
25
  const importPayloadVersions = {
26
+ claude: "v11-claude-message-snapshot-dedupe",
26
27
  codex: "v11-codex-token-accounting-nonbillable",
27
28
  };
28
29
  const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
@@ -690,6 +691,25 @@ function usageDetails(usage) {
690
691
  details.total = usage.total;
691
692
  return Object.keys(details).length > 0 ? details : undefined;
692
693
  }
694
+ function usageTokenTotal(usage) {
695
+ if (!usage)
696
+ return 0;
697
+ return usage.total ??
698
+ (usage.input ?? 0) +
699
+ (usage.output ?? 0) +
700
+ (usage.reasoning ?? 0) +
701
+ (usage.cacheRead ?? 0) +
702
+ (usage.cacheWrite ?? 0) +
703
+ (usage.cacheWrite5m ?? 0) +
704
+ (usage.cacheWrite1h ?? 0);
705
+ }
706
+ function textLength(value) {
707
+ if (typeof value === "string")
708
+ return value.length;
709
+ if (value === undefined || value === null)
710
+ return 0;
711
+ return JSON.stringify(value).length;
712
+ }
693
713
  function calculateCost(event, usage, costRates) {
694
714
  if (!usage)
695
715
  return undefined;
@@ -1373,6 +1393,7 @@ function genericJsonlEvents(agent, files, sessionName) {
1373
1393
  startMs,
1374
1394
  },
1375
1395
  ];
1396
+ const claudeAssistantEventsByMessageId = new Map();
1376
1397
  for (const [index, row] of rows.entries()) {
1377
1398
  const message = asRecord(row.message);
1378
1399
  const role = asString(message.role) ?? asString(row.type);
@@ -1380,11 +1401,31 @@ function genericJsonlEvents(agent, files, sessionName) {
1380
1401
  asString(row.id) ??
1381
1402
  asString(row.toolUseID) ??
1382
1403
  `row-${index}`;
1404
+ let childParentRecordId = recordId;
1383
1405
  const toolUseId = asString(row.toolUseID) ?? asString(row.tool_use_id);
1384
1406
  const content = message.content ?? row.content;
1385
1407
  const timestamp = getTimestampMs(row.timestamp ?? row.time_created, startMs + index);
1386
1408
  const usage = normalizeUsage(message.usage ?? row.usage);
1387
- events.push({
1409
+ const claudeMessageId = agent === "claude" && role === "assistant"
1410
+ ? asString(message.id)
1411
+ : undefined;
1412
+ const eventMetadata = {
1413
+ ...pick(row, [
1414
+ "type",
1415
+ "entrypoint",
1416
+ "version",
1417
+ "gitBranch",
1418
+ "error",
1419
+ ]),
1420
+ ...(claudeMessageId
1421
+ ? {
1422
+ claude_message_id: claudeMessageId,
1423
+ claude_snapshot_count: 1,
1424
+ claude_usage_dedupe: "message_id_max_usage",
1425
+ }
1426
+ : {}),
1427
+ };
1428
+ const event = {
1388
1429
  agent,
1389
1430
  sourcePath: path,
1390
1431
  sessionId: asString(row.sessionId) ?? asString(row.session_id) ??
@@ -1408,14 +1449,36 @@ function genericJsonlEvents(agent, files, sessionName) {
1408
1449
  ? extractText(content)
1409
1450
  : undefined,
1410
1451
  usage,
1411
- metadata: pick(row, [
1412
- "type",
1413
- "entrypoint",
1414
- "version",
1415
- "gitBranch",
1416
- "error",
1417
- ]),
1418
- });
1452
+ metadata: eventMetadata,
1453
+ };
1454
+ if (claudeMessageId) {
1455
+ const existing = claudeAssistantEventsByMessageId.get(claudeMessageId);
1456
+ if (existing) {
1457
+ childParentRecordId = existing.recordId;
1458
+ if (usageTokenTotal(event.usage) > usageTokenTotal(existing.usage)) {
1459
+ existing.usage = event.usage;
1460
+ }
1461
+ if (event.output && textLength(event.output) > textLength(existing.output)) {
1462
+ existing.output = event.output;
1463
+ }
1464
+ if (!existing.model && event.model)
1465
+ existing.model = event.model;
1466
+ if (!existing.cwd && event.cwd)
1467
+ existing.cwd = event.cwd;
1468
+ existing.startMs = Math.min(existing.startMs, event.startMs);
1469
+ existing.metadata = {
1470
+ ...existing.metadata,
1471
+ claude_snapshot_count: (asNumber(existing.metadata?.claude_snapshot_count) ?? 1) + 1,
1472
+ };
1473
+ }
1474
+ else {
1475
+ claudeAssistantEventsByMessageId.set(claudeMessageId, event);
1476
+ events.push(event);
1477
+ }
1478
+ }
1479
+ else {
1480
+ events.push(event);
1481
+ }
1419
1482
  for (const reasoning of reasoningFromContent(content)) {
1420
1483
  events.push({
1421
1484
  agent,
@@ -1425,7 +1488,7 @@ function genericJsonlEvents(agent, files, sessionName) {
1425
1488
  name: `${agent} reasoning`,
1426
1489
  cwd,
1427
1490
  startMs: timestamp,
1428
- parentRecordId: recordId,
1491
+ parentRecordId: childParentRecordId,
1429
1492
  output: reasoning.text,
1430
1493
  metadata: { has_signature: reasoning.hasSignature },
1431
1494
  });
@@ -1439,7 +1502,7 @@ function genericJsonlEvents(agent, files, sessionName) {
1439
1502
  name: `${agent} tool ${tool.name}`,
1440
1503
  cwd,
1441
1504
  startMs: timestamp,
1442
- parentRecordId: asString(row.uuid) ?? asString(row.id),
1505
+ parentRecordId: childParentRecordId,
1443
1506
  input: tool.arguments,
1444
1507
  });
1445
1508
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",