opencode-metrics-plugin 0.1.5 → 0.2.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.
Files changed (43) hide show
  1. package/README.md +108 -390
  2. package/dist/api.d.ts +3 -0
  3. package/dist/api.js +4 -0
  4. package/dist/index.d.ts +2 -51
  5. package/dist/index.js +4 -61
  6. package/dist/metrics/dirs.d.ts +23 -0
  7. package/dist/{dirs.js → metrics/dirs.js} +4 -10
  8. package/dist/metrics/engine/engine.d.ts +15 -0
  9. package/dist/{metrics-engine.js → metrics/engine/engine.js} +33 -146
  10. package/dist/{metrics-handlers.d.ts → metrics/engine/handlers.d.ts} +1 -1
  11. package/dist/{metrics-handlers.js → metrics/engine/handlers.js} +2 -2
  12. package/dist/metrics/engine/state.d.ts +79 -0
  13. package/dist/{metrics-types.js → metrics/engine/state.js} +1 -19
  14. package/dist/{event-logger.d.ts → metrics/eventlog/event-logger.d.ts} +0 -5
  15. package/dist/{event-logger.js → metrics/eventlog/event-logger.js} +2 -18
  16. package/dist/metrics/index.d.ts +9 -0
  17. package/dist/metrics/index.js +6 -0
  18. package/dist/metrics/runtime.d.ts +25 -0
  19. package/dist/metrics/runtime.js +28 -0
  20. package/dist/metrics/snapshot/flush.d.ts +6 -0
  21. package/dist/{metrics-output.js → metrics/snapshot/flush.js} +6 -290
  22. package/dist/metrics/snapshot/merge.d.ts +9 -0
  23. package/dist/metrics/snapshot/merge.js +282 -0
  24. package/dist/{metrics-steps.d.ts → metrics/snapshot/steps.d.ts} +1 -1
  25. package/dist/{metrics-steps.js → metrics/snapshot/steps.js} +1 -1
  26. package/dist/{metrics-types.d.ts → metrics/types.d.ts} +3 -81
  27. package/dist/metrics/types.js +20 -0
  28. package/dist/plugin.d.ts +22 -0
  29. package/dist/plugin.js +49 -0
  30. package/dist/{logger.d.ts → shared/log.d.ts} +2 -0
  31. package/dist/{logger.js → shared/log.js} +9 -2
  32. package/package.json +12 -4
  33. package/dist/backfill-cli.d.ts +0 -1
  34. package/dist/backfill-cli.js +0 -74
  35. package/dist/backfill.d.ts +0 -58
  36. package/dist/backfill.js +0 -550
  37. package/dist/dirs.d.ts +0 -34
  38. package/dist/metrics-engine.d.ts +0 -37
  39. package/dist/metrics-output.d.ts +0 -11
  40. package/dist/summary-store.d.ts +0 -150
  41. package/dist/summary-store.js +0 -560
  42. /package/dist/{compile-analyzer.d.ts → metrics/analysis/hvigor.d.ts} +0 -0
  43. /package/dist/{compile-analyzer.js → metrics/analysis/hvigor.js} +0 -0
