@obsunified/collector 2.2.0 → 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/index.d.ts +53 -3
- package/dist/index.js +10459 -9696
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/autosre-closed-loop.test.ts +964 -17
- package/src/lib/analyses-runner.ts +44 -7
- package/src/lib/autosre-change-correlation.ts +39 -12
- package/src/lib/autosre-findings.ts +68 -30
- package/src/lib/autosre-ontology-cache.test.ts +78 -0
- package/src/lib/autosre-ontology.ts +52 -28
- package/src/lib/autosre-remediation-gate.ts +799 -335
- package/src/lib/housekeeping.test.ts +70 -0
- package/src/lib/housekeeping.ts +91 -83
- package/src/lib/store/ingest.ts +8 -56
- package/src/migrations/051_incident_active_dedup_unique.sql +31 -0
- package/src/migrations/alert-state-notified.test.ts +3 -3
- package/src/migrations-postgres/051_incident_active_dedup_unique.sql +31 -0
- package/src/plugins/alerts-evaluator.ts +15 -3
- package/src/plugins/autosre-routes.ts +223 -108
- package/src/plugins/connected-routes/manifest.ts +1 -4
- package/src/plugins/connected-routes.ts +48 -78
- package/src/plugins/evidence-retrieval-routes.test.ts +32 -0
- package/src/plugins/evidence-retrieval-routes.ts +372 -30
|
@@ -14,11 +14,21 @@ import type {
|
|
|
14
14
|
import type { CollectorPlugin } from "../framework/collector";
|
|
15
15
|
import {
|
|
16
16
|
computeChangeBlastRadius,
|
|
17
|
+
getChangeEvent,
|
|
17
18
|
listChangeEvents,
|
|
18
19
|
} from "../lib/autosre-change-correlation";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
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";
|
|
22
32
|
import type {
|
|
23
33
|
ActionRef,
|
|
24
34
|
EntityManifestExtended,
|
|
@@ -1468,11 +1478,56 @@ const buildActionGraphBundle = (
|
|
|
1468
1478
|
return bundle;
|
|
1469
1479
|
};
|
|
1470
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
|
+
|
|
1471
1525
|
const attachAutoSreTriageContext = async (
|
|
1472
1526
|
db: SqlDb,
|
|
1473
1527
|
projectId: string,
|
|
1474
1528
|
traceId: string | null,
|
|
1475
1529
|
bundle: EvidenceBundle,
|
|
1530
|
+
logger?: { warn: (message: string, meta?: Record<string, unknown>) => void },
|
|
1476
1531
|
): Promise<EvidenceBundle> => {
|
|
1477
1532
|
try {
|
|
1478
1533
|
const faultLocus = traceId
|
|
@@ -1483,11 +1538,24 @@ const attachAutoSreTriageContext = async (
|
|
|
1483
1538
|
bundle.environment ??
|
|
1484
1539
|
"default"
|
|
1485
1540
|
).toLowerCase();
|
|
1486
|
-
const
|
|
1541
|
+
const locusService = faultLocus?.faultLocusSpan?.service_name ?? null;
|
|
1542
|
+
const allRecentChanges = await listChangeEvents(db, {
|
|
1487
1543
|
projectId,
|
|
1488
1544
|
environment,
|
|
1489
|
-
limit: 5,
|
|
1545
|
+
limit: locusService ? 25 : 5,
|
|
1490
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;
|
|
1491
1559
|
const blastRadii = (
|
|
1492
1560
|
await Promise.all(
|
|
1493
1561
|
recentChanges
|
|
@@ -1539,13 +1607,8 @@ const attachAutoSreTriageContext = async (
|
|
|
1539
1607
|
limit: 5,
|
|
1540
1608
|
});
|
|
1541
1609
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
projectId,
|
|
1545
|
-
environment,
|
|
1546
|
-
limit: 5,
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1610
|
+
// No unfiltered fallback: unrelated findings would read as relevant
|
|
1611
|
+
// prior art. An empty list is the honest answer.
|
|
1549
1612
|
const priorFindings = priorFindingsList.map((f) => ({
|
|
1550
1613
|
findingId: f.id,
|
|
1551
1614
|
title: f.title,
|
|
@@ -1567,8 +1630,7 @@ const attachAutoSreTriageContext = async (
|
|
|
1567
1630
|
source: br.overlappingCodePaths.length
|
|
1568
1631
|
? "stack_frame_overlap"
|
|
1569
1632
|
: "cohort_comparison",
|
|
1570
|
-
confidence:
|
|
1571
|
-
br.cohortVerdict === "isolated_to_new_version" ? 0.9 : 0.65,
|
|
1633
|
+
confidence: br.cohortVerdict === "isolated_to_new_version" ? 0.9 : 0.65,
|
|
1572
1634
|
relationshipCertainty: br.relationshipCertainty,
|
|
1573
1635
|
causalProofTier: br.causalProofTier,
|
|
1574
1636
|
reason: br.summary,
|
|
@@ -1624,13 +1686,28 @@ const attachAutoSreTriageContext = async (
|
|
|
1624
1686
|
});
|
|
1625
1687
|
}
|
|
1626
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
|
+
|
|
1627
1704
|
bundle.autosre = {
|
|
1628
1705
|
faultLocus: faultLocus?.faultLocusNodeId
|
|
1629
1706
|
? {
|
|
1630
1707
|
nodeId: faultLocus.faultLocusNodeId,
|
|
1631
1708
|
spanId: faultLocus.faultLocusSpan?.span_id,
|
|
1632
1709
|
relationshipCertainty: faultLocus.relationshipCertainty,
|
|
1633
|
-
isBlackoutSuspected
|
|
1710
|
+
isBlackoutSuspected,
|
|
1634
1711
|
}
|
|
1635
1712
|
: undefined,
|
|
1636
1713
|
candidateChanges,
|
|
@@ -1638,13 +1715,39 @@ const attachAutoSreTriageContext = async (
|
|
|
1638
1715
|
priorFindings,
|
|
1639
1716
|
};
|
|
1640
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
|
+
}
|
|
1641
1740
|
bundle.budget = {
|
|
1642
1741
|
...bundle.budget,
|
|
1643
1742
|
estimatedTokens: estimateTokens(bundle),
|
|
1644
1743
|
};
|
|
1645
1744
|
}
|
|
1646
|
-
} catch {
|
|
1647
|
-
// Gracefully degrade if AutoSRE tables are absent in minimal test setups
|
|
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
|
+
});
|
|
1648
1751
|
}
|
|
1649
1752
|
return bundle;
|
|
1650
1753
|
};
|
|
@@ -1679,11 +1782,17 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1679
1782
|
);
|
|
1680
1783
|
}
|
|
1681
1784
|
|
|
1785
|
+
const isAutoSreAnchor =
|
|
1786
|
+
anchor.entityKind === "incident" ||
|
|
1787
|
+
anchor.entityKind === "change" ||
|
|
1788
|
+
anchor.entityKind === "finding";
|
|
1682
1789
|
const intent =
|
|
1683
1790
|
typeof body.intent === "string" &&
|
|
1684
1791
|
EVIDENCE_INTENTS.has(body.intent as EvidenceBundleIntent)
|
|
1685
1792
|
? (body.intent as EvidenceBundleIntent)
|
|
1686
|
-
:
|
|
1793
|
+
: isAutoSreAnchor
|
|
1794
|
+
? "triage_incident"
|
|
1795
|
+
: "debug_failure";
|
|
1687
1796
|
const budget = body.budget as Record<string, unknown> | undefined;
|
|
1688
1797
|
const targetTokens = clampInt(
|
|
1689
1798
|
budget?.targetTokens,
|
|
@@ -1755,6 +1864,7 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1755
1864
|
projectId,
|
|
1756
1865
|
traceIds[0] ?? null,
|
|
1757
1866
|
bundle,
|
|
1867
|
+
runtime.logger,
|
|
1758
1868
|
);
|
|
1759
1869
|
}
|
|
1760
1870
|
return c.json(await materializeBundle(db, projectId, bundle));
|
|
@@ -1772,14 +1882,12 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1772
1882
|
404,
|
|
1773
1883
|
);
|
|
1774
1884
|
}
|
|
1775
|
-
const
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
.bind(projectId, incident.environment)
|
|
1782
|
-
.first<{ trace_id: string }>();
|
|
1885
|
+
const anchorTraceId = await resolveAnchorTraceId(
|
|
1886
|
+
db,
|
|
1887
|
+
projectId,
|
|
1888
|
+
incident.environment,
|
|
1889
|
+
{ kind: incident.symptomAnchorKind, id: incident.symptomAnchorId },
|
|
1890
|
+
);
|
|
1783
1891
|
|
|
1784
1892
|
const incRoute = `#/incidents/${encodeURIComponent(incident.id)}`;
|
|
1785
1893
|
const baseBundle: EvidenceBundle = {
|
|
@@ -1826,7 +1934,8 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1826
1934
|
entityKind: "incident",
|
|
1827
1935
|
entityId: incident.id,
|
|
1828
1936
|
route: incRoute,
|
|
1829
|
-
reason:
|
|
1937
|
+
reason:
|
|
1938
|
+
"Inspect incident investigation and candidate changes.",
|
|
1830
1939
|
},
|
|
1831
1940
|
],
|
|
1832
1941
|
},
|
|
@@ -1845,8 +1954,240 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1845
1954
|
const enriched = await attachAutoSreTriageContext(
|
|
1846
1955
|
db,
|
|
1847
1956
|
projectId,
|
|
1848
|
-
|
|
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,
|
|
1849
2189
|
baseBundle,
|
|
2190
|
+
runtime.logger,
|
|
1850
2191
|
);
|
|
1851
2192
|
return c.json(await materializeBundle(db, projectId, enriched));
|
|
1852
2193
|
}
|
|
@@ -1856,7 +2197,7 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1856
2197
|
{
|
|
1857
2198
|
error: "Bad Request",
|
|
1858
2199
|
message:
|
|
1859
|
-
"Supported evidence bundle anchors are trace, action, agent_run, tool_call, and incident.",
|
|
2200
|
+
"Supported evidence bundle anchors are trace, action, agent_run, tool_call, change, finding, and incident.",
|
|
1860
2201
|
},
|
|
1861
2202
|
400,
|
|
1862
2203
|
);
|
|
@@ -1886,12 +2227,13 @@ export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
|
1886
2227
|
detailLevel,
|
|
1887
2228
|
hours,
|
|
1888
2229
|
);
|
|
1889
|
-
if (intent === "triage_incident"
|
|
2230
|
+
if (intent === "triage_incident") {
|
|
1890
2231
|
bundle = await attachAutoSreTriageContext(
|
|
1891
2232
|
db,
|
|
1892
2233
|
projectId,
|
|
1893
2234
|
traceId,
|
|
1894
2235
|
bundle,
|
|
2236
|
+
runtime.logger,
|
|
1895
2237
|
);
|
|
1896
2238
|
}
|
|
1897
2239
|
return c.json(await materializeBundle(db, projectId, bundle));
|