Home

mweinbach / agent-coworker

publicmweinbach/agent-coworker
Overview Code History Branches Pull requestsIssuesInsights
main
HomeOverview Code PRsIssues

[desktop] Add JSON-RPC websocket transport and shared desktop control path (#56)

7 months ago

1c42952
Authored
Max Weinbach3/23/2026, 2:53:18 AM
* feat: add websocket protocol negotiation foundation

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* feat: add core jsonrpc websocket thread flows

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* feat: add jsonrpc thread journal replay

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* feat: add jsonrpc client transport and queue bounds

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* docs: add websocket jsonrpc smoke examples

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* feat: add desktop jsonrpc single connection path

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* feat: add jsonrpc overload limits and schema codegen

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* feat: add jsonrpc control namespace foundation

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* feat: route desktop control actions over jsonrpc

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* feat: add jsonrpc session control methods

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* docs: cover cowork jsonrpc control methods

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* fix(desktop): canonicalize jsonrpc workspace state

* fix(desktop): route workspace live state through jsonrpc

* docs: mark desktop websocket path as jsonrpc-only

* refactor(desktop): remove legacy live transport paths

* test(desktop): rewrite transport coverage for jsonrpc

* fix(desktop): keep ipc handlers live during quit

* fix(desktop): sync thread defaults over jsonrpc live state

* test(jsonrpc): cover live session state updates

* docs: record jsonrpc optimistic send lesson

* fix(jsonrpc): restore desktop live event parity

* test(jsonrpc): cover live event reconciliation

* test: stabilize desktop json-rpc socket mocks

* fix: guard desktop json-rpc sends without a workspace socket

* fix: negotiate first supported ws subprotocol

* fix: handle json-rpc turn send failures

* fix(jsonrpc): harden thread replay and summary counts

* fix(desktop): unify queued sends and provider refresh

* fix(desktop): harden refresh and default sync timing

* fix(jsonrpc): fail terminal reconnect states cleanly

* fix(desktop): apply fresh jsonrpc defaults before hydration

* test(server): await jsonrpc flow shutdowns

* refactor(desktop): isolate provider refresh helper

* fix: harden desktop workspace json-rpc lifecycle

* fix: tighten json-rpc reconnect and resume replay

* chore: capture PR review rescan lesson

* fix: dedupe migrated desktop json-rpc connects

* fix: harden json-rpc replay and retry recovery

* fix: address remaining PR 56 review feedback

Return the negotiated websocket subprotocol in the Bun upgrade response so multi-offer clients see the correct wire protocol. Clear provider status refresh spinners even when follow-up control RPCs succeed without event envelopes, and add focused regressions for both behaviors.

Verification:
- bun test test/server.jsonrpc.test.ts apps/desktop/test/provider-actions.test.ts apps/desktop/test/control-socket.test.ts
- bun run typecheck
- bun test

* test: harden remote MCP smoke retries

Retry transient upstream 5xx responses in the default remote MCP smoke test so Docs + Tests keeps its connectivity signal without failing on brief mcp.grep.app outages. Add deterministic helper coverage for retry classification and record the CI lesson in tasks/lessons.md.

* fix: stabilize remote MCP smoke coverage in CI

Split the default Docs + Tests lane from remote MCP smoke coverage so bun test stays deterministic while CI still exercises connect/discover against mcp.grep.app. Gate live remote tool execution behind RUN_REMOTE_MCP_AGENT_TESTS and record the babysitting lesson in tasks/lessons.md.

* Remove duplicate JSON-RPC regression tests

* fix: address JSON-RPC PR review (defaults, refresh race, errors, teardown)

Preserve allowBeforeHydration on busy-thread deferral; clear providerStatusRefreshing
only when the matching refresh generation completes; return JSON-RPC internal
errors when route handlers reject; close the workspace socket after thread
session_close/unsubscribe during workspace remove and restart.

Made-with: Cursor

* fix: emit live JSON-RPC reasoning items

Suppress commentary-phase assistant deltas in the live JSON-RPC projectors, accumulate reasoning stream chunks into completed reasoning items, and add projector plus desktop reasoning regressions so live OpenAI/Codex turns match replay.

* fix: preserve live JSON-RPC feed during hydration

Prevent delayed thread/read snapshots from clobbering fresher live feed items or optimistic first sends, refresh control-socket lifecycle callbacks to use the latest store getter, and add desktop regressions for the hydration race and lifecycle reconnect path.

* fix: tighten workspace defaults control persistence

Narrow the workspace-defaults control message builder to the apply_session_defaults client message shape and remove the redundant runtime type guard before the control RPC so future builder changes cannot silently skip persistence.

* chore: record PR comment cleanup status

Update the task tracker after closing the remaining PR review threads so the repo-local execution ledger matches the branch and PR state.

* fix: preserve reconnect retry queue semantics

Keep queued retryable JSON-RPC requests alive across transient initialize-handshake failures while auto-reconnect is still in progress, and apply notification opt-out filtering to replayed journal events during thread resume. Add runtime and server flow regressions plus tracker updates.

* fix: guard provider refresh spinner by generation

Align desktop bootstrap refresh handling with the existing providerStatusRefreshGeneration contract so older provider refresh responses cannot clear the spinner while a newer refresh is still in flight.

Stop provider_status and generic control error events from owning spinner teardown, add control-socket regressions for the provider_status and bootstrap races, and record the verification evidence in tasks/todo.md.

* fix: wait for JSON-RPC turn request acceptance

Make turn/start and turn/steer wait for the first real session outcome before replying so JSON-RPC callers do not get guessed success payloads while the session is still deciding whether to accept the request.

Use the existing session-event capture helper to promote session_busy and steer_accepted into request results, convert session-level rejections into JSON-RPC errors, add focused server-flow regressions, and record the verification evidence in tasks/todo.md.

* fix: ignore stale JSON-RPC socket closes

Guard workspace socket lifecycle callbacks against stale socket instances so a deferred close from an old socket cannot clear active control state after a server URL swap.

Add a focused desktop regression that defers the old socket close until after the replacement socket opens, record the verification evidence in tasks/todo.md, and keep the change scoped to the shared JSON-RPC socket helper.

* chore: record final PR review sweep

Capture the final review-thread revalidation evidence for the JSON-RPC subprotocol transport path and repeated socket/typecheck verification before resolving the remaining GitHub comments.

* fix(desktop): hydrate jsonrpc control state before diffing defaults

Add a JSON-RPC cowork/session/state/read control method so desktop bootstrap can hydrate the workspace control session's provider/model, session settings, and editable config before computing workspace-default diffs.

Restore truthful control-socket waiter diagnostics by tracking in-flight waits for control session readiness, workspace session listing, and session snapshot reads.

Keep the local workspace runtime in sync after workspace default applies so repeated saves do not resend redundant cowork/session/defaults/apply calls.

Update schema/docs/generated JSON-RPC artifacts and add focused desktop/server regressions covering bootstrap hydration, bundled legacy event envelopes, waiter accounting, and no-op workspace resaves.

* chore: verify provider refresh false positive

Co-authored-by: mweinbach <mweinbach@users.noreply.github.com>

* fix: preserve workspace cleanup on removal

Co-authored-by: mweinbach <mweinbach@users.noreply.github.com>

* fix: simplify desktop provider refresh helper

Co-authored-by: mweinbach <mweinbach@users.noreply.github.com>

* Update startServer.ts

* Update startServer.ts

* fix: keep desktop workspace default sync consistent

Co-authored-by: mweinbach <mweinbach@users.noreply.github.com>

* fix(jsonrpc): stream reasoning delta notifications

Add a dedicated item/reasoning/delta JSON-RPC notification and stop collapsing streamed reasoning into a late completed item.

Persist and replay the new reasoning delta events through the journal/read path, regenerate the JSON-RPC schema artifacts, and update the server-side regression coverage for live streaming and replay.

* fix(desktop): consume streamed reasoning JSON-RPC events

Route shared-socket reasoning notifications through the desktop model-stream pipeline so reasoning cards render before the first assistant reply.

Add the desktop regression coverage for incremental reasoning rendering and record the implementation plus verification in the task ledger.

* Clean Up Server

* fix(jsonrpc): stabilize reconnects and large journal reads

Add handshake timeouts and query-mode fallback so JSON-RPC sockets recover quickly when Bun subprotocol negotiation stalls. Batch journal persistence and replay, and lower CI test concurrency to keep the full suite stable under Bun.

Made-with: Cursor

* fix(server): self-clean thread journal write queues

Keep per-thread journal writes serialized without leaking settled tail promises. Preserve best-effort idle waits and existing thread replay semantics while adding focused queue coverage.

Made-with: Cursor

* refactor(desktop): clarify user message send acceptance vs JSON-RPC

Made-with: Cursor

* Update protocol-v2-events.test.ts

* fix: JSON-RPC pending prompt reconnects

- Clear pendingServerRequests on JSON-RPC socket close after unsubscribing threads

- Add non-emitting pending prompt accessor; resume seeds projector without replayPendingPrompts

- Dedupe journal afterSeq replay vs pending-prompt re-registration

- Add reconnect regressions for ask, approval, and afterSeq dedupe

Made-with: Cursor

* fix(desktop): gate workspace JSON-RPC socket on generation

Replace map-entry identity checks with per-workspace socket generation so
retired sockets cannot clear control state, disconnect threads, or route
events after URL swap or explicit workspace teardown.

- Track workspaceJsonRpcSocketGenerations in RUNTIME; bump on URL change
  and workspace remove/restart
- Stamp sockets and gate onOpen/onClose/onNotification/onServerRequest
- Extend control-socket, thread-reconnect, and protocol-v2-events tests

Made-with: Cursor

* fix(desktop): teardown JSON-RPC workspace state on removeWorkspace

- Add disposeWorkspaceJsonRpcState and call it only from removeWorkspace
- Clear jsonRpcSocket/controlSocket/threadEventReducer per-workspace maps and
  block late async work after permanent deletion
- Reject skill install waiters when control state is disposed without store refs
- Extend workspace-settings-sync tests for teardown vs restart semantics

Made-with: Cursor

* fix(desktop): dispose JSON-RPC state on workspace removal and renderer shutdown

- Thread reducer: store-aware disposal clears reconnect + pending connects per workspace
- Control helpers: disposeAllControlState for full teardown
- store.helpers: disposeWorkspaceJsonRpcState(get, id) and disposeAllJsonRpcState
- removeWorkspace: run disposal in finally after stopWorkspaceServer attempt
- App: beforeunload + unmount cleanup (StrictMode-safe scheduling)
- Tests: ordering assert for thread/unsubscribe, control disposal, app shutdown

Made-with: Cursor

* Update workspace-settings-sync.test.ts

* fix(desktop): guard reasoning item/completed dispatch against existing stream item

When a reasoning item/completed JSON-RPC notification arrives, check whether
a feed item already exists for this stream key (created by streaming deltas).
If so, update it in-place instead of dispatching a legacy reasoning event,
which would create a duplicate item appended to the bottom of the feed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(desktop): apply reasoning-before-assistant repositioning for all turn types

Removed the rawBackedTurns guard from reasoningInsertBeforeAssistantAfterStreamReplay
so that late-arriving reasoning items are inserted before the assistant message item
for JSON-RPC (non-raw-backed) turns as well, not just raw-event-backed turns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(desktop): ensure reasoning precedes tool calls in activity trace entries

Reorder buildActivityTraceEntries to always place reasoning items before tool
items within an activity group. During streaming, tool feed items can arrive
before the reasoning block is flushed, causing tools to appear before reasoning
in the rendered trace even though reasoning logically precedes tool use.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(desktop): register reasoning stream key eagerly on item/started to prevent empty-placeholder push

Always dispatch reasoning_delta from item/started (even when initial text is
empty) so the stream key is registered in reasoningItemIdByStream before tool
items can arrive. Guard feed item creation in the reasoning_delta handler so
an empty-text delta registers the key without pushing a blank item to the feed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(server): clear disconnected replay buffer before journal replay on thread resume

When a client reconnects via thread/resume with afterSeq > 0, the server
was calling ensureDisconnectedReplayBuffer() which preserves events that
accumulated in the buffer since the client disconnected. The subsequent
replayThreadJournalEvents() then sends those same events via the journal,
and the buffer drain in subscribeJsonRpcThread sends them a second time,
producing duplicate turn/item/reasoning/delta notifications.

Switch to beginDisconnectedReplayBuffer() to reset the buffer before the
journal replay. Since all code between the reset and the buffer drain in
subscribeJsonRpcThread runs synchronously (no event-loop yields), no new
events can be generated between the reset and the drain, so the drain is
effectively a no-op. The journal replay covers all missed events cleanly
with no duplicates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(desktop): mirror control config defaults

Co-authored-by: mweinbach <mweinbach@users.noreply.github.com>

* fix: restore desktop replay and bootstrap guards

Co-authored-by: mweinbach <mweinbach@users.noreply.github.com>

* fix: restore reusable desktop jsonrpc workspace state

Co-authored-by: mweinbach <mweinbach@users.noreply.github.com>

* fix: harden jsonrpc control review paths

* docs: record jsonrpc review thread fixes

* fix: return jsonrpc skill installation errors

* fix: restore jsonrpc replay and activity order

* docs: record full test lane cleanup

* fix: resolve final review comment issues

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

Parent0167f73

115 files changed
  • .github/workflows/ci.yml+4−1
  • apps/desktop/electron/main.ts+1−3
  • apps/desktop/electron/services/persistence.ts+1−0
  • apps/desktop/electron/services/shutdown.ts+3−2
  • apps/desktop/src/App.tsx+45−0
  • apps/desktop/src/app/store.actions/backup.ts+22−29
  • apps/desktop/src/app/store.actions/bootstrap.ts+2−1
  • apps/desktop/src/app/store.actions/explorer.ts
+0
−1
  • apps/desktop/src/app/store.actions/mcp.ts+25−30
  • apps/desktop/src/app/store.actions/memory.ts+17−20
  • apps/desktop/src/app/store.actions/provider.ts+70−46
  • apps/desktop/src/app/store.actions/skills.ts+41−60
  • apps/desktop/src/app/store.actions/thread.ts+7−15
  • apps/desktop/src/app/store.actions/workspace.ts+28−30
  • apps/desktop/src/app/store.actions/workspaceDefaults.ts+204−15
  • apps/desktop/src/app/store.feedMapping.ts+2−2
  • apps/desktop/src/app/store.helpers.ts+72−6
  • apps/desktop/src/app/store.helpers/controlSocket.ts+1067−848
  • apps/desktop/src/app/store.helpers/jsonRpcSocket.ts+463−0
  • apps/desktop/src/app/store.helpers/jsonRpcSocketOverride.ts+1−0
  • apps/desktop/src/app/store.helpers/persistence.ts+5−1
  • apps/desktop/src/app/store.helpers/runtimeState.ts+32−6
  • apps/desktop/src/app/store.helpers/threadEventReducer.ts+883−139
  • apps/desktop/src/app/types.ts+1−0
  • apps/desktop/src/lib/agentSocket.ts+2−2
  • apps/desktop/src/lib/desktopSchemas.ts+1−0
  • apps/desktop/src/ui/chat/activityGroups.ts+18−13
  • apps/desktop/test/app-jsonrpc-disposal.test.tsx+337−0
  • apps/desktop/test/backup-page.test.ts+3−0
  • apps/desktop/test/bootstrap-cache.test.ts+30−0
  • apps/desktop/test/chat-activity-group-card.test.tsx+3−0
  • apps/desktop/test/chat-view.stability.test.tsx+3−0
  • apps/desktop/test/control-socket.test.ts+921−486
  • apps/desktop/test/desktop-schemas.test.ts+22−0
  • apps/desktop/test/developer-page.test.tsx+3−0
  • apps/desktop/test/electron-shutdown.test.ts+3−19
  • apps/desktop/test/helpers/jsonRpcSocketMock.ts+25−0
  • apps/desktop/test/jsonrpc-single-connection.test.ts+786−0
  • apps/desktop/test/memory-actions.test.ts+52−0
  • apps/desktop/test/memory-page.test.ts+3−0
  • apps/desktop/test/onboarding-ui.test.tsx+3−0
  • apps/desktop/test/onboarding.test.ts+3−0
  • apps/desktop/test/persistence-state-sanitization.test.ts+2−0
  • apps/desktop/test/protocol-v2-events.test.ts+639−1863
  • apps/desktop/test/provider-actions.test.ts+126−0
  • apps/desktop/test/providers-page.test.ts+3−0
  • apps/desktop/test/runtimeState.test.ts+27−0
  • apps/desktop/test/settings-nav.test.ts+3−0
  • apps/desktop/test/skills-actions.test.ts+60−4
  • apps/desktop/test/skills-catalog-page.test.ts+2−0
  • apps/desktop/test/store-explorer.test.ts+3−0
  • apps/desktop/test/store-feed-mapping.test.ts+14−0
  • apps/desktop/test/thread-reconnect.test.ts+388−1621
  • apps/desktop/test/updates-page.test.ts+3−0
  • apps/desktop/test/usage-page.test.ts+3−0
  • apps/desktop/test/workspace-mcp-editor.test.ts+182−118
  • apps/desktop/test/workspace-settings-sync.test.ts+1098−673
  • apps/desktop/test/workspace-startup.test.ts+50−4
  • apps/desktop/test/workspaces-page.test.ts+3−0
  • docs/generated/websocket-jsonrpc.d.ts+148−0
  • docs/generated/websocket-jsonrpc.schema.json+3820−0
  • docs/websocket-protocol.md+237−1
  • examples/ws-jsonrpc-smoke.ts+92−0
  • examples/ws-legacy-smoke.ts+57−0
  • package.json+1−0
  • scripts/generate_ws_jsonrpc_schema.ts+22−0
  • src/client/index.ts+1−0
  • src/client/jsonRpcSocket.ts+629−0
  • src/server/agents/AgentControl.ts+2−2
  • src/server/index.ts+24−4
  • src/server/jsonrpc/codegen.ts+53−0
  • src/server/jsonrpc/decodeJsonRpcMessage.ts+51−0
  • src/server/jsonrpc/dispatchJsonRpcMessage.ts+156−0
  • src/server/jsonrpc/journalProjector.ts+361−0
  • src/server/jsonrpc/legacyEventProjector.ts+439−0
  • src/server/jsonrpc/protocol.ts+229−0
  • src/server/jsonrpc/routes/index.ts+31−0
  • src/server/jsonrpc/routes/providerAndMcp.ts+353−0
  • src/server/jsonrpc/routes/sessionAndWorkspaceControl.ts+171−0
  • src/server/jsonrpc/routes/shared.ts+114−0
  • src/server/jsonrpc/routes/skillsMemoryAndWorkspaceBackup.ts+504−0
  • src/server/jsonrpc/routes/threadAndTurn.ts+323−0
  • src/server/jsonrpc/routes/types.ts+117−0
  • src/server/jsonrpc/schema.ts+628−0
  • src/server/jsonrpc/threadReadProjector.ts+107−0
  • src/server/protocol.ts+1−1
  • src/server/session/AgentSession.ts+45−1
  • src/server/session/InteractionManager.ts+11−4
  • src/server/session/SessionSnapshotProjector.ts+4−0
  • src/server/sessionDb.ts+37−0
  • src/server/sessionDb/migrations.ts+7−0
  • src/server/sessionDb/repository.ts+152−1
  • src/server/startServer.ts+2027−147
  • src/server/startServer/threadJournalWriteQueue.ts+34−0
  • src/server/startServer/types.ts+26−0
  • src/server/wsProtocol/negotiation.ts+92−0
  • tasks/lessons.md+6−0
  • tasks/todo.md+161−0
  • test/desktop.controlSocket.configUpdated.test.ts+112−0
  • test/desktop.controlSocket.threadList.test.ts+175−0
  • test/jsonrpc.codegen.test.ts+37−0
  • test/jsonrpc.projectors.test.ts+167−0
  • test/jsonrpc.protocol.test.ts+125−0
  • test/jsonrpc.router.test.ts+167−0
  • test/jsonrpc.routes.review-fixes.test.ts+426−0
  • test/jsonrpc.thread-read-projector.test.ts+73−0
  • test/jsonrpcSocket.runtime.test.ts+534−0
  • test/mcp.remote.grep.test.ts+112−16
  • test/server.jsonrpc.control.test.ts+401−0
  • test/server.jsonrpc.flow.test.ts+1042−0
  • test/server.jsonrpc.test.ts+293−0
  • test/session-db.test.ts+180−0
  • test/threadJournalWriteQueue.test.ts+147−0
  • test/ws-protocol-negotiation.test.ts+122−0
  • test_ws.ts+0−55