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) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-05-06 13:23:23 +02:00
parent b86af63055
commit a35726dcf0
2 changed files with 50 additions and 14 deletions
@@ -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;
}
@@ -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
});
});