From 88b61f7522b4ce48cfa75bfcd89ed46c24158d56 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sat, 29 Aug 2026 23:40:44 +0200 Subject: [PATCH] =?UTF-8?q?docs(porch-store):=20call=20replies=20are=20sca?= =?UTF-8?q?lars=20=E2=80=94=20response=20goes=20via=20the=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WO-E226: call's reply must be a copyable scalar, and every receive program-wide must declare the same return type. Verified by fixture: "call's reply type `Out` is not a copyable scalar" - the spec had the actor return the response object, which cannot cross the mailbox. Corrected: the actor stores the response and returns an outcome code; the middleware reads the row and builds the Resp - owner and duplicate now read the SAME durable row, so byte-identical replay is structural rather than careful copying - blocking, exactly-once execution and the mailbox queue are unchanged Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit 77e06c1690b92d456a9bc53503695fdaa2b4b44e) --- ...026-08-29-porch-store-backed-middleware.md | 17 ++++++++++++----- ...29-porch-store-backed-middleware-design.md | 19 ++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/superpowers/plans/2026-08-29-porch-store-backed-middleware.md b/docs/superpowers/plans/2026-08-29-porch-store-backed-middleware.md index 6607819..9abbd05 100644 --- a/docs/superpowers/plans/2026-08-29-porch-store-backed-middleware.md +++ b/docs/superpowers/plans/2026-08-29-porch-store-backed-middleware.md @@ -216,11 +216,18 @@ from Task 1. - [ ] **Step 3 — the digest.** Compute a digest of method, path and body when `include_body` is set, and pass it alongside the bare key. Keep `use json` — the file did not typecheck without it. -- [ ] **Step 4 — the actor arm for the begin message.** Look the bare key up. - A hit whose digest matches returns the stored response. A hit whose - digest differs returns a refusal, answered as **422**. A miss runs the - handler, stores status, body and `content-type` with the digest and the - current tick, and returns the response. +- [ ] **Step 4 — the actor arm for the begin message.** `call`'s reply must be + a copyable scalar (WO-E226 — a response object cannot cross the mailbox), + so the arm returns an **outcome code** and the response travels through + the table. Look the bare key up. A hit whose digest matches returns + "replayed" without running the handler. A hit whose digest differs + returns "mismatch". A miss runs the handler, stores status, body and + `content-type` with the digest and the current tick, and returns + "executed". The middleware then reads the stored row and builds the + response from it — so owner and duplicate return the same durable row, + and byte-identical replay is structural rather than careful. + **Keep the return type identical to the one Task 2's count arm uses**; + WO-E226 also requires every `receive` program-wide to agree. - [ ] **Step 5 — the replay allowlist.** Store and replay `content-type` only. Never `Set-Cookie`, never `Date`. Cookies arrive in porch 2; this is cheap now and expensive to retrofit after. diff --git a/docs/superpowers/specs/2026-08-29-porch-store-backed-middleware-design.md b/docs/superpowers/specs/2026-08-29-porch-store-backed-middleware-design.md index 32bc7a7..b02369e 100644 --- a/docs/superpowers/specs/2026-08-29-porch-store-backed-middleware-design.md +++ b/docs/superpowers/specs/2026-08-29-porch-store-backed-middleware-design.md @@ -123,11 +123,20 @@ History. A keyed request does not run its own handler; it calls its pool actor, passing the request and the route's `Handler`, and the actor invokes the handler inside its own `receive`. -| Actor state for that key | What `receive` returns | -| --- | --- | -| A stored response exists, digest matches | the stored response; the handler never runs | -| A stored response exists, digest differs | a refusal verdict, answered as 422 | -| No record | run the handler here, store the response, return it | +**The reply is a scalar, so the response travels through the table.** `call`'s +reply must be a copyable scalar (WO-E226), and every `receive` program-wide +must declare the same return type. A response object cannot cross the mailbox. +So the actor stores the response in the `@table` and returns an outcome code; +the middleware reads the stored row and builds the response from it. Owner and +duplicate therefore read the *same durable row* — which is what the store is +for, and it means the replay is byte-identical by construction rather than by +careful copying. + +| Actor state for that key | Actor does | Returns | +| --- | --- | --- | +| A stored response exists, digest matches | nothing; the handler never runs | "replayed" | +| A stored response exists, digest differs | nothing | "mismatch", answered as 422 | +| No record | runs the handler here and stores the response | "executed" | There is no fourth row, and that is the point. **A duplicate arriving while the owner's handler runs waits in the mailbox**, because an actor processes one