mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix: keep Custodian receipts value-free
This commit is contained in:
@@ -97,9 +97,15 @@ describe("OpenClaw chat result protocol", () => {
|
||||
expect(
|
||||
Value.Check(SystemAgentChatResultSchema, {
|
||||
...result,
|
||||
wizardAction: { kind: "answer", prompt: "Choose a channel" },
|
||||
wizardAction: { kind: "answer" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
Value.Check(SystemAgentChatResultSchema, {
|
||||
...result,
|
||||
wizardAction: { kind: "answer", prompt: "private prompt" },
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
Value.Check(SystemAgentChatResultSchema, {
|
||||
...result,
|
||||
@@ -160,7 +166,6 @@ describe("OpenClaw chat history protocol", () => {
|
||||
at: 2,
|
||||
wizardAction: {
|
||||
kind: "answer",
|
||||
prompt: "How should OpenClaw appear in Slack?",
|
||||
},
|
||||
};
|
||||
expect(Value.Check(SystemAgentChatHistoryResultSchema, { turns: [turn] })).toBe(true);
|
||||
@@ -169,6 +174,11 @@ describe("OpenClaw chat history protocol", () => {
|
||||
turns: [{ ...turn, sessionId: "slack-session" }],
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
Value.Check(SystemAgentChatHistoryResultSchema, {
|
||||
turns: [{ ...turn, wizardAction: { ...turn.wizardAction, prompt: "private prompt" } }],
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
Value.Check(SystemAgentChatHistoryResultSchema, {
|
||||
turns: [{ ...turn, wizardAction: { ...turn.wizardAction, kind: "unknown" } }],
|
||||
|
||||
@@ -80,8 +80,6 @@ export const SystemAgentChatQuestionSchema = closedObject({
|
||||
|
||||
export const SystemAgentWizardActionReceiptSchema = closedObject({
|
||||
kind: Type.Union([Type.Literal("answer"), Type.Literal("cancel")]),
|
||||
/** Ordinary non-sensitive prompt copy used to label the receipt. */
|
||||
prompt: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
/** One OpenClaw reply; `action` tells clients about conversation handoffs. */
|
||||
|
||||
@@ -119,7 +119,7 @@ describe("openclaw.chat.history wizard recovery", () => {
|
||||
},
|
||||
0,
|
||||
{
|
||||
wizardAction: { kind: "answer", prompt: "Port" },
|
||||
wizardAction: { kind: "answer" },
|
||||
wizardActionAccepted: false,
|
||||
},
|
||||
);
|
||||
@@ -239,7 +239,7 @@ describe("openclaw.chat.history wizard recovery", () => {
|
||||
role: "user" as const,
|
||||
text: "Alpha",
|
||||
at: 2,
|
||||
wizardAction: { kind: "answer" as const, prompt: "Choose one" },
|
||||
wizardAction: { kind: "answer" as const },
|
||||
},
|
||||
];
|
||||
transcriptStoreMocks.readTranscriptTail.mockReturnValue(durableTurns);
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("system-agent chat input", () => {
|
||||
text: "Choose a channel.",
|
||||
action: "none",
|
||||
wizardActionAccepted: false,
|
||||
wizardAction: { kind: "answer", prompt: "Channel" },
|
||||
wizardAction: { kind: "answer" },
|
||||
step: {
|
||||
id: "channel",
|
||||
type: "select",
|
||||
@@ -147,9 +147,9 @@ describe("system-agent chat input", () => {
|
||||
text: "Next step.",
|
||||
action: "none",
|
||||
wizardActionAccepted: true,
|
||||
wizardAction: { kind: "answer", prompt: "Channel" },
|
||||
wizardAction: { kind: "answer" },
|
||||
},
|
||||
}).wizardAction,
|
||||
).toEqual({ kind: "answer", prompt: "Channel" });
|
||||
).toEqual({ kind: "answer" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,7 +248,7 @@ describe("openclaw.chat reset boundary", () => {
|
||||
sessionId: "recover-session",
|
||||
wizardAnswer: { stepId: portStepId, value: "18789" },
|
||||
});
|
||||
expect(accepted.wizardAction).toEqual({ kind: "answer", prompt: "Port" });
|
||||
expect(accepted.wizardAction).toEqual({ kind: "answer" });
|
||||
const secretStepId = expectDefined(accepted.step?.id, "expected sensitive step");
|
||||
const cancelled = await sendChat({
|
||||
sessionId: "recover-session",
|
||||
@@ -279,7 +279,7 @@ describe("openclaw.chat reset boundary", () => {
|
||||
"wizardAction",
|
||||
);
|
||||
expect(restoredTurns.find((turn) => turn.text === "18789")).toMatchObject({
|
||||
wizardAction: { kind: "answer", prompt: "Port" },
|
||||
wizardAction: { kind: "answer" },
|
||||
});
|
||||
const restoredCancel = restoredTurns.find((turn) => turn.text === "Cancel");
|
||||
expect(restoredCancel).toMatchObject({ wizardAction: { kind: "cancel" } });
|
||||
|
||||
@@ -460,7 +460,7 @@ describe("SystemAgentChatEngine wizard", () => {
|
||||
expect(done.step).toBeUndefined();
|
||||
});
|
||||
|
||||
it("submits a typed answer directly and records the server-owned option label", async () => {
|
||||
it("records the server-owned option label without exposing a configured service URL", async () => {
|
||||
useTempStateDir();
|
||||
let selected: unknown;
|
||||
const engine = new SystemAgentChatEngine({
|
||||
@@ -470,7 +470,7 @@ describe("SystemAgentChatEngine wizard", () => {
|
||||
deps: { loadOverview: fakeOverviewLoader() },
|
||||
runChannelSetupWizard: async (_channel: string, prompter: WizardPrompter) => {
|
||||
selected = await prompter.select({
|
||||
message: "Choose one",
|
||||
message: "Use ClickClack at http://10.0.0.7:3000?",
|
||||
options: [
|
||||
{ value: "alpha", label: "Alpha" },
|
||||
{ value: "beta", label: "Beta" },
|
||||
@@ -485,7 +485,8 @@ describe("SystemAgentChatEngine wizard", () => {
|
||||
|
||||
expect(selected).toBe("beta");
|
||||
expect(answered.wizardActionAccepted).toBe(true);
|
||||
expect(answered.wizardAction).toEqual({ kind: "answer", prompt: "Choose one" });
|
||||
expect(answered.wizardAction).toEqual({ kind: "answer" });
|
||||
expect(JSON.stringify(answered.wizardAction)).not.toContain("http://10.0.0.7:3000");
|
||||
expect(engine.historySince(0)).toContainEqual({ role: "user", text: "Beta" });
|
||||
});
|
||||
|
||||
@@ -508,7 +509,7 @@ describe("SystemAgentChatEngine wizard", () => {
|
||||
const stepId = expectDefined(prompt.step?.id, "expected an active wizard step");
|
||||
const invalid = await engine.answerWizard({ stepId, value: "banana" });
|
||||
expect(invalid.wizardActionAccepted).toBe(false);
|
||||
expect(invalid.wizardAction).toEqual({ kind: "answer", prompt: "Port" });
|
||||
expect(invalid.wizardAction).toEqual({ kind: "answer" });
|
||||
expect(invalid.step?.id).toBe(stepId);
|
||||
expect(invalid.text).toContain("Enter port 18789");
|
||||
|
||||
|
||||
@@ -51,17 +51,6 @@ export type ChatWizardAnswerResult = ChatWizardResult & {
|
||||
wizardAction: SystemAgentWizardActionReceipt;
|
||||
};
|
||||
|
||||
function wizardActionReceipt(
|
||||
step: WizardStep,
|
||||
kind: SystemAgentWizardActionReceipt["kind"],
|
||||
): SystemAgentWizardActionReceipt {
|
||||
const prompt =
|
||||
!step.sensitive && !step.deviceCode && !step.externalUrl
|
||||
? (step.title ?? step.message)
|
||||
: undefined;
|
||||
return { kind, ...(prompt ? { prompt } : {}) };
|
||||
}
|
||||
|
||||
export type ChatWizardHostDependencies = {
|
||||
runChannelSetupWizard?: (
|
||||
channel: string,
|
||||
@@ -330,7 +319,7 @@ export class ChatWizardHost {
|
||||
...result,
|
||||
accepted: validationError === undefined,
|
||||
userHistoryText: formatStructuredWizardAnswerForHistory(step, answer.value),
|
||||
wizardAction: wizardActionReceipt(step, "answer"),
|
||||
wizardAction: { kind: "answer" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -350,7 +339,7 @@ export class ChatWizardHost {
|
||||
...(await this.pump()),
|
||||
accepted: true,
|
||||
userHistoryText: "Cancel",
|
||||
wizardAction: wizardActionReceipt(step, "cancel"),
|
||||
wizardAction: { kind: "cancel" },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ suite.define(() => {
|
||||
sessionId: "e2e-rich-wizard",
|
||||
reply: "Choose features.",
|
||||
action: "none",
|
||||
wizardAction: { kind: "answer", prompt: "Which channel?" },
|
||||
wizardAction: { kind: "answer" },
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
id: "features",
|
||||
@@ -466,7 +466,7 @@ suite.define(() => {
|
||||
sessionId: "e2e-rich-wizard",
|
||||
reply: "Enter the secret.",
|
||||
action: "none",
|
||||
wizardAction: { kind: "answer", prompt: "Which features?" },
|
||||
wizardAction: { kind: "answer" },
|
||||
sensitive: true,
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
@@ -528,7 +528,7 @@ suite.define(() => {
|
||||
sessionId: "e2e-rich-wizard",
|
||||
reply: "Confirm setup.",
|
||||
action: "none",
|
||||
wizardAction: { kind: "answer", prompt: "Connection name" },
|
||||
wizardAction: { kind: "answer" },
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
id: "confirm",
|
||||
@@ -563,7 +563,7 @@ suite.define(() => {
|
||||
sessionId: "e2e-rich-wizard",
|
||||
reply: "Setup complete.",
|
||||
action: "none",
|
||||
wizardAction: { kind: "answer", prompt: "Connect Twitch now?" },
|
||||
wizardAction: { kind: "answer" },
|
||||
});
|
||||
await page.getByText("Setup complete.").waitFor();
|
||||
expect(
|
||||
|
||||
@@ -46,7 +46,6 @@ describe("Custodian wizard reload recovery", () => {
|
||||
at: 0,
|
||||
wizardAction: {
|
||||
kind: "cancel",
|
||||
prompt: "Choose a previous channel.",
|
||||
},
|
||||
},
|
||||
{ role: "user", text: "connect twitch", at: 1 },
|
||||
@@ -67,7 +66,6 @@ describe("Custodian wizard reload recovery", () => {
|
||||
at: 3,
|
||||
wizardAction: {
|
||||
kind: "answer",
|
||||
prompt: "How should OpenClaw appear in Twitch?",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GATEWAY_SERVER_CAPS } from "@openclaw/gateway-protocol";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createDeferred } from "../../../../test/helpers/promise.ts";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import { i18n, t } from "../../i18n/index.ts";
|
||||
import { waitForFast } from "../../test-helpers/wait-for.ts";
|
||||
import { createContext, mountPage } from "./custodian-page.test-harness.ts";
|
||||
|
||||
@@ -127,7 +128,7 @@ describe("custodian structured wizard", () => {
|
||||
sessionId: string;
|
||||
reply: string;
|
||||
action: "none";
|
||||
wizardAction: { kind: "answer"; prompt: string };
|
||||
wizardAction: { kind: "answer" };
|
||||
}>();
|
||||
const step = {
|
||||
id: "port",
|
||||
@@ -187,7 +188,7 @@ describe("custodian structured wizard", () => {
|
||||
sessionId: "rotation-session",
|
||||
reply: "Accepted by the retired client.",
|
||||
action: "none",
|
||||
wizardAction: { kind: "answer", prompt: "Gateway port" },
|
||||
wizardAction: { kind: "answer" },
|
||||
});
|
||||
|
||||
await waitForFast(() =>
|
||||
@@ -199,94 +200,97 @@ describe("custodian structured wizard", () => {
|
||||
expect(page.querySelector(".custodian__wizard-step")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("does not duplicate an accepted answer restored during same-scope recovery", async () => {
|
||||
const actionReply = createDeferred<{
|
||||
sessionId: string;
|
||||
reply: string;
|
||||
action: "none";
|
||||
wizardAction: { kind: "answer"; prompt: string };
|
||||
}>();
|
||||
const step = {
|
||||
id: "port",
|
||||
type: "text" as const,
|
||||
message: "Gateway port",
|
||||
};
|
||||
const nextStep = {
|
||||
id: "host",
|
||||
type: "text" as const,
|
||||
message: "Gateway host",
|
||||
};
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
turns: [{ role: "assistant", text: "Earlier setup history.", at: 0 }],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
sessionId: "rotation-session",
|
||||
reply: "Enter a port.",
|
||||
action: "none",
|
||||
wizardInputPending: true,
|
||||
step,
|
||||
})
|
||||
.mockReturnValueOnce(actionReply.promise);
|
||||
const harness = createContext(request, ["openclaw.chat", "openclaw.chat.history"], {
|
||||
gatewayCapabilities: [
|
||||
GATEWAY_SERVER_CAPS.SYSTEM_AGENT_WIZARD_CANCEL,
|
||||
GATEWAY_SERVER_CAPS.SYSTEM_AGENT_CHAT_HISTORY_SESSION_RECOVERY,
|
||||
GATEWAY_SERVER_CAPS.SYSTEM_AGENT_WIZARD_ACTION_RECEIPTS,
|
||||
],
|
||||
recoveryScope: "principal-a",
|
||||
});
|
||||
const { page } = await mountPage(harness.context);
|
||||
|
||||
const input = await waitForFast(() => {
|
||||
const element = page.querySelector<HTMLInputElement>(
|
||||
'.custodian__wizard-step input[name="wizard-text"]',
|
||||
);
|
||||
expect(element).not.toBeNull();
|
||||
return element!;
|
||||
});
|
||||
input.value = "18789";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await page.updateComplete;
|
||||
page.querySelector<HTMLButtonElement>(".custodian__wizard-step .btn.primary")!.click();
|
||||
await waitForFast(() => expect(page.textContent).toContain("Submitting answer"));
|
||||
|
||||
const replacementRequest = vi.fn().mockResolvedValue({
|
||||
turns: [
|
||||
{ role: "assistant", text: "Enter a port.", at: 1 },
|
||||
{
|
||||
role: "user",
|
||||
text: "18789",
|
||||
at: 2,
|
||||
wizardAction: { kind: "answer", prompt: "Gateway port" },
|
||||
},
|
||||
{ role: "assistant", text: "Enter a host.", at: 3 },
|
||||
],
|
||||
activeWizard: { sessionId: "rotation-session", step: nextStep },
|
||||
});
|
||||
harness.setGatewaySnapshot({
|
||||
client: {
|
||||
request: replacementRequest,
|
||||
it("does not duplicate a localized empty multiselect receipt during recovery", async () => {
|
||||
await i18n.setLocale("es");
|
||||
try {
|
||||
const actionReply = createDeferred<{
|
||||
sessionId: string;
|
||||
reply: string;
|
||||
action: "none";
|
||||
wizardAction: { kind: "answer" };
|
||||
}>();
|
||||
const step = {
|
||||
id: "features",
|
||||
type: "multiselect" as const,
|
||||
message: "Choose features",
|
||||
options: [{ label: "Chat", value: "chat" }],
|
||||
};
|
||||
const nextStep = {
|
||||
id: "name",
|
||||
type: "text" as const,
|
||||
message: "Connection name",
|
||||
};
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
turns: [{ role: "assistant", text: "Earlier setup history.", at: 0 }],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
sessionId: "rotation-session",
|
||||
reply: "Choose features.",
|
||||
action: "none",
|
||||
wizardInputPending: true,
|
||||
step,
|
||||
})
|
||||
.mockReturnValueOnce(actionReply.promise);
|
||||
const harness = createContext(request, ["openclaw.chat", "openclaw.chat.history"], {
|
||||
gatewayCapabilities: [
|
||||
GATEWAY_SERVER_CAPS.SYSTEM_AGENT_WIZARD_CANCEL,
|
||||
GATEWAY_SERVER_CAPS.SYSTEM_AGENT_CHAT_HISTORY_SESSION_RECOVERY,
|
||||
GATEWAY_SERVER_CAPS.SYSTEM_AGENT_WIZARD_ACTION_RECEIPTS,
|
||||
],
|
||||
recoveryScope: "principal-a",
|
||||
recoveryScopeReady: true,
|
||||
} as unknown as GatewayBrowserClient,
|
||||
});
|
||||
await waitForFast(() => expect(replacementRequest).toHaveBeenCalledOnce());
|
||||
await waitForFast(() => expect(page.textContent).toContain("Answer submitted"));
|
||||
actionReply.resolve({
|
||||
sessionId: "rotation-session",
|
||||
reply: "Accepted by the retired client.",
|
||||
action: "none",
|
||||
wizardAction: { kind: "answer", prompt: "Gateway port" },
|
||||
});
|
||||
});
|
||||
const { page } = await mountPage(harness.context);
|
||||
|
||||
await waitForFast(() =>
|
||||
expect(page.querySelectorAll(".custodian__structured-response")).toHaveLength(1),
|
||||
);
|
||||
expect(page.querySelectorAll(".chat-group.user")).toHaveLength(0);
|
||||
expect(page.textContent).not.toContain("Earlier setup history.");
|
||||
expect(page.querySelector(".custodian__wizard-step")?.textContent).toContain("Gateway host");
|
||||
await waitForFast(() => {
|
||||
expect(
|
||||
page.querySelectorAll('.custodian__wizard-step input[type="checkbox"]'),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
expect(t("common.none")).not.toBe("None");
|
||||
page.querySelector<HTMLButtonElement>(".custodian__wizard-step .btn.primary")!.click();
|
||||
await waitForFast(() =>
|
||||
expect(page.querySelector(".custodian__structured-response")).not.toBeNull(),
|
||||
);
|
||||
|
||||
const replacementRequest = vi.fn().mockResolvedValue({
|
||||
turns: [
|
||||
{ role: "assistant", text: "Choose features.", at: 1 },
|
||||
{ role: "user", text: "None", at: 2, wizardAction: { kind: "answer" } },
|
||||
{ role: "assistant", text: "Choose a name.", at: 3 },
|
||||
],
|
||||
activeWizard: { sessionId: "rotation-session", step: nextStep },
|
||||
});
|
||||
harness.setGatewaySnapshot({
|
||||
client: {
|
||||
request: replacementRequest,
|
||||
recoveryScope: "principal-a",
|
||||
recoveryScopeReady: true,
|
||||
} as unknown as GatewayBrowserClient,
|
||||
});
|
||||
await waitForFast(() => expect(replacementRequest).toHaveBeenCalledOnce());
|
||||
actionReply.resolve({
|
||||
sessionId: "rotation-session",
|
||||
reply: "Accepted by the retired client.",
|
||||
action: "none",
|
||||
wizardAction: { kind: "answer" },
|
||||
});
|
||||
|
||||
await waitForFast(() =>
|
||||
expect(page.querySelector(".custodian__structured-response-status")?.textContent).toBe(
|
||||
t("custodian.structured.submitted"),
|
||||
),
|
||||
);
|
||||
expect(page.querySelectorAll(".custodian__structured-response")).toHaveLength(1);
|
||||
expect(page.querySelectorAll(".chat-group.user")).toHaveLength(0);
|
||||
expect(page.textContent).not.toContain("Earlier setup history.");
|
||||
expect(page.querySelector(".custodian__wizard-step")?.textContent).toContain(
|
||||
"Connection name",
|
||||
);
|
||||
} finally {
|
||||
await i18n.useSystemLocale();
|
||||
}
|
||||
});
|
||||
|
||||
it("reconciles a visible fallback across successive same-scope recovery", async () => {
|
||||
@@ -400,7 +404,7 @@ describe("custodian structured wizard", () => {
|
||||
role: "user",
|
||||
text: "18789",
|
||||
at: 2,
|
||||
wizardAction: { kind: "answer", prompt: "Gateway port" },
|
||||
wizardAction: { kind: "answer" },
|
||||
},
|
||||
{ role: "user", text: "18888", at: 3 },
|
||||
{ role: "assistant", text: "Enter a host.", at: 3 },
|
||||
|
||||
@@ -100,7 +100,7 @@ describe("custodian page", () => {
|
||||
sessionId: "rich-wizard-session",
|
||||
reply: "Choose features.",
|
||||
action: "none",
|
||||
wizardAction: { kind: "answer", prompt: "Which channel?" },
|
||||
wizardAction: { kind: "answer" },
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
id: "features",
|
||||
@@ -117,7 +117,7 @@ describe("custodian page", () => {
|
||||
sessionId: "rich-wizard-session",
|
||||
reply: "Enter the secret.",
|
||||
action: "none",
|
||||
wizardAction: { kind: "answer", prompt: "Which features?" },
|
||||
wizardAction: { kind: "answer" },
|
||||
sensitive: true,
|
||||
wizardInputPending: true,
|
||||
step: {
|
||||
@@ -210,7 +210,7 @@ describe("custodian page", () => {
|
||||
[...page.querySelectorAll(".custodian__structured-response-prompt")].map(
|
||||
(element) => element.textContent,
|
||||
),
|
||||
).toEqual(["Which channel?", "Which features?", "Setup answer"]);
|
||||
).toEqual(["Setup answer", "Setup answer", "Setup answer"]);
|
||||
expect(page.querySelector(".chat-group.user")).toBeNull();
|
||||
expect(page.textContent).not.toContain("fake-client-secret");
|
||||
expect(page.querySelector(".agent-chat__composer-shell")).not.toBeNull();
|
||||
|
||||
@@ -247,7 +247,8 @@ export class CustodianSessionStore extends CustodianTranscriptState {
|
||||
structuredResponse: null,
|
||||
};
|
||||
if (userTurnProjection === "unless-accepted") {
|
||||
this.stageTranscriptFallback(params.sessionId, userMessage);
|
||||
const kind = params.wizardCancel === undefined ? "answer" : "cancel";
|
||||
this.stageTranscriptFallback(params.sessionId, userMessage, kind);
|
||||
}
|
||||
if (userTurnProjection === "always") {
|
||||
this.messages = [...this.messages, userMessage];
|
||||
|
||||
@@ -107,7 +107,6 @@ export function createCustodianStructuredInteraction(host: StructuredInteraction
|
||||
display: params.display,
|
||||
kind: result.wizardAction.kind,
|
||||
state: "submitted",
|
||||
...(result.wizardAction?.prompt ? { prompt: result.wizardAction.prompt } : {}),
|
||||
};
|
||||
const messages = withResponse(current.messages, params.message.id, response);
|
||||
if (messages) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { SystemAgentChatResult } from "@openclaw/gateway-protocol";
|
||||
import type {
|
||||
SystemAgentChatResult,
|
||||
SystemAgentWizardActionReceipt,
|
||||
} from "@openclaw/gateway-protocol";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import type { WizardStep } from "../../api/types.ts";
|
||||
import type { ApplicationContext } from "../../app/context.ts";
|
||||
@@ -26,6 +29,7 @@ type CustodianTranscriptTurnPosition = {
|
||||
};
|
||||
|
||||
type CustodianPendingTranscriptFallback = CustodianTranscriptTurnPosition & {
|
||||
kind: SystemAgentWizardActionReceipt["kind"];
|
||||
message: CustodianMessage;
|
||||
};
|
||||
|
||||
@@ -62,6 +66,7 @@ export abstract class CustodianTranscriptState {
|
||||
protected stageTranscriptFallback(
|
||||
sessionId: string | undefined,
|
||||
message: CustodianMessage,
|
||||
kind: CustodianPendingTranscriptFallback["kind"],
|
||||
): void {
|
||||
const globalIndex = this.messages.length;
|
||||
const boundaryIndex =
|
||||
@@ -69,6 +74,7 @@ export abstract class CustodianTranscriptState {
|
||||
? -1
|
||||
: this.messages.findIndex((candidate) => candidate.id === this.earlierBoundaryAfterId);
|
||||
this.pendingTranscriptFallbacks.push({
|
||||
kind,
|
||||
message,
|
||||
globalIndex,
|
||||
sessionId,
|
||||
@@ -128,9 +134,13 @@ export abstract class CustodianTranscriptState {
|
||||
if (existing === fallback.message || this.messages.includes(fallback.message)) {
|
||||
return;
|
||||
}
|
||||
const recoveredDisplay =
|
||||
existing?.structuredResponse?.display ?? (existing?.role === "user" ? existing.text : null);
|
||||
if (recoveredDisplay === fallback.message.text) {
|
||||
// Recovery owns turn order and action kind; display text is localized and cannot identify the
|
||||
// same submission. A plain user turn at this slot is the authoritative fallback projection.
|
||||
const recoveredActionKind = existing?.structuredResponse?.kind;
|
||||
if (
|
||||
existing?.role === "user" ||
|
||||
(recoveredActionKind !== undefined && recoveredActionKind === fallback.kind)
|
||||
) {
|
||||
this.removeTranscriptFallback(fallback);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ export type CustodianMessage = {
|
||||
export type CustodianStructuredResponse = {
|
||||
display: string;
|
||||
kind: "answer" | "cancel";
|
||||
prompt?: string;
|
||||
state: "submitting" | "submitted";
|
||||
};
|
||||
|
||||
@@ -138,7 +137,6 @@ function createCustodianTranscriptMessages(
|
||||
display,
|
||||
kind: turn.wizardAction.kind,
|
||||
state: "submitted",
|
||||
...(turn.wizardAction.prompt ? { prompt: turn.wizardAction.prompt } : {}),
|
||||
},
|
||||
});
|
||||
continue;
|
||||
@@ -214,10 +212,6 @@ function renderCustodianEarlierDivider(message: CustodianMessage, boundaryAfterI
|
||||
: nothing;
|
||||
}
|
||||
|
||||
function structuredPrompt(message: CustodianMessage): string {
|
||||
return message.structuredResponse?.prompt ?? t("custodian.structured.response");
|
||||
}
|
||||
|
||||
function renderStructuredResponse(message: CustodianMessage) {
|
||||
const response = message.structuredResponse;
|
||||
if (!response) {
|
||||
@@ -244,7 +238,9 @@ function renderStructuredResponse(message: CustodianMessage) {
|
||||
>${cancelled ? icons.stop : icons.check}</span
|
||||
>
|
||||
<span class="custodian__structured-response-copy">
|
||||
<span class="custodian__structured-response-prompt">${structuredPrompt(message)}</span>
|
||||
<span class="custodian__structured-response-prompt"
|
||||
>${t("custodian.structured.response")}</span
|
||||
>
|
||||
<strong>${response.display}</strong>
|
||||
<span class="custodian__structured-response-status">${status}</span>
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user