Skip to content

fix(server): /answer takes a question id and refuses a stale reply - #95

Merged
EdmondDantes merged 1 commit into
mainfrom
fix/answer-gate-question-id
Jul 27, 2026
Merged

fix(server): /answer takes a question id and refuses a stale reply#95
EdmondDantes merged 1 commit into
mainfrom
fix/answer-gate-question-id

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

What

Closes the correctness gap queued from #94's review (Fable finding 1). While claw run blocks on the terminal prompt, the same gate can be answered in the dashboard and the run can open a new question — the terminal reply, composed for the first question, then landed on the second with a 202. The window is as wide as the human's typing time, so it is a real race, not a microsecond one.

Fix — in the API, not in a rule

Per the project's "constraints belong in the API" principle, the reply now carries the id of the question it answers, and the server enforces the match:

  • POST /api/projects/{key}/issues/{id}/answer accepts {"text": …, "question": <id>}. When question is present and is not the gate the run is currently open on (Server::openQuestionId() → the newest unanswered question for the issue's running run, via the existing TraceReader::openGate()), the server returns 409 that question has been answered — a newer one is waiting.
  • The CLI reads the question's id from the trace row (spanId) and sends it. On the 409 it surfaces the server's own reason and, because the trace already carries the newer question row, re-prompts for the current question on the next poll — self-correcting.
  • A reply with no question id keeps the previous behaviour (backward compatible with the dashboard and older callers).

Why this is the right shape

The CLI can't close the race alone — fgets() blocks and polls nothing while the person types, and the old body carried no id to check. The server is the only party that knows which question is open, so the check belongs there; the id makes a stale reply unexpressible rather than merely discouraged.

Not live-verified

Same caveat as #94: a real gate fires non-deterministically, so the interactive path isn't exercised end-to-end. The server-side check and the client's id-carrying/409-surfacing are unit-tested.

Checks

composer qa green — cs, PHPStan level 8, Testo (461 tests). New tests: the answer body names the question (and omits it when 0), and a 409 surfaces the server's own message.

Closes the terminal-vs-dashboard race from #94's review: while the CLI blocks on input, the same gate can be answered in the dashboard and the run can open a new question — the terminal reply then landed on the wrong question with a 202. /answer now accepts the question id the caller is reading from the trace and returns 409 when it is no longer the open one; the CLI re-prompts for the current question. A missing id keeps the old behaviour.
@EdmondDantes
EdmondDantes merged commit 92a8422 into main Jul 27, 2026
2 checks passed
@EdmondDantes
EdmondDantes deleted the fix/answer-gate-question-id branch July 27, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant