fix: treat lost relay sends as ambiguous instead of failed
A prompt whose response is lost after the request left the client may
already be running server-side. The relay tunnel reported those failures
as plain text errors ("stream aborted by host", "relay keepalive
timeout"), which matched none of the patterns in isAmbiguousSendFailure,
so an accepted prompt was rolled back and the message queue re-sent it —
two independent AI responses for one user message (#2425). Direct
connections never hit the path.
Transports now tag dispatched-but-unconfirmed failures and the classifier
reads the tag before falling back to status/text heuristics. Confirmation
waits for the connection to actually return (bounded) and retries with
backoff instead of two attempts 150ms apart over the just-broken tunnel.
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
} from './crypto';
|
||||
import { createHostHandshake } from './handshake';
|
||||
import { TunnelFrameType } from './protocol';
|
||||
import { isAmbiguousTransportFailure } from './transport-error';
|
||||
import {
|
||||
createFragmentAssembler,
|
||||
decodeFrameBatch,
|
||||
@@ -339,6 +340,24 @@ describe('createRelayTunnelClient', () => {
|
||||
await expect(reader.read()).rejects.toThrow();
|
||||
});
|
||||
|
||||
// A POST that dies after dispatch may already have been processed by the
|
||||
// server. Callers must be able to tell that apart from a definite failure —
|
||||
// a prompt re-sent on this error produces a second AI response (#2425).
|
||||
test('tags an in-flight request killed by reconnect as an ambiguous failure', async () => {
|
||||
const { client, killWire } = await setupClient({ silent: true });
|
||||
track(client);
|
||||
const pending = client.fetch('/api/session/s1/prompt_async', { method: 'POST', body: '{}' });
|
||||
let caught: unknown = null;
|
||||
const settled = pending.catch((error: unknown) => {
|
||||
caught = error;
|
||||
});
|
||||
await wait(20);
|
||||
killWire();
|
||||
await settled;
|
||||
expect(caught).toBeInstanceOf(Error);
|
||||
expect(isAmbiguousTransportFailure(caught)).toBe(true);
|
||||
});
|
||||
|
||||
test('opens, echoes, and closes a tunneled WebSocket', async () => {
|
||||
const { client } = await setupClient();
|
||||
track(client);
|
||||
|
||||
Reference in New Issue
Block a user