diff --git a/src/McpServer/autopilot-monitor-mcp/src/__tests__/session-coverage.test.ts b/src/McpServer/autopilot-monitor-mcp/src/__tests__/session-coverage.test.ts index 662d9c87..2bdc1cad 100644 --- a/src/McpServer/autopilot-monitor-mcp/src/__tests__/session-coverage.test.ts +++ b/src/McpServer/autopilot-monitor-mcp/src/__tests__/session-coverage.test.ts @@ -132,7 +132,7 @@ describe('buildSessionCoverage — IME log tracker', () => { ev('ime_tracker_degraded', 50, { file: 'AppWorkload.log', lineBudgetBreaks: 1, regexTimeouts: 0, oversizedLines: 0 }), ev('ime_pattern_hits', 900, { linesRead: 12000, lineBudgetBreaks: 7, regexTimeouts: 2, oversizedLines: 1 }, { severity: 'Debug' }), ]); - expect(c.imeTracker.sessionTotals).toEqual({ linesRead: 12000, lineBudgetBreaks: 7, regexTimeouts: 2, oversizedLines: 1 }); + expect(c.imeTracker.sessionTotals).toEqual({ linesRead: 12000, lineBudgetBreaks: 7, regexTimeouts: 2, oversizedLines: 1, overwriteRewinds: null, overwriteBytesReprocessed: null }); expect(c.gaps[0]).toContain('skipped the remaining patterns on 7 line(s)'); expect(c.gaps[0]).toContain('gave up on 2 line(s) after a regex timeout'); expect(c.gaps[0]).toContain('dropped 1 oversized line(s)'); @@ -143,9 +143,30 @@ describe('buildSessionCoverage — IME log tracker', () => { ev('agent_started', 0), ev('ime_pattern_hits', 900, { linesRead: 800, lineBudgetBreaks: 0, regexTimeouts: 0, oversizedLines: 0 }), ]); - expect(c.imeTracker).toEqual({ degraded: false, sessionTotals: { linesRead: 800, lineBudgetBreaks: 0, regexTimeouts: 0, oversizedLines: 0 } }); + expect(c.imeTracker).toEqual({ degraded: false, sessionTotals: { linesRead: 800, lineBudgetBreaks: 0, regexTimeouts: 0, oversizedLines: 0, overwriteRewinds: null, overwriteBytesReprocessed: null } }); expect(c.gaps).toEqual([]); }); + + it('reports re-read overwritten log blocks as recovered data — a note, never a gap or degradation', () => { + // Agents ≥ 2.0.1458: IME processes overwrite each other's bytes in AgentExecutor.log and + // IntuneManagementExtension.log; the tracker re-reads the changed blocks and counts them. + const c = buildSessionCoverage(SESSION, [ + ev('agent_started', 0), + ev('ime_pattern_hits', 900, { linesRead: 8957, lineBudgetBreaks: 0, regexTimeouts: 0, oversizedLines: 0, overwriteRewinds: 6, overwriteBytesReprocessed: 5120 }, { severity: 'Debug' }), + ]); + expect(c.imeTracker.degraded).toBe(false); + expect(c.imeTracker.sessionTotals).toEqual({ linesRead: 8957, lineBudgetBreaks: 0, regexTimeouts: 0, oversizedLines: 0, overwriteRewinds: 6, overwriteBytesReprocessed: 5120 }); + expect(c.imeTracker.note).toBe('6 log block(s) a concurrent IME process had overwritten were re-read (5 KB) — script results and exit codes in them are complete, not missing.'); + expect(c.gaps).toEqual([]); + }); + + it('adds no note when nothing was overwritten or the agent predates the counter', () => { + const c = buildSessionCoverage(SESSION, [ + ev('agent_started', 0), + ev('ime_pattern_hits', 900, { linesRead: 800, lineBudgetBreaks: 0, regexTimeouts: 0, oversizedLines: 0, overwriteRewinds: 0, overwriteBytesReprocessed: 0 }), + ]); + expect(c.imeTracker.note).toBeUndefined(); + }); }); describe('buildSessionCoverage — collectors, upload, disk', () => { diff --git a/src/McpServer/autopilot-monitor-mcp/src/session-coverage.ts b/src/McpServer/autopilot-monitor-mcp/src/session-coverage.ts index f373dab4..ccee2eb6 100644 --- a/src/McpServer/autopilot-monitor-mcp/src/session-coverage.ts +++ b/src/McpServer/autopilot-monitor-mcp/src/session-coverage.ts @@ -46,6 +46,7 @@ export const COVERAGE_EVENT_FIELDS = 'data.bootToAgentStartSeconds,data.agentUptimeSeconds,data.outcome,data.note,' + 'data.earliestRejectedSourceTimestamp,' + 'data.file,data.firstSkippedPattern,data.lineBudgetBreaks,data.regexTimeouts,data.oversizedLines,data.unanchoredPatterns,data.linesRead,' + + 'data.overwriteRewinds,data.overwriteBytesReprocessed,' + 'data.collector,data.reason,data.errorType,' + 'data.pendingItemCount,data.pendingBytes,data.kind,data.itemCount,data.disk_free_gb,' + 'data.truncated,data.includedFiles,data.includedBytes,data.skippedFiles,data.skippedByReason,data.problemsByKind,' + @@ -87,7 +88,15 @@ export interface ImeTrackerCoverage { lineBudgetBreaks: number | null; regexTimeouts: number | null; oversizedLines: number | null; + /** + * Times the tracker re-read a log block a concurrent IME process had written over bytes + * it had already read (agents ≥ 2.0.1458). Recovered data — never a gap and not degradation. + */ + overwriteRewinds: number | null; + overwriteBytesReprocessed: number | null; }; + /** Present when overwriteRewinds > 0: says in words that those blocks were recovered, not lost. */ + note?: string; } export interface CollectorDegradation { @@ -276,8 +285,20 @@ export function buildSessionCoverage( lineBudgetBreaks: num(hits.data?.lineBudgetBreaks), regexTimeouts: num(hits.data?.regexTimeouts), oversizedLines: num(hits.data?.oversizedLines), + overwriteRewinds: num(hits.data?.overwriteRewinds), + overwriteBytesReprocessed: num(hits.data?.overwriteBytesReprocessed), } : undefined; + // IME processes append to AgentExecutor.log and IntuneManagementExtension.log with independent + // stream positions and overwrite each other; the tracker re-reads what changed. That is + // recovered data — it must never read as a gap or as a degraded tracker. + const rewinds = sessionTotals?.overwriteRewinds ?? 0; + const overwriteNote = + rewinds > 0 + ? `${rewinds} log block(s) a concurrent IME process had overwritten were re-read` + + `${sessionTotals?.overwriteBytesReprocessed ? ` (${Math.round(sessionTotals.overwriteBytesReprocessed / 1024)} KB)` : ''}` + + ' — script results and exit codes in them are complete, not missing.' + : undefined; let imeTracker: ImeTrackerCoverage; if (degradedEv) { imeTracker = { @@ -309,6 +330,7 @@ export function buildSessionCoverage( } else { imeTracker = { degraded: false, ...(sessionTotals ? { sessionTotals } : {}) }; } + if (overwriteNote) imeTracker.note = overwriteNote; // ── Collectors ────────────────────────────────────────────────────────────── const collectors: CollectorDegradation[] = []; diff --git a/src/Web/autopilot-monitor-web/app/sessions/components/DeviceDetailsCard.tsx b/src/Web/autopilot-monitor-web/app/sessions/components/DeviceDetailsCard.tsx index 92f34495..4b1371f5 100644 --- a/src/Web/autopilot-monitor-web/app/sessions/components/DeviceDetailsCard.tsx +++ b/src/Web/autopilot-monitor-web/app/sessions/components/DeviceDetailsCard.tsx @@ -220,6 +220,10 @@ export default function DeviceDetailsCard({ events, latestAgentVersion, session ]; const imeSkipParts = imeSkipCounters.filter(([, v]) => Number(v) > 0).map(([k, v]) => `${v} ${k}`); const imeSkips = imeSkipParts.length; + // Log blocks a concurrent Intune process wrote over after the agent had read them, re-read by + // the agent (agents ≥ 2.0.1458). Recovered data — shown apart from "Skipped work" on purpose. + const imeOverwriteRewinds = patternHits?.overwriteRewinds ?? imeTracker?.ime_overwrite_rewinds; + const imeOverwriteBytes = Number(patternHits?.overwriteBytesReprocessed ?? imeTracker?.ime_overwrite_bytes_reprocessed ?? 0); const apiLatencyMs = session?.avgApiLatencyMs ?? 0; const hasConnectivity = apiLatencyMs > 0 || bandwidthEstimate || metricsSnapshot; @@ -676,6 +680,17 @@ export default function DeviceDetailsCard({ events, latestAgentVersion, session ? "Matching was cut short on some log lines — pattern coverage may be incomplete for this session. Nothing to fix on your side; use Report Session if the stats look wrong." : undefined} /> + {imeOverwriteRewinds !== undefined && ( + 0 + ? `${Number(imeOverwriteRewinds)}${imeOverwriteBytes > 0 ? ` (${formatBytesCompact(imeOverwriteBytes)} re-read)` : ""}` + : "none"} + title={Number(imeOverwriteRewinds) > 0 + ? "Intune processes wrote over log lines the agent had already read; the agent re-read them, so script results and exit codes in those blocks are complete. Nothing to fix on your side." + : undefined} + /> + )} {(patternHits?.unanchoredPatterns ?? imeTracker?.ime_unanchored_patterns) !== undefined && Number(patternHits?.unanchoredPatterns ?? imeTracker?.ime_unanchored_patterns) > 0 && ( )}