Skip to content

Add pull-wake runners for desktop dispatch#4305

Open
KyleAMathews wants to merge 31 commits intomainfrom
pull-wake
Open

Add pull-wake runners for desktop dispatch#4305
KyleAMathews wants to merge 31 commits intomainfrom
pull-wake

Conversation

@KyleAMathews
Copy link
Copy Markdown
Contributor

Summary

Adds pull-wake runners — a polling-based dispatch mechanism where desktop runners register with the server and pull wake notifications from a dedicated durable stream, replacing the need for direct webhook pushes to desktop clients that sit behind NAT.

Reviewer Guidance

Motivation

Desktop agents can't receive inbound webhooks (NAT, firewalls, dynamic IPs). Pull-wake flips the model: the server writes wake notifications to a per-runner durable stream, and the runner tails it. This gives desktop runners the same dispatch semantics as cloud workers without requiring a stable inbound address.

Architecture

The dispatch lifecycle follows a state machine tracked in Postgres:

idle → pending → outstanding (wake written to runner stream) → claimed (runner acquired) → idle

Key components:

  • DispatchWakeRouter (dispatch-wake-router.ts) — Orchestrates wake enrichment, materialization, and delivery to either webhook or runner-stream targets. Handles coalescing (skip duplicate wakes for entities already being processed).

  • PostgresRegistry extensions (electric-agents-registry.ts) — Tracks entity_dispatch_state, wake_notifications, consumer_claims, and runners tables. Manages lease-based runner liveness, claim lifecycle, and recovery of expired/stale dispatches.

  • createPullWakeRunner (pull-wake-runner.ts) — Client-side runner that tails the wake stream with reconnection + exponential backoff, heartbeats for liveness, and dispatches received wakes to the runtime.

  • Callback-forward auth (server.ts handleCallbackForward) — Proxies claim/heartbeat/done requests from runners through the server, rewriting claim tokens and validating runner ownership. This lets the server remain the single authority for entity lifecycle.

  • Asserted identity (assertedIdentity.ts, dev-asserted-auth.ts) — Desktop app propagates user identity through X-Electric-Asserted-* headers, enabling created_by tagging on spawned entities.

Key Invariants

  • A runner must heartbeat within its lease TTL or the server marks it offline and expires its active claims
  • Only the runner that owns a claim can heartbeat or complete it (enforced via runner-id + claim-token checks)
  • Wake dispatch is idempotent per entity — if an entity already has an outstanding or active wake, new triggers coalesce into the pending state
  • The periodic recovery loop re-dispatches expired claims and stale outstanding wakes to prevent entities from getting stuck

Non-goals

  • Worker-pool dispatch target (type defined but removed during review — will add when wired)
  • Reconnection was added during review; more sophisticated health-check degradation deferred
  • EntityDispatchState type redesign as discriminated union (deferred — works correctly, just permissive at the type level)

Trade-offs

  • Pull vs push: Pull adds latency (stream tail interval) but eliminates NAT traversal. For desktop agents where sub-second dispatch isn't critical, this is the right trade-off.
  • Postgres state tracking vs in-memory: Postgres survives server restarts and supports multi-instance deployments, at the cost of a write per dispatch state transition.
  • Lease-based liveness vs persistent connections: Leases are simpler to reason about and recover from than tracking WebSocket connection state.

Verification

# Unit tests (no server needed)
cd packages/agents-runtime && pnpm vitest run test/pull-wake-runner.test.ts test/process-wake.test.ts

# Server unit tests
cd packages/agents-server && pnpm vitest run test/dispatch-wake-router.test.ts test/electric-agents-routes.test.ts test/server-start.test.ts test/callback-forward-auth.test.ts

# Registry integration tests (needs Postgres)
cd packages/agents-server && pnpm vitest run test/electric-agents-registry.test.ts

# E2E pull-wake lifecycle
cd packages/agents-server && pnpm vitest run test/horton-pull-wake-e2e.test.ts

Files Changed

New files:

  • pull-wake-runner.ts — Client-side pull-wake runner with reconnection, heartbeat, and stream tailing
  • dispatch-wake-router.ts — Server-side wake enrichment, materialization, and delivery routing
  • electric-agents-types.ts — Shared type definitions (DispatchTarget, DispatchPolicy, EntityDispatchState, WakeNotificationRow)
  • stream-client.ts extensions — headOffset() and mintWakeNotification() for dispatch
  • authenticated-user-format.ts / dev-asserted-auth.ts — Identity formatting and dev-mode auth
  • assertedIdentity.ts / auth-fetch.ts — UI-side identity extraction and authenticated fetch
  • 0004_pull_wake_control_plane.sql — Migration for runners, entity_dispatch_state, wake_notifications, consumer_claims tables
  • entrypoint-lib.ts (server + agents) — Environment variable parsing for new dispatch options

Modified files:

  • server.ts — Dispatch recovery loop, callback-forward proxy, wake-on-send integration, runner management endpoints
  • electric-agents-registry.ts — Runner CRUD, dispatch state machine, claim lifecycle, stale recovery queries
  • electric-agents-routes.ts — Runner HTTP endpoints, dispatch_policy on spawn, auth plumbing
  • process-wake.ts — Claim token header transport, idempotent producers, BigInt offset comparison
  • create-handler.tsdispatchWake method on RuntimeRouter for pull-wake notifications
  • agents/server.ts — BuiltinAgentsServer pull-wake runner lifecycle (start/stop/drain)
  • agents-desktop/main.ts — Desktop runner registration, settings persistence, asserted auth headers
  • ElectricAgentsProvider.tsx — Collection caching with cleanup, dispatch_policy in spawn, preloading
  • entity-connection.ts — Reconnecting SSE with abort support and auth headers

KyleAMathews and others added 22 commits May 9, 2026 15:15
- Fix pull-wake runner reconnection on clean stream close
- Add missing .catch() on fire-and-forget dispatch
- Log auth errors instead of silently swallowing them
- Log heartbeat error-handler failures as last-resort fallback
- Separate JSON parse errors from business logic in wake registration
- Eliminate duplicate authenticateIncomingRequest call in spawn
- Log drainWakes shutdown errors instead of discarding
- Log desktop settings load failures
- Align compareOffsets to use BigInt (matching registry)
- Replace non-null assertions with explicit guard in dispatch router
- Consolidate WakeNotification to single source in agents-runtime
- Remove unsupported worker-pool dispatch target variant
- Make DispatchPolicy.targets readonly
- Use satisfies-based validation Sets for compile-time safety
- Extract InternalWakeNotification type for writeToken handling
- Replace nested ternary with if/else in tags logic
- Remove duplicate readBody, use shared import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

❌ Patch coverage is 74.52896% with 365 lines in your changes missing coverage. Please review.
✅ Project coverage is 40.05%. Comparing base (7f8947a) to head (13aae92).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/agents-server/src/server.ts 74.30% 93 Missing ⚠️
...ckages/agents-server/src/electric-agents-routes.ts 76.89% 58 Missing ⚠️
...gents-server-ui/src/lib/ElectricAgentsProvider.tsx 0.00% 44 Missing ⚠️
packages/agents/src/server.ts 63.46% 38 Missing ⚠️
...ages/agents-server/src/electric-agents-registry.ts 86.49% 36 Missing and 1 partial ⚠️
packages/agents-server/src/dispatch-wake-router.ts 77.96% 26 Missing ⚠️
...ages/agents-server-ui/src/lib/entity-connection.ts 56.66% 13 Missing ⚠️
...kages/agents-server/src/electric-agents-manager.ts 77.08% 11 Missing ⚠️
...ckages/agents-server-ui/src/components/Sidebar.tsx 0.00% 8 Missing ⚠️
packages/electric-ax/src/start.ts 0.00% 7 Missing ⚠️
... and 7 more

❗ There is a different number of reports uploaded between BASE (7f8947a) and HEAD (13aae92). Click for more details.

HEAD has 3 uploads less than BASE
Flag BASE (7f8947a) HEAD (13aae92)
unit-tests 5 4
typescript 5 4
packages/agents-runtime 1 0
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #4305       +/-   ##
===========================================
- Coverage   54.09%   40.05%   -14.05%     
===========================================
  Files         206      170       -36     
  Lines       20775    13264     -7511     
  Branches     5493     4270     -1223     
===========================================
- Hits        11239     5313     -5926     
+ Misses       9532     7945     -1587     
- Partials        4        6        +2     
Flag Coverage Δ
packages/agents 70.75% <66.94%> (+2.95%) ⬆️
packages/agents-runtime ?
packages/agents-server 72.64% <80.01%> (+3.36%) ⬆️
packages/agents-server-ui 6.80% <39.43%> (+1.48%) ⬆️
packages/electric-ax 36.84% <0.00%> (-1.21%) ⬇️
typescript 40.05% <74.52%> (-14.05%) ⬇️
unit-tests 40.05% <74.52%> (-14.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

KyleAMathews and others added 7 commits May 11, 2026 09:19
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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