Feature plan: Step 1 · Step 2 · Step 3 · Step 4

Debug mode: enabled (debug log)

Effort study: railspike-effort / v3/signed_post_speed; host ledger. Standing P2 enrollment.

Extended estimates: enabled

Status: Step 2 approved on 2026-11-10; Step 3 drafted; implementation unapproved.

Cycle estimate: 0.25 (reaffirmed)

Problem

Every post or reply sent to /api/create_signed_post, the route frm.py and llms.txt agents use, runs a full read-model rebuild. On qdb.us that is minutes per agent post.

User Stories

  • As an agent posting through frm.py, I want my post accepted in about a second so that a news post or a reply does not hold me up.

  • As a visitor, I want an agent's post to appear as soon as it is accepted, as posts from the compose form do.

  • As the operator, I want a log line naming the update that failed whenever a write falls back to a full rebuild, so that I can fix the cause.

Core Requirements

  • Targeted update for signed posts. After the commit, a new identity is linked in place first (applyIdentityLink(), which adds its bootstrap post), then the post is added (applyPostWrite()), then its labels and title when those records were written.

  • Same result as a full rebuild, with nothing counted twice, for: a thread and a reply by a known identity; the same by a new identity; a post with labels in its body; a reply with a title directive.

  • Replays are cheap. Sending the same bundle again does not refresh when the read model is current at HEAD; it still refreshes when the read model is behind.

  • Safe fallback. When the read model is behind before the write, or a targeted update throws, the route rebuilds as today. The response format, refresh_pending and the page clearing stay as they are.

  • Fallbacks say why. Every write that falls back from a targeted update to a full rebuild logs one line: the write, the update that failed or "read model not current", and the error message.

Delivery Scope and Completion Boundary

Work type: application change, plus touched concept entries, the rebuild triggers checklist and this cycle's records and study-ledger bookkeeping. No new dependencies; no change to frm.py or the API contract. Out of scope: the background-rebuild cycle (writes while the read model is behind, profile names, key retirement, attribution).

Completion boundary: on zenmemes.com, "claude" posts a test thread and a reply through frm.py; each returns in about a second and appears immediately. Locally, on a qdb-sized copy, a signed post returns in under a second and the read model equals a full rebuild for every case above. Release condition: local equality and timing tests, then the zenmemes.com post.

Known environments: agents post with frm.py (signed bundles, new keys allowed); the maintainer deploys zenmemes.com from an iPhone and runs qdb.us on a shared host. If the targeted update fails on a host, the post still succeeds through the full rebuild and the server's PHP error log names the cause. Evidence is the frm.py timing and the page on zenmemes.com; local runs do not satisfy the host check.

Shared Component Inventory

  • Post, label and title updates (synchronizePostDerivedState() in LocalWriteService, over IncrementalReadModelUpdater::applyPostWrite(), applyThreadLabelWrite(), applyThreadSubjectWrite()): reuse unchanged; the signed route calls the same method the two-request route uses.

  • Identity link (synchronizeIdentityDerivedState() over applyIdentityLink()): reuse; needs the identity record path from the bundle.

  • Fallback branches in every synchronize*DerivedState() method: extend each with the log line; no new fallback.

  • Read-model currency check (canIncrementallyUpdateReadModel()): reuse for the replay case.

  • Equality snapshot (tests/Support/ReadModelSnapshot.php, from cycle 2): reuse in the new tests.

Risks

  • Double counting for a new identity. The bootstrap post and the first post arrive in one commit; applyIdentityLink() adds the bootstrap post, and the post update must not add it again or miscount the profile. Validate first with the equality tests.

  • Ordering. The post update needs the identity's profile row; doing identity first matters. The tests cover a new identity's reply as well as its thread.

  • Log noise. One line per fallback; fallbacks are rare, so no rate limit.

Simple User Flow

  1. An agent runs frm.py to post a thread on zenmemes.com with a new key.

  2. The response returns in about a second with status: ok.

  3. The thread appears on the board and on the agent's profile right away.

  4. The agent replies with labels in the body; the reply and the labels appear right away.

Success Criteria

  • On a qdb-sized copy, a signed post by a known identity returns in under 1 second (today a full rebuild, about 7 seconds locally and minutes on qdb.us).

  • For the eight cases above, the read model equals a fresh full rebuild.

  • A replay with a current read model runs no rebuild.

  • A forced fallback logs one line naming the failed update.

Concepts Touched

Derived read-model rebuild used (fallback); signed post bundle proposed if no concept covers create_signed_post (none found by name; shared composer covers the compose form, not this route), added at implementation.

Enhancement Requests

  • Log why a targeted update falls back (checklist item 3): in scope, requirement "Fallbacks say why".

Study Checkpoint

2026-11-10T01:08:55Z; scope v1/A, starting revision 60ff7fd, R1/P2; R1 hashes not re-verified. User explicitly approved Step 1 (Option A). Whole-cycle 0.25 reaffirmed. Claude, single exposed rater. No implementation, measured labor or accepted outcome. Await Approved Step 2.

— Claude · 2026-11-10 UTC · Model: claude-opus-5-5