@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.
Files changed (37) hide show
  1. package/dist/{chunk-XKQTVIRE.js → chunk-ZSF5XP3B.js} +8 -2
  2. package/dist/chunk-ZSF5XP3B.js.map +1 -0
  3. package/dist/index.d.ts +359 -2
  4. package/dist/index.js +5000 -334
  5. package/dist/index.js.map +1 -1
  6. package/dist/{openai-FJYOQ7WO.js → openai-GSJZGFGL.js} +2 -2
  7. package/package.json +2 -2
  8. package/src/__tests__/autosre-closed-loop.test.ts +2222 -0
  9. package/src/index.ts +43 -0
  10. package/src/lib/analyses-runner.ts +46 -0
  11. package/src/lib/autosre-change-correlation.ts +807 -0
  12. package/src/lib/autosre-findings.ts +763 -0
  13. package/src/lib/autosre-ontology-cache.test.ts +78 -0
  14. package/src/lib/autosre-ontology.ts +938 -0
  15. package/src/lib/autosre-remediation-gate.ts +2127 -0
  16. package/src/lib/eval-cases-store.ts +63 -1
  17. package/src/lib/evidence-references.ts +6 -0
  18. package/src/lib/housekeeping.test.ts +150 -0
  19. package/src/lib/housekeeping.ts +109 -1
  20. package/src/lib/store/ingest.ts +46 -6
  21. package/src/migrations/050_autosre_causal_ontology.sql +216 -0
  22. package/src/migrations/051_incident_active_dedup_unique.sql +31 -0
  23. package/src/migrations/alert-state-notified.test.ts +3 -3
  24. package/src/migrations/parity.test.ts +4 -0
  25. package/src/migrations/project-scoped-keys.test.ts +1 -1
  26. package/src/migrations-postgres/050_autosre_causal_ontology.sql +216 -0
  27. package/src/migrations-postgres/051_incident_active_dedup_unique.sql +31 -0
  28. package/src/plugins/alerts-evaluator.ts +29 -1
  29. package/src/plugins/autosre-routes.ts +1134 -0
  30. package/src/plugins/connected-routes/manifest.ts +10 -1
  31. package/src/plugins/connected-routes.ts +241 -0
  32. package/src/plugins/default-span-enrichment.ts +13 -1
  33. package/src/plugins/eval-cases-routes.ts +57 -2
  34. package/src/plugins/evidence-retrieval-routes.test.ts +32 -0
  35. package/src/plugins/evidence-retrieval-routes.ts +643 -5
  36. package/dist/chunk-XKQTVIRE.js.map +0 -1
  37. /package/dist/{openai-FJYOQ7WO.js.map → openai-GSJZGFGL.js.map} +0 -0
