diff --git a/server/src/routes/goals.ts b/server/src/routes/goals.ts index 81756f7..d79029d 100644 --- a/server/src/routes/goals.ts +++ b/server/src/routes/goals.ts @@ -25,7 +25,9 @@ goals.post("/", async (c) => { return c.json({ error: "deviceId and goal are required" }, 400); } - const device = sessions.getDevice(body.deviceId); + // Look up by connection ID first, then by persistent DB ID + const device = sessions.getDevice(body.deviceId) + ?? sessions.getDeviceByPersistentId(body.deviceId); if (!device) { return c.json({ error: "device not connected" }, 404); } @@ -35,8 +37,9 @@ goals.post("/", async (c) => { } // Prevent multiple agent loops on the same device - if (activeSessions.has(body.deviceId)) { - const existing = activeSessions.get(body.deviceId)!; + const trackingKey = device.persistentDeviceId ?? device.deviceId; + if (activeSessions.has(trackingKey)) { + const existing = activeSessions.get(trackingKey)!; return c.json( { error: "agent already running on this device", sessionId: existing.sessionId, goal: existing.goal }, 409 @@ -55,7 +58,8 @@ goals.post("/", async (c) => { } const options: AgentLoopOptions = { - deviceId: body.deviceId, + deviceId: device.deviceId, + persistentDeviceId: device.persistentDeviceId, userId: user.id, goal: body.goal, llmConfig, @@ -67,20 +71,19 @@ goals.post("/", async (c) => { const loopPromise = runAgentLoop(options); // Track as active until it completes - const trackingId = body.deviceId; const sessionPlaceholder = { sessionId: "pending", goal: body.goal }; - activeSessions.set(trackingId, sessionPlaceholder); + activeSessions.set(trackingKey, sessionPlaceholder); loopPromise .then((result) => { - activeSessions.delete(trackingId); + activeSessions.delete(trackingKey); console.log( - `[Agent] Completed on ${body.deviceId}: ${result.success ? "success" : "incomplete"} in ${result.stepsUsed} steps (session ${result.sessionId})` + `[Agent] Completed on ${device.deviceId}: ${result.success ? "success" : "incomplete"} in ${result.stepsUsed} steps (session ${result.sessionId})` ); }) .catch((err) => { - activeSessions.delete(trackingId); - console.error(`[Agent] Error on ${body.deviceId}: ${err}`); + activeSessions.delete(trackingKey); + console.error(`[Agent] Error on ${device.deviceId}: ${err}`); }); // We need the sessionId from the loop, but it's created inside runAgentLoop. diff --git a/server/src/ws/device.ts b/server/src/ws/device.ts index 5858a4b..7b6c59d 100644 --- a/server/src/ws/device.ts +++ b/server/src/ws/device.ts @@ -122,6 +122,7 @@ export async function handleDeviceMessage( // Register device in session manager sessions.addDevice({ deviceId, + persistentDeviceId, userId, ws, deviceInfo: msg.deviceInfo, diff --git a/server/src/ws/sessions.ts b/server/src/ws/sessions.ts index cb999b0..19df9c1 100644 --- a/server/src/ws/sessions.ts +++ b/server/src/ws/sessions.ts @@ -14,6 +14,7 @@ export interface WebSocketData { /** A connected Android device */ export interface ConnectedDevice { deviceId: string; + persistentDeviceId?: string; userId: string; ws: ServerWebSocket; deviceInfo?: DeviceInfo; @@ -56,6 +57,16 @@ class SessionManager { return this.devices.get(deviceId); } + /** Look up a device by its persistent DB ID (survives reconnects) */ + getDeviceByPersistentId(persistentId: string): ConnectedDevice | undefined { + for (const device of this.devices.values()) { + if (device.persistentDeviceId === persistentId) { + return device; + } + } + return undefined; + } + getDevicesForUser(userId: string): ConnectedDevice[] { const result: ConnectedDevice[] = []; for (const device of this.devices.values()) { diff --git a/web/src/routes/dashboard/devices/[deviceId]/+page.svelte b/web/src/routes/dashboard/devices/[deviceId]/+page.svelte index 53e1b0a..980e3ad 100644 --- a/web/src/routes/dashboard/devices/[deviceId]/+page.svelte +++ b/web/src/routes/dashboard/devices/[deviceId]/+page.svelte @@ -370,7 +370,7 @@ - {#if steps.length > 0 || runStatus === 'running'} + {#if steps.length > 0 || runStatus !== 'idle'}