Menu
popagent
publicLatest change 7f0ff66d6d9fb6468416c58bee46bd3d08169501 - Checkpoint browser channels and memory work by AkurAI Build
import type { KeyboardEventParams, MouseEventParams, ScreencastOptions, ScreencastStream } from "@mastra/core/browser";
import type { BrowserSession } from "./api-types";
export type ManagedBrowserState = {
currentUrl: string | null;
tabs: Array<{ id: string; url: string; title: string }>;
activeTabIndex: number;
};
export type ManagedBrowser = {
getState(threadId?: string): Promise<ManagedBrowserState | null>;
closeThreadSession(threadId: string): Promise<void>;
startScreencast(options?: ScreencastOptions): Promise<ScreencastStream>;
injectMouseEvent(event: MouseEventParams, threadId?: string): Promise<void>;
injectKeyboardEvent(event: KeyboardEventParams, threadId?: string): Promise<void>;
};
type Entry = {
id: string;
threadId: string;
access: "interactive" | "read-only";
browser: ManagedBrowser;
createdAt: number;
lastActivityAt: number;
takeover: boolean;
};
export class BrowserSessionRegistry {
private entries = new Map<string, Entry>();
private maxSessions = 4;
private idleTimeoutMs = 900_000;
constructor(private readonly now: () => number = Date.now) {}
configure(settings: { maxSessions: number; idleTimeoutMs: number }) {
if (!Number.isInteger(settings.maxSessions) || settings.maxSessions < 1) throw new RangeError("maxSessions must be positive");
if (!Number.isFinite(settings.idleTimeoutMs) || settings.idleTimeoutMs < 1) throw new RangeError("idleTimeoutMs must be positive");
this.maxSessions = settings.maxSessions;
this.idleTimeoutMs = settings.idleTimeoutMs;
}
async touch(threadId: string, access: "interactive" | "read-only", browser: ManagedBrowser) {
const id = `${access}:${threadId}`;
const existing = this.entries.get(id);
if (existing) {
existing.lastActivityAt = this.now();
existing.browser = browser;
return;
}
while (this.entries.size >= this.maxSessions) {
const oldest = [...this.entries.values()].sort((left, right) => left.lastActivityAt - right.lastActivityAt)[0];
if (!oldest) break;
await this.close(oldest.id);
}
const now = this.now();
this.entries.set(id, { id, threadId, access, browser, createdAt: now, lastActivityAt: now, takeover: false });
}
async list(threadId?: string): Promise<BrowserSession[]> {
const entries = [...this.entries.values()]
.filter((entry) => threadId === undefined || entry.threadId === threadId)
.sort((left, right) => right.lastActivityAt - left.lastActivityAt);
return Promise.all(entries.map(async (entry) => {
let state: ManagedBrowserState | null = null;
let status: BrowserSession["status"] = "active";
try {
state = await entry.browser.getState(entry.threadId);
} catch {
status = "error";
}
return {
id: entry.id,
threadId: entry.threadId,
access: entry.access,
status,
currentUrl: state?.currentUrl ?? null,
tabs: state?.tabs ?? [],
activeTabIndex: state?.activeTabIndex ?? 0,
createdAt: new Date(entry.createdAt).toISOString(),
lastActivityAt: new Date(entry.lastActivityAt).toISOString(),
};
}));
}
async close(id: string): Promise<boolean> {
const entry = this.entries.get(id);
if (!entry) return false;
this.entries.delete(id);
await entry.browser.closeThreadSession(entry.threadId);
return true;
}
async closeAll(): Promise<number> {
const entries = [...this.entries.values()];
await Promise.all(entries.map((entry) => this.close(entry.id)));
return entries.length;
}
async closeThread(threadId: string): Promise<number> {
const matches = [...this.entries.values()].filter((entry) => entry.threadId === threadId);
await Promise.all(matches.map((entry) => this.close(entry.id)));
return matches.length;
}
async cleanupIdle(): Promise<number> {
const threshold = this.now() - this.idleTimeoutMs;
const expired = [...this.entries.values()].filter((entry) => entry.lastActivityAt < threshold);
await Promise.all(expired.map((entry) => this.close(entry.id)));
return expired.length;
}
setTakeover(id: string, enabled: boolean): boolean {
const entry = this.entries.get(id);
if (!entry || entry.access !== "interactive") return false;
entry.takeover = enabled;
return true;
}
async startScreencast(id: string): Promise<ScreencastStream> {
const entry = this.entries.get(id);
if (!entry) throw new Error("Browser session not found");
entry.lastActivityAt = this.now();
return entry.browser.startScreencast({ threadId: entry.threadId });
}
async injectMouse(id: string, event: MouseEventParams): Promise<void> {
const entry = this.takeoverEntry(id);
await entry.browser.injectMouseEvent(event, entry.threadId);
entry.lastActivityAt = this.now();
}
async injectKeyboard(id: string, event: KeyboardEventParams): Promise<void> {
const entry = this.takeoverEntry(id);
await entry.browser.injectKeyboardEvent(event, entry.threadId);
entry.lastActivityAt = this.now();
}
private takeoverEntry(id: string): Entry {
const entry = this.entries.get(id);
if (!entry) throw new Error("Browser session not found");
if (!entry.takeover || entry.access !== "interactive") throw new Error("Browser takeover is not enabled");
return entry;
}
}