@@ -1,3 +1,4 @@
1
+ import type { CausalProofTier, RelationshipCertainty } from "@obsunified/types";
1
2
  import type {
2
3
  ActionRef,
3
4
  AICallRef,
@@ -26,7 +27,10 @@ export type ConnectedEntityKind =
26
27
  | "user"
27
28
  | "action"
28
29
  | "agent_run"
29
- | "tool_call";
30
+ | "tool_call"
31
+ | "change"
32
+ | "finding"
33
+ | "incident";
30
34
 
31
35
  export const KNOWN_KINDS: ReadonlySet<string> = new Set<ConnectedEntityKind>([
32
36
  "span",
@@ -41,6 +45,9 @@ export const KNOWN_KINDS: ReadonlySet<string> = new Set<ConnectedEntityKind>([
41
45
  "action",
42
46
  "agent_run",
43
47
  "tool_call",
48
+ "change",
49
+ "finding",
50
+ "incident",
44
51
  ]);
45
52
 
46
53
  export interface ConnectedLink {
@@ -54,6 +61,8 @@ export interface ConnectedLink {
54
61
  confidence?: number;
55
62
  reason?: string;
56
63
  causalConfidence?: CausalConfidence;
64
+ relationshipCertainty?: RelationshipCertainty;
65
+ causalProofTier?: CausalProofTier;
57
66
  }
58
67
 
59
68
  export interface ConnectedSection {
@@ -15,8 +15,12 @@
15
15
  * RFC 0006 § Connected rail / Empty-state.
16
16
  */
17
17
 
18
+ import type { CausalProofTier } from "@obsunified/types";
18
19
  import { ActionConfidence } from "@obsunified/types/constants";
19
20
  import type { CollectorPlugin } from "../framework/collector";
21
+ import { getChangeEvent } from "../lib/autosre-change-correlation";
22
+ import { getInvestigationFinding } from "../lib/autosre-findings";
23
+ import { getIncidentInvestigation } from "../lib/autosre-remediation-gate";
20
24
  import { IdentityIndex } from "../lib/identity-index";
21
25
  import {
22
26
  mapAction,
@@ -1049,6 +1053,243 @@ export const connectedRoutesPlugin: CollectorPlugin = {
1049
1053
  manifest.rawManifest = actionManifest;
1050
1054
  }
1051
1055
  }
1056
+ } else if (kind === "change") {
1057
+ const db = sqlDbFor(c.env);
1058
+ const changeRow = await getChangeEvent(
1059
+ db,
1060
+ projectId,
1061
+ id,
1062
+ c.req.query("environment") || undefined,
1063
+ );
1064
+ if (changeRow) {
1065
+ manifest.up = [
1066
+ {
1067
+ label: "Target Ontology Node",
1068
+ links: [
1069
+ {
1070
+ label: `${changeRow.targetNodeId} (${changeRow.environment})`,
1071
+ href: `#/health?node=${encodeURIComponent(changeRow.targetNodeId)}`,
1072
+ relationshipCertainty: "exact",
1073
+ reason: "Target node mutated by this change event.",
1074
+ },
1075
+ ],
1076
+ },
1077
+ ];
1078
+
1079
+ const findings = await db
1080
+ .prepare(
1081
+ `SELECT id, title, causal_proof_tier, confidence, incident_id
1082
+ FROM investigation_findings
1083
+ WHERE project_id = ? AND environment = ? AND contributing_change_id = ?
1084
+ ORDER BY created_at DESC LIMIT 10`,
1085
+ )
1086
+ .bind(projectId, changeRow.environment, changeRow.id)
1087
+ .all<{
1088
+ id: string;
1089
+ title: string;
1090
+ causal_proof_tier: string;
1091
+ confidence: number;
1092
+ incident_id: string | null;
1093
+ }>();
1094
+
1095
+ manifest.down = [
1096
+ {
1097
+ label: "Attributed Findings",
1098
+ links: findings.results.map((f) => ({
1099
+ label: f.title,
1100
+ href: `#/findings/${encodeURIComponent(f.id)}`,
1101
+ entityKind: "finding",
1102
+ entityId: f.id,
1103
+ relationshipCertainty: "exact",
1104
+ causalProofTier: f.causal_proof_tier as CausalProofTier,
1105
+ confidence: f.confidence,
1106
+ reason: "Finding attributes this change event.",
1107
+ })),
1108
+ emptyReason:
1109
+ findings.results.length === 0
1110
+ ? "No investigation findings cite this change event."
1111
+ : undefined,
1112
+ },
1113
+ ];
1114
+
1115
+ const spans = changeRow.version
1116
+ ? await db
1117
+ .prepare(
1118
+ `SELECT DISTINCT trace_id, span_id, service_name, span_name
1119
+ FROM telemetry_spans
1120
+ WHERE project_id = ? AND environment = ? AND service_version = ? AND status_code = 2
1121
+ ORDER BY start_time DESC LIMIT 5`,
1122
+ )
1123
+ .bind(projectId, changeRow.environment, changeRow.version)
1124
+ .all<{
1125
+ trace_id: string;
1126
+ span_id: string;
1127
+ service_name: string;
1128
+ span_name: string;
1129
+ }>()
1130
+ : { results: [] };
1131
+
1132
+ manifest.across = [
1133
+ {
1134
+ label: "Failing Spans on Cohort Version",
1135
+ links: spans.results.map((s) => ({
1136
+ label: `${s.service_name} · ${s.span_name}`,
1137
+ href: `#/traces/${s.trace_id}#span=${s.span_id}`,
1138
+ entityKind: "span",
1139
+ entityId: `${s.trace_id}:${s.span_id}`,
1140
+ relationshipCertainty: "exact",
1141
+ reason: `Span executed on version ${changeRow.version}.`,
1142
+ })),
1143
+ emptyReason:
1144
+ spans.results.length === 0
1145
+ ? "No failing spans recorded on this change version."
1146
+ : undefined,
1147
+ },
1148
+ ];
1149
+ }
1150
+ } else if (kind === "finding") {
1151
+ const db = sqlDbFor(c.env);
1152
+ const findingRow = await getInvestigationFinding(
1153
+ db,
1154
+ projectId,
1155
+ id,
1156
+ c.req.query("environment") || undefined,
1157
+ );
1158
+ if (findingRow) {
1159
+ const proofTier = findingRow.causalProofTier as CausalProofTier;
1160
+ manifest.up = [];
1161
+ if (findingRow.incidentId) {
1162
+ manifest.up.push({
1163
+ label: "Incident Investigation",
1164
+ links: [
1165
+ {
1166
+ label: `incident ${findingRow.incidentId}`,
1167
+ href: `#/incidents/${encodeURIComponent(findingRow.incidentId)}`,
1168
+ entityKind: "incident",
1169
+ entityId: findingRow.incidentId,
1170
+ relationshipCertainty: "exact",
1171
+ causalProofTier: proofTier,
1172
+ },
1173
+ ],
1174
+ });
1175
+ }
1176
+ if (findingRow.contributingChangeId) {
1177
+ manifest.up.push({
1178
+ label: "Contributing Change",
1179
+ links: [
1180
+ {
1181
+ label: `change ${findingRow.contributingChangeId}`,
1182
+ href: `#/changes/${encodeURIComponent(findingRow.contributingChangeId)}`,
1183
+ entityKind: "change",
1184
+ entityId: findingRow.contributingChangeId,
1185
+ relationshipCertainty: "exact",
1186
+ causalProofTier: proofTier,
1187
+ confidence: findingRow.confidence,
1188
+ },
1189
+ ],
1190
+ });
1191
+ }
1192
+ manifest.across = [
1193
+ {
1194
+ label: "Fault Locus & Symptom",
1195
+ links: [
1196
+ {
1197
+ label: `fault locus: ${findingRow.faultLocusEntityId}`,
1198
+ href: `#/traces?q=${encodeURIComponent(findingRow.faultLocusEntityId)}`,
1199
+ relationshipCertainty: "exact",
1200
+ causalProofTier: proofTier,
1201
+ },
1202
+ ...(findingRow.contendingTraceId
1203
+ ? [
1204
+ {
1205
+ label: `contending trace ${findingRow.contendingTraceId}`,
1206
+ href: `#/traces/${encodeURIComponent(findingRow.contendingTraceId)}`,
1207
+ entityKind: "trace" as const,
1208
+ entityId: findingRow.contendingTraceId,
1209
+ relationshipCertainty: "inferred" as const,
1210
+ causalProofTier: proofTier,
1211
+ },
1212
+ ]
1213
+ : []),
1214
+ ],
1215
+ },
1216
+ ];
1217
+ manifest.down = [
1218
+ {
1219
+ label: "Agent Lineage",
1220
+ links: [
1221
+ ...(findingRow.agentRunId
1222
+ ? [
1223
+ {
1224
+ ...linkToAgentRun(findingRow.agentRunId),
1225
+ relationshipCertainty: "exact" as const,
1226
+ causalProofTier: proofTier,
1227
+ },
1228
+ ]
1229
+ : []),
1230
+ ],
1231
+ emptyReason: !findingRow.agentRunId
1232
+ ? "No agent run linked to this finding."
1233
+ : undefined,
1234
+ },
1235
+ ];
1236
+ }
1237
+ } else if (kind === "incident") {
1238
+ const db = sqlDbFor(c.env);
1239
+ const incRow = await getIncidentInvestigation(
1240
+ db,
1241
+ projectId,
1242
+ id,
1243
+ c.req.query("environment") || undefined,
1244
+ );
1245
+ if (incRow) {
1246
+ manifest.up = [
1247
+ {
1248
+ label: "Trigger Signal",
1249
+ links: [
1250
+ {
1251
+ label: `${incRow.symptomAnchorKind}: ${incRow.symptomAnchorId}`,
1252
+ href: `#/alerts?id=${encodeURIComponent(incRow.symptomAnchorId)}`,
1253
+ entityKind:
1254
+ incRow.symptomAnchorKind === "analysis"
1255
+ ? "analysis"
1256
+ : "alert",
1257
+ entityId: incRow.symptomAnchorId,
1258
+ relationshipCertainty: "exact",
1259
+ },
1260
+ ],
1261
+ },
1262
+ ];
1263
+ const incFindings = await db
1264
+ .prepare(
1265
+ `SELECT id, title, causal_proof_tier FROM investigation_findings
1266
+ WHERE project_id = ? AND incident_id = ?
1267
+ ORDER BY created_at DESC LIMIT 5`,
1268
+ )
1269
+ .bind(projectId, incRow.id)
1270
+ .all<{
1271
+ id: string;
1272
+ title: string;
1273
+ causal_proof_tier: string;
1274
+ }>();
1275
+ manifest.across = [
1276
+ {
1277
+ label: "Findings",
1278
+ links: incFindings.results.map((f) => ({
1279
+ label: f.title,
1280
+ href: `#/findings/${encodeURIComponent(f.id)}`,
1281
+ entityKind: "finding" as const,
1282
+ entityId: f.id,
1283
+ relationshipCertainty: "exact" as const,
1284
+ causalProofTier: f.causal_proof_tier as CausalProofTier,
1285
+ })),
1286
+ emptyReason:
1287
+ incFindings.results.length > 0
1288
+ ? undefined
1289
+ : "No finding recorded for this incident yet.",
1290
+ },
1291
+ ];
1292
+ }
1052
1293
  } else if (kind === "alert" || kind === "analysis") {
1053
1294
  // Alerts and Analyses are topic-related, not identity-related.
1054
1295
  // Their connections come from the alert_rules.analysis_id and
@@ -4,6 +4,7 @@ import {
4
4
  SESSION_ID_KEY,
5
5
  } from "@obsunified/types/constants";
6
6
  import type { CollectorPlugin } from "../framework/collector";
7
+ import { extractSpanLineageAndTopology } from "../lib/autosre-ontology";
7
8
  import { parseJsonRecord } from "../lib/json";
8
9
 
9
10
  export const defaultSpanEnrichmentPlugin: CollectorPlugin = {
@@ -77,7 +78,7 @@ export const defaultSpanEnrichmentPlugin: CollectorPlugin = {
77
78
  ? (resourceAttributes["telemetry.sdk.name"] as string)
78
79
  : null;
79
80
 
80
- return {
81
+ const candidate: StoredSpan = {
81
82
  ...span,
82
83
  serviceName,
83
84
  attributesJson: JSON.stringify(normalizedAttributes),
@@ -85,6 +86,17 @@ export const defaultSpanEnrichmentPlugin: CollectorPlugin = {
85
86
  interactionId,
86
87
  telemetrySdkName,
87
88
  };
89
+
90
+ const lineage = extractSpanLineageAndTopology(candidate);
91
+
92
+ return {
93
+ ...candidate,
94
+ environment: lineage.environment,
95
+ serviceVersion: lineage.serviceVersion,
96
+ commitSha: lineage.commitSha,
97
+ targetNodeId: lineage.targetNode?.id ?? null,
98
+ errorSignature: lineage.errorSignature,
99
+ };
88
100
  });
89
101
  },
90
102
  });
@@ -50,6 +50,8 @@ interface EvalCaseResultResponse {
50
50
  passed: boolean;
51
51
  score: number | null;
52
52
  actualOutcome: string | null;
53
+ inputPayloadHash: string | null;
54
+ failureFingerprint: string | null;
53
55
  details: unknown;
54
56
  createdAt: string;
55
57
  evidenceReferences?: EvidenceReference[];
@@ -62,6 +64,12 @@ interface EvalRunRequestBody {
62
64
  evalCaseId?: unknown;
63
65
  status?: unknown;
64
66
  candidate?: unknown;
67
+ environment?: unknown;
68
+ serviceVersion?: unknown;
69
+ commitSha?: unknown;
70
+ configDigest?: unknown;
71
+ inputPayloadHash?: unknown;
72
+ failureFingerprint?: unknown;
65
73
  startedAt?: unknown;
66
74
  endedAt?: unknown;
67
75
  totalCount?: unknown;
@@ -294,6 +302,20 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
294
302
  id: id ?? undefined,
295
303
  evalCaseId,
296
304
  status,
305
+ environment:
306
+ asOptionalString(body.environment) ??
307
+ asOptionalString(candidate.environment),
308
+ serviceVersion:
309
+ asOptionalString(body.serviceVersion) ??
310
+ asOptionalString(candidate.serviceVersion),
311
+ commitSha:
312
+ asOptionalString(body.commitSha) ??
313
+ asOptionalString(candidate.commitSha),
314
+ configDigest:
315
+ asOptionalString(body.configDigest) ??
316
+ asOptionalString(candidate.configDigest),
317
+ inputPayloadHash: asOptionalString(body.inputPayloadHash),
318
+ failureFingerprint: asOptionalString(body.failureFingerprint),
297
319
  candidate: {
298
320
  agentId: asOptionalString(candidate.agentId),
299
321
  agentVersion: asOptionalString(candidate.agentVersion),
@@ -302,6 +324,18 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
302
324
  modelProvider: asOptionalString(candidate.modelProvider),
303
325
  model: asOptionalString(candidate.model),
304
326
  modelVersion: asOptionalString(candidate.modelVersion),
327
+ environment:
328
+ asOptionalString(candidate.environment) ??
329
+ asOptionalString(body.environment),
330
+ serviceVersion:
331
+ asOptionalString(candidate.serviceVersion) ??
332
+ asOptionalString(body.serviceVersion),
333
+ commitSha:
334
+ asOptionalString(candidate.commitSha) ??
335
+ asOptionalString(body.commitSha),
336
+ configDigest:
337
+ asOptionalString(candidate.configDigest) ??
338
+ asOptionalString(body.configDigest),
305
339
  },
306
340
  startedAt: asOptionalString(body.startedAt),
307
341
  endedAt: asOptionalString(body.endedAt),
@@ -369,6 +403,8 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
369
403
  passed?: unknown;
370
404
  score?: unknown;
371
405
  actualOutcome?: unknown;
406
+ inputPayloadHash?: unknown;
407
+ failureFingerprint?: unknown;
372
408
  details?: unknown;
373
409
  };
374
410
  const parsed = await readJsonObject<typeof body>(c);
@@ -411,6 +447,16 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
411
447
  : null;
412
448
  const actualOutcome =
413
449
  typeof body.actualOutcome === "string" ? body.actualOutcome : null;
450
+ const inputPayloadHash =
451
+ typeof body.inputPayloadHash === "string" &&
452
+ body.inputPayloadHash.trim()
453
+ ? body.inputPayloadHash.trim()
454
+ : null;
455
+ const failureFingerprint =
456
+ typeof body.failureFingerprint === "string" &&
457
+ body.failureFingerprint.trim()
458
+ ? body.failureFingerprint.trim()
459
+ : null;
414
460
  const details_json =
415
461
  body.details === undefined ? null : JSON.stringify(body.details);
416
462
  const now = new Date().toISOString();
@@ -418,8 +464,9 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
418
464
  await db
419
465
  .prepare(
420
466
  `INSERT INTO eval_case_results (
421
- id, project_id, eval_case_id, run_id, passed, score, actual_outcome, details_json, created_at
422
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
467
+ id, project_id, eval_case_id, run_id, passed, score, actual_outcome,
468
+ input_payload_hash, failure_fingerprint, details_json, created_at
469
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
423
470
  )
424
471
  .bind(
425
472
  id,
@@ -429,6 +476,8 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
429
476
  passed,
430
477
  score,
431
478
  actualOutcome,
479
+ inputPayloadHash,
480
+ failureFingerprint,
432
481
  details_json,
433
482
  now,
434
483
  )
@@ -446,6 +495,8 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
446
495
  passed: body.passed,
447
496
  score,
448
497
  actualOutcome,
498
+ inputPayloadHash,
499
+ failureFingerprint,
449
500
  details: body.details ?? null,
450
501
  createdAt: now,
451
502
  evidenceReferences: evalCaseResultEvidenceReferences(evalCase, id),
@@ -482,6 +533,8 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
482
533
  passed: number;
483
534
  score: number | null;
484
535
  actual_outcome: string | null;
536
+ input_payload_hash?: string | null;
537
+ failure_fingerprint?: string | null;
485
538
  details_json: string | null;
486
539
  created_at: string;
487
540
  }>();
@@ -504,6 +557,8 @@ export const evalCasesRoutesPlugin: CollectorPlugin = {
504
557
  passed: row.passed === 1,
505
558
  score: row.score,
506
559
  actualOutcome: row.actual_outcome,
560
+ inputPayloadHash: row.input_payload_hash ?? null,
561
+ failureFingerprint: row.failure_fingerprint ?? null,
507
562
  details: parseJsonMaybe(row.details_json),
508
563
  createdAt: row.created_at,
509
564
  evidenceReferences: evalCaseResultEvidenceReferences(
@@ -519,6 +519,38 @@ describe("evidenceRetrievalRoutesPlugin", () => {
519
519
  return (await bundleRes.json()) as EvidenceBundle;
520
520
  };
521
521
 
522
+ it("attaches the AutoSRE triage section only for intent triage_incident", async () => {
523
+ const { app, env } = setup();
524
+ const post = async (intent: string, targetTokens = 4000) => {
525
+ const res = await app.request(
526
+ "/internal/evidence/bundle",
527
+ {
528
+ method: "POST",
529
+ body: JSON.stringify({
530
+ anchor: { entityKind: "trace", entityId: "trace-1" },
531
+ intent,
532
+ budget: { targetTokens, detailLevel: "standard" },
533
+ }),
534
+ headers: { "content-type": "application/json" },
535
+ },
536
+ env,
537
+ );
538
+ expect(res.status).toBe(200);
539
+ return (await res.json()) as EvidenceBundle;
540
+ };
541
+
542
+ const debug = await post("debug_failure");
543
+ expect(debug.autosre).toBeUndefined();
544
+
545
+ const triage = await post("triage_incident");
546
+ expect(triage.autosre).toBeDefined();
547
+ // No unfiltered prior-findings fallback: an empty memory yields none.
548
+ expect(triage.autosre?.priorFindings).toEqual([]);
549
+ expect(triage.autosre?.faultLocus?.isBlackoutSuspected ?? false).toBe(
550
+ false,
551
+ );
552
+ });
553
+
522
554
  it("returns an incident-local trace bundle with log compaction provenance", async () => {
523
555
  const { app, env } = setup();
524
556
  const res = await app.request(