@@ -1,10 +1,10 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { log } from "./logger.js";
4
- import { createTokenUsage } from "./metrics-types.js";
5
- import { getMetricsDir, getSummaryFile } from "./dirs.js";
6
- import { upsertSessionSnapshot } from "./summary-store.js";
7
- import { buildSteps, extractStepContent } from "./metrics-steps.js";
3
+ import { log } from "../../shared/log.js";
4
+ import { createTokenUsage } from "../types.js";
5
+ import { getMetricsDir } from "../dirs.js";
6
+ import { buildSteps, extractStepContent } from "./steps.js";
7
+ import { mergeMetricsOutput } from "./merge.js";
8
8
  function filterByKeys(source, keys) {
9
9
  const result = new Map();
10
10
  for (const key of keys) {
@@ -326,9 +326,6 @@ function flushMetrics(sessionId, state, now, dirs) {
326
326
  const tmpPath = filePath + ".tmp";
327
327
  fs.writeFileSync(tmpPath, JSON.stringify(merged, null, 2));
328
328
  fs.renameSync(tmpPath, filePath);
329
- // 摘要/详情双表联动入库(force:与文件同拍最新;缺省写全局共享库,显式 summaryFile 可隔离)
330
- const summaryFile = dirs?.summaryFile ?? getSummaryFile();
331
- upsertSessionSnapshot(summaryFile, merged, filePath, "force");
332
329
  }
333
330
  catch (err) {
334
331
  log.info("Failed to flush metrics", { sessionId, error: String(err) });
@@ -389,285 +386,4 @@ function handleSessionIdle(state, sessionId, dirs) {
389
386
  state._idleFlushed = true;
390
387
  return flushMetrics(sessionId, state, now, dirs);
391
388
  }
392
- function mergeChildMetrics(parent, child, meta) {
393
- // Merge tokens
394
- parent.tokens.input += child.tokens.input;
395
- parent.tokens.output += child.tokens.output;
396
- parent.tokens.reasoning += child.tokens.reasoning;
397
- parent.tokens.cacheRead += child.tokens.cacheRead;
398
- parent.tokens.cacheWrite += child.tokens.cacheWrite;
399
- parent.tokens.total += child.tokens.total;
400
- // Merge tools
401
- parent.tools.totalCalls += child.tools.totalCalls;
402
- parent.tools.invalidCalls += child.tools.invalidCalls;
403
- parent.tools.completedCalls += child.tools.completedCalls;
404
- parent.tools.errorCalls += child.tools.errorCalls;
405
- if (child.tools.slowestCall.duration > parent.tools.slowestCall.duration) {
406
- parent.tools.slowestCall = { ...child.tools.slowestCall };
407
- }
408
- for (const [tool, stats] of child.tools.distribution) {
409
- const existing = parent.tools.distribution.get(tool) ?? { calls: 0, errors: 0, totalDuration: 0, maxDuration: 0 };
410
- existing.calls += stats.calls;
411
- existing.errors += stats.errors;
412
- existing.totalDuration += stats.totalDuration;
413
- if (stats.maxDuration > existing.maxDuration)
414
- existing.maxDuration = stats.maxDuration;
415
- parent.tools.distribution.set(tool, existing);
416
- }
417
- // Merge compactions
418
- parent.compactions += child.compactions;
419
- // Merge anomaly
420
- if (child.anomaly.triggered) {
421
- parent.anomaly.triggered = true;
422
- parent.anomaly.events.push(...child.anomaly.events);
423
- }
424
- // Merge rounds (skip empty rounds)
425
- for (const round of child.rounds) {
426
- if (round.tokens.total > 0 || round.toolCalls > 0) {
427
- parent.rounds.push({ ...round, roundIndex: parent.rounds.length });
428
- }
429
- }
430
- // Merge stages (skip stages with no tokens 鈥?these correspond to empty rounds)
431
- for (const stage of child.stages) {
432
- if (stage.tokens.total > 0) {
433
- parent.stages.push({ ...stage });
434
- }
435
- }
436
- // Store child maps in subAgents (not merged into parent)
437
- parent.subAgents.set(meta.childId, {
438
- sessionId: meta.childId,
439
- agentName: meta.agentName,
440
- title: meta.title,
441
- messageMap: child.messageMap,
442
- toolCallMap: child.toolCallMap,
443
- textLengthMap: child.textLengthMap,
444
- orderCounter: 0,
445
- _userMessageIds: new Set(),
446
- _pendingUserMessage: [],
447
- });
448
- // Merge compileStats (additive)
449
- parent.compileStats.hvigorwCalls += child.compileStats.hvigorwCalls;
450
- parent.compileStats.hvigorwErrors += child.compileStats.hvigorwErrors;
451
- parent.compileStats.etsLines += child.compileStats.etsLines;
452
- // lastBuildSuccess: take child's value if child had any builds
453
- if (child.compileStats.hvigorwCalls > 0 || child.compileStats.hvigorwErrors > 0) {
454
- parent.compileStats.lastBuildSuccess = child.compileStats.lastBuildSuccess;
455
- }
456
- // Merge firstBuildPerRound
457
- parent.compileStats.firstBuildPerRound.push(...child.compileStats.firstBuildPerRound);
458
- // Merge errorCodes
459
- for (const [code, info] of child.compileStats.errorCodes) {
460
- const existing = parent.compileStats.errorCodes.get(code);
461
- if (existing) {
462
- existing.count += info.count;
463
- }
464
- else {
465
- parent.compileStats.errorCodes.set(code, { ...info });
466
- }
467
- }
468
- // Merge warnings
469
- for (const [type, info] of child.compileStats.warnings) {
470
- const existing = parent.compileStats.warnings.get(type);
471
- if (existing) {
472
- existing.count += info.count;
473
- existing.entries.push(...info.entries);
474
- }
475
- else {
476
- parent.compileStats.warnings.set(type, { count: info.count, entries: [...info.entries] });
477
- }
478
- }
479
- // Merge moduleTimings
480
- for (const [module, timing] of child.compileStats.moduleTimings) {
481
- const existing = parent.compileStats.moduleTimings.get(module);
482
- if (existing) {
483
- existing.totalDuration += timing.totalDuration;
484
- existing.taskCount += timing.taskCount;
485
- if (timing.slowestTask.duration > existing.slowestTask.duration) {
486
- existing.slowestTask = { ...timing.slowestTask };
487
- }
488
- }
489
- else {
490
- parent.compileStats.moduleTimings.set(module, { ...timing, slowestTask: { ...timing.slowestTask } });
491
- }
492
- }
493
- // Merge fixCycles
494
- parent.compileStats.fixCycles.push(...child.compileStats.fixCycles.map(c => ({ ...c })));
495
- // Merge skillCallMap
496
- for (const [callID, entry] of child.skillCallMap) {
497
- if (!parent.skillCallMap.has(callID)) {
498
- parent.skillCallMap.set(callID, { ...entry });
499
- }
500
- }
501
- // Merge skillSearchMap
502
- for (const [callID, entry] of child.skillSearchMap) {
503
- if (!parent.skillSearchMap.has(callID)) {
504
- parent.skillSearchMap.set(callID, {
505
- ...entry,
506
- followUpReads: [...entry.followUpReads],
507
- });
508
- }
509
- }
510
- // Merge htmlPreviewMsgId (take first non-null)
511
- if (!parent.htmlPreviewMsgId && child.htmlPreviewMsgId) {
512
- parent.htmlPreviewMsgId = child.htmlPreviewMsgId;
513
- }
514
- // Merge planning data
515
- parent.planningCalls.push(...child.planningCalls);
516
- }
517
- export function mergeMetricsOutput(existing, fresh) {
518
- if (!existing)
519
- return fresh;
520
- // startTime: min of both
521
- const startTime = Math.min(existing.startTime, fresh.startTime);
522
- // duration: fresh.endTime - min startTime
523
- const duration = fresh.endTime - startTime;
524
- // Rounds: keep existing, append fresh rounds with new indices only
525
- // roundIndex resets on restart, so we can't use it as a dedup key with fresh-wins.
526
- // Strategy: existing rounds are always kept; fresh rounds are added only if their
527
- // index doesn't already exist in existing. This preserves old session data.
528
- const existingRoundIndices = new Set(existing.rounds.map(r => r.roundIndex));
529
- const rounds = [
530
- ...existing.rounds,
531
- ...fresh.rounds.filter(r => !existingRoundIndices.has(r.roundIndex)),
532
- ].sort((a, b) => a.roundIndex - b.roundIndex);
533
- // Steps: append with dedup by startTime+endTime (fresh wins on collision)
534
- const stepSeen = new Set();
535
- const steps = [];
536
- for (const s of fresh.steps) {
537
- const k = `${s.startTime}:${s.endTime}`;
538
- stepSeen.add(k);
539
- steps.push(s);
540
- }
541
- for (const s of existing.steps) {
542
- if (!stepSeen.has(`${s.startTime}:${s.endTime}`))
543
- steps.push(s);
544
- }
545
- // Subagents: append with dedup by sessionId (fresh wins)
546
- const subagentMap = new Map();
547
- for (const s of existing.subagents)
548
- subagentMap.set(s.sessionId, s);
549
- for (const s of fresh.subagents)
550
- subagentMap.set(s.sessionId, s);
551
- const subagents = [...subagentMap.values()];
552
- // Planning: append calls with dedup by callID (existing wins on collision)
553
- const callSeen = new Set();
554
- const mergedCalls = [];
555
- for (const c of existing.planning.calls) {
556
- if (!callSeen.has(c.callID)) {
557
- callSeen.add(c.callID);
558
- mergedCalls.push(c);
559
- }
560
- }
561
- for (const c of fresh.planning.calls) {
562
- if (!callSeen.has(c.callID)) {
563
- callSeen.add(c.callID);
564
- mergedCalls.push(c);
565
- }
566
- }
567
- const planning = {
568
- ...fresh.planning,
569
- calls: mergedCalls,
570
- };
571
- // Skills: append with dedup by skillName (fresh wins)
572
- const skillMap = new Map();
573
- for (const s of existing.skills)
574
- skillMap.set(s.skillName, s);
575
- for (const s of fresh.skills)
576
- skillMap.set(s.skillName, s);
577
- const skills = [...skillMap.values()];
578
- // SkillSearches: append with dedup by query+skillPath (fresh wins)
579
- const searchMap = new Map();
580
- for (const s of existing.skillSearches)
581
- searchMap.set(`${s.query}:${s.skillPath}`, s);
582
- for (const s of fresh.skillSearches)
583
- searchMap.set(`${s.query}:${s.skillPath}`, s);
584
- const skillSearches = [...searchMap.values()];
585
- // codeStats.errorCodes: merge by code, accumulate count, keep existing type/message
586
- const errorCodeMap = new Map();
587
- for (const e of existing.codeStats.errorCodes)
588
- errorCodeMap.set(e.code, { ...e });
589
- for (const e of fresh.codeStats.errorCodes) {
590
- const prev = errorCodeMap.get(e.code);
591
- if (prev) {
592
- prev.count += e.count;
593
- // keep existing type/message
594
- }
595
- else {
596
- errorCodeMap.set(e.code, { ...e });
597
- }
598
- }
599
- const errorCodes = [...errorCodeMap.values()];
600
- // codeStats.warnings: merge by type, accumulate count, append entries
601
- const warnByType = { ...existing.codeStats.warnings.byType };
602
- const warnEntries = [...existing.codeStats.warnings.entries];
603
- let warnTotal = existing.codeStats.warnings.total;
604
- for (const [type, count] of Object.entries(fresh.codeStats.warnings.byType)) {
605
- warnByType[type] = (warnByType[type] || 0) + count;
606
- warnTotal += count;
607
- }
608
- warnEntries.push(...fresh.codeStats.warnings.entries);
609
- const warnings = { total: warnTotal, byType: warnByType, entries: warnEntries };
610
- // codeStats.moduleTimings: merge by module, accumulate totalDuration/taskCount, max slowestTask
611
- const moduleMap = new Map();
612
- for (const m of existing.codeStats.moduleTimings)
613
- moduleMap.set(m.module, { ...m, slowestTask: { ...m.slowestTask } });
614
- for (const m of fresh.codeStats.moduleTimings) {
615
- const prev = moduleMap.get(m.module);
616
- if (prev) {
617
- prev.totalDurationMs += m.totalDurationMs;
618
- prev.taskCount += m.taskCount;
619
- if (m.slowestTask.duration > prev.slowestTask.duration) {
620
- prev.slowestTask = { ...m.slowestTask };
621
- }
622
- }
623
- else {
624
- moduleMap.set(m.module, { ...m, slowestTask: { ...m.slowestTask } });
625
- }
626
- }
627
- const moduleTimings = [...moduleMap.values()];
628
- // codeStats.fixCycles: append (skip dedup)
629
- const fixCycles = {
630
- ...fresh.codeStats.fixCycles,
631
- cycles: [...existing.codeStats.fixCycles.cycles, ...fresh.codeStats.fixCycles.cycles],
632
- };
633
- // codeStats.firstBuildPerRound: fresh accumulates all rounds, use fresh
634
- const firstBuildPerRound = fresh.codeStats.firstBuildPerRound;
635
- // systemPrompts: union of keys, fresh values win
636
- const systemPrompts = { ...existing.systemPrompts };
637
- for (const [key, val] of Object.entries(fresh.systemPrompts)) {
638
- systemPrompts[key] = val;
639
- }
640
- return {
641
- sessionId: fresh.sessionId,
642
- startTime,
643
- endTime: fresh.endTime,
644
- duration,
645
- systemPrompts,
646
- rounds,
647
- tokens: fresh.tokens,
648
- tools: fresh.tools,
649
- compactions: fresh.compactions,
650
- anomaly: fresh.anomaly,
651
- stages: fresh.stages,
652
- header: fresh.header,
653
- codeStats: {
654
- etsLines: fresh.codeStats.etsLines,
655
- buildSuccess: fresh.codeStats.buildSuccess,
656
- fixCompileCount: fresh.codeStats.fixCompileCount,
657
- totalCompileErrors: fresh.codeStats.totalCompileErrors,
658
- firstBuildPerRound,
659
- firstBuildPassRate: fresh.codeStats.firstBuildPassRate,
660
- errorCodes,
661
- warnings,
662
- fixCycles,
663
- moduleTimings,
664
- },
665
- responseLength: fresh.responseLength,
666
- skills,
667
- skillSearches,
668
- steps,
669
- subagents,
670
- planning,
671
- };
672
- }
673
- export { flushMetrics, handleSessionIdle, mergeChildMetrics };
389
+ export { flushMetrics, handleSessionIdle };
@@ -0,0 +1,9 @@
1
+ import type { MetricsOutput } from "../types.js";
2
+ import type { SessionMetricsState } from "../engine/state.js";
3
+ declare function mergeChildMetrics(parent: SessionMetricsState, child: SessionMetricsState, meta: {
4
+ childId: string;
5
+ agentName: string;
6
+ title: string;
7
+ }): void;
8
+ export declare function mergeMetricsOutput(existing: MetricsOutput | null, fresh: MetricsOutput): MetricsOutput;
9
+ export { mergeChildMetrics };
@@ -0,0 +1,282 @@
1
+ function mergeChildMetrics(parent, child, meta) {
2
+ // Merge tokens
3
+ parent.tokens.input += child.tokens.input;
4
+ parent.tokens.output += child.tokens.output;
5
+ parent.tokens.reasoning += child.tokens.reasoning;
6
+ parent.tokens.cacheRead += child.tokens.cacheRead;
7
+ parent.tokens.cacheWrite += child.tokens.cacheWrite;
8
+ parent.tokens.total += child.tokens.total;
9
+ // Merge tools
10
+ parent.tools.totalCalls += child.tools.totalCalls;
11
+ parent.tools.invalidCalls += child.tools.invalidCalls;
12
+ parent.tools.completedCalls += child.tools.completedCalls;
13
+ parent.tools.errorCalls += child.tools.errorCalls;
14
+ if (child.tools.slowestCall.duration > parent.tools.slowestCall.duration) {
15
+ parent.tools.slowestCall = { ...child.tools.slowestCall };
16
+ }
17
+ for (const [tool, stats] of child.tools.distribution) {
18
+ const existing = parent.tools.distribution.get(tool) ?? { calls: 0, errors: 0, totalDuration: 0, maxDuration: 0 };
19
+ existing.calls += stats.calls;
20
+ existing.errors += stats.errors;
21
+ existing.totalDuration += stats.totalDuration;
22
+ if (stats.maxDuration > existing.maxDuration)
23
+ existing.maxDuration = stats.maxDuration;
24
+ parent.tools.distribution.set(tool, existing);
25
+ }
26
+ // Merge compactions
27
+ parent.compactions += child.compactions;
28
+ // Merge anomaly
29
+ if (child.anomaly.triggered) {
30
+ parent.anomaly.triggered = true;
31
+ parent.anomaly.events.push(...child.anomaly.events);
32
+ }
33
+ // Merge rounds (skip empty rounds)
34
+ for (const round of child.rounds) {
35
+ if (round.tokens.total > 0 || round.toolCalls > 0) {
36
+ parent.rounds.push({ ...round, roundIndex: parent.rounds.length });
37
+ }
38
+ }
39
+ // Merge stages (skip stages with no tokens 鈥?these correspond to empty rounds)
40
+ for (const stage of child.stages) {
41
+ if (stage.tokens.total > 0) {
42
+ parent.stages.push({ ...stage });
43
+ }
44
+ }
45
+ // Store child maps in subAgents (not merged into parent)
46
+ parent.subAgents.set(meta.childId, {
47
+ sessionId: meta.childId,
48
+ agentName: meta.agentName,
49
+ title: meta.title,
50
+ messageMap: child.messageMap,
51
+ toolCallMap: child.toolCallMap,
52
+ textLengthMap: child.textLengthMap,
53
+ orderCounter: 0,
54
+ _userMessageIds: new Set(),
55
+ _pendingUserMessage: [],
56
+ });
57
+ // Merge compileStats (additive)
58
+ parent.compileStats.hvigorwCalls += child.compileStats.hvigorwCalls;
59
+ parent.compileStats.hvigorwErrors += child.compileStats.hvigorwErrors;
60
+ parent.compileStats.etsLines += child.compileStats.etsLines;
61
+ // lastBuildSuccess: take child's value if child had any builds
62
+ if (child.compileStats.hvigorwCalls > 0 || child.compileStats.hvigorwErrors > 0) {
63
+ parent.compileStats.lastBuildSuccess = child.compileStats.lastBuildSuccess;
64
+ }
65
+ // Merge firstBuildPerRound
66
+ parent.compileStats.firstBuildPerRound.push(...child.compileStats.firstBuildPerRound);
67
+ // Merge errorCodes
68
+ for (const [code, info] of child.compileStats.errorCodes) {
69
+ const existing = parent.compileStats.errorCodes.get(code);
70
+ if (existing) {
71
+ existing.count += info.count;
72
+ }
73
+ else {
74
+ parent.compileStats.errorCodes.set(code, { ...info });
75
+ }
76
+ }
77
+ // Merge warnings
78
+ for (const [type, info] of child.compileStats.warnings) {
79
+ const existing = parent.compileStats.warnings.get(type);
80
+ if (existing) {
81
+ existing.count += info.count;
82
+ existing.entries.push(...info.entries);
83
+ }
84
+ else {
85
+ parent.compileStats.warnings.set(type, { count: info.count, entries: [...info.entries] });
86
+ }
87
+ }
88
+ // Merge moduleTimings
89
+ for (const [module, timing] of child.compileStats.moduleTimings) {
90
+ const existing = parent.compileStats.moduleTimings.get(module);
91
+ if (existing) {
92
+ existing.totalDuration += timing.totalDuration;
93
+ existing.taskCount += timing.taskCount;
94
+ if (timing.slowestTask.duration > existing.slowestTask.duration) {
95
+ existing.slowestTask = { ...timing.slowestTask };
96
+ }
97
+ }
98
+ else {
99
+ parent.compileStats.moduleTimings.set(module, { ...timing, slowestTask: { ...timing.slowestTask } });
100
+ }
101
+ }
102
+ // Merge fixCycles
103
+ parent.compileStats.fixCycles.push(...child.compileStats.fixCycles.map(c => ({ ...c })));
104
+ // Merge skillCallMap
105
+ for (const [callID, entry] of child.skillCallMap) {
106
+ if (!parent.skillCallMap.has(callID)) {
107
+ parent.skillCallMap.set(callID, { ...entry });
108
+ }
109
+ }
110
+ // Merge skillSearchMap
111
+ for (const [callID, entry] of child.skillSearchMap) {
112
+ if (!parent.skillSearchMap.has(callID)) {
113
+ parent.skillSearchMap.set(callID, {
114
+ ...entry,
115
+ followUpReads: [...entry.followUpReads],
116
+ });
117
+ }
118
+ }
119
+ // Merge htmlPreviewMsgId (take first non-null)
120
+ if (!parent.htmlPreviewMsgId && child.htmlPreviewMsgId) {
121
+ parent.htmlPreviewMsgId = child.htmlPreviewMsgId;
122
+ }
123
+ // Merge planning data
124
+ parent.planningCalls.push(...child.planningCalls);
125
+ }
126
+ export function mergeMetricsOutput(existing, fresh) {
127
+ if (!existing)
128
+ return fresh;
129
+ // startTime: min of both
130
+ const startTime = Math.min(existing.startTime, fresh.startTime);
131
+ // duration: fresh.endTime - min startTime
132
+ const duration = fresh.endTime - startTime;
133
+ // Rounds: keep existing, append fresh rounds with new indices only
134
+ // roundIndex resets on restart, so we can't use it as a dedup key with fresh-wins.
135
+ // Strategy: existing rounds are always kept; fresh rounds are added only if their
136
+ // index doesn't already exist in existing. This preserves old session data.
137
+ const existingRoundIndices = new Set(existing.rounds.map(r => r.roundIndex));
138
+ const rounds = [
139
+ ...existing.rounds,
140
+ ...fresh.rounds.filter(r => !existingRoundIndices.has(r.roundIndex)),
141
+ ].sort((a, b) => a.roundIndex - b.roundIndex);
142
+ // Steps: append with dedup by startTime+endTime (fresh wins on collision)
143
+ const stepSeen = new Set();
144
+ const steps = [];
145
+ for (const s of fresh.steps) {
146
+ const k = `${s.startTime}:${s.endTime}`;
147
+ stepSeen.add(k);
148
+ steps.push(s);
149
+ }
150
+ for (const s of existing.steps) {
151
+ if (!stepSeen.has(`${s.startTime}:${s.endTime}`))
152
+ steps.push(s);
153
+ }
154
+ // Subagents: append with dedup by sessionId (fresh wins)
155
+ const subagentMap = new Map();
156
+ for (const s of existing.subagents)
157
+ subagentMap.set(s.sessionId, s);
158
+ for (const s of fresh.subagents)
159
+ subagentMap.set(s.sessionId, s);
160
+ const subagents = [...subagentMap.values()];
161
+ // Planning: append calls with dedup by callID (existing wins on collision)
162
+ const callSeen = new Set();
163
+ const mergedCalls = [];
164
+ for (const c of existing.planning.calls) {
165
+ if (!callSeen.has(c.callID)) {
166
+ callSeen.add(c.callID);
167
+ mergedCalls.push(c);
168
+ }
169
+ }
170
+ for (const c of fresh.planning.calls) {
171
+ if (!callSeen.has(c.callID)) {
172
+ callSeen.add(c.callID);
173
+ mergedCalls.push(c);
174
+ }
175
+ }
176
+ const planning = {
177
+ ...fresh.planning,
178
+ calls: mergedCalls,
179
+ };
180
+ // Skills: append with dedup by skillName (fresh wins)
181
+ const skillMap = new Map();
182
+ for (const s of existing.skills)
183
+ skillMap.set(s.skillName, s);
184
+ for (const s of fresh.skills)
185
+ skillMap.set(s.skillName, s);
186
+ const skills = [...skillMap.values()];
187
+ // SkillSearches: append with dedup by query+skillPath (fresh wins)
188
+ const searchMap = new Map();
189
+ for (const s of existing.skillSearches)
190
+ searchMap.set(`${s.query}:${s.skillPath}`, s);
191
+ for (const s of fresh.skillSearches)
192
+ searchMap.set(`${s.query}:${s.skillPath}`, s);
193
+ const skillSearches = [...searchMap.values()];
194
+ // codeStats.errorCodes: merge by code, accumulate count, keep existing type/message
195
+ const errorCodeMap = new Map();
196
+ for (const e of existing.codeStats.errorCodes)
197
+ errorCodeMap.set(e.code, { ...e });
198
+ for (const e of fresh.codeStats.errorCodes) {
199
+ const prev = errorCodeMap.get(e.code);
200
+ if (prev) {
201
+ prev.count += e.count;
202
+ // keep existing type/message
203
+ }
204
+ else {
205
+ errorCodeMap.set(e.code, { ...e });
206
+ }
207
+ }
208
+ const errorCodes = [...errorCodeMap.values()];
209
+ // codeStats.warnings: merge by type, accumulate count, append entries
210
+ const warnByType = { ...existing.codeStats.warnings.byType };
211
+ const warnEntries = [...existing.codeStats.warnings.entries];
212
+ let warnTotal = existing.codeStats.warnings.total;
213
+ for (const [type, count] of Object.entries(fresh.codeStats.warnings.byType)) {
214
+ warnByType[type] = (warnByType[type] || 0) + count;
215
+ warnTotal += count;
216
+ }
217
+ warnEntries.push(...fresh.codeStats.warnings.entries);
218
+ const warnings = { total: warnTotal, byType: warnByType, entries: warnEntries };
219
+ // codeStats.moduleTimings: merge by module, accumulate totalDuration/taskCount, max slowestTask
220
+ const moduleMap = new Map();
221
+ for (const m of existing.codeStats.moduleTimings)
222
+ moduleMap.set(m.module, { ...m, slowestTask: { ...m.slowestTask } });
223
+ for (const m of fresh.codeStats.moduleTimings) {
224
+ const prev = moduleMap.get(m.module);
225
+ if (prev) {
226
+ prev.totalDurationMs += m.totalDurationMs;
227
+ prev.taskCount += m.taskCount;
228
+ if (m.slowestTask.duration > prev.slowestTask.duration) {
229
+ prev.slowestTask = { ...m.slowestTask };
230
+ }
231
+ }
232
+ else {
233
+ moduleMap.set(m.module, { ...m, slowestTask: { ...m.slowestTask } });
234
+ }
235
+ }
236
+ const moduleTimings = [...moduleMap.values()];
237
+ // codeStats.fixCycles: append (skip dedup)
238
+ const fixCycles = {
239
+ ...fresh.codeStats.fixCycles,
240
+ cycles: [...existing.codeStats.fixCycles.cycles, ...fresh.codeStats.fixCycles.cycles],
241
+ };
242
+ // codeStats.firstBuildPerRound: fresh accumulates all rounds, use fresh
243
+ const firstBuildPerRound = fresh.codeStats.firstBuildPerRound;
244
+ // systemPrompts: union of keys, fresh values win
245
+ const systemPrompts = { ...existing.systemPrompts };
246
+ for (const [key, val] of Object.entries(fresh.systemPrompts)) {
247
+ systemPrompts[key] = val;
248
+ }
249
+ return {
250
+ sessionId: fresh.sessionId,
251
+ startTime,
252
+ endTime: fresh.endTime,
253
+ duration,
254
+ systemPrompts,
255
+ rounds,
256
+ tokens: fresh.tokens,
257
+ tools: fresh.tools,
258
+ compactions: fresh.compactions,
259
+ anomaly: fresh.anomaly,
260
+ stages: fresh.stages,
261
+ header: fresh.header,
262
+ codeStats: {
263
+ etsLines: fresh.codeStats.etsLines,
264
+ buildSuccess: fresh.codeStats.buildSuccess,
265
+ fixCompileCount: fresh.codeStats.fixCompileCount,
266
+ totalCompileErrors: fresh.codeStats.totalCompileErrors,
267
+ firstBuildPerRound,
268
+ firstBuildPassRate: fresh.codeStats.firstBuildPassRate,
269
+ errorCodes,
270
+ warnings,
271
+ fixCycles,
272
+ moduleTimings,
273
+ },
274
+ responseLength: fresh.responseLength,
275
+ skills,
276
+ skillSearches,
277
+ steps,
278
+ subagents,
279
+ planning,
280
+ };
281
+ }
282
+ export { mergeChildMetrics };
@@ -1,4 +1,4 @@
1
- import type { MessageEntry, ToolCallEntry, StepData } from "./metrics-types.js";
1
+ import type { MessageEntry, ToolCallEntry, StepData } from "../types.js";
2
2
  declare function buildSteps(maps: {
3
3
  messageMap: Map<string, MessageEntry>;
4
4
  toolCallMap: Map<string, ToolCallEntry>;
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { getEventsDir } from "./dirs.js";
3
+ import { getEventsDir } from "../dirs.js";
4
4
  function buildSteps(maps, contentMaps) {
5
5
  const sorted = [...maps.messageMap.values()]
6
6
  .filter(m => m.finish && m.tokens.total > 0)