mirror of https://github.com/openclaw/openclaw.git
fix: reject stale ACP reconnect prompts
This commit is contained in:
parent
ac5bc4fb37
commit
52d2bd5cc6
|
|
@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
|
|||
- MS Teams/streaming: strip already-streamed text from fallback block delivery when replies exceed the 4000-character streaming limit so long responses stop duplicating content. (#59297) Thanks @bradgroux.
|
||||
- MS Teams/logging: format non-`Error` failures with the shared unknown-error helper so logs stop collapsing caught SDK or Axios objects into `[object Object]`. (#59321) Thanks @bradgroux.
|
||||
- Slack/thread context: filter thread starter and history by the effective conversation allowlist without dropping valid open-room, DM, or group DM context. (#58380)
|
||||
- ACP/gateway reconnects: reject stale pre-ack ACP prompts after reconnect grace expiry so callers fail cleanly instead of hanging indefinitely when the gateway never confirms the run.
|
||||
|
||||
## 2026.4.1-beta.1
|
||||
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ describe("acp translator stop reason mapping", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("keeps pre-ack prompts pending after reconnect timeout", async () => {
|
||||
it("rejects pre-ack prompts when reconnect timeout still finds no run", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const sessionId = "session-1";
|
||||
|
|
@ -442,9 +442,7 @@ describe("acp translator stop reason mapping", () => {
|
|||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
await expect(Promise.race([promptPromise, Promise.resolve("pending")])).resolves.toBe(
|
||||
"pending",
|
||||
);
|
||||
await expect(promptPromise).rejects.toThrow("Gateway disconnected: 1006: connection lost");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
|
@ -491,7 +489,7 @@ describe("acp translator stop reason mapping", () => {
|
|||
await expect(Promise.race([secondPrompt, Promise.resolve("pending")])).resolves.toBe("pending");
|
||||
});
|
||||
|
||||
it("keeps disconnect deadline when a superseded send resolves late", async () => {
|
||||
it("rejects stale pre-ack prompts when a superseded send resolves late", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const sessionId = "session-1";
|
||||
|
|
@ -548,15 +546,13 @@ describe("acp translator stop reason mapping", () => {
|
|||
agent.handleGatewayReconnect();
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
|
||||
await expect(Promise.race([secondPrompt, Promise.resolve("pending")])).resolves.toBe(
|
||||
"pending",
|
||||
);
|
||||
await expect(secondPrompt).rejects.toThrow("Gateway disconnected: 1006: connection lost");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("finishes terminal prompts without rejecting other reconnecting prompts", async () => {
|
||||
it("finishes terminal prompts while rejecting stale pre-ack prompts", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
let acceptedRunId: string | undefined;
|
||||
|
|
@ -621,9 +617,7 @@ describe("acp translator stop reason mapping", () => {
|
|||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
|
||||
await expect(acceptedPrompt).resolves.toEqual({ stopReason: "end_turn" });
|
||||
await expect(Promise.race([preAckPrompt, Promise.resolve("pending")])).resolves.toBe(
|
||||
"pending",
|
||||
);
|
||||
await expect(preAckPrompt).rejects.toThrow("Gateway disconnected: 1006: connection lost");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1088,6 +1088,17 @@ export class AcpGatewayAgent implements Agent {
|
|||
pending.disconnectContext = undefined;
|
||||
}
|
||||
|
||||
private shouldRejectPendingAtDisconnectDeadline(
|
||||
pending: PendingPrompt,
|
||||
disconnectContext: DisconnectContext,
|
||||
): boolean {
|
||||
return (
|
||||
pending.disconnectContext === disconnectContext &&
|
||||
(!pending.sendAccepted ||
|
||||
this.activeDisconnectContext?.generation === disconnectContext.generation)
|
||||
);
|
||||
}
|
||||
|
||||
private async reconcilePendingPrompts(
|
||||
observedDisconnectGeneration: number,
|
||||
deadlineExpired: boolean,
|
||||
|
|
@ -1101,8 +1112,6 @@ export class AcpGatewayAgent implements Agent {
|
|||
|
||||
const pendingEntries = [...this.pendingPrompts.entries()];
|
||||
let keepDisconnectTimer = false;
|
||||
const shouldRejectPending =
|
||||
deadlineExpired && this.activeDisconnectContext?.generation === observedDisconnectGeneration;
|
||||
for (const [sessionId, pending] of pendingEntries) {
|
||||
if (this.pendingPrompts.get(sessionId) !== pending) {
|
||||
continue;
|
||||
|
|
@ -1114,7 +1123,6 @@ export class AcpGatewayAgent implements Agent {
|
|||
sessionId,
|
||||
pending,
|
||||
deadlineExpired,
|
||||
shouldRejectPending,
|
||||
);
|
||||
if (shouldKeepPending) {
|
||||
keepDisconnectTimer = true;
|
||||
|
|
@ -1130,7 +1138,6 @@ export class AcpGatewayAgent implements Agent {
|
|||
sessionId: string,
|
||||
pending: PendingPrompt,
|
||||
deadlineExpired: boolean,
|
||||
shouldRejectPending: boolean,
|
||||
): Promise<boolean> {
|
||||
const disconnectContext = pending.disconnectContext;
|
||||
if (!disconnectContext) {
|
||||
|
|
@ -1149,7 +1156,7 @@ export class AcpGatewayAgent implements Agent {
|
|||
} catch (err) {
|
||||
this.log(`agent.wait reconcile failed for ${pending.idempotencyKey}: ${String(err)}`);
|
||||
if (deadlineExpired) {
|
||||
if (shouldRejectPending) {
|
||||
if (this.shouldRejectPendingAtDisconnectDeadline(pending, disconnectContext)) {
|
||||
this.rejectPendingPrompt(
|
||||
pending,
|
||||
new Error(`Gateway disconnected: ${disconnectContext.reason}`),
|
||||
|
|
@ -1175,7 +1182,7 @@ export class AcpGatewayAgent implements Agent {
|
|||
return false;
|
||||
}
|
||||
if (deadlineExpired) {
|
||||
if (shouldRejectPending) {
|
||||
if (this.shouldRejectPendingAtDisconnectDeadline(currentPending, disconnectContext)) {
|
||||
const currentDisconnectContext = currentPending.disconnectContext;
|
||||
if (!currentDisconnectContext) {
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue