Repository source: docs/plans/production_slow_actions_client_responsiveness_plan_v1.md
Production Slow Actions and Client Responsiveness Plan V1
This plan identifies user-visible actions that can still feel slow in production and sets a starting point for making them feel instantaneous while preserving canonical server-side integrity.
Problem Summary
The current production write path is intentionally conservative:
accept a POST
validate request data and viewer identity
write one or more canonical files
commit the canonical files to git
update or rebuild the SQLite read model
invalidate static artifacts
return a response or redirect
Several actions already expose Server-Timing details, and previous incremental read-model work reduced the worst full-rebuild cases. Users are still reporting slow interactions, so the next step is to inventory every action that can block on disk, git, SQLite, static invalidation, identity generation, OpenPGP inspection, or external model calls.
The product goal is not only to reduce server latency. The interface should optimistically perform any action that can be predicted locally, then reconcile with the server response. Canonical writes remain authoritative.
Slow-Capable Action Inventory
Compose Thread
Entry points:
POST /compose/threadPOST /api/create_thread
Server path:
Application::handleComposeThreadSubmit()Application::handleCreateThread()LocalWriteService::createThread()
Why it can be slow:
global write lock
canonical post file write
optional thread-label record write when body contains labels
git add, commit, and rev-parse
read-model incremental update or full rebuild fallback
board and thread artifact invalidation
redirect waits for all write work to complete
Client-side opportunity:
validate and normalize compose fields before submit
create an optimistic thread shell immediately with temporary id, subject, body, board tags, author hint, and pending state
navigate immediately to a pending thread view or insert the thread at the top of the board
reconcile temporary id with returned
thread_idandcommit_sha
Integrity requirement:
server still assigns canonical ids and timestamps
optimistic thread must be marked failed or retried if validation, identity, git, or read-model work fails
Compose Reply
Entry points:
POST /compose/replyPOST /api/create_reply
Server path:
Application::handleComposeReplySubmit()Application::handleCreateReply()LocalWriteService::createReply()
Why it can be slow:
global write lock
parent and thread canonical reads
canonical reply file write
optional thread-label record write from reply body labels
git add, commit, and rev-parse
read-model incremental update or full rebuild fallback
reply, thread, board, and post artifact invalidation
redirect waits for all write work to complete
Client-side opportunity:
append a pending reply to the current thread immediately
preserve form draft until server confirms success
update reply count and last-activity affordances locally
replace temporary id and pending status with returned
post_id
Integrity requirement:
server must verify parent belongs to target thread
failed writes must restore the draft and remove or mark the pending reply
Apply Thread Tag
Entry point:
POST /api/apply_thread_tag
Server path:
Application::handleApplyThreadTag()LocalWriteService::applyThreadTag()public/assets/thread_reactions.js
Why it can be slow:
browser identity readiness can run before the request
global write lock
duplicate tag detection scans existing records
canonical thread-label record write
git add, commit, and rev-parse
read-model incremental update or full rebuild fallback
board and thread artifact invalidation
Client-side opportunity:
immediately set the button pressed state and feedback text
increment or otherwise adjust visible thread score optimistically when the viewer is known approved
store a pending reaction marker keyed by
thread_id,tag, and viewer identityreconcile
score_total,wrote_record, andviewer_is_approvedfrom the server response
Integrity requirement:
server remains responsible for deduping one identity/tag/thread tuple
optimistic score must roll back when server returns an error or
viewer_is_approved=no
Apply Post Tag
Entry point:
POST /api/apply_post_tag
Server path:
Application::handleApplyPostTag()LocalWriteService::applyPostTag()public/assets/thread_reactions.js
Why it can be slow:
browser identity readiness can run before the request
global write lock
target post canonical read
duplicate reaction detection scans existing records
canonical post-reaction record write
git add, commit, and rev-parse
read-model incremental update or full rebuild fallback
reply/thread artifact invalidation
Client-side opportunity:
immediately set the button pressed state and show applied feedback
optimistically hide a post when the selected tag is expected to hide it, while allowing undo of the local pending state before confirmation
reconcile
post_score_total,approved_flag_count,is_hidden,wrote_record, and viewer approval from the server response
Integrity requirement:
server remains authoritative for approval-sensitive hiding and score totals
failed writes must restore visibility and button state
Link Identity
Entry points:
POST /account/keyPOST /api/link_identity
Server path:
Application::handleAccountKeySubmit()Application::handleLinkIdentity()LocalWriteService::linkIdentity()
Why it can be slow:
OpenPGP public key inspection
optional bootstrap post creation
public key and identity canonical file writes
git add, commit, and rev-parse
identity read-model incremental update or full rebuild fallback
profile, bootstrap post, and thread artifact invalidation
Client-side opportunity:
parse and inspect the public key in the browser before submit where feasible
show a pending profile state immediately after submit
set identity hint locally after server confirmation
Integrity requirement:
server must remain authoritative for fingerprint uniqueness, username extraction, bootstrap anchoring, and canonical key storage
never treat a client-parsed key as linked until the server returns
identity_idandcommit_sha
Approve User
Entry points:
POST /profiles/{slug}/approvePOST /api/approve_user
Server path:
Application::handleApproveUserSubmit()Application::handleApproveUserApi()LocalWriteService::approveUser()
Why it can be slow:
viewer profile and approval checks
target profile lookup
bootstrap thread and parent canonical reads
canonical approval reply write
git add, commit, and rev-parse
approval-sensitive read-model update or full rebuild fallback
profile, thread, and approval artifact invalidation
Client-side opportunity:
immediately disable the approve button and show pending approval state
optimistically move the user out of pending lists only when the current viewer is already known approved
reconcile profile approval status after server confirmation
Integrity requirement:
server must enforce approved approver, no self-approval, valid bootstrap parent, and approval-derived score refresh
local pending approval must be reversible
Analyze Post
Entry point:
POST /api/analyze_post
Server path:
Application::handleAnalyzePost()PostAnalysisService::analyze()DedalusPostAnalyzer::analyze()when configured
Why it can be slow:
fetches post and builds analysis context
may call an external model provider
may check or update analysis storage
may also compute agent reply eligibility and summary
Client-side opportunity:
cache completed analysis responses by post id and content hash
render stale cached analysis immediately with a refreshing state
split agent reply status fetch from analysis when it adds avoidable latency
Integrity requirement:
server remains authoritative for moderation visibility and viewer permission
client must not expose privileged analysis details unless server has already returned them for the current viewer
Generate Agent Reply
Entry point:
POST /api/generate_agent_reply
Server path:
Application::handleGenerateAgentReply()Application::agentReplyResultForPost()DedalusAgentReplyGeneratorLocalWriteService::createReply()AgentIdentityService::ensureReplyAgentIdentity()
Why it can be slow:
requires complete post analysis
may call an external model provider or reuse stored generated text
may create or ensure an agent identity
posts a canonical reply through the normal slow reply write path
stores generation status before and after posting
Client-side opportunity:
treat generation as a background job from the user's perspective
show pending/in-progress status immediately
poll or refresh only the affected post/thread region
reuse the same optimistic pending reply rendering as human replies when posting begins
Integrity requirement:
server must gate recommendation, generation, idempotency, and canonical posting
client must never synthesize final agent reply text unless it came from server-authorized generation output
Set Identity Hint
Entry point:
GET|POST /api/set_identity_hint
Server path:
Application::handleSetIdentityHint()
Why it can be slow:
this should be fast, but it still requires a request round trip and cookie write
Client-side opportunity:
update local identity-dependent controls immediately before server confirmation
reconcile with cookie response on completion
Integrity requirement:
this is only a hint, not authorization; write endpoints must continue resolving and validating viewer identity server-side
Cross-Cutting Server Bottlenecks to Measure
Every write-like endpoint should expose or log these timings consistently:
lock_wait: time spent waiting to acquire the global write lockrequest_data: parsing form or API inputidentity_ready: viewer profile, identity hint, or key inspection workcanonical_read: reads needed to validate parent/thread/profile statewrite_file: canonical file writesgit_addgit_commitgit_rev_parseread_model_incremental_*read_model_*_fallbackartifact_invalidateexternal_provider: model-provider calls for analysis and agent repliesresponse_renderorredirecttotal
Current timing already covers several write phases. Missing lock-wait and identity/client-preparation timing are important because user reports can include time before the POST starts.
Client-Side Duplication Strategy
The client should duplicate deterministic, user-visible effects, not canonical authority.
Use this contract for each action:
Build a local operation object with a temporary id, action type, viewer identity hint, target ids, form data, and created-at client timestamp.
Apply a local optimistic reducer to update the visible UI immediately.
Send the server request with a client operation id.
On success, replace temporary ids and pending markers with canonical ids, server totals, and
commit_sha.On duplicate/no-op success, keep the final UI state but remove pending markers.
On failure, roll back or mark the local operation failed with a retry path.
On reload, persist pending operations in session storage and reconcile against canonical server state.
This keeps the interface fast while preserving server-side validation, deduplication, authorization, git history, and read-model correctness.
Implementation Slices
Slice 1: Instrument the Full User-Perceived Path
Goal:
distinguish server latency from browser preparation, network time, redirect/render time, and lock wait
Work:
add
lock_waittiming aroundExecutionLock::withExclusiveLock()ensure all write endpoints return
Server-Timing, including approval APIsadd client-side performance marks around reaction clicks, compose submits, identity readiness, fetch start, first optimistic paint, response received, and reconciliation
log slow action samples in a structured browser-safe format during production debugging
Acceptance:
for each slow-capable action, a production report can identify whether the delay is client identity prep, lock wait, git, read-model work, artifact invalidation, provider calls, redirect, or rendering
Slice 2: Optimistic Reactions First
Goal:
make the smallest high-frequency actions feel instantaneous
Work:
update
thread_reactions.jsto apply button state, feedback, and visible score/post visibility before the fetch completespreserve current server response parsing for reconciliation
add rollback handling for errors
persist pending reaction operations until confirmation
Acceptance:
clicking like/flag changes visible state in under one frame after identity is ready
server response still corrects score totals, duplicate states, approval-sensitive visibility, and failures
Slice 3: Optimistic Reply Submit
Goal:
remove the most painful discussion-flow delay
Work:
submit replies through
fetchwhere JavaScript is availableappend a pending reply card immediately
keep the form draft until server success
reconcile returned
post_id,thread_id, andcommit_shafall back to the current POST redirect when JavaScript is unavailable
Acceptance:
reply content appears in the thread immediately after submit
canonical response replaces the temporary reply without duplicate cards
failed replies keep recoverable text and show a retryable error
Slice 4: Optimistic Thread Creation
Goal:
make starting a new thread feel immediate without losing canonical redirect behavior
Work:
submit thread creation through
fetchwhen JavaScript is availableinsert a pending board item and optionally navigate to a pending thread route
reconcile the final
thread_id,post_id, andcommit_sharetain non-JavaScript redirect behavior
Acceptance:
users see their new thread immediately
final canonical route replaces the temporary state after success
failed thread creation preserves the draft
Slice 5: Identity and Approval Pending States
Goal:
make slow account and moderation actions visibly progress instead of appearing stuck
Work:
add pending UI states for identity linking and user approval
avoid claiming success until the server confirms
move pending users optimistically only when rollback is clear and visible
Acceptance:
account and approval actions give immediate state feedback
all authorization and uniqueness decisions remain server-owned
Slice 6: Provider-Backed Actions as Background Jobs
Goal:
stop model-backed analysis and agent reply flows from blocking the interactive path
Work:
split analysis request, status, and agent-reply generation where currently coupled
render cached or pending states immediately
poll or refresh only affected fragments
use the same pending reply renderer for agent reply posting
Acceptance:
analysis and agent reply controls move immediately to pending/in-progress states
users are not blocked waiting for external provider latency
Data Integrity Rules
The server is always authoritative for canonical ids, timestamps, authorization, duplicate detection, approval-sensitive scores, hidden state, and
commit_sha.Client operations are speculative until the server confirms.
Every optimistic operation must have rollback or failed-pending behavior.
The non-JavaScript POST and redirect path must keep working.
Client-side validation may prevent obvious invalid submits, but server validation must not be weakened.
Reconciliation must tolerate duplicate submissions, back/forward navigation, refreshes, and multiple tabs.
Initial Priority
Start with:
add missing end-to-end timing for lock wait and client-perceived action duration
ship optimistic thread/post reactions
ship optimistic replies
use measured production data to decide whether thread creation, approval, identity linking, or provider-backed actions are next
This order targets frequent small interactions first, then the main discussion workflow, while collecting enough data to avoid guessing about the remaining slow paths.