From a35726dcf05e6b8979ed014208a1db2143c44d59 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 6 May 2026 13:23:23 +0200 Subject: [PATCH] fix(web): split WhiteGlove timeline at Part-2 agent_started, not at whiteglove_resumed The post-reseal agent_started + its agent_version_check are part of the User Enrollment block in the user's mental model, but the resumed-marker-based split kept them inside Part 1 because whiteglove_resumed is emitted ~0-2s after the boot. Walk back from whiteglove_resumed to the agent_started that sits between Part-1 close and resume, and split there. Falls back to the resumed marker when the boot event isn't present. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../[sessionId]/utils/eventHelpers.ts | 27 ++++++++++++-- .../lib/__tests__/whiteGloveSplit.test.ts | 37 ++++++++++++++----- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/utils/eventHelpers.ts b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/utils/eventHelpers.ts index d740b10d..7118b871 100644 --- a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/utils/eventHelpers.ts +++ b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/utils/eventHelpers.ts @@ -7,10 +7,14 @@ import { EnrollmentEvent } from "@/types"; // (Part 1 still in progress, or session is not WhiteGlove at all). // // Resolution order — mirrors the agent-side V1-symmetric resume mechanic (PR-A): -// 1. `whiteglove_resumed` is the definitive Part 2 marker (emitted by the orchestrator -// after Archive-and-Reset detects WhiteGloveSealed snapshot). Returns its sequence - 1. -// 2. Fallback for older agents that never emit `whiteglove_resumed`: the first -// `agent_started` AFTER `whiteglove_complete` is the Part 2 boot. Returns its sequence - 1. +// 1. `whiteglove_resumed` indicates a Part-2 resume happened. The boundary the user +// perceives is the Part-2 boot itself, not the resumed marker that follows it +// ~0–2s later. Look for the `agent_started` that sits between whiteglove_complete +// and whiteglove_resumed and split THERE; this keeps the post-reseal boot +// (agent_started + its agent_version_check) inside the User Enrollment block. +// Falls back to `resumed.sequence - 1` if no agent_started is found in between. +// 2. Older agents that never emit `whiteglove_resumed`: the first `agent_started` +// AFTER `whiteglove_complete` is the Part 2 boot. Returns its sequence - 1. // 3. Pre-provisioning only (no Part 2 yet): the last cleanup event after `whiteglove_complete` // (`agent_shutdown`) ends Part 1; otherwise the `whiteglove_complete` sequence itself. // 4. Nothing: -1. @@ -19,6 +23,21 @@ export function computeWhiteGloveSplitSequence(events: EnrollmentEvent[]): numbe const resumedEvent = events.find(e => e.eventType === "whiteglove_resumed"); if (resumedEvent) { + // Lower bound = whiteglove_complete iff it precedes resumed; in the race case + // (Windows writes whiteglove_complete AFTER the Part-2 reboot) we drop the bound to 0. + const lowerBound = (wgEvent && wgEvent.sequence < resumedEvent.sequence) + ? wgEvent.sequence + : 0; + // The most recent agent_started before whiteglove_resumed is the post-reseal Part-2 boot. + // Sort candidates desc so we don't depend on the input being pre-sorted by sequence. + const part2Boot = events + .filter(e => + e.eventType === "agent_started" && + e.sequence > lowerBound && + e.sequence < resumedEvent.sequence + ) + .sort((a, b) => b.sequence - a.sequence)[0]; + if (part2Boot) return part2Boot.sequence - 1; return resumedEvent.sequence - 1; } diff --git a/src/Web/autopilot-monitor-web/lib/__tests__/whiteGloveSplit.test.ts b/src/Web/autopilot-monitor-web/lib/__tests__/whiteGloveSplit.test.ts index 7bf177dd..ad81ee11 100644 --- a/src/Web/autopilot-monitor-web/lib/__tests__/whiteGloveSplit.test.ts +++ b/src/Web/autopilot-monitor-web/lib/__tests__/whiteGloveSplit.test.ts @@ -27,18 +27,34 @@ describe("computeWhiteGloveSplitSequence", () => { expect(computeWhiteGloveSplitSequence(events)).toBe(-1); }); - it("returns whiteglove_resumed.sequence-1 when the agent emitted the V1-symmetric Part 2 marker", () => { - // PR-A: orchestrator archives state and emits whiteglove_resumed after Part-1 reseal. + it("splits at the Part-2 agent_started that sits between whiteglove_complete and whiteglove_resumed", () => { + // PR-A: orchestrator archives state and emits whiteglove_resumed ~0-2s after the + // Part-2 agent_started boot. The user-perceived boundary is the boot itself, so the + // post-reseal agent_started + its agent_version_check belong to the User Enrollment block. const events: EnrollmentEvent[] = [ ev(1, "agent_started"), ev(2, "whiteglove_complete"), ev(3, "agent_shutdown"), - ev(4, "agent_started"), // Part 2 boot - ev(5, "whiteglove_resumed"), // definitive Part 2 marker (post Archive-and-Reset) + ev(4, "agent_started"), // Part 2 boot — the actual split point + ev(5, "whiteglove_resumed"), // resumed marker arrives moments later ev(6, "esp_phase_changed"), ev(7, "enrollment_complete"), ]; - expect(computeWhiteGloveSplitSequence(events)).toBe(4); // 5 - 1 + expect(computeWhiteGloveSplitSequence(events)).toBe(3); // 4 - 1 + }); + + it("falls back to whiteglove_resumed.sequence-1 when no agent_started is found between Part-1 close and resume", () => { + // Defensive: if the Part-2 boot event somehow isn't in the list (replay/filter), keep + // the resumed marker as the boundary so Part-2 events still group correctly. + const events: EnrollmentEvent[] = [ + ev(1, "agent_started"), + ev(2, "whiteglove_complete"), + ev(3, "agent_shutdown"), + ev(4, "whiteglove_resumed"), + ev(5, "esp_phase_changed"), + ev(6, "enrollment_complete"), + ]; + expect(computeWhiteGloveSplitSequence(events)).toBe(3); // 4 - 1 }); it("falls back to first agent_started after whiteglove_complete when no whiteglove_resumed (older agents)", () => { @@ -70,17 +86,18 @@ describe("computeWhiteGloveSplitSequence", () => { expect(computeWhiteGloveSplitSequence(events)).toBe(2); }); - it("prefers whiteglove_resumed over the agent_started fallback even if both are present", () => { + it("splits at agent_started even when whiteglove_complete arrives late (race condition)", () => { // Race: Windows writes the WhiteGlove success event after the Part-2 reboot, so the - // event list contains both signals. The resumed marker is authoritative. + // event list contains both signals. The Part-2 boot still is the boundary the user + // perceives — whiteglove_complete then naturally lands inside the User Enrollment block. const events: EnrollmentEvent[] = [ ev(1, "agent_started"), ev(2, "agent_shutdown"), - ev(3, "agent_started"), // Part 2 boot — would be the fallback split - ev(4, "whiteglove_resumed"), // primary trigger wins + ev(3, "agent_started"), // Part 2 boot — split point + ev(4, "whiteglove_resumed"), ev(5, "whiteglove_complete"), // arrived late from Windows ev(6, "enrollment_complete"), ]; - expect(computeWhiteGloveSplitSequence(events)).toBe(3); // 4 - 1, NOT 2 + expect(computeWhiteGloveSplitSequence(events)).toBe(2); // 3 - 1 }); });