docs: mark Phase 2 complete in jerboa-edge.md with verified results

ober

445c66fecb74c6e605f51216c665d61eb2211b05

diff --git a/docs/jerboa-edge.md b/docs/jerboa-edge.md
index 260eed8..98b67f7 100644
--- a/docs/jerboa-edge.md
+++ b/docs/jerboa-edge.md
@@ -4,7 +4,7 @@
 demonstrates why Jerboa exists — Clojure's data philosophy, Erlang's fault
 tolerance, Go's deployment story, and built-in security, all in one 15MB binary.
 
-**Status:** 2026-04-12 — Design
+**Status:** 2026-04-12 — Phase 2 complete
 
 ---
 
@@ -664,7 +664,6 @@ curl localhost:8080/api/stats
 **Non-goals for Phase 1:**
 - Persistence across restarts (in-memory only)
 - TLS termination (use a reverse proxy, or wait for Phase 3)
-- User-defined filters (Phase 2)
 - Static binary packaging (Phase 3)
 
 **Success criteria:**
@@ -679,45 +678,52 @@ curl localhost:8080/api/stats
 
 **Deliverable:** Sandboxed user filter support + handler registry
 
-2.1 **Sandboxed eval** — Users submit filter expressions via
-    `POST /api/filters`.  Filters are stored and evaluated in a Landlock
-    sandbox for each incoming event.  Events that fail the filter are
-    dropped before reaching workers.
-
-```scheme
-;; User submits:
-;; POST /api/filters
-;; {"name": "big-payments", "code": "(> (cdr (assoc \"amount\" evt)) 1000)"}
-
-;; Internally:
-(def (apply-user-filters event filters)
-  (every
-    (lambda (f)
-      (let ([result (eval-user-filter (hash-ref f "code") event)])
-        (and (not (err? result))
-             (unwrap result))))
-    filters))
-```
-
-2.2 **Hot handler registration** — New webhook types can be registered at
-    runtime via the REPL or an admin API.  No restart required.
-
-```scheme
-;; Via REPL connected to running service:
-(hash-put! handlers "order.shipped"
-  (lambda (evt)
-    (send-notification! (hash-ref (hash-ref evt "payload") "email")
-      "Your order has shipped!")))
-```
-
-2.3 **Retry with exponential backoff** — Failed events re-enter the channel
-    with a retry count.  Workers check the count and delay processing.
-    After N retries, events go to a dead-letter store.
+- [x] 2.1 **Sandboxed eval** — Users submit filter expressions via
+    `POST /api/filters`.  Filters evaluated per-event via
+    `restricted-eval-string` (allowlist-only: no I/O, no FFI) with a
+    2-second engine-based timeout (no fork — fiber-safe).  Events that
+    fail any filter are dropped silently before processing.
+
+    ```
+    POST /api/filters  {"name":"payments-only","code":"(equal? (cdr (assoc \"type\" evt)) \"payment.completed\")"}
+    GET  /api/filters
+    DELETE /api/filters/:name
+    ```
+
+    Key implementation detail: event data is bound as `evt` (a deep
+    alist), so filter code uses `assoc` with string keys (not `assq`).
+
+- [x] 2.2 **Hot handler registration** — New webhook types registered at
+    runtime via `POST /api/handlers`.  No restart required.  Handler
+    code runs in a custom restricted environment (`safe-bindings` +
+    `displayln`) with a 5-second engine timeout.  Exceptions propagate
+    to the worker's retry machinery.
+
+    ```
+    POST /api/handlers  {"type":"order.shipped","code":"(displayln \"shipped: \" (cdr (assoc \"id\" evt)))"}
+    GET  /api/handlers
+    ```
+
+- [x] 2.3 **Retry with exponential backoff** — Failed events re-enter the
+    channel with an incremented retry count.  Workers delay via
+    `fork-thread` + `sleep` (non-blocking, OS-thread safe).  Dedup
+    transducer passes retried events (`retry-count > 0`).  After 3
+    retries, events go to a dead-letter STM store.
+
+    ```
+    GET  /api/dead-letter
+    POST /api/dead-letter/:id/replay
+    ```
+
+    Replay sets `retry-count: 1` to bypass the dedup filter.
 
 **Success criteria:**
-- User filter code cannot read files, open sockets, or run longer than 2s
-- Hot-registered handlers process events without service restart
-- Failed events retry 3 times with 1s/2s/4s backoff
+- [x] Filter code cannot call `open-file`, `socket`, or any I/O (allowlist env)
+- [x] Filter with 2s timeout verified: `with-timeout` uses Chez engines, no fork
+- [x] Hot-registered handlers process events without service restart (verified: `order.shipped`, `inventory.updated`)
+- [x] Failed events retry 3× with 1s/2s/4s backoff then dead-letter (verified in log)
+- [x] Dead-letter replay re-processes event and clears queue (verified: status → "ok")
+- [x] Runs with `make run` (690 lines of Scheme)
 
 ### Phase 3: Production Hardening