Skip to content

fix: sync mobile-initiated messages and threads back to desktop - #55

Merged
vedranjukic merged 11 commits into
mainfrom
fix/proxy-to-host-message-sync
Apr 12, 2026
Merged

vedranjukic merged 11 commits into
mainfrom
fix/proxy-to-host-message-sync

Conversation

@vedranjukic

Copy link
Copy Markdown
Owner

Summary

  • When a prompt is sent from the mobile dashboard (via the proxy's POST /prompts), messages and threads are stored only in the proxy's local JSON store. The desktop app never sees them because the sync was one-directional (host -> proxy only).
  • This adds reverse sync (proxy -> host) that runs transparently when the desktop loads data:
    • getMessages() pulls missing messages from the proxy and imports them into the host SQLite DB
    • findByProject() pulls missing threads from the proxy and imports them
    • findById() syncs thread status from the proxy when the proxy has a more recent update (e.g. mobile prompt completed)
  • New ProxyProjectsService methods: fetchThreadMessages, fetchThread, fetchProjectThreads for pulling data from the proxy API
  • Import uses onConflictDoNothing so duplicate IDs are safely ignored
  • Only runs for Daytona projects (proxy is Daytona-only)

Test plan

  • Send a prompt from the mobile dashboard for a Daytona project
  • Switch to the desktop app, open the same thread -- verify the mobile-sent prompt and agent response are visible
  • Create a new thread from the mobile dashboard, verify it appears in the desktop thread list
  • Verify thread status is synced (e.g. completed on mobile shows completed on desktop)
  • Verify no performance regression for local provider threads (proxy fetch is skipped)

Made with Cursor

When prompts are sent from the mobile dashboard (via proxy), messages
and threads are stored only in the proxy's local JSON. The desktop
never sees them because the sync was one-directional (host -> proxy).

Add reverse sync (proxy -> host) that runs when the desktop loads data:
- getMessages() imports missing messages from the proxy into host DB
- findByProject() imports missing threads from the proxy into host DB
- findById() syncs thread status from proxy when it's more recent
- Add fetchThreadMessages, fetchThread, fetchProjectThreads methods
  to ProxyProjectsService for pulling data from the proxy API

Made-with: Cursor
When a mobile-initiated agent run is active, the desktop didn't see new
messages in real-time because the proxy bridge path bypasses the host's
agent WebSocket. Add a 5s proxy poller that runs for Daytona projects
with subscribed desktop clients:

- Detects new messages imported from the proxy (via getMessages)
- Emits them as agent_message events to subscribed desktop clients
- Syncs thread status changes from the proxy
- Skips threads with active host-side handlers (avoids duplicates)
- Starts on subscribe_project, stops when last client disconnects

Made-with: Cursor
The previous agent_message approach didn't work for user text prompts
because the dashboard only processes user messages containing tool_result
blocks. Switch to a dedicated proxy_sync event that sends full DB-format
messages (all roles). The dashboard handler deduplicates by message ID
and merges into the active thread's message list.

Made-with: Cursor
The bridge previously stored a single state.ws reference, overwritten by
each new connection. When the proxy connected to execute a mobile prompt,
it replaced the desktop orchestrator's connection. After the proxy
disconnected, state.ws became null, causing "Bridge did not acknowledge
the prompt" for desktop prompts.

Convert to a multi-client model:
- Track all connected clients in a wsClients Set
- Add broadcastWs() helper that sends to all connected clients
- On close, remove from Set and fall back to another client for state.ws
- Replace all state.ws.send() calls with broadcastWs()

Both the desktop orchestrator and proxy can now be connected simultaneously.

Made-with: Cursor
Two issues fixed:
1. Desktop send_prompt never persisted the user's text message to the
   DB, so it was invisible to the proxy/webapp. Now addMessage is called
   before executeAgainstSandbox.
2. User messages are synced to the proxy immediately when added (not
   just on agent completion), so the webapp sees the prompt right away
   even while the agent is still running.

Made-with: Cursor
1. Desktop duplication: proxy_sync handler now de-duplicates user
   messages by text content (not just ID), since the optimistically
   added message has a different ID than the server-persisted one.
2. Webapp live updates: sync all messages to the proxy on every
   addMessage call (not just user prompts), so assistant responses
   appear on the webapp during the agent run without needing a refresh.

Made-with: Cursor
The mobile dashboard only polled for new messages after sending a prompt
itself. When the desktop sent a prompt, the webapp required a manual
refresh. Add a 5s background poll that:
- Fetches new messages and updates the view when count changes
- Checks promptStatus to detect desktop-initiated agent runs
- Switches to fast 2s polling when an agent run is detected
- Bump PROXY_SCRIPT_VERSION to 39 to deploy updated mobile dashboard

Made-with: Cursor
The proxy poller was emitting all new messages including system result
rows (empty content, just cost/usage metadata) and tool_result user
messages, which rendered as empty lines with error icons on the desktop.
Filter to only emit messages with renderable text content.

Made-with: Cursor
The proxy poller used slice(prevCount) to find new messages, but
getMessages() returns results ordered by createdAt. When proxy-imported
messages have timestamps that sort before existing desktop messages,
they land in the middle of the sorted list and slice misses them.

Switch to tracking known message IDs in a Set. New messages are found
by set difference, which is order-independent and catches messages
regardless of where they sort in the timeline.

Made-with: Cursor
The text-based dedup matched ALL user messages in the thread history,
so repeated prompts like "are you there?" were silently dropped if
the same text had been sent before. This is wrong — users can
legitimately send the same text multiple times.

ID-based dedup is sufficient because:
- Desktop-sent prompts: server-persisted ID enters knownIds before
  proxy_sync detects it, so no duplicate is emitted
- Webapp-sent prompts: unique proxy IDs aren't in the desktop store,
  so they correctly get added

Made-with: Cursor
Document the full bidirectional data flow between desktop, proxy, and
mobile dashboard including proxy poller, import mechanisms, bridge
multi-client support, and the proxy_sync WebSocket event.

Made-with: Cursor
@vedranjukic
vedranjukic force-pushed the fix/proxy-to-host-message-sync branch from 00c0631 to ce65ac9 Compare April 12, 2026 13:19
@vedranjukic
vedranjukic merged commit b643a2a into main Apr 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant