mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(gateway): record QR follow-up prompts
This commit is contained in:
@@ -966,6 +966,9 @@ describe("openclaw.chat", () => {
|
||||
).resolves.toMatchObject({ payload: { wizardSettling: true } });
|
||||
releaseFollowUp.resolve();
|
||||
await followUpPresented.promise;
|
||||
const historyBeforeObservation = engine.historyLength();
|
||||
await engine.resolveOperatorApproval(null, "queue-drain");
|
||||
expect(engine.historyLength()).toBe(historyBeforeObservation);
|
||||
|
||||
const droppedReply = await callChat(context, {
|
||||
sessionId: "lost-follow-up",
|
||||
@@ -976,9 +979,18 @@ describe("openclaw.chat", () => {
|
||||
throw new Error("retained follow-up response must contain a step");
|
||||
}
|
||||
const followUpStepId = droppedReply.payload.step.id;
|
||||
const followUpText = droppedReply.payload.reply;
|
||||
if (typeof followUpStepId !== "string") {
|
||||
throw new Error("retained follow-up step must contain an id");
|
||||
}
|
||||
if (typeof followUpText !== "string") {
|
||||
throw new Error("retained follow-up response must contain reply text");
|
||||
}
|
||||
expect(
|
||||
transcriptStoreMocks.appendTranscriptTurn.mock.calls.filter(
|
||||
([turn]) => turn.role === "assistant" && turn.text === followUpText,
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(engine.hasPendingQrCode()).toBe(false);
|
||||
expect(engine.hasRecoverableQrReply()).toBe(true);
|
||||
|
||||
@@ -996,6 +1008,11 @@ describe("openclaw.chat", () => {
|
||||
await expect(
|
||||
callChat(context, { sessionId: "lost-follow-up", pollStepId: qrStepId }),
|
||||
).resolves.toEqual(droppedReply);
|
||||
expect(
|
||||
transcriptStoreMocks.appendTranscriptTurn.mock.calls.filter(
|
||||
([turn]) => turn.role === "assistant" && turn.text === followUpText,
|
||||
),
|
||||
).toHaveLength(1);
|
||||
|
||||
await expect(
|
||||
callChat(context, {
|
||||
@@ -1004,6 +1021,19 @@ describe("openclaw.chat", () => {
|
||||
}),
|
||||
).resolves.toMatchObject({ ok: true });
|
||||
expect(engine.hasRecoverableQrReply()).toBe(false);
|
||||
const persistedTurns = transcriptStoreMocks.appendTranscriptTurn.mock.calls.map(
|
||||
([turn]) => turn,
|
||||
);
|
||||
expect(
|
||||
persistedTurns.filter(
|
||||
(turn) =>
|
||||
(turn.role === "assistant" && turn.text === followUpText) ||
|
||||
(turn.role === "user" && turn.text === "OpenClaw"),
|
||||
),
|
||||
).toEqual([
|
||||
expect.objectContaining({ role: "assistant", text: followUpText }),
|
||||
expect.objectContaining({ role: "user", text: "OpenClaw" }),
|
||||
]);
|
||||
await expect(
|
||||
callChat(context, { sessionId: "lost-follow-up", pollStepId: qrStepId }),
|
||||
).resolves.toMatchObject({
|
||||
|
||||
@@ -141,15 +141,25 @@ describe("SystemAgentChatEngine passive QR polling", () => {
|
||||
await expect(engine.pollStep(qrStepId)).resolves.toMatchObject({ wizardSettling: true });
|
||||
releaseFollowUp.resolve();
|
||||
await followUpPresented.promise;
|
||||
const historyBeforeObservation = engine.historyLength();
|
||||
await engine.resolveOperatorApproval(null, "queue-drain");
|
||||
expect(engine.historyLength()).toBe(historyBeforeObservation);
|
||||
|
||||
let followUp: Awaited<ReturnType<SystemAgentChatEngine["pollStep"]>> | undefined;
|
||||
await vi.waitFor(async () => {
|
||||
followUp = await engine.pollStep(qrStepId);
|
||||
expect(followUp.step?.type).toBe("text");
|
||||
});
|
||||
const droppedReply = expectDefined(followUp, "follow-up reply");
|
||||
const droppedReply = await engine.pollStep(qrStepId);
|
||||
expect(droppedReply.step?.type).toBe("text");
|
||||
const followUpStep = expectDefined(droppedReply.step, "follow-up step");
|
||||
const followUpText = droppedReply.text;
|
||||
expect(
|
||||
engine
|
||||
.historySince(0)
|
||||
.filter((turn) => turn.role === "assistant" && turn.text === followUpText),
|
||||
).toHaveLength(1);
|
||||
await expect(engine.pollStep(qrStepId)).resolves.toEqual(droppedReply);
|
||||
expect(
|
||||
engine
|
||||
.historySince(0)
|
||||
.filter((turn) => turn.role === "assistant" && turn.text === followUpText),
|
||||
).toHaveLength(1);
|
||||
expect(engine.hasPendingQrCode()).toBe(false);
|
||||
expect(engine.hasRecoverableQrReply()).toBe(true);
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export type SystemAgentChatEngineOptions = {
|
||||
|
||||
type RetainedPollReply = {
|
||||
expiresAtMs: number;
|
||||
terminalHistoryRecorded: boolean;
|
||||
historyRecorded: boolean;
|
||||
reply: SystemAgentChatReply;
|
||||
};
|
||||
|
||||
@@ -145,8 +145,7 @@ export class SystemAgentChatEngine {
|
||||
this.wizard.hasPendingQrCode() ||
|
||||
this.passivePollObservations.size > 0 ||
|
||||
[...this.retainedPollReplies.values()].some(
|
||||
({ reply, terminalHistoryRecorded }) =>
|
||||
isTerminalPollReply(reply) && !terminalHistoryRecorded,
|
||||
({ reply, historyRecorded }) => isTerminalPollReply(reply) && !historyRecorded,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -155,8 +154,7 @@ export class SystemAgentChatEngine {
|
||||
hasRecoverableQrReply(): boolean {
|
||||
this.pruneExpiredPollReplies();
|
||||
return [...this.retainedPollReplies.values()].some(
|
||||
({ reply, terminalHistoryRecorded }) =>
|
||||
!isTerminalPollReply(reply) || terminalHistoryRecorded,
|
||||
({ reply, historyRecorded }) => !isTerminalPollReply(reply) || historyRecorded,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -266,7 +264,7 @@ export class SystemAgentChatEngine {
|
||||
expiresAtMs:
|
||||
result.passiveQrRetentionExpiresAtMs ??
|
||||
Date.now() + SYSTEM_AGENT_HOSTED_WIZARD_TIMEOUT_MS,
|
||||
terminalHistoryRecorded: false,
|
||||
historyRecorded: false,
|
||||
reply: { ...reply },
|
||||
});
|
||||
}
|
||||
@@ -301,10 +299,10 @@ export class SystemAgentChatEngine {
|
||||
}
|
||||
|
||||
private recordObservedPollReply(retained: RetainedPollReply): SystemAgentChatReply {
|
||||
if (isTerminalPollReply(retained.reply) && !retained.terminalHistoryRecorded) {
|
||||
// Only the request that observes completion may make it durable. The
|
||||
if (!retained.historyRecorded) {
|
||||
// Only the request that observes a retained reply may make it durable. The
|
||||
// background observer can finish after its Gateway request has returned.
|
||||
retained.terminalHistoryRecorded = true;
|
||||
retained.historyRecorded = true;
|
||||
if (retained.reply.text) {
|
||||
this.history.push({ role: "assistant", text: retained.reply.text });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user