if (this.#intervalHandle) { lazy.clearInterval(this.#intervalHandle); this.#intervalHandle = 0; } } /** * Places "page-visited" observer callback. * * Increments the internal counter of pages visited since the last * successful memory generation run. */ #onPageVisited = () => { this.#pagesVisited++; }; /** * Periodic interval handler. * * - Skips if the scheduler is destroyed or already running. * - Skips if the minimum pages-visited threshold is not met. * - Computes history drift metrics and decides whether to run memories. * - Invokes {@link lazy.MemoriesManager.generateMemoriesFromBrowsingHistory} * when appropriate. * * @private * @returns {Promise} Resolves once the interval run completes. */ #onInterval = async () => { if (this.#destroyed) { lazy.console.warn( "[MemoriesHistoryScheduler] Interval fired after destroy; ignoring." ); return; } if (this.#running) { lazy.console.debug( "[MemoriesHistoryScheduler] Skipping run because a previous run is still in progress." ); return; } this.#running = true; this.#stopInterval(); try { // Detect whether generated history memories were before. const lastMemoryTs = (await lazy.MemoriesManager.getLastHistoryMemoryTimestamp()) ?? 0; const isFirstRun = lastMemoryTs === 0; const minPagesThreshold = isFirstRun ? INITIAL_MEMORIES_PAGES_THRESHOLD : MEMORIES_SCHEDULER_PAGES_THRESHOLD; if (this.#pagesVisited < minPagesThreshold) { lazy.console.debug( `[MemoriesHistoryScheduler] Not enough pages visited (${this.#pagesVisited}/${minPagesThreshold}); ` + `skipping analysis. isFirstRun=${isFirstRun}` ); return; } if (!isFirstRun) { lazy.console.debug( "[MemoriesHistoryScheduler] Computing history drift metrics before running memories..." ); const { baselineMetrics, deltaMetrics, trigger } = await lazy.MemoriesDriftDetector.computeHistoryDriftAndTrigger({ triggerQuantile: lazy.DRIFT_TRIGGER_QUANTILE, evalDeltaCount: lazy.DRIFT_EVAL_DELTA_COUNT, }); if (!baselineMetrics.length || !deltaMetrics.length) { lazy.console.debug( "[MemoriesHistoryScheduler] Drift metrics incomplete (no baseline or delta); falling back to non-drift scheduling." ); } else if (!trigger.triggered) { lazy.console.debug( "[MemoriesHistoryScheduler] History drift below threshold; skipping memories run for this interval." ); // Reset pages so we don’t repeatedly attempt with the same data. this.#pagesVisited = 0; return; } else { lazy.console.debug( `[MemoriesHistoryScheduler] Drift triggered (jsThreshold=${trigger.jsThreshold.toFixed(4)}, ` + `surpriseThreshold=${trigger.surpriseThreshold.toFixed(4)}); sessions=${trigger.triggeredSessionIds.join( "," )}` ); } } lazy.console.debug( `[MemoriesHistoryScheduler] Generating memories from history with ${this.#pagesVisited} new pages` ); await lazy.MemoriesManager.generateMemoriesFromBrowsingHistory(); this.#pagesVisited = 0; lazy.console.debug( "[MemoriesHistoryScheduler] History memories generation complete." ); } catch (error) { lazy.console.error( "[MemoriesHistoryScheduler] Failed to generate history memories", error ); } finally { if (!this.#destroyed) { this.#startInterval(); } this.#running = false; } }; /** * Cleans up scheduler resources. * * Stops the interval, unsubscribes from Places notifications, * and marks the scheduler as destroyed so future interval ticks * are ignored. */ destroy() { this.#stopInterval(); lazy.PlacesUtils.observers.removeListener( ["page-visited"], this.#onPageVisited ); this.#destroyed = true; lazy.console.debug("[MemoriesHistoryScheduler] Destroyed"); } /** * Testing helper: set pagesVisited count. * Not used in production code. * * @param {number} count */ setPagesVisitedForTesting(count) { this.#pagesVisited = count; } /** * Testing helper: runs the interval handler once immediately. * Not used in production code. */ async runNowForTesting() { await this.#onInterval(); } } PK