Solver-backed
Goal: Review your recently changed files for code reuse, quality, and efficiency issues, then fix them. Run the Playwright test, debug any failures, fix the real root cause, and keep going until the test passes reliably.
Important working rules:
What I want you to do
Phase 1: Inspect
Phase 2: Run the test
Phase 3: Debug the root cause
Phase 4: Re-run and stabilize
Technical expectations
Use Playwright MCP to test the website end to end:
Validation After fixing, run:
Output format
Critical instruction Do not stop at a superficial pass. The task is only done when the Playwright test passes and the underlying issue is fixed with clear, simple, and robust logic.
I’m inspecting the workspace first to locate the Playwright setup, the relevant app flow, and the files changed in the current worktree before I run anything. After that I’ll execute the exact failing e2e path, collect the failure artifacts, and only then make code changes.
I’ve found the Playwright harness and there’s a single e2e flow under tests/e2e/analysis-flow.spec.ts, with global setup enforcing an existing authenticated storage state at playwright/.auth/google-user.json. Next I’m reading the config, setup, and that test to determine the exact route coverage and prerequisites before I run it.
The e2e flow covers /, /table/[roomId], /hands, and /hands/[handId], and it depends on a live web app on http://localhost:3000, API on http://localhost:3001, the analysis worker being healthy, plus a valid Playwright auth state file. I’m reading the route/components tied to those selectors now, along with the existing worktree diff, so I can correlate any failure with the recent changes instead of treating the test in isolation.
Plan:
analysis-flow.spec.ts.I’m checking whether the local stack is actually available before invoking Playwright. If it isn’t, I’ll start the repo’s existing pnpm dev flow rather than inventing a separate test harness.
Before I start the stack, I’m checking the analysis debug UI flags the e2e test depends on. That flow explicitly requires the debug panel to be enabled, so I need to confirm whether the app boots with those settings or whether the failure would just be an environment mismatch.
The stack isn’t up, so I’m starting the repo’s own pnpm dev entrypoint with the debug flags the e2e requires. After it comes up, I’ll run the Playwright spec exactly as configured and keep the first failure intact for diagnosis.
The repo’s own dev script failed because port 3000 was already occupied by another process, so I’m identifying that owner next. If it’s this same repo, I’ll replace it with a fresh run under the required debug flags; if it’s unrelated, I’ll route around it instead of guessing.
Ports 3000 and 3001 are already served by this same repo’s Node processes, which explains the startup conflict. I’m enumerating the rest of the repo-owned processes now so I can restart the stack cleanly under the required env flags instead of mixing old and new instances.
I found an older repo dev stack still running: Next dev, API watch, worker watch, and the table watcher. I’m stopping just those repo watcher processes, then I’ll restart pnpm dev cleanly so the e2e runs against one consistent stack with the debug UI enabled.
The stack is now healthy enough to run the e2e, so I’m invoking the single configured Playwright spec directly. I’m not touching code yet; the next step is to preserve the first real failure with its artifacts and correlate it to the product path.
The first failure is specific: the analysis completed, but the hand-review page no longer exposes a Preflop 1 button that the existing test expects. I’m inspecting the hand-detail navigation logic and the failure artifacts now to determine whether this is a stale test assumption or a real UI regression in the recent hand-review changes.
I’ve confirmed from the failure screenshot that the page is already on the preflop decision when there is only one hero decision on that street, and the navigation intentionally renders Preflop instead of a separate Preflop 1 button. I’m validating that against the existing unit tests and then I’ll update the e2e flow to use the stable street/decision test ids the page already exposes.
I’ve got the failure pinned to the e2e selector logic, not the analysis pipeline. I’m patching the spec so it selects the street’s active decision the same way the product does: a street tab when there’s one hero decision, or the numbered first decision when there are multiple.
The spec is patched to follow the page’s actual navigation model instead of assuming every street has a numbered first-decision button. I’m rerunning the full Playwright flow now; if anything else fails, I’ll keep tracing until the real path passes cleanly.
The next failure is a real product issue: the whole-hand pipeline is getting blocked because a postflop decision ends in hero_combo_unavailable, so later streets never finish. I’m tracing that through the analysis pipeline and the decision canonicalization code now, using the exact error path from the worker output rather than patching the test.
The pipeline failure is specifically hero_key_not_in_combo_map, which means the solver returned a combo policy map but the hero hand lookup key didn’t match any combo entry. I’m tracing the combo-key generation path now, including the shared TexasSolver key helpers and any recent table-state changes that might have altered the hero cards used for lookup.
I need to see the actual failing hand data next, not just the status summary. If the hero combo truly isn’t in the solver map, the likely causes are either a bad hero combo lookup or an impossible hand state, so I’m pulling the stored hand/decision/event records for that exact failed hand.
The debug events expose a stronger root cause than the generic solver failure: the turn decision is selecting solver path BET 13.000000, which belongs to the later river action, not the turn spot being analyzed. I’m tracing how the worker filters hand events for a decision now, because it looks like later actions are leaking into the solver node selection.
The event replay itself is using the correct handEventSeq cutoff, so the leak is likely happening later when the derived solver history is built. I’m checking whether that history is being built from the full hand instead of the decision-scoped event list.
The hand record itself is inconsistent across sources: the stored participant hole cards, the stored deal event, and the UI screenshot do not agree. That makes the hero-combo lookup fundamentally unreliable, so I’m switching to the hand-persistence path now to find where hole cards are being recorded incorrectly.
The placeholder deal event is a red herring; analysis is already preferring the persisted participant hole cards. I’m now checking whether the solver-service is returning combo policies for the wrong player perspective at the selected node, because this spot is IP facing a turn bet and the hero combo may be looked up on the wrong side of the tree.
I’m reproducing the solver call directly with the same turn spot data from the failed hand so I can see whether the combo map omission comes from the request itself or from how we interpret the solver response.
The direct solver reproduction exposes the exact mismatch: the solver returns representative combo keys like Qs2c, while our lookup uses the hero’s exact physical combo Qs2h. That means the bug is in hero-combo key canonicalization for solver output, not in the request or the queueing logic. I’m looking for existing combo-key utilities first so I can fix this in one place instead of patching the worker ad hoc.
I’ve isolated the remaining failure to combo lookup against TexasSolver’s normalized policy keys. I’m reading the solver normalization and API lookup paths now, then I’ll implement the smallest board-aware fix and rerun the targeted tests plus the full Playwright flow.
The failure path is clear enough now to edit safely. I’m tracing both places that resolve the hero combo so the fix lands in the shared lookup logic instead of as a one-off fallback.
There’s already test coverage around attachHeroComboPolicy, which is useful. I’m extending that path rather than inventing a new branch, and I’ll add a regression case for the exact isomorphic combo that broke the turn analysis.
I need the solver’s actual key pattern before writing mapping logic. I’m querying the normalized combo keys for the reproduced turn spot so the fix matches TexasSolver’s real isomorphism behavior instead of an assumed suit rule.
The solver keys are clearly abstract representatives, not literal live combos, because the selected node includes keys like Qh2c on a board that already contains Qh. I’m checking a few more suit patterns now so the fix can choose the right representative deterministically instead of falling back arbitrarily.
There’s a cleaner fix path than reverse-engineering TexasSolver’s private suit canonicalization: when the solver returns only isomorphic representative keys and the exact hero combo is unavailable, rerun that solve with useIsomorphism: false and reuse the exact combo map. I’m validating the existing retry pipeline now to see if it can absorb that case cleanly.
I’m taking the fallback at the solver-service layer so the API and UI keep consuming a normal exact-combo response. Next step is to wire a controlled non-isomorphic rerun behind the existing solver child boundary and cover that branch with a focused test.
I’ve got the shape of the fix. I’m patching the solver-service retry path now: exact-combo requests that fail only because of isomorphic combo keys will rerun once with useIsomorphism: false, update the cache on success, and otherwise fall back cleanly to the original result.
I’m editing the solver-service files now. The change is localized to the child request path and the route-level fallback logic; no broad refactor.
The stream route needs a more surgical edit than the first patch chunk. I’m reading the current block and patching it in smaller pieces so I don’t trample the existing error-handling paths.
The code path is patched. I’m running focused solver-service tests first to catch any type or route regressions before I go back to the full browser flow.
TypeScript caught two leftover internal type references from the override rename. I’m patching those now, then I’ll rerun the solver-service typecheck before going back to the browser flow.
The targeted tests are green. I’m checking the real solver-service next with the same turn spot that was failing, before I rerun the full Playwright path.
The failing repro is still hitting the Docker-backed solver-service on port 4010, not the local apps/solver-service process, so my code hasn’t been exercised by the app yet. I’m checking the root dev script now so I can point the running stack at the updated local service and verify the real fix end to end.
The app is using the Docker solver path on Windows, so the fix won’t show up until that container is rebuilt. I’m confirming the current Docker solver env and then I’ll rebuild just the solver-service container from the updated workspace.
I’ve confirmed the repo has the Linux console_solver bundle, so I’m rebuilding the Docker solver-service directly from the updated workspace and rechecking the same failing turn spot before the Playwright rerun.
The rebuilt container is live, but my uncached direct /solve repro hit a solver timeout rather than the old combo-map bug. I’m checking the container logs and then I’ll validate the real app flow first, because the Playwright path uses the streaming API and may already behave correctly on its own request shape/cache pattern.
The e2e flow progressed past the original combo-map failure. It’s now failing later because the hand action status endpoint itself is returning 500, so I’m pulling the app logs and the failing trace details before I touch code again.
I’m checking the actual process topology now. The log pattern suggests duplicate repo processes or worker restarts are stacking Prisma pools, and I want to prove that before changing application code.
I found the immediate cause of the 500: a few ad-hoc repo scripts from earlier debugging are still running and each holds its own Prisma pool. I’m terminating just those non-dev processes, then I’ll rerun the e2e flow to confirm the backend recovers without code changes.
The stray Prisma clients are gone. I’m rerunning the exact Playwright spec now against the live stack to see whether the only remaining issue is the solver fallback behavior or if the full flow is clean.
The backend is healthier now. The remaining blocker is a queued whole-hand pipeline where one decision gets through solver and then hangs in calling_llm, which stalls the rest of the hand. I’m pulling the worker logs and the LLM call path next to see whether this is a missing timeout, retry loop, or queue starvation issue.
I have enough evidence that the pipeline itself isn’t stuck anymore; it’s draining a stale analysis backlog in Redis from the earlier failed runs. I’m checking the queue setup so I can clear only the leftover analysis jobs, rerun the flow on a clean worker, and confirm the real end-to-end behavior.
I found the queue config. I’m reading the live BullMQ counts and active jobs now so I can clear only the stale analysis backlog instead of blindly nuking Redis.
The live queue is down to one active and one waiting decision from the previous failed hand, so this is now an environment backlog issue, not a new code defect. I’m resetting the analysis queue and letting the worker come back up clean before the final verification runs.
The queue is clean. I’m doing another full Playwright run now; if it passes, I’ll run it one more time for confidence.
This is the actual product issue now: multiple analysis workers are consuming the same queue at once, but solver-service only allows one active solve and rejects the others with 429 Solver busy. I’m reading the worker boot path in the API server and the dedicated worker entrypoint to stop the duplicate worker startup at the source.
I’ve confirmed the concurrency bug: the queue worker starts up to 8 decision jobs in parallel by default, while solver-service accepts only one solve and rejects the rest with 429. I’m lowering the worker’s default concurrency to match the single-solver slot so the queue can’t outpace the solver anymore.
The concurrency fix is in. I’m verifying the worker actually reloaded with analysisWorkerConcurrency: 1, then I’ll clear the now-invalid queue state and rerun the Playwright spec on the updated worker.
The worker did restart, but it still booted with concurrency 8, which means this isn’t the code default anymore, it’s an env override. I’m checking the launcher env next so I can remove the override rather than patch around it in code.
I found the mismatch: apps/api/.env forces ANALYSIS_WORKER_CONCURRENCY=8 while SOLVER_SLOTS=1. I’m capping the effective worker concurrency to the configured solver slots in the worker boot code so this can’t regress from env drift.
The API typecheck is clean. I’m waiting for the worker reload, confirming it booted with effective concurrency 1, then I’ll clear the failed run’s queue state and rerun the browser test.
The worker is now actually running with effective concurrency 1 against a clean queue. I’m rerunning the full Playwright flow on that configuration now.
The last timeout was still contaminated by an in-flight job from the prior run. I’m doing a hard cleanup now: abort the active solver request, restart the worker process, wipe the analysis queue, then rerun the spec on a truly clean pipeline.
The worker has been restarted clean, the solver run was aborted, and Redis shows zero active or waiting analysis jobs. I’m running the Playwright spec again on that fully reset state.
I’ve isolated the earlier failures; the current blocker is that the analysis pipeline still stalls long enough for the e2e timeout even after fixing the stale selector, combo lookup bug, and solver concurrency mismatch. I’m checking the latest API and solver logs around the most recent hand/decision IDs now, then I’ll confirm whether the delay is in queueing, solver startup, or completion handling before changing code.
The latest logs point to a more concrete backend issue than “solver is just slow”: BullMQ is losing the job lock while a solve is in progress (could not renew lock / Missing key ... moveToDelayed). I’m reading the worker and analysis pipeline code around the job processor and retry path now, because that can strand the queue and make later decisions appear “stuck” even when solver requests are accepted.
I’ve confirmed the queue isn’t merely “slow”; the worker is losing its BullMQ lock mid-run, which is consistent with jobs getting stranded and later status polling never reaching completion. Next I’m checking the worker timeout/lock settings and the exact control flow around solver calls and retries to see whether the job is legitimately exceeding the lock window or whether we’re doing something that blocks renewal.
There’s another structural risk in the current setup: the API server still boots an analysis worker from src/index.ts, and there is also a dedicated src/worker.ts process running. I’m verifying whether pnpm dev is starting both at once, because that would inflate DB usage and queue/process contention even if BullMQ limits concurrency.
I found a plausible queue-level root cause that fits the timestamps: the hand pipeline currently queues non-overview hand-report jobs before it enqueues the actual per-decision solver jobs. Because they share the same worker/queue, those report jobs can consume the single available worker slot and delay flop/turn/river analysis enough for the e2e timeout. I’m validating that by tracing the report-queue path and job ordering now.
The timestamp gap now makes sense: the queue mixes three different job types, and the pipeline currently adds non-overview hand-report jobs ahead of the actual decision-analysis jobs. With a single solver slot, those report jobs can burn the queue first and starve the flop/turn/river analyses that the whole-hand status is waiting on. I’m verifying the call path that starts the pipeline, then I’ll change the ordering and add a focused test so the solver-critical decision jobs are always queued first.
The queue starvation path is clear enough to change safely: I’m moving non-overview hand-report enqueueing behind the decision-job enqueueing, and I’m tightening the pipeline test so it asserts that ordering explicitly. That keeps the solver-critical jobs at the front of the shared queue without adding new queue semantics.
The pipeline ordering change is in and the focused pipeline test passes. Before rerunning e2e I’m resetting the analysis queue and checking for any active solver/queue state, so the next run measures the current code instead of leftover jobs from earlier failures.
I’ve verified the targeted pipeline test and typecheck after the queue-order change. I’m running the full Playwright spec again now against a clean queue so we can see whether the timeout was resolved or if there’s another bottleneck behind it.
The rerun narrowed it further. The pipeline ordering helped, but the worker is still taking decisions in a nondeterministic order because startHandAnalysisPipeline enqueues them with Promise.all, so the longest flop solve can start last. I’m checking the latest logs around the flop decision now to confirm whether it actually timed out, completed late, or got stuck despite the advertised 300s timeout.
The new evidence is twofold: Promise.all makes decision enqueue order nondeterministic, and the remaining blocker is the flop solve itself, not queue starvation. I’m checking whether the code already has a dedicated flop budget knob and what defaults the repo expects, because if a simple checked-down flop regularly takes several minutes, the product-level fix is to tighten the default flop solve budget rather than let e2e sit behind an oversized tree.
The remaining inconsistency is important: the API sends the flop solve with timeoutMs: 300000, but after more than six minutes the job is still in calling_solver with no terminal event. I’m tracing timeout handling through the solver-service now, because if the service ignores the requested timeout on some flop trees, that’s the real source of the stuck run.
I found the timeout mismatch. The solver-service treats the API’s timeoutMs as a per-attempt budget, then silently retries timed-out flop solves once more with a compact tree. That can turn a 300000ms request into roughly 10 minutes, which is exactly why the API is still polling calling_solver after the supposed timeout budget. I’m checking the existing runner tests now, then I’ll change the retry so an explicit request timeout remains an end-to-end ceiling instead of a per-attempt multiplier.
There are two clean fixes left, both on the product side. First, the pipeline should enqueue decision jobs sequentially so street order is deterministic. Second, solver-service needs to stop turning an explicit timeoutMs into multiple full-budget attempts. I’m patching both now and then I’ll rerun the targeted tests before another e2e pass.
The fixes are in: decision jobs now enqueue sequentially, and solver-service can be told not to spend multiple full timeout budgets on a single timed-out request. I’m running the focused unit tests and typechecks on both packages before another end-to-end run.
The code and tests are clean locally, but this environment is using the Docker solver-service, so the end-to-end run will still hit old behavior until that container is rebuilt. I’m rebuilding the solver-service image against the current workspace now, then I’ll rerun the Playwright spec on a clean queue.
The Docker solver-service is rebuilt and healthy. I’m clearing any leftover queue state one more time and then rerunning the Playwright flow against the new worker and solver-service behavior.
The timeout-retry fix explains the earlier “stuck” behavior, but it also confirms the remaining product problem: the default flop tree itself is too wide for this workflow. Because flop/turn/river are solved independently in this app, the flop request does not need full future-street branching. I’m tightening the default future-street tree for flop solves now so the first flop solve finishes instead of timing out.
I tightened the default flop tree so the first solve isn’t dragging full turn/river branching behind it. I’m running the existing worker integration suite now to make sure that change doesn’t regress request construction before I go back to end-to-end.
The worker reload picked up the flop-tree change. I’m rerunning the Playwright spec now; if it still misses the budget, the remaining issue will be visible as either a true solver timeout or a UI/status regression rather than the earlier queue and timeout-accounting bugs.
The CLI e2e is green now. I still need a real browser pass through Playwright MCP per your request, so I’m loading the saved Playwright auth state and using MCP to open the app and inspect the analyzed hand in the actual UI.
I’ve fixed the underlying queueing, timeout, and solver issues that were keeping the analysis flow unstable. I’m rerunning the Playwright spec once more now to confirm the flow passes reliably after the product changes.
The second stability run exposed a real intermittent failure: the flop decision still times out and the solver process exits with SIGSEGV under the current tree shape. I’m inspecting the exact flop request builder and the existing solver limits now so I can reduce the actual root cause instead of masking it.
The failure is still specific to real flop solves, not the test harness: the debug payload shows the product pipeline blocking on a solver SIGSEGV after the explicit 300s flop budget. I’m querying the persisted hand/decision data next so I can see exactly what request shape this failing hand produced and tighten the tree based on that data rather than guessing.
The persisted debug event exposed something concrete: the failing flop request was still being sent with effectiveStack: 240 on a pot: 20 flop, even though this is a short-stacked heads-up spot and the request should use the actual effective stack, not a looser actor-only value. I’m tracing the stack calculation path now; if that’s inflated, it explains why some flops still build a larger tree than the real hand warrants.
I’ve isolated a product-side stack reconstruction bug. The analysis worker was replaying hands with the room’s default startingStack for every player, so a hero who bought in for 200 was still being analyzed as if stacks were closer to 1000 and then capped to pot * 12, which is why some flops still blew up the solver tree. I’m patching the hand replay stack source and the effective-stack calculation now, then I’ll rerun the targeted tests and the e2e flow.
I’m making two localized product fixes: persisting real per-player starting stacks into replay data for new hands, and correcting analysis to use reconstructed per-player stacks plus true effective stack across active players. After that I’ll add focused integration coverage and rerun the tests.
I’ve patched the stack reconstruction path and added focused integration coverage around the two failure modes: replaying the wrong starting stacks and sending the wrong effective stack to the solver. I’m running those targeted tests now before I hit the full Playwright flow again.
The stack fixes themselves are working, but TypeScript caught one narrow type issue in the new replay parser: final_state is a persisted replay-only event, so I need to read it through the generic event payload shape rather than the stricter shared HandEvent union. I’m tightening that now and rerunning lint before going back to Playwright.
The targeted analysis-worker tests are passing and the API typecheck is clean. I’m running the real Playwright flow again now against the app to confirm the stack fix removes the remaining flop solver instability in the end-to-end path.
The stack reconstruction fix is in effect; the new failing hand shows the flop request at effectiveStack: 190, which is now correct. The remaining issue is separate: even that corrected flop tree still times out on some random boards. I’m pulling the exact failed solver workdir now so I can compare this real request shape against safer variants and shrink the actual unstable part of the tree instead of guessing.