@lcv-ideas-software/cross-review 4.2.4 → 4.3.0

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.
@@ -3,9 +3,107 @@ function valueOrDash(value) {
3
3
  return "-";
4
4
  return String(value);
5
5
  }
6
- function costText(session) {
7
- const total = session.totals.cost.total_cost;
8
- return total == null ? "unknown" : `$${total.toFixed(6)} ${session.totals.cost.currency}`;
6
+ function moneyText(value, currency = "USD") {
7
+ return value == null ? "unknown" : `$${value.toFixed(6)} ${currency}`;
8
+ }
9
+ function moneyAmountText(value) {
10
+ return `$${value.toFixed(6)}`;
11
+ }
12
+ export function sessionCostBreakdown(session) {
13
+ const currency = session.totals.cost.currency ?? "USD";
14
+ let peerTotal = 0;
15
+ let peerSeen = false;
16
+ for (const round of session.rounds) {
17
+ for (const peer of round.peers) {
18
+ const value = peer.cost?.total_cost;
19
+ if (value == null || !Number.isFinite(value))
20
+ continue;
21
+ peerSeen = true;
22
+ peerTotal += value;
23
+ }
24
+ }
25
+ let generationTotal = 0;
26
+ let generationSeen = false;
27
+ for (const generation of session.generation_files ?? []) {
28
+ const value = generation.cost?.total_cost;
29
+ if (value == null || !Number.isFinite(value))
30
+ continue;
31
+ generationSeen = true;
32
+ generationTotal += value;
33
+ }
34
+ const total = session.totals.cost.total_cost ?? null;
35
+ return {
36
+ currency,
37
+ total,
38
+ peer_total: peerSeen ? peerTotal : null,
39
+ generation_total: generationSeen ? generationTotal : null,
40
+ };
41
+ }
42
+ function costSummaryLines(session) {
43
+ const breakdown = sessionCostBreakdown(session);
44
+ const lines = [
45
+ `- Cost: ${moneyText(breakdown.total, breakdown.currency)}`,
46
+ `- Peer call cost: ${moneyText(breakdown.peer_total, breakdown.currency)}`,
47
+ `- Generation cost: ${moneyText(breakdown.generation_total, breakdown.currency)}`,
48
+ ];
49
+ if (breakdown.total != null &&
50
+ breakdown.peer_total != null &&
51
+ breakdown.generation_total != null) {
52
+ lines.push(`- Cost reconciliation: ${moneyText(breakdown.total, breakdown.currency)} = ${moneyAmountText(breakdown.peer_total)} peer + ${moneyAmountText(breakdown.generation_total)} generation`);
53
+ }
54
+ return lines;
55
+ }
56
+ function evidenceChecklistLines(session) {
57
+ const checklist = session.evidence_checklist ?? [];
58
+ if (!checklist.length)
59
+ return [];
60
+ const counts = new Map();
61
+ for (const item of checklist) {
62
+ const status = item.status ?? "open";
63
+ counts.set(status, (counts.get(status) ?? 0) + 1);
64
+ }
65
+ const lines = ["## Evidence Checklist", ""];
66
+ for (const [status, count] of [...counts.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
67
+ lines.push(`- ${status}: ${count}`);
68
+ }
69
+ const notResurfaced = checklist.filter((item) => item.status === "not_resurfaced");
70
+ if (notResurfaced.length) {
71
+ lines.push("- not_resurfaced means the ask was not repeated; it is not proof that evidence was satisfied.");
72
+ for (const item of notResurfaced.slice(0, 10)) {
73
+ lines.push(` - ${item.peer}/${item.id}: ${item.ask}`);
74
+ }
75
+ }
76
+ lines.push("");
77
+ return lines;
78
+ }
79
+ export function unresolvedEvidenceItems(session) {
80
+ return (session.evidence_checklist ?? []).filter((item) => {
81
+ const status = item.status ?? "open";
82
+ return status === "open" || status === "not_resurfaced";
83
+ });
84
+ }
85
+ function unresolvedEvidenceDispositionLines(session) {
86
+ const unresolved = unresolvedEvidenceItems(session);
87
+ if (!unresolved.length)
88
+ return [];
89
+ const lines = [
90
+ "## Unresolved Evidence Disposition",
91
+ "",
92
+ "- These items are not terminally satisfied, deferred, or rejected.",
93
+ "- `open` still requires a concrete response; `not_resurfaced` only means the peer did not repeat the ask in a later round.",
94
+ "- To close them explicitly, attach the requested evidence or use `session_evidence_checklist_update` with `satisfied`, `deferred`, or `rejected`.",
95
+ "",
96
+ ];
97
+ for (const item of unresolved.slice(0, 20)) {
98
+ const status = item.status ?? "open";
99
+ const chronic = item.round_count >= 3 ? " chronic" : "";
100
+ lines.push(`- ${status}${chronic} ${item.peer}/${item.id}: ${item.ask}`);
101
+ }
102
+ if (unresolved.length > 20) {
103
+ lines.push(`- ... ${unresolved.length - 20} additional unresolved item(s) omitted.`);
104
+ }
105
+ lines.push("");
106
+ return lines;
9
107
  }
10
108
  export function sessionReportMarkdown(session, events = []) {
11
109
  const latestRound = session.rounds.at(-1);
@@ -22,7 +120,7 @@ export function sessionReportMarkdown(session, events = []) {
22
120
  `- Outcome reason: ${valueOrDash(session.outcome_reason)}`,
23
121
  `- Health: ${valueOrDash(session.convergence_health?.state)} - ${valueOrDash(session.convergence_health?.detail)}`,
24
122
  `- Rounds: ${session.rounds.length}`,
25
- `- Cost: ${costText(session)}`,
123
+ ...costSummaryLines(session),
26
124
  `- Total tokens: ${valueOrDash(session.totals.usage.total_tokens)}`,
27
125
  "",
28
126
  "## Task",
@@ -46,6 +144,8 @@ export function sessionReportMarkdown(session, events = []) {
46
144
  "## Peer Decisions",
47
145
  "",
48
146
  ];
147
+ lines.push(...evidenceChecklistLines(session));
148
+ lines.push(...unresolvedEvidenceDispositionLines(session));
49
149
  if (session.generation_files?.length) {
50
150
  lines.push("## Generations", "");
51
151
  for (const generation of session.generation_files) {
@@ -1 +1 @@
1
- {"version":3,"file":"reports.js","sourceRoot":"","sources":["../../../src/core/reports.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"reports.js","sourceRoot":"","sources":["../../../src/core/reports.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { AppConfig, ConvergenceResult, ConvergenceScope, EvidenceChecklistItem, EvidenceChecklistStatus, EvidenceStatusHistoryEntry, GenerationResult, JudgmentPrecisionReport, PeerFailure, PeerId, PeerProbeResult, PeerResult, ReviewRound, ReviewStatus, RuntimeEvent, RuntimeMetrics, SessionDoctorReport, SessionEvent, SessionMeta, ShadowJudgmentRollup } from "./types.js";
1
+ import type { AppConfig, ConvergenceResult, ConvergenceScope, EvidenceChecklistItem, EvidenceChecklistStatus, EvidenceStatusHistoryEntry, GenerationResult, JudgmentPrecisionReport, PeerFailure, PeerId, PeerProbeResult, PeerReliabilityReport, PeerResult, ReviewRound, ReviewStatus, RuntimeEvent, RuntimeMetrics, SessionDoctorReport, SessionEvent, SessionMeta, ShadowJudgmentRollup } from "./types.js";
2
2
  export declare const SWEEP_MIN_IDLE_MS: number;
3
3
  export declare class SessionStore {
4
4
  private readonly config;
@@ -25,6 +25,7 @@ export declare class SessionStore {
25
25
  readTextArtifact(sessionId: string, relativePath: string, maxChars: number): string;
26
26
  private peekNextSeq;
27
27
  private commitSeq;
28
+ private appendEventRecord;
28
29
  appendEvent(event: RuntimeEvent): Promise<void>;
29
30
  flushPendingEvents(): Promise<void>;
30
31
  readEvents(sessionId: string, sinceSeq?: number): SessionEvent[];
@@ -94,6 +95,7 @@ export declare class SessionStore {
94
95
  recoverInterruptedSessions(activeSessionIds?: Set<string>): Promise<SessionMeta[]>;
95
96
  aggregateShadowJudgments(sessionId?: string): ShadowJudgmentRollup;
96
97
  metrics(sessionId?: string): RuntimeMetrics;
98
+ peerReliabilityReport(sessionId?: string): PeerReliabilityReport;
97
99
  sessionDoctor(limit?: number, includeLegacy?: boolean, repair?: boolean): Promise<SessionDoctorReport>;
98
100
  computeJudgmentPrecisionReport(opts?: {
99
101
  peer?: PeerId | undefined;
@@ -9,6 +9,45 @@ export const SWEEP_MIN_IDLE_MS = 24 * 60 * 60 * 1000;
9
9
  function now() {
10
10
  return new Date().toISOString();
11
11
  }
12
+ function isStubSession(session) {
13
+ const peerCosts = session.rounds.flatMap((round) => round.peers.map((peer) => peer.cost));
14
+ const generationCosts = (session.generation_files ?? []).map((generation) => generation.cost);
15
+ const costs = [...peerCosts, ...generationCosts].filter(Boolean);
16
+ if (costs.length > 0)
17
+ return costs.every((cost) => cost?.source === "stub");
18
+ return session.capability_snapshot.some((probe) => probe.provider.startsWith("stub-") || probe.model.startsWith("stub-"));
19
+ }
20
+ function sessionPeerCostTotal(session) {
21
+ let total = 0;
22
+ let seen = false;
23
+ for (const round of session.rounds) {
24
+ for (const peer of round.peers) {
25
+ const value = peer.cost?.total_cost;
26
+ if (value == null || !Number.isFinite(value))
27
+ continue;
28
+ seen = true;
29
+ total += value;
30
+ }
31
+ }
32
+ return seen ? total : null;
33
+ }
34
+ function sessionGenerationCostTotal(session) {
35
+ let total = 0;
36
+ let seen = false;
37
+ for (const generation of session.generation_files ?? []) {
38
+ const value = generation.cost?.total_cost;
39
+ if (value == null || !Number.isFinite(value))
40
+ continue;
41
+ seen = true;
42
+ total += value;
43
+ }
44
+ return seen ? total : null;
45
+ }
46
+ function addNullableCost(a, b) {
47
+ if (a == null && b == null)
48
+ return null;
49
+ return (a ?? 0) + (b ?? 0);
50
+ }
12
51
  // v2.4.0 / audit closure (P1.3): atomicWriteFile retry on Windows.
13
52
  // `fs.renameSync` in Win32 fails with EPERM/EACCES/EBUSY when the
14
53
  // destination is briefly held by another handle (AV scan, indexing,
@@ -392,6 +431,16 @@ export class SessionStore {
392
431
  commitSeq(sessionId, committed) {
393
432
  this.seqCache.set(sessionId, committed);
394
433
  }
434
+ appendEventRecord(event) {
435
+ const sessionId = event.session_id;
436
+ if (!sessionId)
437
+ return;
438
+ const file = this.eventsPath(sessionId);
439
+ fs.mkdirSync(path.dirname(file), { recursive: true });
440
+ const seq = this.peekNextSeq(sessionId, file);
441
+ fs.appendFileSync(file, `${JSON.stringify({ ...event, seq, ts: event.ts ?? now() })}\n`, "utf8");
442
+ this.commitSeq(sessionId, seq);
443
+ }
395
444
  // v4.1.0: durable event persistence. withSessionLock became async
396
445
  // with the proper-lockfile refactor; appendEvent awaits the lock so
397
446
  // callers that read events after persisting get the expected
@@ -405,13 +454,11 @@ export class SessionStore {
405
454
  const write = (async () => {
406
455
  try {
407
456
  await this.withSessionLock(sessionId, () => {
408
- const file = this.eventsPath(sessionId);
409
- const seq = this.peekNextSeq(sessionId, file);
410
- fs.appendFileSync(file, `${JSON.stringify({ ...event, seq, ts: event.ts ?? now() })}\n`, "utf8");
411
457
  // Only commit the cache AFTER the durable append succeeded.
412
- // If appendFileSync threw above, the cache still reflects the
413
- // last persisted seq and the next call reuses this seq number.
414
- this.commitSeq(sessionId, seq);
458
+ // If appendFileSync threw inside appendEventRecord, the cache
459
+ // still reflects the last persisted seq and the next call
460
+ // reuses this seq number.
461
+ this.appendEventRecord(event);
415
462
  });
416
463
  }
417
464
  catch {
@@ -730,13 +777,26 @@ export class SessionStore {
730
777
  if (reason)
731
778
  meta.outcome_reason = reason;
732
779
  delete meta.in_flight;
780
+ const ts = now();
733
781
  meta.convergence_health = {
734
782
  state: outcome === "converged" ? "converged" : outcome === "max-rounds" ? "blocked" : "stale",
735
- last_event_at: now(),
783
+ last_event_at: ts,
736
784
  detail: reason ?? outcome,
737
785
  };
738
- meta.updated_at = now();
786
+ meta.updated_at = ts;
739
787
  await writeJson(this.metaPath(sessionId), meta);
788
+ try {
789
+ this.appendEventRecord({
790
+ type: "session.finalized",
791
+ session_id: sessionId,
792
+ ts,
793
+ message: `Session finalized as ${outcome}${reason ? `: ${reason}` : ""}`,
794
+ data: { outcome, reason: reason ?? null },
795
+ });
796
+ }
797
+ catch {
798
+ /* event persistence is best-effort; session_doctor will flag gaps */
799
+ }
740
800
  return meta;
741
801
  });
742
802
  }
@@ -763,6 +823,7 @@ export class SessionStore {
763
823
  async markCancelled(sessionId, reason = "cancelled") {
764
824
  return this.withSessionLock(sessionId, async () => {
765
825
  const meta = this.read(sessionId);
826
+ const ts = now();
766
827
  meta.outcome = "aborted";
767
828
  meta.outcome_reason = reason;
768
829
  delete meta.in_flight;
@@ -771,15 +832,27 @@ export class SessionStore {
771
832
  reason,
772
833
  job_id: meta.control?.job_id,
773
834
  requested_at: meta.control?.requested_at,
774
- updated_at: now(),
835
+ updated_at: ts,
775
836
  };
776
837
  meta.convergence_health = {
777
838
  state: "stale",
778
- last_event_at: now(),
839
+ last_event_at: ts,
779
840
  detail: reason,
780
841
  };
781
- meta.updated_at = now();
842
+ meta.updated_at = ts;
782
843
  await writeJson(this.metaPath(sessionId), meta);
844
+ try {
845
+ this.appendEventRecord({
846
+ type: "session.cancelled",
847
+ session_id: sessionId,
848
+ ts,
849
+ message: `Session cancelled: ${reason}`,
850
+ data: { outcome: "aborted", reason },
851
+ });
852
+ }
853
+ catch {
854
+ /* event persistence is best-effort; session_doctor will flag gaps */
855
+ }
783
856
  return meta;
784
857
  });
785
858
  }
@@ -1274,6 +1347,147 @@ export class SessionStore {
1274
1347
  shadow_judgment: this.aggregateShadowJudgments(sessionId),
1275
1348
  };
1276
1349
  }
1350
+ peerReliabilityReport(sessionId) {
1351
+ const sessions = sessionId ? [this.read(sessionId)] : this.list();
1352
+ const peerSet = new Set(PEERS);
1353
+ const byPeer = {};
1354
+ const acc = (peer) => {
1355
+ let entry = byPeer[peer];
1356
+ if (!entry) {
1357
+ entry = {
1358
+ peer,
1359
+ session_ids: new Set(),
1360
+ results_total: 0,
1361
+ ready: 0,
1362
+ not_ready: 0,
1363
+ needs_evidence: 0,
1364
+ unresolved_status: 0,
1365
+ parser_warnings_total: 0,
1366
+ parser_warnings_by_type: {},
1367
+ decision_quality: {},
1368
+ rejected_total: 0,
1369
+ provider_errors: 0,
1370
+ failures_by_class: {},
1371
+ open_asks: 0,
1372
+ not_resurfaced_asks: 0,
1373
+ addressed_asks: 0,
1374
+ satisfied_asks: 0,
1375
+ deferred_asks: 0,
1376
+ rejected_asks: 0,
1377
+ fabrication_events: 0,
1378
+ latency_sum: 0,
1379
+ latency_count: 0,
1380
+ cost_sum: 0,
1381
+ cost_count: 0,
1382
+ };
1383
+ byPeer[peer] = entry;
1384
+ }
1385
+ return entry;
1386
+ };
1387
+ for (const session of sessions) {
1388
+ for (const round of session.rounds) {
1389
+ for (const peerResult of round.peers) {
1390
+ const entry = acc(peerResult.peer);
1391
+ entry.session_ids.add(session.session_id);
1392
+ entry.results_total += 1;
1393
+ if (peerResult.status === "READY")
1394
+ entry.ready += 1;
1395
+ else if (peerResult.status === "NOT_READY")
1396
+ entry.not_ready += 1;
1397
+ else if (peerResult.status === "NEEDS_EVIDENCE")
1398
+ entry.needs_evidence += 1;
1399
+ else
1400
+ entry.unresolved_status += 1;
1401
+ const quality = peerResult.decision_quality ?? "failed";
1402
+ entry.decision_quality[quality] = (entry.decision_quality[quality] ?? 0) + 1;
1403
+ for (const warning of peerResult.parser_warnings) {
1404
+ entry.parser_warnings_total += 1;
1405
+ entry.parser_warnings_by_type[warning] =
1406
+ (entry.parser_warnings_by_type[warning] ?? 0) + 1;
1407
+ }
1408
+ if (Number.isFinite(peerResult.latency_ms)) {
1409
+ entry.latency_sum += peerResult.latency_ms;
1410
+ entry.latency_count += 1;
1411
+ }
1412
+ if (peerResult.cost?.total_cost != null &&
1413
+ Number.isFinite(peerResult.cost.total_cost) &&
1414
+ peerResult.cost.source !== "stub") {
1415
+ entry.cost_sum += peerResult.cost.total_cost;
1416
+ entry.cost_count += 1;
1417
+ }
1418
+ }
1419
+ for (const failure of round.rejected) {
1420
+ const entry = acc(failure.peer);
1421
+ entry.session_ids.add(session.session_id);
1422
+ entry.rejected_total += 1;
1423
+ if (failure.failure_class === "provider_error")
1424
+ entry.provider_errors += 1;
1425
+ entry.failures_by_class[failure.failure_class] =
1426
+ (entry.failures_by_class[failure.failure_class] ?? 0) + 1;
1427
+ }
1428
+ }
1429
+ for (const item of session.evidence_checklist ?? []) {
1430
+ const entry = acc(item.peer);
1431
+ entry.session_ids.add(session.session_id);
1432
+ const status = item.status ?? "open";
1433
+ if (status === "open")
1434
+ entry.open_asks += 1;
1435
+ else if (status === "not_resurfaced")
1436
+ entry.not_resurfaced_asks += 1;
1437
+ else if (status === "addressed")
1438
+ entry.addressed_asks += 1;
1439
+ else if (status === "satisfied")
1440
+ entry.satisfied_asks += 1;
1441
+ else if (status === "deferred")
1442
+ entry.deferred_asks += 1;
1443
+ else if (status === "rejected")
1444
+ entry.rejected_asks += 1;
1445
+ }
1446
+ for (const event of this.readEvents(session.session_id)) {
1447
+ if (!event.type.includes("fabrication"))
1448
+ continue;
1449
+ const dataPeer = (event.data?.peer ?? event.peer);
1450
+ if (!dataPeer || !peerSet.has(dataPeer))
1451
+ continue;
1452
+ const entry = acc(dataPeer);
1453
+ entry.session_ids.add(session.session_id);
1454
+ entry.fabrication_events += 1;
1455
+ }
1456
+ }
1457
+ const reportByPeer = {};
1458
+ for (const [peer, entry] of Object.entries(byPeer)) {
1459
+ reportByPeer[peer] = {
1460
+ peer,
1461
+ sessions_seen: entry.session_ids.size,
1462
+ results_total: entry.results_total,
1463
+ ready: entry.ready,
1464
+ not_ready: entry.not_ready,
1465
+ needs_evidence: entry.needs_evidence,
1466
+ unresolved_status: entry.unresolved_status,
1467
+ parser_warnings_total: entry.parser_warnings_total,
1468
+ parser_warnings_by_type: entry.parser_warnings_by_type,
1469
+ decision_quality: entry.decision_quality,
1470
+ rejected_total: entry.rejected_total,
1471
+ provider_errors: entry.provider_errors,
1472
+ failures_by_class: entry.failures_by_class,
1473
+ open_asks: entry.open_asks,
1474
+ not_resurfaced_asks: entry.not_resurfaced_asks,
1475
+ addressed_asks: entry.addressed_asks,
1476
+ satisfied_asks: entry.satisfied_asks,
1477
+ deferred_asks: entry.deferred_asks,
1478
+ rejected_asks: entry.rejected_asks,
1479
+ fabrication_events: entry.fabrication_events,
1480
+ avg_latency_ms: entry.latency_count > 0 ? entry.latency_sum / entry.latency_count : null,
1481
+ total_cost_usd: entry.cost_count > 0 ? entry.cost_sum : null,
1482
+ };
1483
+ }
1484
+ return {
1485
+ generated_at: now(),
1486
+ scope: sessionId ? "session" : "all",
1487
+ session_id: sessionId,
1488
+ by_peer: reportByPeer,
1489
+ };
1490
+ }
1277
1491
  // v2.16.0: read-only operational doctor. This is intentionally a
1278
1492
  // reporting surface, not a cleanup tool: it never finalizes, rewrites
1279
1493
  // or deletes sessions. Operators use it after audits to see which
@@ -1349,11 +1563,19 @@ export class SessionStore {
1349
1563
  const maxRoundsSessions = [];
1350
1564
  const selfLeadMetadata = [];
1351
1565
  const openEvidenceSessions = [];
1566
+ const notResurfacedEvidenceSessions = [];
1352
1567
  const grokProviderErrorSessions = [];
1353
1568
  const eventReadErrorSessions = [];
1569
+ const terminalEventMissingSessions = [];
1354
1570
  let eventsTotal = 0;
1355
1571
  let tokenDeltaEvents = 0;
1356
1572
  let tokenCompletedEvents = 0;
1573
+ let realSessions = 0;
1574
+ let stubSessions = 0;
1575
+ let peerCallCostUsd = null;
1576
+ let generationCostUsd = null;
1577
+ let totalCostUsd = null;
1578
+ let terminalEventMissingCount = 0;
1357
1579
  const pushLimited = (target, entry) => {
1358
1580
  if (target.length < cappedLimit)
1359
1581
  target.push(entry);
@@ -1365,7 +1587,18 @@ export class SessionStore {
1365
1587
  const evidenceList = session.evidence_checklist ?? [];
1366
1588
  const openEvidenceItemsList = evidenceList.filter((item) => (item.status ?? "open") === "open");
1367
1589
  const openEvidenceItems = openEvidenceItemsList.length;
1590
+ const notResurfacedEvidenceItems = evidenceList.filter((item) => item.status === "not_resurfaced").length;
1368
1591
  const grokProviderErrors = (session.failed_attempts ?? []).filter((failure) => failure.peer === "grok" && failure.failure_class === "provider_error").length;
1592
+ if (isStubSession(session))
1593
+ stubSessions += 1;
1594
+ else
1595
+ realSessions += 1;
1596
+ peerCallCostUsd = addNullableCost(peerCallCostUsd, sessionPeerCostTotal(session));
1597
+ generationCostUsd = addNullableCost(generationCostUsd, sessionGenerationCostTotal(session));
1598
+ const sessionTotalCost = session.totals.cost.total_cost;
1599
+ if (sessionTotalCost != null && Number.isFinite(sessionTotalCost)) {
1600
+ totalCostUsd = addNullableCost(totalCostUsd, sessionTotalCost);
1601
+ }
1369
1602
  const entry = {
1370
1603
  session_id: session.session_id,
1371
1604
  version: session.version,
@@ -1379,6 +1612,9 @@ export class SessionStore {
1379
1612
  rounds: session.rounds.length,
1380
1613
  updated_at: session.updated_at,
1381
1614
  ...(openEvidenceItems > 0 ? { open_evidence_items: openEvidenceItems } : {}),
1615
+ ...(notResurfacedEvidenceItems > 0
1616
+ ? { not_resurfaced_evidence_items: notResurfacedEvidenceItems }
1617
+ : {}),
1382
1618
  ...(grokProviderErrors > 0 ? { grok_provider_errors: grokProviderErrors } : {}),
1383
1619
  };
1384
1620
  // v2.22.0 (B.P2): drill-down for open-evidence entries. Aggregate
@@ -1419,6 +1655,8 @@ export class SessionStore {
1419
1655
  pushLimited(selfLeadMetadata, entry);
1420
1656
  if (openEvidenceItems > 0)
1421
1657
  pushLimited(openEvidenceSessions, entry);
1658
+ if (notResurfacedEvidenceItems > 0)
1659
+ pushLimited(notResurfacedEvidenceSessions, entry);
1422
1660
  if (grokProviderErrors > 0)
1423
1661
  pushLimited(grokProviderErrorSessions, entry);
1424
1662
  let sessionEvents = [];
@@ -1429,6 +1667,18 @@ export class SessionStore {
1429
1667
  entry.event_read_error = redact(error instanceof Error ? error.message : String(error));
1430
1668
  pushLimited(eventReadErrorSessions, entry);
1431
1669
  }
1670
+ if (session.outcome) {
1671
+ const expectedTerminalEvent = session.control?.status === "cancelled" || session.outcome_reason === "session_cancelled"
1672
+ ? "session.cancelled"
1673
+ : "session.finalized";
1674
+ const hasExpectedTerminalEvent = sessionEvents.some((event) => event.type === expectedTerminalEvent);
1675
+ if (!hasExpectedTerminalEvent) {
1676
+ terminalEventMissingCount += 1;
1677
+ entry.terminal_event_missing = true;
1678
+ entry.terminal_event_expected = expectedTerminalEvent;
1679
+ pushLimited(terminalEventMissingSessions, entry);
1680
+ }
1681
+ }
1432
1682
  for (const event of sessionEvents) {
1433
1683
  eventsTotal += 1;
1434
1684
  if (event.type === "peer.token.delta")
@@ -1464,6 +1714,9 @@ export class SessionStore {
1464
1714
  if (openEvidenceSessions.length > 0) {
1465
1715
  recommendations.push("Address or explicitly terminal-mark open evidence checklist items before expecting convergence.");
1466
1716
  }
1717
+ if (notResurfacedEvidenceSessions.length > 0) {
1718
+ recommendations.push("`not_resurfaced` evidence items are inference-only; review them separately from satisfied/deferred/rejected items.");
1719
+ }
1467
1720
  if (grokProviderErrorSessions.length > 0) {
1468
1721
  recommendations.push("Run a Grok-specific smoke/probe for sessions with grok provider errors before relying on Grok in release gates.");
1469
1722
  }
@@ -1473,21 +1726,32 @@ export class SessionStore {
1473
1726
  if (eventsTotal > 0 && tokenDeltaEvents / eventsTotal > 0.5) {
1474
1727
  recommendations.push("Token delta events dominate this corpus; increase CROSS_REVIEW_TOKEN_DELTA_CHARS_THRESHOLD or disable token streaming for low-noise audits.");
1475
1728
  }
1729
+ if (terminalEventMissingCount > 0) {
1730
+ recommendations.push("Terminal outcome metadata exists without matching terminal events; treat as legacy/event-gap evidence and inspect before relying on event-only analytics.");
1731
+ }
1476
1732
  return {
1477
1733
  generated_at: now(),
1478
1734
  scope: "all",
1479
1735
  limit: cappedLimit,
1480
1736
  totals: {
1481
1737
  sessions: sessions.length,
1738
+ real_sessions: realSessions,
1739
+ stub_sessions: stubSessions,
1482
1740
  open: sessions.filter((session) => !session.outcome).length,
1483
- stale: sessions.filter((session) => session.convergence_health?.state === "stale").length,
1484
- blocked: sessions.filter((session) => session.convergence_health?.state === "blocked")
1485
- .length,
1741
+ stale: sessions.filter((session) => !session.outcome && session.convergence_health?.state === "stale").length,
1742
+ blocked: sessions.filter((session) => !session.outcome && session.convergence_health?.state === "blocked").length,
1486
1743
  max_rounds: sessions.filter((session) => session.outcome === "max-rounds").length,
1487
1744
  self_lead_metadata: selfLeadCount,
1488
1745
  open_evidence_sessions: sessions.filter((session) => (session.evidence_checklist ?? []).some((item) => (item.status ?? "open") === "open")).length,
1746
+ not_resurfaced_evidence_sessions: sessions.filter((session) => (session.evidence_checklist ?? []).some((item) => item.status === "not_resurfaced")).length,
1489
1747
  grok_provider_error_sessions: sessions.filter((session) => (session.failed_attempts ?? []).some((failure) => failure.peer === "grok" && failure.failure_class === "provider_error")).length,
1490
1748
  event_read_error_sessions: eventReadErrorSessions.length,
1749
+ terminal_event_missing_sessions: terminalEventMissingCount,
1750
+ },
1751
+ cost_breakdown: {
1752
+ total_cost_usd: totalCostUsd,
1753
+ peer_call_cost_usd: peerCallCostUsd,
1754
+ generation_cost_usd: generationCostUsd,
1491
1755
  },
1492
1756
  findings: {
1493
1757
  open_sessions: openSessions,
@@ -1499,8 +1763,10 @@ export class SessionStore {
1499
1763
  // in `totals.self_lead_metadata`.
1500
1764
  self_lead_metadata: includeLegacy ? selfLeadMetadata : [],
1501
1765
  open_evidence_sessions: openEvidenceSessions,
1766
+ not_resurfaced_evidence_sessions: notResurfacedEvidenceSessions,
1502
1767
  grok_provider_error_sessions: grokProviderErrorSessions,
1503
1768
  event_read_error_sessions: eventReadErrorSessions,
1769
+ terminal_event_missing_sessions: terminalEventMissingSessions,
1504
1770
  },
1505
1771
  event_noise: {
1506
1772
  events_total: eventsTotal,
@@ -1793,17 +2059,30 @@ export class SessionStore {
1793
2059
  continue;
1794
2060
  const finalized = await this.withSessionLock(session.session_id, async () => {
1795
2061
  const current = this.read(session.session_id);
2062
+ const ts = now();
1796
2063
  current.outcome = outcome;
1797
2064
  current.outcome_reason = reason;
1798
2065
  delete current.in_flight;
1799
2066
  current.convergence_health = {
1800
2067
  state: "stale",
1801
- last_event_at: now(),
2068
+ last_event_at: ts,
1802
2069
  detail: reason,
1803
2070
  idle_ms: idleFor,
1804
2071
  };
1805
- current.updated_at = now();
2072
+ current.updated_at = ts;
1806
2073
  await writeJson(this.metaPath(session.session_id), current);
2074
+ try {
2075
+ this.appendEventRecord({
2076
+ type: "session.finalized",
2077
+ session_id: session.session_id,
2078
+ ts,
2079
+ message: `Session finalized as ${outcome}${reason ? `: ${reason}` : ""}`,
2080
+ data: { outcome, reason, idle_ms: idleFor },
2081
+ });
2082
+ }
2083
+ catch {
2084
+ /* event persistence is best-effort; session_doctor will flag gaps */
2085
+ }
1807
2086
  return current;
1808
2087
  });
1809
2088
  swept.push(finalized);