import type { Page } from "@playwright/test"; import { daemonWsRoutePattern, wsRoutePatternForPort } from "./daemon-port"; type WebSocketMessage = string | Buffer; interface CreatedAgentTimelineGate { release(): void; waitForCreatedAgent(): Promise; waitForDelayedResponse(): Promise; waitForForwardedResponse(): Promise; } export interface AgentTimelineResponseGate { release(): void; waitForDelayedResponse(): Promise; } export interface OlderTimelinePagesGate { getRequestCount(): number; getRepeatedEntryCount(): number; getOwnedEntryCountContaining(text: string): number; releasePage(pageNumber: number): void; waitForRequestCount(count: number): Promise; } export interface DaemonHydrationGate { release(): void; } export interface PromptJumpRequestTracker { requests(): Array<{ cursorSeq: number | null; limit: number | null; mergeWindow: boolean }>; } export async function trackPromptJumpRequests( page: Page, agentId: string, ): Promise { const seen: Array<{ cursorSeq: number | null; limit: number | null; mergeWindow: boolean }> = []; await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { const server = ws.connectToServer(); ws.onMessage((message) => { const sessionMessage = getSessionMessage(message); if ( sessionMessage?.type === "fetch_agent_timeline_request" && sessionMessage.agentId === agentId && sessionMessage.direction === "before" && sessionMessage.mergeWindow === true ) { const cursor = sessionMessage.cursor as { seq?: unknown } | undefined; seen.push({ cursorSeq: typeof cursor?.seq === "number" ? cursor.seq : null, limit: typeof sessionMessage.limit === "number" ? sessionMessage.limit : null, mergeWindow: true, }); } server.send(message); }); server.onMessage((message) => ws.send(message)); }); return { requests: () => [...seen], }; } export interface BootstrapTimelineGate extends AgentTimelineResponseGate { releaseCatchUp(): void; waitForDelayedCatchUp(): Promise; } function parseWebSocketJson(message: WebSocketMessage): unknown { const rawMessage = typeof message === "string" ? message : message.toString("utf8"); try { return JSON.parse(rawMessage); } catch { return null; } } function getSessionMessage(message: WebSocketMessage): Record | null { const envelope = parseWebSocketJson(message); if (!envelope || typeof envelope !== "object") { return null; } const maybeEnvelope = envelope as { type?: unknown; message?: unknown }; if (maybeEnvelope.type !== "session" || !maybeEnvelope.message) { return null; } if (typeof maybeEnvelope.message !== "object") { return null; } return maybeEnvelope.message as Record; } function getPayload(message: Record): Record | null { return message.payload && typeof message.payload === "object" ? (message.payload as Record) : null; } interface ObservedTimelineEntry { seqStart: number; seqEnd: number; item: { type: string }; } function readTimelineEntries(payload: Record): ObservedTimelineEntry[] { if (!Array.isArray(payload.entries)) return []; return payload.entries.filter((entry): entry is ObservedTimelineEntry => { if (!entry || typeof entry !== "object") return false; const candidate = entry as Partial; return ( typeof candidate.seqStart === "number" && typeof candidate.seqEnd === "number" && typeof candidate.item?.type === "string" ); }); } function readCursorSeq(cursor: unknown): number | undefined { if (!cursor || typeof cursor !== "object") return undefined; const seq = (cursor as { seq?: unknown }).seq; return typeof seq === "number" ? seq : undefined; } function recordOwnedTimelineEntries( payload: Record, ownedEntries: Map, ): void { const pageStartSeq = readCursorSeq(payload.startCursor); const pageEndSeq = readCursorSeq(payload.endCursor); for (const entry of readTimelineEntries(payload)) { const belongsToPage = payload.direction === "tail" || (pageStartSeq !== undefined && pageEndSeq !== undefined && entry.seqStart >= pageStartSeq && entry.seqStart <= pageEndSeq); if (!belongsToPage) continue; ownedEntries.set(`${entry.seqStart}:${entry.seqEnd}`, JSON.stringify(entry.item)); } } function recordRepeatedTimelineEntries( payload: Record, entryKeys: Set, ): number { let repeats = 0; for (const entry of readTimelineEntries(payload)) { const key = `${entry.seqStart}:${entry.seqEnd}:${entry.item.type}`; if (entryKeys.has(key)) repeats += 1; else entryKeys.add(key); } return repeats; } export async function holdDaemonHydration(page: Page): Promise { let released = false; const delayedForwards: Array<() => void> = []; await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { const server = ws.connectToServer(); ws.onMessage((message) => server.send(message)); server.onMessage((message) => { if (released) { ws.send(message); return; } delayedForwards.push(() => ws.send(message)); }); }); return { release() { released = true; for (const forward of delayedForwards.splice(0)) { forward(); } }, }; } export async function delayCreatedAgentInitialTailResponse( page: Page, ): Promise { let createdAgentId: string | null = null; let releaseRequested = false; let delayedResponseSeen = false; const delayedForwards: Array<() => void> = []; let resolveCreatedAgent: ((agentId: string) => void) | null = null; let resolveDelayedResponse: (() => void) | null = null; let resolveForwardedResponse: (() => void) | null = null; const createdAgentSeen = new Promise((resolve) => { resolveCreatedAgent = resolve; }); const delayedResponse = new Promise((resolve) => { resolveDelayedResponse = resolve; }); const forwardedResponse = new Promise((resolve) => { resolveForwardedResponse = resolve; }); await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { const server = ws.connectToServer(); const forwardToClient = (message: WebSocketMessage) => { ws.send(message); resolveForwardedResponse?.(); }; ws.onMessage((message) => { server.send(message); }); server.onMessage((message) => { const sessionMessage = getSessionMessage(message); const payload = sessionMessage ? getPayload(sessionMessage) : null; if (sessionMessage?.type === "status" && payload?.status === "agent_created") { const agentId = payload.agentId; if (typeof agentId === "string") { createdAgentId = agentId; resolveCreatedAgent?.(agentId); } } if (sessionMessage?.type === "fetch_agent_timeline_response") { const agentId = payload?.agentId; const direction = payload?.direction; if ( !delayedResponseSeen && typeof agentId === "string" && agentId === createdAgentId && direction === "tail" ) { delayedResponseSeen = true; resolveDelayedResponse?.(); if (releaseRequested) { forwardToClient(message); return; } delayedForwards.push(() => forwardToClient(message)); return; } } ws.send(message); }); }); return { release() { releaseRequested = true; for (const forward of delayedForwards.splice(0)) { forward(); } }, waitForCreatedAgent: () => createdAgentSeen, waitForDelayedResponse: () => delayedResponse, waitForForwardedResponse: () => forwardedResponse, }; } export async function delayAgentOlderTimelineResponse( page: Page, agentId: string, ): Promise { return delayAgentTimelineResponse(page, agentId, "before"); } export async function holdAgentOlderTimelinePages( page: Page, agentId: string, daemonPort?: number, ): Promise { let requestCount = 0; let responseCount = 0; let repeatedEntryCount = 0; const entryKeys = new Set(); const ownedEntries = new Map(); const releasedPages = new Set(); const delayedForwards = new Map void>>(); const requestWaiters = new Map void>>(); const resolveRequestWaiters = () => { for (const [count, resolvers] of requestWaiters) { if (requestCount < count) continue; requestWaiters.delete(count); for (const resolve of resolvers) resolve(); } }; const routePattern = daemonPort === undefined ? daemonWsRoutePattern() : wsRoutePatternForPort(String(daemonPort)); await page.routeWebSocket(routePattern, (ws) => { const server = ws.connectToServer(); ws.onMessage((message) => { const sessionMessage = getSessionMessage(message); if ( sessionMessage?.type === "fetch_agent_timeline_request" && sessionMessage.agentId === agentId && sessionMessage.direction === "before" ) { requestCount += 1; resolveRequestWaiters(); } server.send(message); }); server.onMessage((message) => { const sessionMessage = getSessionMessage(message); const payload = sessionMessage ? getPayload(sessionMessage) : null; if ( sessionMessage?.type === "fetch_agent_timeline_response" && payload?.agentId === agentId && (payload.direction === "tail" || payload.direction === "before") ) { recordOwnedTimelineEntries(payload, ownedEntries); } if ( sessionMessage?.type === "fetch_agent_timeline_response" && payload?.agentId === agentId && payload.direction === "before" ) { responseCount += 1; repeatedEntryCount += recordRepeatedTimelineEntries(payload, entryKeys); const pageNumber = responseCount; if (releasedPages.has(pageNumber)) { ws.send(message); return; } const forwards = delayedForwards.get(pageNumber) ?? []; forwards.push(() => ws.send(message)); delayedForwards.set(pageNumber, forwards); return; } ws.send(message); }); }); return { getRequestCount: () => requestCount, getRepeatedEntryCount: () => repeatedEntryCount, getOwnedEntryCountContaining: (value) => [...ownedEntries.values()].filter((text) => text.includes(value)).length, releasePage(pageNumber) { releasedPages.add(pageNumber); for (const forward of delayedForwards.get(pageNumber) ?? []) forward(); delayedForwards.delete(pageNumber); }, waitForRequestCount(count) { if (requestCount >= count) return Promise.resolve(); return new Promise((resolve) => { const resolvers = requestWaiters.get(count) ?? []; resolvers.push(resolve); requestWaiters.set(count, resolvers); }); }, }; } export async function delayAgentBootstrapTailResponse( page: Page, agentId: string, ): Promise { let tailReleased = false; let catchUpReleased = false; const delayedTailForwards: Array<() => void> = []; const delayedCatchUpForwards: Array<() => void> = []; let resolveDelayedTail: (() => void) | null = null; let resolveDelayedCatchUp: (() => void) | null = null; const delayedTail = new Promise((resolve) => { resolveDelayedTail = resolve; }); const delayedCatchUp = new Promise((resolve) => { resolveDelayedCatchUp = resolve; }); await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { const server = ws.connectToServer(); ws.onMessage((message) => server.send(message)); server.onMessage((message) => { const sessionMessage = getSessionMessage(message); const payload = sessionMessage ? getPayload(sessionMessage) : null; const isTimelineResponse = sessionMessage?.type === "fetch_agent_timeline_response" && payload?.agentId === agentId; if (isTimelineResponse && payload.direction === "tail") { resolveDelayedTail?.(); if (tailReleased) ws.send(message); else delayedTailForwards.push(() => ws.send(message)); return; } if (isTimelineResponse && payload.direction === "after") { resolveDelayedCatchUp?.(); if (catchUpReleased) ws.send(message); else delayedCatchUpForwards.push(() => ws.send(message)); return; } ws.send(message); }); }); return { release() { tailReleased = true; for (const forward of delayedTailForwards.splice(0)) forward(); }, releaseCatchUp() { catchUpReleased = true; for (const forward of delayedCatchUpForwards.splice(0)) forward(); }, waitForDelayedResponse: () => delayedTail, waitForDelayedCatchUp: () => delayedCatchUp, }; } async function delayAgentTimelineResponse( page: Page, agentId: string, direction: "before" | "tail", ): Promise { let releaseRequested = false; let delayedResponseSeen = false; const delayedForwards: Array<() => void> = []; let resolveDelayedResponse: (() => void) | null = null; const delayedResponse = new Promise((resolve) => { resolveDelayedResponse = resolve; }); await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { const server = ws.connectToServer(); ws.onMessage((message) => { server.send(message); }); server.onMessage((message) => { const sessionMessage = getSessionMessage(message); const payload = sessionMessage ? getPayload(sessionMessage) : null; if ( !delayedResponseSeen && sessionMessage?.type === "fetch_agent_timeline_response" && payload?.agentId === agentId && payload.direction === direction ) { delayedResponseSeen = true; resolveDelayedResponse?.(); if (releaseRequested) { ws.send(message); return; } delayedForwards.push(() => ws.send(message)); return; } ws.send(message); }); }); return { release() { releaseRequested = true; for (const forward of delayedForwards.splice(0)) { forward(); } }, waitForDelayedResponse: () => delayedResponse, }; }