Repository source: docs/plans/new_thread_board_stall_investigation_v1.md
New Thread Board Stall Investigation
Date: 2026-06-26
Symptom
After creating a new thread, the created thread page renders successfully, but subsequent board requests appear unresponsive for roughly 5-15 seconds.
Current Findings
The first suspected cause was automatic post-create Dedalus analysis. That path can block the PHP built-in server because it starts /api/analyze_post from public/assets/post_analysis.js after a newly created post page renders. A local mitigation was added:
'DEDALUS_AGENT_REPLIES_AUTOMATIC_ENABLED' => false,
The local private config currently loads that value. PrivateConfig::load() reports:
DEDALUS_AGENT_REPLIES_AUTOMATIC_ENABLED => false
The first implementation of the mitigation mishandled this exact config shape. The private config stores a PHP boolean false; the code cast config values to string before comparison. In PHP, (string) false is "", so the automatic-agent flag was accidentally treated as enabled unless it came from an environment variable string such as "false".
That explains why restarting the server did not stop /api/analyze_post: the server was loading the intended config, but interpreting it incorrectly.
The flag parser now handles PHP booleans, numeric values, and common string values consistently. A reflection check against the real local config now returns:
raw config value: bool(false)
agentRepliesAutomaticEnabled(): bool(false)
A regression test now covers a temporary PHP secrets file containing:
return [
'DEDALUS_AGENT_REPLIES_AUTOMATIC_ENABLED' => false,
];
The normal write path is also not slow in a clean probe. Using ./v3 start 127.0.0.1:8765, POST /api/create_thread completed in about 23 ms wall time. Its Server-Timing showed:
lock_wait=0.0ms
git_commit=7.4ms
read_model_incremental_update=5.9ms
write_total=19.4ms
total=19.6ms
Follow-up requests against the same server were also fast:
GET /threads/<new-id>?created_post_id=<new-id>&__v=<sha> ~12.6ms
GET / with identity_hint cookie ~8.5ms
POST /api/set_identity_hint ~3.9ms
GET /api/read_model_status ~7.0ms
GET /threads/<new-id> ~13.2ms
The current read-model metadata matches the writable repository:
rebuild_reason=write_incremental
repository_head=7ddba1729eb29c4ed0b6bbc0c00b0e4814fbd4fb
repository_root=/home/wsl/v3/state/local_repository
schema_version=10
No stale read-model marker was present in state/cache.
Ruled Out For Current State
A normal successful
createThread()taking 5-15 seconds.Immediate full read-model rebuild after every thread write.
Automatic agent reply work being intentionally enabled by the current local config. It was still happening because of the boolean parsing bug above.
Static artifact generation on the exact created-thread landing URL. That URL has query parameters, and
FrontController::buildStaticArtifactOnEligibleMiss()skips query-bearing requests.
Still Plausible
The PHP built-in server is single-process. Any one long request can make unrelated board requests look hung until it finishes. Since the clean curl path is fast, the remaining likely cause is a browser-only or stale-process follow-up request that the curl probe did not trigger.
Most likely candidates after the boolean parser fix:
A browser tab with old HTML containing
data-agent-reply-work, causingpost_analysis.jsto call/api/analyze_post.A request to
/api/analyze_postor/api/generate_agent_replyfrom another tab. With live Dedalus config, that can occupy the local PHP server for the Dedalus timeout window.The create-thread write itself sometimes taking the slow path on the user's actual interaction, unlike the clean curl probe. This should be visible in
Server-Timing.A rare incremental read-model failure falling back to full rebuild. This should show up in
Server-Timingasread_model_rebuildorread_model_*_fallback.Lock contention from another local command or request. This should show up in
Server-Timingaslock_wait, or as a 503 busy page if the wait exceeds the lock timeout.Queryless, cookie-free dynamic route misses that build static artifacts after rendering. This should not happen for the created URL, but could happen if a browser later requests
/threads/<id>without query/cookies and no artifact exists.
The user observed that /api/analyze_post returns much faster than stalled /api/set_identity_hint and /api/create_thread. That means analyze_post is not necessarily the 20-second request itself. Its importance is that it proved the automatic-work disable flag was not being honored.
Next Diagnostic Step
Use the browser Network tab immediately after creating a thread and identify the request that is pending for 5-15 seconds. The important columns are URL, Initiator, Status, and Timing.
If the pending URL is:
/api/analyze_postor/api/generate_agent_reply: automatic work is still being triggered somewhere. After the boolean parser fix, this would likely be stale page HTML or another tab./api/set_identity_hint: identity prewarm is blocked behind another slow request, not slow by itself./or/threads/<id>: inspectServer-Timing; look forlock_wait,read_model_rebuild, or fallback timings./api/create_thread: inspectServer-Timing; a 20-second server-side create should name the phase, such aslock_wait,git_commit,read_model_rebuild, orread_model_incremental_*./api/version: unlikely to be the real blocker because it bypassesensureReadModel().
Useful curl comparison after a stall:
curl -sS -D /tmp/headers.txt -o /tmp/body.txt -w 'total=%{time_total} code=%{http_code}\n' \
-b 'identity_hint=guest' \
'http://127.0.0.1:8000/'
grep -i 'server-timing' /tmp/headers.txt
Working Conclusion
There was a real bug in automatic-agent-work flag parsing. That bug explains why /api/analyze_post still appeared after restart despite the local private config setting.
The current repository state still does not reproduce a slow core write/read path in a clean curl probe. If the 20-second stall continues after this parser fix and a fresh page load, the next fix should be driven by Server-Timing from the slow /api/create_thread response or by adding per-request logging around FrontController::handle() to print method, URI, elapsed time, and phase names.