Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
import {
Monitor,
monitorAgeMs,
trimAndFilterWatchUrls,
TOTAL_NUM_MONITORS,
MONITOR_PROMPT_VERSION,
MONITOR_AGENTS_CHANGED_TOPIC,
} from "moz-src:///browser/components/aiwindow/models/agents/Monitor.sys.mjs";
import { Schedule } from "moz-src:///browser/components/aiwindow/models/agents/Schedule.sys.mjs";
export {
Monitor,
TOTAL_NUM_MONITORS,
MAX_HISTORY_ENTRIES,
MONITOR_PROMPT_VERSION,
TOTAL_NUM_URLS_IN_MONITOR,
MONITOR_AGENTS_CHANGED_TOPIC,
} from "moz-src:///browser/components/aiwindow/models/agents/Monitor.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
MonitorStore:
"moz-src:///browser/components/aiwindow/models/agents/MonitorStore.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "log", () =>
console.createInstance({
prefix: "MonitorAgent",
maxLogLevelPref: "browser.smartwindow.monitorAgent.logLevel",
})
);
let gMonitors = null;
let gLoadPromise = null;
function monitorTelemetryExtra(monitor) {
return {
monitors: gMonitors?.size ?? 0,
urls: monitor.watchUrls.length,
length: monitor.monitorPrompt.length,
age: monitorAgeMs(monitor),
schedule_type: monitor.schedule.type,
prompt_version: MONITOR_PROMPT_VERSION,
enabled: monitor.enabled,
};
}
/**
* Monitor Agent state and behavior.
*
* Monitor agent is responsible for managing the collection of monitors, including creating,
* updating, deleting, and running them on a schedule.
* It also handles persisting the monitor state to IndexedDB and notifying observers of changes.
*/
export const MonitorAgent = {
async init() {
await this._ensureLoaded();
for (const monitor of gMonitors.values()) {
monitor.restore();
monitor.scheduleNextRun();
}
},
uninit() {
if (!gMonitors) {
return;
}
for (const monitor of gMonitors.values()) {
monitor.dispose();
}
},
async listMonitors() {
await this._ensureLoaded();
return Array.from(gMonitors.values(), monitor => monitor.toSerializable());
},
async createMonitor({ prompt, watchUrls, pageTitle = "", schedule }) {
await this._ensureLoaded();
if (gMonitors.size >= TOTAL_NUM_MONITORS) {
throw new Error(
`Cannot create more than ${TOTAL_NUM_MONITORS} monitors.`
);
}
const monitor = new Monitor({
title: pageTitle,
monitorPrompt: prompt,
watchUrls: trimAndFilterWatchUrls(watchUrls),
schedule: Schedule.fromJSON(schedule),
});
try {
gMonitors.set(monitor.id, monitor);
await this._saveAndNotify(monitor);
} catch (error) {
gMonitors.delete(monitor.id);
throw error;
}
monitor.scheduleNextRun();
Glean.smartWindow.monitorCreate.record(monitorTelemetryExtra(monitor));
},
async updateMonitor(id, updates) {
await this._ensureLoaded();
const monitor = gMonitors.get(id);
if (!monitor) {
return;
}
const next = {
enabled: monitor.enabled,
monitorPrompt: monitor.monitorPrompt,
nextRunTime: monitor.nextRunTime,
schedule: monitor.schedule,
title: monitor.title,
watchUrls: monitor.watchUrls.slice(),
};
// clean up and validate updates
if ("monitorPrompt" in updates) {
next.monitorPrompt = String(updates.monitorPrompt ?? "").trim();
}
if ("watchUrls" in updates) {
next.watchUrls = trimAndFilterWatchUrls(updates.watchUrls);
}
if ("title" in updates) {
next.title = String(updates.title ?? "").trim();
}
if ("enabled" in updates) {
next.enabled = !!updates.enabled;
}
if ("schedule" in updates) {
next.schedule = Schedule.fromJSON(updates.schedule);
// Base the next run on now rather than a possibly-stale lastRunTime so a
// schedule edit can't resolve to a time in the past and fire immediately.
next.nextRunTime = next.schedule
.getNextRunTime(new Date().toISOString())
.toISOString();
} else if (!monitor.enabled && next.enabled) {
// else if so we don't compute nextRunTime twice
// Re-enabling: schedule the next run a full interval from now rather than
// reusing a stale nextRunTime that may already be in the past.
next.nextRunTime = next.schedule
.getNextRunTime(new Date().toISOString())
.toISOString();
}
if (!next.monitorPrompt || !next.watchUrls.length) {
throw new Error("Monitor is invalid.");
}
// save old in case the update fails, so we can restore it
const previous = {
enabled: monitor.enabled,
monitorPrompt: monitor.monitorPrompt,
nextRunTime: monitor.nextRunTime,
schedule: monitor.schedule,
title: monitor.title,
updatedAt: monitor.updatedAt,
watchUrls: monitor.watchUrls,
};
monitor.enabled = next.enabled;
monitor.monitorPrompt = next.monitorPrompt;
monitor.nextRunTime = next.nextRunTime;
monitor.schedule = next.schedule;
monitor.title = next.title;
monitor.watchUrls = next.watchUrls;
monitor.updatedAt = new Date().toISOString();
try {
await this._saveAndNotify(monitor);
} catch (error) {
Object.assign(monitor, previous);
throw error;
}
monitor.scheduleNextRun();
const telemetryExtra = monitorTelemetryExtra(monitor);
Glean.smartWindow.monitorEdit.record(telemetryExtra);
if (previous.enabled !== monitor.enabled) {
const event = monitor.enabled
? Glean.smartWindow.monitorEnable
: Glean.smartWindow.monitorDisable;
event.record(telemetryExtra);
}
},
async deleteMonitor(id) {
await this._ensureLoaded();
const monitor = gMonitors.get(id);
if (!monitor) {
return false;
}
try {
monitor.dispose();
gMonitors.delete(id);
await lazy.MonitorStore.deleteMonitor(id);
} catch (error) {
monitor.restore();
gMonitors.set(id, monitor);
monitor.scheduleNextRun();
throw error;
} finally {
Services.obs.notifyObservers(null, MONITOR_AGENTS_CHANGED_TOPIC);
}
Glean.smartWindow.monitorDelete.record(monitorTelemetryExtra(monitor));
return true;
},
async runNow(id) {
await this._ensureLoaded();
const monitor = gMonitors.get(id);
if (!monitor) {
return;
}
await monitor.run({ manual: true });
},
async _ensureLoaded() {
if (gMonitors) {
return;
}
if (gLoadPromise) {
await gLoadPromise;
return;
}
gLoadPromise = this._loadMonitors();
try {
await gLoadPromise;
} finally {
gLoadPromise = null;
}
},
async _loadMonitors() {
const monitors = new Map();
for (const savedMonitor of await lazy.MonitorStore.listMonitors()) {
if (monitors.size >= TOTAL_NUM_MONITORS) {
break;
}
try {
const monitor = Monitor.fromJSON(savedMonitor);
monitors.set(monitor.id, monitor);
} catch (error) {
lazy.log.warn(`Skipping invalid stored monitor: ${error.message}`);
}
}
gMonitors = monitors;
},
async _saveAndNotify(monitor = null) {
await this._ensureLoaded();
// persisting a single known monitor avoids rewriting every monitor
if (monitor && gMonitors.has(monitor.id)) {
await lazy.MonitorStore.saveMonitor(monitor);
// bulk path is only needed for migration and unscoped saves
} else {
await lazy.MonitorStore.saveMonitors(Array.from(gMonitors.values()));
}
Services.obs.notifyObservers(null, MONITOR_AGENTS_CHANGED_TOPIC);
},
_unloadForTesting() {
this.uninit();
gMonitors = null;
gLoadPromise = null;
},
async _resetForTesting() {
this._unloadForTesting();
await lazy.MonitorStore.destroyDatabase();
},
_monitorCountForTelemetry() {
return gMonitors?.size ?? 0;
},
};