Fix mobile agent timeline catch-up

This commit is contained in:
Mohamed Boudra
2026-05-29 09:31:16 +07:00
parent 2e1b907c63
commit 65ace20880
11 changed files with 410 additions and 77 deletions
@@ -96,6 +96,31 @@ describe("client activity tracking", () => {
});
}
function waitForAssistantTimeline(
client: DaemonClient,
agentId: string,
timeout = 60000,
): Promise<string> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
cleanup();
reject(new Error(`Timeout waiting for assistant timeline (${timeout}ms)`));
}, timeout);
const cleanup = client.on("agent_stream", (msg) => {
if (msg.type !== "agent_stream") return;
if (msg.payload.agentId !== agentId) return;
if (msg.payload.event.type !== "timeline") return;
const item = msg.payload.event.item;
if (item.type !== "assistant_message") return;
clearTimeout(timer);
cleanup();
resolve(item.text);
});
});
}
// ===========================================================================
// SINGLE CLIENT SCENARIOS
// ===========================================================================
@@ -355,6 +380,31 @@ describe("client activity tracking", () => {
// ===========================================================================
describe("web and mobile clients", () => {
test("mobile stream delivery does not depend on heartbeat focus", async () => {
client1 = await createClient(); // web
client2 = await createClient(); // mobile
const agent = await createAgent({
client: client1,
title: "Mobile Stale Focus Stream Test",
});
client2.sendHeartbeat({
deviceType: "mobile",
focusedAgentId: null,
lastActivityAt: new Date(Date.now() - PRESENCE_THRESHOLD_MS - 60_000).toISOString(),
appVisible: false,
appVisibilityChangedAt: new Date(Date.now() - 120_000).toISOString(),
});
await new Promise((r) => setTimeout(r, 100));
const mobileStream = waitForAssistantTimeline(client2, agent.id);
await client1.sendMessage(agent.id, "Say 'hello' and nothing else");
await expect(mobileStream).resolves.toMatch(/hello/i);
}, 120000);
test("no notification to either when user actively on agent (web)", async () => {
client1 = await createClient(); // web
client2 = await createClient(); // mobile
-23
View File
@@ -783,7 +783,6 @@ export class Session {
appVisible: boolean;
appVisibilityChangedAt: Date;
} | null = null;
private readonly MOBILE_BACKGROUND_STREAM_GRACE_MS = 60_000;
private readonly terminalManager: TerminalManager | null;
private readonly providerSnapshotManager: ProviderSnapshotManager;
private unsubscribeProviderSnapshotEvents: (() => void) | null = null;
@@ -1302,13 +1301,6 @@ export class Session {
});
}
// Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
// for the focused agent, with a short grace window while backgrounded.
// History catch-up is handled via pull-based `fetch_agent_timeline_request`.
if (this.shouldSkipAgentStreamForward(event.agentId)) {
return;
}
const serializedEvent = serializeAgentStreamEvent(event.event);
if (!serializedEvent) {
return;
@@ -1355,21 +1347,6 @@ export class Session {
);
}
private shouldSkipAgentStreamForward(agentId: string): boolean {
const activity = this.clientActivity;
if (activity?.deviceType !== "mobile") {
return false;
}
if (!activity.focusedAgentId || activity.focusedAgentId !== agentId) {
return true;
}
if (activity.appVisible) {
return false;
}
const hiddenForMs = Date.now() - activity.appVisibilityChangedAt.getTime();
return hiddenForMs >= this.MOBILE_BACKGROUND_STREAM_GRACE_MS;
}
private buildAgentStreamPayload(
event: Extract<AgentManagerEvent, { type: "agent_stream" }>,
serializedEvent: Extract<SessionOutboundMessage, { type: "agent_stream" }>["payload"]["event"],