/** * Navigation safety (§41): keep recent page fingerprints / urls / actions and detect A→B→A→B patterns. */ export class LoopDetector { private fingerprints: string[] = []; private actions: string[] = []; constructor(private readonly window = 12) {} record(pageFingerprint: string, actionKey: string): void { this.fingerprints.push(pageFingerprint); this.actions.push(actionKey); if (this.fingerprints.length > this.window) this.fingerprints.shift(); if (this.actions.length > this.window) this.actions.shift(); } /** True when the last four page states alternate (A B A B) or the same page repeats 4×. */ detect(): { loop: boolean; pattern?: string } { const f = this.fingerprints; if (f.length >= 4) { const [a, b, c, d] = f.slice(-4); if (a === c && b === d && a !== b) return { loop: true, pattern: "ABAB" }; if (a === b && b === c && c === d) return { loop: true, pattern: "AAAA" }; } const acts = this.actions.slice(-6); if (acts.length === 6 && new Set(acts).size === 1 && !acts[0]!.startsWith("SCROLL_DOWN")) return { loop: true, pattern: "same_action×6" }; return { loop: false }; } recentActionTypes(): string[] { return this.actions.map((a) => a.split(":")[0]!); } }