@obsunified/collector 2.1.1 → 2.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/dist/{chunk-XKQTVIRE.js → chunk-ZSF5XP3B.js} +8 -2
- package/dist/chunk-ZSF5XP3B.js.map +1 -0
- package/dist/index.d.ts +359 -2
- package/dist/index.js +5000 -334
- package/dist/index.js.map +1 -1
- package/dist/{openai-FJYOQ7WO.js → openai-GSJZGFGL.js} +2 -2
- package/package.json +2 -2
- package/src/__tests__/autosre-closed-loop.test.ts +2222 -0
- package/src/index.ts +43 -0
- package/src/lib/analyses-runner.ts +46 -0
- package/src/lib/autosre-change-correlation.ts +807 -0
- package/src/lib/autosre-findings.ts +763 -0
- package/src/lib/autosre-ontology-cache.test.ts +78 -0
- package/src/lib/autosre-ontology.ts +938 -0
- package/src/lib/autosre-remediation-gate.ts +2127 -0
- package/src/lib/eval-cases-store.ts +63 -1
- package/src/lib/evidence-references.ts +6 -0
- package/src/lib/housekeeping.test.ts +150 -0
- package/src/lib/housekeeping.ts +109 -1
- package/src/lib/store/ingest.ts +46 -6
- package/src/migrations/050_autosre_causal_ontology.sql +216 -0
- package/src/migrations/051_incident_active_dedup_unique.sql +31 -0
- package/src/migrations/alert-state-notified.test.ts +3 -3
- package/src/migrations/parity.test.ts +4 -0
- package/src/migrations/project-scoped-keys.test.ts +1 -1
- package/src/migrations-postgres/050_autosre_causal_ontology.sql +216 -0
- package/src/migrations-postgres/051_incident_active_dedup_unique.sql +31 -0
- package/src/plugins/alerts-evaluator.ts +29 -1
- package/src/plugins/autosre-routes.ts +1134 -0
- package/src/plugins/connected-routes/manifest.ts +10 -1
- package/src/plugins/connected-routes.ts +241 -0
- package/src/plugins/default-span-enrichment.ts +13 -1
- package/src/plugins/eval-cases-routes.ts +57 -2
- package/src/plugins/evidence-retrieval-routes.test.ts +32 -0
- package/src/plugins/evidence-retrieval-routes.ts +643 -5
- package/dist/chunk-XKQTVIRE.js.map +0 -1
- /package/dist/{openai-FJYOQ7WO.js.map → openai-GSJZGFGL.js.map} +0 -0
|
@@ -12,6 +12,23 @@ import type {
|
|
|
12
12
|
TelemetryTraceDetailResponse,
|
|
13
13
|
} from "@obsunified/types";
|
|
14
14
|
import type { CollectorPlugin } from "../framework/collector";
|
|
15
|
+
import {
|
|
16
|
+
computeChangeBlastRadius,
|
|
17
|
+
getChangeEvent,
|
|
18
|
+
listChangeEvents,
|
|
19
|
+
} from "../lib/autosre-change-correlation";
|
|
20
|
+
import {
|
|
21
|
+
getInvestigationFinding,
|
|
22
|
+
searchInvestigationFindings,
|
|
23
|
+
} from "../lib/autosre-findings";
|
|
24
|
+
import {
|
|
25
|
+
getOntologyGraph,
|
|
26
|
+
loadTraceFaultLocusFromDb,
|
|
27
|
+
} from "../lib/autosre-ontology";
|
|
28
|
+
import {
|
|
29
|
+
getIncidentInvestigation,
|
|
30
|
+
listRemediationProposals,
|
|
31
|
+
} from "../lib/autosre-remediation-gate";
|
|
15
32
|
import type {
|
|
16
33
|
ActionRef,
|
|
17
34
|
EntityManifestExtended,
|
|
@@ -75,7 +92,7 @@ interface ReplayEvidenceRow {
|
|
|
75
92
|
|
|
76
93
|
interface AICallEvidenceRow {
|
|
77
94
|
call_id: string;
|
|
78
|
-
trace_id: string
|
|
95
|
+
trace_id: string;
|
|
79
96
|
span_id?: string | null;
|
|
80
97
|
service_name?: string | null;
|
|
81
98
|
model_name: string;
|
|
@@ -105,6 +122,7 @@ const DEFAULT_PROFILE_FRAME_LIMIT = 50;
|
|
|
105
122
|
const MAX_PROFILE_FRAME_LIMIT = 200;
|
|
106
123
|
const EVIDENCE_INTENTS = new Set<EvidenceBundleIntent>([
|
|
107
124
|
"debug_failure",
|
|
125
|
+
"triage_incident",
|
|
108
126
|
"explain_latency",
|
|
109
127
|
"explain_cost",
|
|
110
128
|
"inspect_agent_run",
|
|
@@ -1460,6 +1478,280 @@ const buildActionGraphBundle = (
|
|
|
1460
1478
|
return bundle;
|
|
1461
1479
|
};
|
|
1462
1480
|
|
|
1481
|
+
/**
|
|
1482
|
+
* Resolves the trace to enrich an anchor with. Prefers the anchor's own
|
|
1483
|
+
* symptom entity (a trace id, or the trace owning a span); only when that
|
|
1484
|
+
* cannot be resolved does it fall back to the most recent error span in the
|
|
1485
|
+
* environment (a heuristic: it may belong to an unrelated request).
|
|
1486
|
+
*/
|
|
1487
|
+
const resolveAnchorTraceId = async (
|
|
1488
|
+
db: SqlDb,
|
|
1489
|
+
projectId: string,
|
|
1490
|
+
environment: string,
|
|
1491
|
+
symptom?: { kind: string; id: string } | null,
|
|
1492
|
+
): Promise<string | null> => {
|
|
1493
|
+
if (symptom?.id) {
|
|
1494
|
+
if (symptom.kind === "trace") {
|
|
1495
|
+
const row = await db
|
|
1496
|
+
.prepare(
|
|
1497
|
+
`SELECT trace_id FROM telemetry_spans
|
|
1498
|
+
WHERE project_id = ? AND trace_id = ? LIMIT 1`,
|
|
1499
|
+
)
|
|
1500
|
+
.bind(projectId, symptom.id)
|
|
1501
|
+
.first<{ trace_id: string }>();
|
|
1502
|
+
if (row?.trace_id) return row.trace_id;
|
|
1503
|
+
} else if (symptom.kind === "span") {
|
|
1504
|
+
const row = await db
|
|
1505
|
+
.prepare(
|
|
1506
|
+
`SELECT trace_id FROM telemetry_spans
|
|
1507
|
+
WHERE project_id = ? AND span_id = ? LIMIT 1`,
|
|
1508
|
+
)
|
|
1509
|
+
.bind(projectId, symptom.id)
|
|
1510
|
+
.first<{ trace_id: string }>();
|
|
1511
|
+
if (row?.trace_id) return row.trace_id;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
const recent = await db
|
|
1515
|
+
.prepare(
|
|
1516
|
+
`SELECT trace_id FROM telemetry_spans
|
|
1517
|
+
WHERE project_id = ? AND environment = ? AND status_code = 2
|
|
1518
|
+
ORDER BY start_time DESC LIMIT 1`,
|
|
1519
|
+
)
|
|
1520
|
+
.bind(projectId, environment)
|
|
1521
|
+
.first<{ trace_id: string }>();
|
|
1522
|
+
return recent?.trace_id ?? null;
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
const attachAutoSreTriageContext = async (
|
|
1526
|
+
db: SqlDb,
|
|
1527
|
+
projectId: string,
|
|
1528
|
+
traceId: string | null,
|
|
1529
|
+
bundle: EvidenceBundle,
|
|
1530
|
+
logger?: { warn: (message: string, meta?: Record<string, unknown>) => void },
|
|
1531
|
+
): Promise<EvidenceBundle> => {
|
|
1532
|
+
try {
|
|
1533
|
+
const faultLocus = traceId
|
|
1534
|
+
? await loadTraceFaultLocusFromDb(db, projectId, traceId)
|
|
1535
|
+
: null;
|
|
1536
|
+
const environment = (
|
|
1537
|
+
faultLocus?.faultLocusSpan?.environment ??
|
|
1538
|
+
bundle.environment ??
|
|
1539
|
+
"default"
|
|
1540
|
+
).toLowerCase();
|
|
1541
|
+
const locusService = faultLocus?.faultLocusSpan?.service_name ?? null;
|
|
1542
|
+
const allRecentChanges = await listChangeEvents(db, {
|
|
1543
|
+
projectId,
|
|
1544
|
+
environment,
|
|
1545
|
+
limit: locusService ? 25 : 5,
|
|
1546
|
+
});
|
|
1547
|
+
// When the fault locus service is known, only changes that target it
|
|
1548
|
+
// (or its node) are candidates; unrelated deploys are noise.
|
|
1549
|
+
const recentChanges = locusService
|
|
1550
|
+
? allRecentChanges
|
|
1551
|
+
.filter(
|
|
1552
|
+
(ch) =>
|
|
1553
|
+
ch.serviceName === locusService ||
|
|
1554
|
+
(faultLocus?.faultLocusNodeId &&
|
|
1555
|
+
ch.targetNodeId === faultLocus.faultLocusNodeId),
|
|
1556
|
+
)
|
|
1557
|
+
.slice(0, 5)
|
|
1558
|
+
: allRecentChanges;
|
|
1559
|
+
const blastRadii = (
|
|
1560
|
+
await Promise.all(
|
|
1561
|
+
recentChanges
|
|
1562
|
+
.slice(0, 3)
|
|
1563
|
+
.map((ch) =>
|
|
1564
|
+
computeChangeBlastRadius(db, projectId, ch.id, { environment }),
|
|
1565
|
+
),
|
|
1566
|
+
)
|
|
1567
|
+
).filter((br): br is NonNullable<typeof br> => Boolean(br));
|
|
1568
|
+
|
|
1569
|
+
const candidateChanges = blastRadii.map((br) => ({
|
|
1570
|
+
changeId: br.change.id,
|
|
1571
|
+
kind: br.change.kind,
|
|
1572
|
+
version: br.change.version,
|
|
1573
|
+
previousVersion: br.change.previousVersion,
|
|
1574
|
+
occurredAt: br.change.occurredAt,
|
|
1575
|
+
cohortVerdict: br.cohortVerdict,
|
|
1576
|
+
causalProofTier: br.causalProofTier,
|
|
1577
|
+
rateNew: br.newCohort.errorRate,
|
|
1578
|
+
rateOld: br.oldCohort.errorRate,
|
|
1579
|
+
sampleNew: br.newCohort.requestCount,
|
|
1580
|
+
sampleOld: br.oldCohort.requestCount,
|
|
1581
|
+
overlappingCodePaths: br.overlappingCodePaths,
|
|
1582
|
+
}));
|
|
1583
|
+
const contentionCandidates = blastRadii
|
|
1584
|
+
.flatMap((br) => br.sharedResourceAggressors)
|
|
1585
|
+
.map((ag) => ({
|
|
1586
|
+
aggressorTraceId: ag.aggressorTraceId,
|
|
1587
|
+
aggressorSpanId: ag.aggressorSpanId,
|
|
1588
|
+
aggressorServiceName: ag.aggressorServiceName,
|
|
1589
|
+
sharedTargetNodeId: ag.sharedTargetNodeId,
|
|
1590
|
+
relationshipCertainty: ag.relationshipCertainty,
|
|
1591
|
+
totalResourceTimeMs: ag.totalResourceTimeMs,
|
|
1592
|
+
}));
|
|
1593
|
+
let priorFindingsList = await searchInvestigationFindings(db, {
|
|
1594
|
+
projectId,
|
|
1595
|
+
environment,
|
|
1596
|
+
rootCauseEntityId:
|
|
1597
|
+
faultLocus?.faultLocusNodeId ??
|
|
1598
|
+
faultLocus?.faultLocusSpan?.span_id ??
|
|
1599
|
+
undefined,
|
|
1600
|
+
limit: 5,
|
|
1601
|
+
});
|
|
1602
|
+
if (priorFindingsList.length === 0 && faultLocus?.faultLocusSpan?.span_id) {
|
|
1603
|
+
priorFindingsList = await searchInvestigationFindings(db, {
|
|
1604
|
+
projectId,
|
|
1605
|
+
environment,
|
|
1606
|
+
rootCauseEntityId: faultLocus.faultLocusSpan.span_id,
|
|
1607
|
+
limit: 5,
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
// No unfiltered fallback: unrelated findings would read as relevant
|
|
1611
|
+
// prior art. An empty list is the honest answer.
|
|
1612
|
+
const priorFindings = priorFindingsList.map((f) => ({
|
|
1613
|
+
findingId: f.id,
|
|
1614
|
+
title: f.title,
|
|
1615
|
+
causalProofTier: f.causalProofTier,
|
|
1616
|
+
causalVerificationStatus: f.causalVerificationStatus,
|
|
1617
|
+
recoveryStatus: f.recoveryStatus,
|
|
1618
|
+
contributingChangeId: f.contributingChangeId,
|
|
1619
|
+
occurredAt: f.createdAt,
|
|
1620
|
+
}));
|
|
1621
|
+
|
|
1622
|
+
for (const br of blastRadii) {
|
|
1623
|
+
const changeId = br.change.id;
|
|
1624
|
+
const changeRoute = `#/changes/${encodeURIComponent(changeId)}`;
|
|
1625
|
+
bundle.evidenceReferences.push({
|
|
1626
|
+
evidenceId: `change:${changeId}`,
|
|
1627
|
+
entityKind: "change",
|
|
1628
|
+
entityId: changeId,
|
|
1629
|
+
route: changeRoute,
|
|
1630
|
+
source: br.overlappingCodePaths.length
|
|
1631
|
+
? "stack_frame_overlap"
|
|
1632
|
+
: "cohort_comparison",
|
|
1633
|
+
confidence: br.cohortVerdict === "isolated_to_new_version" ? 0.9 : 0.65,
|
|
1634
|
+
relationshipCertainty: br.relationshipCertainty,
|
|
1635
|
+
causalProofTier: br.causalProofTier,
|
|
1636
|
+
reason: br.summary,
|
|
1637
|
+
citations: [
|
|
1638
|
+
{
|
|
1639
|
+
label: `change ${changeId} (${br.change.version ?? br.change.kind})`,
|
|
1640
|
+
entityKind: "change",
|
|
1641
|
+
entityId: changeId,
|
|
1642
|
+
route: changeRoute,
|
|
1643
|
+
},
|
|
1644
|
+
],
|
|
1645
|
+
suggestedNextPivots: [
|
|
1646
|
+
{
|
|
1647
|
+
label: "Inspect change blast radius",
|
|
1648
|
+
entityKind: "change",
|
|
1649
|
+
entityId: changeId,
|
|
1650
|
+
route: changeRoute,
|
|
1651
|
+
reason: "Inspect traffic-normalized cohort lift and contention.",
|
|
1652
|
+
},
|
|
1653
|
+
],
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
for (const finding of priorFindingsList) {
|
|
1658
|
+
const findingRoute = `#/findings/${encodeURIComponent(finding.id)}`;
|
|
1659
|
+
bundle.evidenceReferences.push({
|
|
1660
|
+
evidenceId: `finding:${finding.id}`,
|
|
1661
|
+
entityKind: "finding",
|
|
1662
|
+
entityId: finding.id,
|
|
1663
|
+
route: findingRoute,
|
|
1664
|
+
source: "incident_memory_fingerprint",
|
|
1665
|
+
confidence: finding.confidence,
|
|
1666
|
+
relationshipCertainty: "exact",
|
|
1667
|
+
causalProofTier: finding.causalProofTier,
|
|
1668
|
+
reason: `Prior finding (${finding.causalProofTier}): ${finding.title}`,
|
|
1669
|
+
citations: [
|
|
1670
|
+
{
|
|
1671
|
+
label: finding.title,
|
|
1672
|
+
entityKind: "finding",
|
|
1673
|
+
entityId: finding.id,
|
|
1674
|
+
route: findingRoute,
|
|
1675
|
+
},
|
|
1676
|
+
],
|
|
1677
|
+
suggestedNextPivots: [
|
|
1678
|
+
{
|
|
1679
|
+
label: "Open prior finding",
|
|
1680
|
+
entityKind: "finding",
|
|
1681
|
+
entityId: finding.id,
|
|
1682
|
+
route: findingRoute,
|
|
1683
|
+
reason: "Search incident memory for similar recurring findings.",
|
|
1684
|
+
},
|
|
1685
|
+
],
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
let isBlackoutSuspected = false;
|
|
1690
|
+
if (faultLocus?.faultLocusNodeId) {
|
|
1691
|
+
try {
|
|
1692
|
+
const graph = await getOntologyGraph(db, { projectId, environment });
|
|
1693
|
+
isBlackoutSuspected = Boolean(
|
|
1694
|
+
graph.nodes.find((n) => n.id === faultLocus.faultLocusNodeId)
|
|
1695
|
+
?.isBlackoutSuspected,
|
|
1696
|
+
);
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
logger?.warn("[evidence-retrieval] blackout check failed", {
|
|
1699
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
bundle.autosre = {
|
|
1705
|
+
faultLocus: faultLocus?.faultLocusNodeId
|
|
1706
|
+
? {
|
|
1707
|
+
nodeId: faultLocus.faultLocusNodeId,
|
|
1708
|
+
spanId: faultLocus.faultLocusSpan?.span_id,
|
|
1709
|
+
relationshipCertainty: faultLocus.relationshipCertainty,
|
|
1710
|
+
isBlackoutSuspected,
|
|
1711
|
+
}
|
|
1712
|
+
: undefined,
|
|
1713
|
+
candidateChanges,
|
|
1714
|
+
contentionCandidates,
|
|
1715
|
+
priorFindings,
|
|
1716
|
+
};
|
|
1717
|
+
if (bundle.budget) {
|
|
1718
|
+
// Keep the autosre section inside the caller's token budget by
|
|
1719
|
+
// dropping the least-relevant tail entries (and their evidence refs).
|
|
1720
|
+
const autosre = bundle.autosre;
|
|
1721
|
+
const target = bundle.budget.targetTokens ?? Number.POSITIVE_INFINITY;
|
|
1722
|
+
const dropRef = (evidenceId: string) => {
|
|
1723
|
+
bundle.evidenceReferences = bundle.evidenceReferences.filter(
|
|
1724
|
+
(ref) => ref.evidenceId !== evidenceId,
|
|
1725
|
+
);
|
|
1726
|
+
};
|
|
1727
|
+
while (estimateTokens(bundle) > target) {
|
|
1728
|
+
if (autosre.priorFindings.length > 0) {
|
|
1729
|
+
const dropped = autosre.priorFindings.pop();
|
|
1730
|
+
if (dropped) dropRef(`finding:${dropped.findingId}`);
|
|
1731
|
+
} else if (autosre.contentionCandidates.length > 0) {
|
|
1732
|
+
autosre.contentionCandidates.pop();
|
|
1733
|
+
} else if (autosre.candidateChanges.length > 0) {
|
|
1734
|
+
const dropped = autosre.candidateChanges.pop();
|
|
1735
|
+
if (dropped) dropRef(`change:${dropped.changeId}`);
|
|
1736
|
+
} else {
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
bundle.budget = {
|
|
1741
|
+
...bundle.budget,
|
|
1742
|
+
estimatedTokens: estimateTokens(bundle),
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
// Gracefully degrade if AutoSRE tables are absent in minimal test setups.
|
|
1747
|
+
logger?.warn("[evidence-retrieval] autosre triage enrichment skipped", {
|
|
1748
|
+
trace_id: traceId,
|
|
1749
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
return bundle;
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1463
1755
|
const parseBundleRequest = async (request: Request) => {
|
|
1464
1756
|
try {
|
|
1465
1757
|
const body = await request.json();
|
|
@@ -1490,11 +1782,17 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1490
1782
|
);
|
|
1491
1783
|
}
|
|
1492
1784
|
|
|
1785
|
+
const isAutoSreAnchor =
|
|
1786
|
+
anchor.entityKind === "incident" ||
|
|
1787
|
+
anchor.entityKind === "change" ||
|
|
1788
|
+
anchor.entityKind === "finding";
|
|
1493
1789
|
const intent =
|
|
1494
1790
|
typeof body.intent === "string" &&
|
|
1495
1791
|
EVIDENCE_INTENTS.has(body.intent as EvidenceBundleIntent)
|
|
1496
1792
|
? (body.intent as EvidenceBundleIntent)
|
|
1497
|
-
:
|
|
1793
|
+
: isAutoSreAnchor
|
|
1794
|
+
? "triage_incident"
|
|
1795
|
+
: "debug_failure";
|
|
1498
1796
|
const budget = body.budget as Record<string, unknown> | undefined;
|
|
1499
1797
|
const targetTokens = clampInt(
|
|
1500
1798
|
budget?.targetTokens,
|
|
@@ -1550,7 +1848,7 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1550
1848
|
all.findIndex((candidate) => candidate.id === profile.id) ===
|
|
1551
1849
|
index,
|
|
1552
1850
|
);
|
|
1553
|
-
|
|
1851
|
+
let bundle = buildActionGraphBundle(
|
|
1554
1852
|
projectId,
|
|
1555
1853
|
anchor.entityKind,
|
|
1556
1854
|
anchor.entityId,
|
|
@@ -1560,15 +1858,346 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1560
1858
|
targetTokens,
|
|
1561
1859
|
detailLevel,
|
|
1562
1860
|
);
|
|
1861
|
+
if (intent === "triage_incident") {
|
|
1862
|
+
bundle = await attachAutoSreTriageContext(
|
|
1863
|
+
db,
|
|
1864
|
+
projectId,
|
|
1865
|
+
traceIds[0] ?? null,
|
|
1866
|
+
bundle,
|
|
1867
|
+
runtime.logger,
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1563
1870
|
return c.json(await materializeBundle(db, projectId, bundle));
|
|
1564
1871
|
}
|
|
1565
1872
|
|
|
1873
|
+
if (anchor.entityKind === "incident") {
|
|
1874
|
+
const incident = await getIncidentInvestigation(
|
|
1875
|
+
db,
|
|
1876
|
+
projectId,
|
|
1877
|
+
anchor.entityId,
|
|
1878
|
+
);
|
|
1879
|
+
if (!incident) {
|
|
1880
|
+
return c.json(
|
|
1881
|
+
{ error: "Not Found", message: "Incident not found" },
|
|
1882
|
+
404,
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
const anchorTraceId = await resolveAnchorTraceId(
|
|
1886
|
+
db,
|
|
1887
|
+
projectId,
|
|
1888
|
+
incident.environment,
|
|
1889
|
+
{ kind: incident.symptomAnchorKind, id: incident.symptomAnchorId },
|
|
1890
|
+
);
|
|
1891
|
+
|
|
1892
|
+
const incRoute = `#/incidents/${encodeURIComponent(incident.id)}`;
|
|
1893
|
+
const baseBundle: EvidenceBundle = {
|
|
1894
|
+
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
1895
|
+
intent,
|
|
1896
|
+
environment: incident.environment,
|
|
1897
|
+
incidentId: incident.id,
|
|
1898
|
+
symptomFingerprint: incident.symptomFingerprint ?? undefined,
|
|
1899
|
+
anchor: { entityKind: "incident", entityId: incident.id },
|
|
1900
|
+
budget: { targetTokens, detailLevel },
|
|
1901
|
+
summary: `Incident ${incident.id} (${incident.status}) on ${incident.targetNodeId} in environment ${incident.environment}.`,
|
|
1902
|
+
derivedSummaries: [
|
|
1903
|
+
{
|
|
1904
|
+
title: `Incident ${incident.id}`,
|
|
1905
|
+
reason: `Trigger ${incident.symptomAnchorKind}:${incident.symptomAnchorId} on ${incident.targetNodeId} (status=${incident.status}).`,
|
|
1906
|
+
confidence: 1.0,
|
|
1907
|
+
evidenceIds: [`incident:${incident.id}`],
|
|
1908
|
+
retrievalRefIds: [],
|
|
1909
|
+
},
|
|
1910
|
+
],
|
|
1911
|
+
findings: [],
|
|
1912
|
+
compactions: [],
|
|
1913
|
+
evidenceReferences: [
|
|
1914
|
+
{
|
|
1915
|
+
evidenceId: `incident:${incident.id}`,
|
|
1916
|
+
entityKind: "incident",
|
|
1917
|
+
entityId: incident.id,
|
|
1918
|
+
route: incRoute,
|
|
1919
|
+
source: "incident_investigations",
|
|
1920
|
+
confidence: 1.0,
|
|
1921
|
+
relationshipCertainty: "exact",
|
|
1922
|
+
reason: `Incident triggered by ${incident.symptomAnchorKind}:${incident.symptomAnchorId}.`,
|
|
1923
|
+
citations: [
|
|
1924
|
+
{
|
|
1925
|
+
label: incident.targetNodeId,
|
|
1926
|
+
entityKind: "incident",
|
|
1927
|
+
entityId: incident.id,
|
|
1928
|
+
route: incRoute,
|
|
1929
|
+
},
|
|
1930
|
+
],
|
|
1931
|
+
suggestedNextPivots: [
|
|
1932
|
+
{
|
|
1933
|
+
label: "Open incident",
|
|
1934
|
+
entityKind: "incident",
|
|
1935
|
+
entityId: incident.id,
|
|
1936
|
+
route: incRoute,
|
|
1937
|
+
reason:
|
|
1938
|
+
"Inspect incident investigation and candidate changes.",
|
|
1939
|
+
},
|
|
1940
|
+
],
|
|
1941
|
+
},
|
|
1942
|
+
],
|
|
1943
|
+
retrievalRefs: [],
|
|
1944
|
+
suggestedNextPivots: [
|
|
1945
|
+
{
|
|
1946
|
+
label: "Open incident",
|
|
1947
|
+
entityKind: "incident",
|
|
1948
|
+
entityId: incident.id,
|
|
1949
|
+
route: incRoute,
|
|
1950
|
+
reason: "Inspect incident investigation and candidate changes.",
|
|
1951
|
+
},
|
|
1952
|
+
],
|
|
1953
|
+
};
|
|
1954
|
+
const enriched = await attachAutoSreTriageContext(
|
|
1955
|
+
db,
|
|
1956
|
+
projectId,
|
|
1957
|
+
anchorTraceId,
|
|
1958
|
+
baseBundle,
|
|
1959
|
+
runtime.logger,
|
|
1960
|
+
);
|
|
1961
|
+
return c.json(await materializeBundle(db, projectId, enriched));
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (anchor.entityKind === "change") {
|
|
1965
|
+
const change = await getChangeEvent(db, projectId, anchor.entityId);
|
|
1966
|
+
if (!change) {
|
|
1967
|
+
return c.json(
|
|
1968
|
+
{ error: "Not Found", message: "Change event not found" },
|
|
1969
|
+
404,
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
const blastRadius = await computeChangeBlastRadius(
|
|
1973
|
+
db,
|
|
1974
|
+
projectId,
|
|
1975
|
+
change.id,
|
|
1976
|
+
{ environment: change.environment },
|
|
1977
|
+
);
|
|
1978
|
+
const anchorTraceId = await resolveAnchorTraceId(
|
|
1979
|
+
db,
|
|
1980
|
+
projectId,
|
|
1981
|
+
change.environment,
|
|
1982
|
+
null,
|
|
1983
|
+
);
|
|
1984
|
+
|
|
1985
|
+
const changeRoute = `#/changes/${encodeURIComponent(change.id)}`;
|
|
1986
|
+
const summary =
|
|
1987
|
+
blastRadius?.summary ??
|
|
1988
|
+
`Change ${change.id} (${change.kind} ${change.version ?? ""}) in ${change.environment}: ${change.summary}`;
|
|
1989
|
+
const baseBundle: EvidenceBundle = {
|
|
1990
|
+
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
1991
|
+
intent,
|
|
1992
|
+
environment: change.environment,
|
|
1993
|
+
anchor: { entityKind: "change", entityId: change.id },
|
|
1994
|
+
budget: { targetTokens, detailLevel },
|
|
1995
|
+
summary,
|
|
1996
|
+
derivedSummaries: [
|
|
1997
|
+
{
|
|
1998
|
+
title: `Change ${change.id} (${change.kind})`,
|
|
1999
|
+
reason: summary,
|
|
2000
|
+
confidence:
|
|
2001
|
+
blastRadius?.cohortVerdict === "isolated_to_new_version"
|
|
2002
|
+
? 0.9
|
|
2003
|
+
: 0.7,
|
|
2004
|
+
evidenceIds: [`change:${change.id}`],
|
|
2005
|
+
retrievalRefIds: [],
|
|
2006
|
+
},
|
|
2007
|
+
],
|
|
2008
|
+
findings: blastRadius
|
|
2009
|
+
? [
|
|
2010
|
+
{
|
|
2011
|
+
title: `Blast radius (${blastRadius.cohortVerdict})`,
|
|
2012
|
+
reason: blastRadius.summary,
|
|
2013
|
+
confidence:
|
|
2014
|
+
blastRadius.cohortVerdict === "isolated_to_new_version"
|
|
2015
|
+
? 0.9
|
|
2016
|
+
: 0.65,
|
|
2017
|
+
evidenceIds: [`change:${change.id}`],
|
|
2018
|
+
retrievalRefIds: [],
|
|
2019
|
+
},
|
|
2020
|
+
]
|
|
2021
|
+
: [],
|
|
2022
|
+
compactions: [],
|
|
2023
|
+
evidenceReferences: [
|
|
2024
|
+
{
|
|
2025
|
+
evidenceId: `change:${change.id}`,
|
|
2026
|
+
entityKind: "change",
|
|
2027
|
+
entityId: change.id,
|
|
2028
|
+
route: changeRoute,
|
|
2029
|
+
source: "change_events",
|
|
2030
|
+
confidence: 1.0,
|
|
2031
|
+
relationshipCertainty:
|
|
2032
|
+
blastRadius?.relationshipCertainty ??
|
|
2033
|
+
change.relationshipCertainty,
|
|
2034
|
+
causalProofTier: blastRadius?.causalProofTier,
|
|
2035
|
+
reason: summary,
|
|
2036
|
+
citations: [
|
|
2037
|
+
{
|
|
2038
|
+
label: `${change.kind} ${change.version ?? change.id}`,
|
|
2039
|
+
entityKind: "change",
|
|
2040
|
+
entityId: change.id,
|
|
2041
|
+
route: changeRoute,
|
|
2042
|
+
},
|
|
2043
|
+
],
|
|
2044
|
+
suggestedNextPivots: [
|
|
2045
|
+
{
|
|
2046
|
+
label: "Inspect change blast radius",
|
|
2047
|
+
entityKind: "change",
|
|
2048
|
+
entityId: change.id,
|
|
2049
|
+
route: changeRoute,
|
|
2050
|
+
reason:
|
|
2051
|
+
"Inspect traffic-normalized cohort lift and contention.",
|
|
2052
|
+
},
|
|
2053
|
+
],
|
|
2054
|
+
},
|
|
2055
|
+
],
|
|
2056
|
+
retrievalRefs: [],
|
|
2057
|
+
suggestedNextPivots: [
|
|
2058
|
+
{
|
|
2059
|
+
label: "Inspect change blast radius",
|
|
2060
|
+
entityKind: "change",
|
|
2061
|
+
entityId: change.id,
|
|
2062
|
+
route: changeRoute,
|
|
2063
|
+
reason: "Inspect traffic-normalized cohort lift and contention.",
|
|
2064
|
+
},
|
|
2065
|
+
],
|
|
2066
|
+
};
|
|
2067
|
+
const enriched = await attachAutoSreTriageContext(
|
|
2068
|
+
db,
|
|
2069
|
+
projectId,
|
|
2070
|
+
anchorTraceId,
|
|
2071
|
+
baseBundle,
|
|
2072
|
+
runtime.logger,
|
|
2073
|
+
);
|
|
2074
|
+
return c.json(await materializeBundle(db, projectId, enriched));
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
if (anchor.entityKind === "finding") {
|
|
2078
|
+
const finding = await getInvestigationFinding(
|
|
2079
|
+
db,
|
|
2080
|
+
projectId,
|
|
2081
|
+
anchor.entityId,
|
|
2082
|
+
);
|
|
2083
|
+
if (!finding) {
|
|
2084
|
+
return c.json(
|
|
2085
|
+
{ error: "Not Found", message: "Finding not found" },
|
|
2086
|
+
404,
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
const proposals = await listRemediationProposals(db, {
|
|
2090
|
+
projectId,
|
|
2091
|
+
environment: finding.environment,
|
|
2092
|
+
findingId: finding.id,
|
|
2093
|
+
limit: 10,
|
|
2094
|
+
});
|
|
2095
|
+
const anchorTraceId = await resolveAnchorTraceId(
|
|
2096
|
+
db,
|
|
2097
|
+
projectId,
|
|
2098
|
+
finding.environment,
|
|
2099
|
+
{ kind: finding.symptomEntityKind, id: finding.symptomEntityId },
|
|
2100
|
+
);
|
|
2101
|
+
|
|
2102
|
+
const findingRoute = `#/findings/${encodeURIComponent(finding.id)}`;
|
|
2103
|
+
const summary = `${finding.title} (${finding.causalProofTier}, verification=${finding.causalVerificationStatus}, recovery=${finding.recoveryStatus}): ${finding.summary}`;
|
|
2104
|
+
const baseBundle: EvidenceBundle = {
|
|
2105
|
+
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
2106
|
+
intent,
|
|
2107
|
+
environment: finding.environment,
|
|
2108
|
+
incidentId: finding.incidentId ?? undefined,
|
|
2109
|
+
symptomFingerprint: finding.fingerprint,
|
|
2110
|
+
anchor: { entityKind: "finding", entityId: finding.id },
|
|
2111
|
+
budget: { targetTokens, detailLevel },
|
|
2112
|
+
summary,
|
|
2113
|
+
derivedSummaries: [
|
|
2114
|
+
{
|
|
2115
|
+
title: finding.title,
|
|
2116
|
+
reason: summary,
|
|
2117
|
+
confidence: finding.confidence,
|
|
2118
|
+
evidenceIds: [`finding:${finding.id}`],
|
|
2119
|
+
retrievalRefIds: [],
|
|
2120
|
+
},
|
|
2121
|
+
],
|
|
2122
|
+
findings: [
|
|
2123
|
+
{
|
|
2124
|
+
title: finding.title,
|
|
2125
|
+
reason: finding.summary,
|
|
2126
|
+
confidence: finding.confidence,
|
|
2127
|
+
evidenceIds: [
|
|
2128
|
+
`finding:${finding.id}`,
|
|
2129
|
+
...finding.citedEvidenceIds,
|
|
2130
|
+
],
|
|
2131
|
+
retrievalRefIds: [],
|
|
2132
|
+
},
|
|
2133
|
+
...proposals.map((p) => ({
|
|
2134
|
+
title: `Remediation proposal ${p.id} (${p.actionKind})`,
|
|
2135
|
+
reason: `status=${p.status}, autonomy=${p.effectiveAutonomy} — ${p.policyDecisionReason}`,
|
|
2136
|
+
confidence: finding.confidence,
|
|
2137
|
+
evidenceIds: [`finding:${finding.id}`],
|
|
2138
|
+
retrievalRefIds: [],
|
|
2139
|
+
})),
|
|
2140
|
+
],
|
|
2141
|
+
compactions: [],
|
|
2142
|
+
evidenceReferences: [
|
|
2143
|
+
{
|
|
2144
|
+
evidenceId: `finding:${finding.id}`,
|
|
2145
|
+
entityKind: "finding",
|
|
2146
|
+
entityId: finding.id,
|
|
2147
|
+
route: findingRoute,
|
|
2148
|
+
source: "investigation_findings",
|
|
2149
|
+
confidence: finding.confidence,
|
|
2150
|
+
relationshipCertainty: "exact",
|
|
2151
|
+
causalProofTier: finding.causalProofTier,
|
|
2152
|
+
reason: summary,
|
|
2153
|
+
citations: [
|
|
2154
|
+
{
|
|
2155
|
+
label: finding.title,
|
|
2156
|
+
entityKind: "finding",
|
|
2157
|
+
entityId: finding.id,
|
|
2158
|
+
route: findingRoute,
|
|
2159
|
+
},
|
|
2160
|
+
],
|
|
2161
|
+
suggestedNextPivots: [
|
|
2162
|
+
{
|
|
2163
|
+
label: "Open finding",
|
|
2164
|
+
entityKind: "finding",
|
|
2165
|
+
entityId: finding.id,
|
|
2166
|
+
route: findingRoute,
|
|
2167
|
+
reason:
|
|
2168
|
+
"Inspect causal chain, counterfactual verification, and remediation proposals.",
|
|
2169
|
+
},
|
|
2170
|
+
],
|
|
2171
|
+
},
|
|
2172
|
+
],
|
|
2173
|
+
retrievalRefs: [],
|
|
2174
|
+
suggestedNextPivots: [
|
|
2175
|
+
{
|
|
2176
|
+
label: "Open finding",
|
|
2177
|
+
entityKind: "finding",
|
|
2178
|
+
entityId: finding.id,
|
|
2179
|
+
route: findingRoute,
|
|
2180
|
+
reason:
|
|
2181
|
+
"Inspect causal chain, counterfactual verification, and remediation proposals.",
|
|
2182
|
+
},
|
|
2183
|
+
],
|
|
2184
|
+
};
|
|
2185
|
+
const enriched = await attachAutoSreTriageContext(
|
|
2186
|
+
db,
|
|
2187
|
+
projectId,
|
|
2188
|
+
anchorTraceId,
|
|
2189
|
+
baseBundle,
|
|
2190
|
+
runtime.logger,
|
|
2191
|
+
);
|
|
2192
|
+
return c.json(await materializeBundle(db, projectId, enriched));
|
|
2193
|
+
}
|
|
2194
|
+
|
|
1566
2195
|
if (anchor.entityKind !== "trace") {
|
|
1567
2196
|
return c.json(
|
|
1568
2197
|
{
|
|
1569
2198
|
error: "Bad Request",
|
|
1570
2199
|
message:
|
|
1571
|
-
"Supported evidence bundle anchors are trace, action, agent_run, and
|
|
2200
|
+
"Supported evidence bundle anchors are trace, action, agent_run, tool_call, change, finding, and incident.",
|
|
1572
2201
|
},
|
|
1573
2202
|
400,
|
|
1574
2203
|
);
|
|
@@ -1588,7 +2217,7 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1588
2217
|
limit: MAX_LOGS_FOR_BUNDLE,
|
|
1589
2218
|
});
|
|
1590
2219
|
const profiles = await profilesForTrace(db, projectId, traceId);
|
|
1591
|
-
|
|
2220
|
+
let bundle = await buildTraceBundle(
|
|
1592
2221
|
projectId,
|
|
1593
2222
|
detail,
|
|
1594
2223
|
logs.logs,
|
|
@@ -1598,6 +2227,15 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1598
2227
|
detailLevel,
|
|
1599
2228
|
hours,
|
|
1600
2229
|
);
|
|
2230
|
+
if (intent === "triage_incident") {
|
|
2231
|
+
bundle = await attachAutoSreTriageContext(
|
|
2232
|
+
db,
|
|
2233
|
+
projectId,
|
|
2234
|
+
traceId,
|
|
2235
|
+
bundle,
|
|
2236
|
+
runtime.logger,
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
1601
2239
|
return c.json(await materializeBundle(db, projectId, bundle));
|
|
1602
2240
|
});
|
|
1603
2241
|
|