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.
- package/README.md +108 -390
- package/dist/api.d.ts +3 -0
- package/dist/api.js +4 -0
- package/dist/index.d.ts +2 -51
- package/dist/index.js +4 -61
- package/dist/metrics/dirs.d.ts +23 -0
- package/dist/{dirs.js → metrics/dirs.js} +4 -10
- package/dist/metrics/engine/engine.d.ts +15 -0
- package/dist/{metrics-engine.js → metrics/engine/engine.js} +33 -146
- package/dist/{metrics-handlers.d.ts → metrics/engine/handlers.d.ts} +1 -1
- package/dist/{metrics-handlers.js → metrics/engine/handlers.js} +2 -2
- package/dist/metrics/engine/state.d.ts +79 -0
- package/dist/{metrics-types.js → metrics/engine/state.js} +1 -19
- package/dist/{event-logger.d.ts → metrics/eventlog/event-logger.d.ts} +0 -5
- package/dist/{event-logger.js → metrics/eventlog/event-logger.js} +2 -18
- package/dist/metrics/index.d.ts +9 -0
- package/dist/metrics/index.js +6 -0
- package/dist/metrics/runtime.d.ts +25 -0
- package/dist/metrics/runtime.js +28 -0
- package/dist/metrics/snapshot/flush.d.ts +6 -0
- package/dist/{metrics-output.js → metrics/snapshot/flush.js} +6 -290
- package/dist/metrics/snapshot/merge.d.ts +9 -0
- package/dist/metrics/snapshot/merge.js +282 -0
- package/dist/{metrics-steps.d.ts → metrics/snapshot/steps.d.ts} +1 -1
- package/dist/{metrics-steps.js → metrics/snapshot/steps.js} +1 -1
- package/dist/{metrics-types.d.ts → metrics/types.d.ts} +3 -81
- package/dist/metrics/types.js +20 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/plugin.js +49 -0
- package/dist/{logger.d.ts → shared/log.d.ts} +2 -0
- package/dist/{logger.js → shared/log.js} +9 -2
- package/package.json +12 -4
- package/dist/backfill-cli.d.ts +0 -1
- package/dist/backfill-cli.js +0 -74
- package/dist/backfill.d.ts +0 -58
- package/dist/backfill.js +0 -550
- package/dist/dirs.d.ts +0 -34
- package/dist/metrics-engine.d.ts +0 -37
- package/dist/metrics-output.d.ts +0 -11
- package/dist/summary-store.d.ts +0 -150
- package/dist/summary-store.js +0 -560
- /package/dist/{compile-analyzer.d.ts → metrics/analysis/hvigor.d.ts} +0 -0
- /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 "
|
|
4
|
-
import { createTokenUsage } from "
|
|
5
|
-
import { getMetricsDir
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
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)
|