Features: #302 — a reply with no content is refused

Dai Ha
2026-09-04 12:39:55 +07:00
parent 49211149bc
commit a9ff4a11f1
+23
@@ -3643,3 +3643,26 @@ does not recognise — it rethrows it unchanged, and a test pins that. Closing t
the exception are different things, and only the first was missing. Do not "simplify" this by
mapping the error to `PeerUnreachableException`; that reintroduces the exact behaviour #176 wrote
`failFastOnGoneBackend` to keep distinct.
## A reply with no content is refused instead of resolving the waiter
**What.** `POST /sessions/{id}/reply` read the `content` field with a silent default, so a body that
omitted the field became an empty reply. That empty reply resolved the lead's waiter and the turn
completed. The required-content check now lives in `MessageService.reply`, which both doors call, so
the REST door returns `400 bad_request` and the MCP tool returns a tool error. Blank and
whitespace-only content are treated the same as missing.
**On.** Always on.
**Why it exists.** The lead could not tell an empty reply from a member that genuinely said nothing.
This project already has several real conditions that look like that — a clipped completion scrape,
a backend that died mid-turn — so the bad case hid among them. The guard was written in one handler
instead of in the thing both handlers call, which is the same drift shape as #284 and #297.
**One thing to know for maintenance.** `fleet_reply` had a smaller version of the same hole:
its guard checked `content == null`, not blank, so whitespace went through. Moving the check into
`MessageService.reply` closed that too, but it also made the shared method throw. `replyHandler` in
`FleetMcp` is a bare `BiFunction` with no try/catch, so that throw would have escaped the tool
handler as an uncaught exception. The tool's own guard was widened to `isBlank` for that reason, and
`FleetMcpTest.replyWithBlankContentIsACleanToolErrorNotAnUncaughtException` pins it. If you ever move
the check again, check the handler between the door and the service, not only the two ends.