electric-sql / electric-sql/electric

Entities become silently unreachable after `deleteEntity`+respawn or `agents stop`/`start` — stale wake/runner bindings

Open
#4,368 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
10.4k
Forks
375
Avg merge
3d 1h
Merged PRs (30d)
18

Description

## Summary

There are two ways to leave a spawned entity silently unreachable. In both, the entity row exists in postgres and the agents UI shows the entity as present, but messages sent to it are never processed. The two have different triggers and slightly different failure signatures, but they look like the same underlying problem: the wake/runner binding for an entity URL is not reconciled, so the entity is "alive" in storage but has nothing wired up to actually run its handler.

Filing them together because a fix for one likely addresses the other.

## Environment

- `@electric-ax/agents-runtime` 0.2.2
- `@electric-ax/durable-streams-client-beta` 0.3.1
- `agents-server` 0.4.x (local `electric-ax agents` Docker stack)
- `electric-ax` CLI 0.2.2
- macOS, Docker Desktop

Setup in all repros: a long-running host process ("the bridge") that calls `registerTypes()` to register entity types against its own webhook URL, then spawns entities and relays messages to them.

## Trigger 1 — entities unreachable after `agents stop && agents start` (no volume wipe)

### Repro

1. Start the stack: `electric-ax agents start`.
2. From the bridge: `registerTypes()`, then `spawnEntity({ type, id })`. Verify the entity wakes and processes a `sendEntityMessage` (works).
3. `electric-ax agents stop` (no `--remove-volumes`).
4. `electric-ax agents start`.
5. Restart the bridge process.
6. `sendEntityMessage` to the entity → server returns **HTTP 204**, but no wake is ever dispatched to the bridge webhook. The entity never runs. A direct HTTP send to the entity's inbox endpoint behaves the same: 204, silent, no wake.

### Scope

Only entities that existed at the moment of `agents stop` are affected. An entity spawned *after* the restart works normally — until the next `stop`.

### Diagnostic that isolated it

Changing the entity `id` to a value that had never been used, then restarting the bridge, produced a fresh entity that woke normally. We then spawned a *new* entity inside that working post-restart session — it worked — and ran `agents stop && agents start` a second time. The new entity was now also dead. So the discriminator is the substrate `stop` event itself, not how recently the entity was spawned or which bridge process created it.

### Reading

Entity state in postgres survives the restart intact. The wake-registration that binds "entity URL → webhook URL" is not rebound to the restarted bridge's webhook on `agents start`, so wake dispatch never fires.

## Trigger 2 — entities unreachable after `deleteEntity` + respawn on the same URL

### Repro

1. Start the stack and spawn an entity; confirm it wakes on `sendEntityMessage` (works).
2. `client.deleteEntity('/type/id')` → returns 2xx success.
3. `client.spawnEntity({ type, id })` again on the same URL → returns success, no error.
4. `sendEntityMessage` to that URL → **`409 {"code":"NOT_RUNNING","message":"Entity is stopped"}`**.

No `agents stop` is involved — this happens within a single substrate uptime.

### What we investigated

- **Postgres is not cleaned by `deleteEntity`.** After step 2, `SELECT url FROM entities WHERE url LIKE '/type/%'` still returns the row.
- **A direct SQL purge does not fix it.** We `DELETE`d the URL's rows directly across all 8 per-entity tables (`entities`, `wake_registrations`, `entity_dispatch_state`, `entity_manifest_sources`, `scheduled_tasks`, `tag_stream_outbox`, `wake_notifications`, `consumer_claims`), then respawned. The send still returns `409`.
- **`entity_dispatch_state` is half-initialized after respawn.** The row exists, but every `active_*` column (`active_consumer_id`, `active_runner_id`, `active_epoch`, `active_claimed_at`, `active_lease_expires_at`) is `NULL` — no runner is bound.
- **Only a process restart clears it.** `electric-ax agents stop --remove-volumes && electric-ax agents start` lets a fresh spawn on the same URL work again. Since a clean postgres alone (via the SQL purge above) does *not* fix it, the broken state appears to live in agents-server **process memory**, not in postgres.

## Common thread

In both cases: the entity row exists, postgres looks normal, the agents UI shows the entity — but no runner/wake binding is actually live for it. A full process restart of agents-server is the only thing that reliably clears the bad state.

## Worst-shaped part

Neither failure surfaces at spawn time. `spawnEntity` returns success and the agents UI shows the entity green. Trigger 1 is the worse of the two: an inbox write to an orphaned entity returns **HTTP 204** — a success response — and the message is silently dropped. Application code has no signal that the message will never be processed. Trigger 2 at least returns an explicit `409`.

## Suggested fixes

- On `agents start`, walk the entity registry and re-validate wake-registrations against the webhook URLs registered by the current `registerTypes()` calls. Rebind rows whose target entity still exists; drop rows whose entity is gone. Log a summary of rehydrated vs. dropped registrations.
- Make `deleteEntity` truly destructive — clear postgres rows, in-process cache, and wake/runner bindings — so a URL is genuinely free for reuse; *or* make `spawnEntity` on a stale URL re-initialize the in-process lifecycle state and bind a runner.
- At minimum: an inbox write or `sendEntityMessage` to an entity with no live wake binding should return a loud error (e.g. `409` / `503`), never a silent `204`. A successful HTTP response should mean the message will actually be dispatched.

## Workaround

In application code: never reuse an entity URL. Generate a unique suffix per session (e.g. `/type/id-`); old entities become dead rows in postgres but never collide with new ones. Also avoid `agents stop && agents start` on a substrate with live entities you still need. Both workarounds leak dead entity rows over time.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.