diff --git a/11-Features.md b/11-Features.md index 71d4640..0f80b22 100644 --- a/11-Features.md +++ b/11-Features.md @@ -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.