mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(gateway): bound QR result retention
This commit is contained in:
@@ -15,6 +15,7 @@ import { getActiveGatewayRootWorkCount } from "../../process/gateway-work-admiss
|
||||
import { CommandLane } from "../../process/lanes.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { SystemAgentChatEngine } from "../../system-agent/chat-engine.js";
|
||||
import { SYSTEM_AGENT_HOSTED_WIZARD_TIMEOUT_MS } from "../../system-agent/chat-wizard-host.js";
|
||||
import {
|
||||
createSystemAgentVerifiedInferenceTestFixture,
|
||||
installSystemAgentPluginMetadataTestSnapshot,
|
||||
@@ -1078,10 +1079,11 @@ describe("openclaw.chat", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not evict a terminal QR result while audit and polling finish", async () => {
|
||||
it("protects terminal QR delivery through audit, then expires abandoned retention", async () => {
|
||||
const ownerSettled = createDeferred();
|
||||
const runnerFinished = createDeferred();
|
||||
const auditStarted = createDeferred();
|
||||
const auditFinished = createDeferred();
|
||||
const releaseAudit = createDeferred();
|
||||
const qrEngine = new SystemAgentChatEngine(
|
||||
{
|
||||
@@ -1103,6 +1105,7 @@ describe("openclaw.chat", () => {
|
||||
appendAuditEntry: async () => {
|
||||
auditStarted.resolve();
|
||||
await releaseAudit.promise;
|
||||
auditFinished.resolve();
|
||||
return "audit-entry";
|
||||
},
|
||||
},
|
||||
@@ -1135,14 +1138,18 @@ describe("openclaw.chat", () => {
|
||||
expect(sessions.has("ordinary-1")).toBe(false);
|
||||
|
||||
releaseAudit.resolve();
|
||||
let completed: Awaited<ReturnType<SystemAgentChatEngine["pollStep"]>> | undefined;
|
||||
await vi.waitFor(async () => {
|
||||
const reply = await qrEngine.pollStep(stepId);
|
||||
expect(reply.wizardSettling).not.toBe(true);
|
||||
completed = reply;
|
||||
});
|
||||
expect(completed?.text).toContain("telegram is configured");
|
||||
await auditFinished.promise;
|
||||
await waitOneTask();
|
||||
expect(qrEngine.hasPendingQrCode()).toBe(true);
|
||||
|
||||
const retainedAt = Date.now();
|
||||
vi.spyOn(Date, "now").mockReturnValue(retainedAt + SYSTEM_AGENT_HOSTED_WIZARD_TIMEOUT_MS + 1);
|
||||
expect(qrEngine.hasPendingQrCode()).toBe(false);
|
||||
await expect(
|
||||
callChat(makeContext(sessions), { sessionId: "after-retention" }),
|
||||
).resolves.toMatchObject({ ok: true });
|
||||
expect(disposeQr).toHaveBeenCalledOnce();
|
||||
expect(sessions.has("qr-applying")).toBe(false);
|
||||
});
|
||||
|
||||
it("resets a session on request", async () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "./chat-turn-router.js";
|
||||
import {
|
||||
ChatWizardHost,
|
||||
SYSTEM_AGENT_HOSTED_WIZARD_TIMEOUT_MS,
|
||||
type ChatWizardHostDependencies,
|
||||
type SystemAgentChatReply,
|
||||
} from "./chat-wizard-host.js";
|
||||
@@ -54,6 +55,11 @@ export type SystemAgentChatEngineOptions = {
|
||||
operatorApprovalOnly?: boolean;
|
||||
};
|
||||
|
||||
type RetainedPollReply = {
|
||||
expiresAtMs: number;
|
||||
reply: SystemAgentChatReply;
|
||||
};
|
||||
|
||||
type SystemAgentChatEngineInternals = {
|
||||
wizardDependencies?: ChatWizardHostDependencies;
|
||||
executeOperation?: typeof import("./operations.js").executeSystemAgentOperation;
|
||||
@@ -74,7 +80,7 @@ export class SystemAgentChatEngine {
|
||||
private disposed = false;
|
||||
private disposal: Promise<void> | null = null;
|
||||
private persistentApplySettlement: Promise<void> | null = null;
|
||||
private retainedPollReplies = new Map<string, SystemAgentChatReply>();
|
||||
private retainedPollReplies = new Map<string, RetainedPollReply>();
|
||||
private passivePollsInFlight = 0;
|
||||
|
||||
constructor(
|
||||
@@ -123,6 +129,7 @@ export class SystemAgentChatEngine {
|
||||
}
|
||||
|
||||
hasPendingQrCode(): boolean {
|
||||
this.pruneExpiredPollReplies();
|
||||
return (
|
||||
this.wizard.hasPendingQrCode() ||
|
||||
this.passivePollsInFlight > 0 ||
|
||||
@@ -207,21 +214,23 @@ export class SystemAgentChatEngine {
|
||||
/** Observe a passive wizard step while keeping its continuation in the turn queue. */
|
||||
async pollStep(stepId: string): Promise<SystemAgentChatReply> {
|
||||
this.assertActive();
|
||||
this.pruneExpiredPollReplies();
|
||||
const retained = this.retainedPollReplies.get(stepId);
|
||||
if (retained) {
|
||||
this.retainedPollReplies.delete(stepId);
|
||||
if (retained.text) {
|
||||
this.history.push({ role: "assistant", text: retained.text });
|
||||
if (retained.reply.text) {
|
||||
this.history.push({ role: "assistant", text: retained.reply.text });
|
||||
}
|
||||
return { ...retained };
|
||||
return { ...retained.reply };
|
||||
}
|
||||
this.passivePollsInFlight += 1;
|
||||
const observation = this.turnQueue
|
||||
.then(async () => {
|
||||
this.assertActive();
|
||||
this.pruneExpiredPollReplies();
|
||||
const queuedRetained = this.retainedPollReplies.get(stepId);
|
||||
if (queuedRetained) {
|
||||
return { ...queuedRetained };
|
||||
return { ...queuedRetained.reply };
|
||||
}
|
||||
const result = await this.router.finalizeWizardResult(await this.wizard.pollStep(stepId));
|
||||
this.assertActive();
|
||||
@@ -231,7 +240,10 @@ export class SystemAgentChatEngine {
|
||||
reply.wizardInputPending !== true &&
|
||||
reply.wizardSettling !== true
|
||||
) {
|
||||
this.retainedPollReplies.set(stepId, { ...reply });
|
||||
this.retainedPollReplies.set(stepId, {
|
||||
expiresAtMs: Date.now() + SYSTEM_AGENT_HOSTED_WIZARD_TIMEOUT_MS,
|
||||
reply: { ...reply },
|
||||
});
|
||||
}
|
||||
return reply;
|
||||
})
|
||||
@@ -267,6 +279,14 @@ export class SystemAgentChatEngine {
|
||||
};
|
||||
}
|
||||
|
||||
private pruneExpiredPollReplies(nowMs = Date.now()): void {
|
||||
for (const [stepId, retained] of this.retainedPollReplies) {
|
||||
if (retained.expiresAtMs <= nowMs) {
|
||||
this.retainedPollReplies.delete(stepId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async answerWizard(answer: WizardAnswer): Promise<SystemAgentChatReply> {
|
||||
this.assertActive();
|
||||
const turn = this.turnQueue.then(async () => {
|
||||
|
||||
@@ -99,7 +99,7 @@ type ActiveWizardBridge = {
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("system-agent/chat-wizard-host");
|
||||
const SYSTEM_AGENT_HOSTED_WIZARD_TIMEOUT_MS = 25 * 60 * 1000;
|
||||
export const SYSTEM_AGENT_HOSTED_WIZARD_TIMEOUT_MS = 25 * 60 * 1000;
|
||||
const WIZARD_CANCEL_HINT = "Say `cancel` to stop this setup.";
|
||||
const WIZARD_QR_EXPIRED_MESSAGE =
|
||||
"This setup QR code expired. Setup is still finishing the attempt automatically.";
|
||||
|
||||
Reference in New Issue
Block a user