| Field |
Value |
| Build SHA |
7e935b0f9 |
| Plugin versions |
all four Wazuh plugins 5.0.0-06 |
| Environment profile |
A (AIO + 5 agents) |
| Provider used |
openai_compatible fault-injection endpoint |
| Where found |
CV-083, run with fault injection |
| Proposed severity |
MAJOR — a partial answer presented as a complete one, with no signal |
| Known-issue match |
none |
Summary
When the connection to the AI provider drops after the answer has started streaming, the plugin finalizes the turn normally: the partial text is kept, a done event is emitted, and no error event is sent. The user is shown an answer that stops mid-thought, with nothing to indicate it was cut off.
Reproduced 3 of 3 attempts.
Reproduction
Register an openai_compatible provider pointing at a local endpoint that streams two content deltas and then closes the socket with no [DONE] frame and no finish_reason. fault_listener.py in the attachment does this — mode drop-mid-stream. Ask any normal question.
Event stream received:
status: Routing…
status: Querying Wazuh…
delta: 'Here are the top rules for today: the busiest is rule 5501 (PAM login). '
done
Baseline, identical question and provider, no fault injected:
delta: 'Here are the top rules for today: the busiest is rule 5501 (PAM login). That is the full picture.'
done
97 characters against 72. The turn terminates the same way in both cases.
Expected vs got
Expected — CV-083's outcome (6): an infrastructure-failure notice in distinct copy. The plugin already has exactly the right copy for its sibling failure modes:
| Fault |
What the user is told |
| 429 on the data call |
"The AI provider rejected this request due to rate limits — try again in a moment." |
| Provider stops responding |
"The AI provider stopped responding." |
| HTTP 500 |
"Provider responded with HTTP 500: Internal server error" |
| Connection dropped mid-answer |
nothing |
At minimum the partial text should be marked as incomplete and an error (or a terminal status) emitted. The retry machinery that handles the other three modes, including visible "retrying in 30s…" progress, does not engage here at all.
Got — a truncated answer, indistinguishable from a complete one.
Why this is worse than a silent empty
Forbidden outcome (a) in CV-083's own list is "silent empty response with no explanation". This is not that — it is one step worse. A silent empty at least signals that something went wrong. A silently truncated answer looks like the assistant simply finished, so a reader has no reason to doubt it or retry.
Concretely: the sentence that was lost in the reproduction was the summary line. An answer that stops after naming one rule reads as "that is the answer", not as "the answer was cut off".
The recovery path exists — it just cannot fire here
Dropping the connection before the first delta does not produce this. The plugin falls back to a no-tools retry (synthesizeNoTextFallback in server/routes/chat.js) and presents what that returns. So a recovery path is already implemented and works.
It cannot help once text has been emitted, because the partial text is already committed to the turn — and that is precisely the window this defect occupies.
Suggested fix
Treat "stream ended without a finish_reason / [DONE]" as a failure rather than as a normal end of stream. Two options, in preference order:
- Retry the completion the way a 429 or 500 is retried, and if the retry also fails, emit the same class of infrastructure-failure notice — the copy already exists.
- If the partial text must be kept, keep it and emit an error, so the UI can mark the answer as incomplete rather than final.
Context — this closes CV-083
battery.json flags CV-083 ambiguous: true because the shipped runner cannot force a provider failure. With the injector attached, five fault modes were run:
- 429 on the data call — PASS
- HTTP 500 on the data call — PASS (final copy is raw plumbing; minor)
- provider stalls — PASS
- 429 on the router call — recovers correctly after 60 s, but silently: the "retrying in 30s…" status the data stage emits is not wired to the router stage, so a rate-limited router call is indistinguishable from a hang. MINOR, recorded in
cv083/REPORT.md.
- connection dropped mid-stream — FAIL, this issue.
The injector is offered for folding into qa-package.zip so CV-083 stops being an un-runnable item; see cv083/REPORT.md for the two operational notes it needs (threaded server, detached start).
Evidence
cv083.zip
7e935b0f95.0.0-06openai_compatiblefault-injection endpointSummary
When the connection to the AI provider drops after the answer has started streaming, the plugin finalizes the turn normally: the partial text is kept, a
doneevent is emitted, and noerrorevent is sent. The user is shown an answer that stops mid-thought, with nothing to indicate it was cut off.Reproduced 3 of 3 attempts.
Reproduction
Register an
openai_compatibleprovider pointing at a local endpoint that streams two content deltas and then closes the socket with no[DONE]frame and nofinish_reason.fault_listener.pyin the attachment does this — modedrop-mid-stream. Ask any normal question.Event stream received:
Baseline, identical question and provider, no fault injected:
97 characters against 72. The turn terminates the same way in both cases.
Expected vs got
Expected — CV-083's outcome (6): an infrastructure-failure notice in distinct copy. The plugin already has exactly the right copy for its sibling failure modes:
At minimum the partial text should be marked as incomplete and an
error(or a terminalstatus) emitted. The retry machinery that handles the other three modes, including visible "retrying in 30s…" progress, does not engage here at all.Got — a truncated answer, indistinguishable from a complete one.
Why this is worse than a silent empty
Forbidden outcome (a) in CV-083's own list is "silent empty response with no explanation". This is not that — it is one step worse. A silent empty at least signals that something went wrong. A silently truncated answer looks like the assistant simply finished, so a reader has no reason to doubt it or retry.
Concretely: the sentence that was lost in the reproduction was the summary line. An answer that stops after naming one rule reads as "that is the answer", not as "the answer was cut off".
The recovery path exists — it just cannot fire here
Dropping the connection before the first delta does not produce this. The plugin falls back to a no-tools retry (
synthesizeNoTextFallbackinserver/routes/chat.js) and presents what that returns. So a recovery path is already implemented and works.It cannot help once text has been emitted, because the partial text is already committed to the turn — and that is precisely the window this defect occupies.
Suggested fix
Treat "stream ended without a
finish_reason/[DONE]" as a failure rather than as a normal end of stream. Two options, in preference order:Context — this closes CV-083
battery.jsonflags CV-083ambiguous: truebecause the shipped runner cannot force a provider failure. With the injector attached, five fault modes were run:cv083/REPORT.md.The injector is offered for folding into
qa-package.zipso CV-083 stops being an un-runnable item; seecv083/REPORT.mdfor the two operational notes it needs (threaded server, detached start).Evidence
cv083.zip