From e82cda0b0b438988a90016c104f464998d6f2bee Mon Sep 17 00:00:00 2001 From: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:47:31 +1000 Subject: [PATCH] fix(ui): use owner-sanitized setup receipts --- .../OpenClawProtocol/GatewayModels.swift | 8 ++-- .../agent-harness-runtime.json | 2 +- .../agent-harness.json | 2 +- .../plugin-sdk-api-baseline/channel-core.json | 2 +- .../channel-entry-contract.json | 2 +- .../channel-message.json | 2 +- .../channel-outbound.json | 2 +- .../channel-plugin-common.json | 2 +- .../plugin-sdk-api-baseline/core.json | 2 +- .../plugin-sdk-api-baseline/discord.json | 2 +- .../gateway-runtime.json | 2 +- .../inbound-reply-dispatch.json | 2 +- .../meeting-runtime.json | 2 +- .../plugin-sdk-api-baseline/plugin-entry.json | 2 +- .../plugin-runtime.json | 2 +- .../provider-catalog-runtime.json | 2 +- .../plugin-sdk-api-baseline/tool-plugin.json | 2 +- .../webhook-ingress.json | 2 +- packages/gateway-protocol/CHANGELOG.md | 2 +- .../src/schema/openclaw.test.ts | 15 +++--- .../gateway-protocol/src/schema/openclaw.ts | 22 ++++----- .../system-agent-chat-history.ts | 4 +- .../system-agent-chat-turn.test.ts | 46 ++++++++++++------- .../server-methods/system-agent-chat-turn.ts | 4 +- .../system-agent-reset-boundary.test.ts | 6 +-- src/system-agent/chat-wizard-host.ts | 10 ++-- ui/src/e2e/custodian-event-nudge.e2e.test.ts | 12 ++--- .../custodian-page.reload-recovery.test.ts | 2 +- .../custodian-page.structured-wizard.test.ts | 10 ++-- ui/src/pages/custodian/custodian-page.test.ts | 16 +++++-- .../custodian/custodian-session-store.ts | 33 ++++++++----- .../custodian-structured-interaction.ts | 26 ++++++----- ui/src/pages/custodian/transcript.ts | 8 +--- 33 files changed, 140 insertions(+), 118 deletions(-) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index 5b427eba85f7..258a5f1df9af 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -10872,7 +10872,7 @@ public struct SystemAgentChatResult: Codable, Sendable { public let reply: String public let sensitive: Bool? public let wizardinputpending: Bool? - public let wizardactionaccepted: Bool? + public let wizardaction: [String: AnyCodable]? public let action: AnyCodable public let agentdraft: String? public let agentid: String? @@ -10886,7 +10886,7 @@ public struct SystemAgentChatResult: Codable, Sendable { reply: String, sensitive: Bool? = nil, wizardinputpending: Bool? = nil, - wizardactionaccepted: Bool? = nil, + wizardaction: [String: AnyCodable]? = nil, action: AnyCodable, agentdraft: String? = nil, agentid: String? = nil, @@ -10899,7 +10899,7 @@ public struct SystemAgentChatResult: Codable, Sendable { self.reply = reply self.sensitive = sensitive self.wizardinputpending = wizardinputpending - self.wizardactionaccepted = wizardactionaccepted + self.wizardaction = wizardaction self.action = action self.agentdraft = agentdraft self.agentid = agentid @@ -10914,7 +10914,7 @@ public struct SystemAgentChatResult: Codable, Sendable { case reply case sensitive case wizardinputpending = "wizardInputPending" - case wizardactionaccepted = "wizardActionAccepted" + case wizardaction = "wizardAction" case action case agentdraft = "agentDraft" case agentid = "agentId" diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json index 35b6293a2925..d54ed7b6aeb9 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json @@ -1 +1 @@ -{"contentHash":"79a4c99fa78209550335cbcf81b39ce261be41a90de04b64eb69526ef84349bc","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} +{"contentHash":"f3bc029eef9d8e93bc73fb0869c9dbfbe4071f0b469f3bacc7018efb8a14ca0e","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json index df699308c24b..eefbc9cee34a 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json @@ -1 +1 @@ -{"contentHash":"fdcb3e2536e7d4e3090322fac2793f497a0b6f331a8b583fa6156007721b177f","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} +{"contentHash":"54a4c5b39ea87409b6cd7e417fe0cacbbcf4881ab4590642f3909ffe15fbc7c9","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-core.json b/docs/.generated/plugin-sdk-api-baseline/channel-core.json index a7a1893477d5..dbed8e92e5ad 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-core.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-core.json @@ -1 +1 @@ -{"contentHash":"3bdfd31e7decd15278ea7864d8c16a0674be246e827b55b389593dcba3f2066c","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} +{"contentHash":"ca0cf7a409663f6bc5b5fae0b5085a1e64fc4436f869f7960fab8e56708ffdaa","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json index d8be27672e88..969529012c3d 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json @@ -1 +1 @@ -{"contentHash":"a5ff017a6e98b53dee31102591e7304d4d754b150b2a7684c257059921fa2523","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} +{"contentHash":"9bfe9ccffb96f6291f04c744e4246581a681981fe07b8f3da579362c70fefb16","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-message.json b/docs/.generated/plugin-sdk-api-baseline/channel-message.json index d80911b2febe..9d7f2ae66aa1 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-message.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-message.json @@ -1 +1 @@ -{"contentHash":"5d62b0618558e9a0093e4c8ad09b6cd143ed5b4c214de2eeefb255781a34c3bd","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} +{"contentHash":"69a39e836698999eb6963c97480dead8ea95a4b2b5d50545e8fd3cb881a57596","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json index a2e5cdce5147..32a24e81ec3c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json @@ -1 +1 @@ -{"contentHash":"32d2d5450bc2e08ebd3e2afc9302cf321469684f5997a059dddf28ace36675a8","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} +{"contentHash":"c7d3f4dbfc8b63cdcd6c1d7997e551b1b05177d38c1f58b65bed17fe8948eac9","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json index d214c96e6405..6961da241f9b 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json @@ -1 +1 @@ -{"contentHash":"3fea3804bec8232a55c95d3abb5bab8de5d8778652bddfafbb6362d27a4b3460","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} +{"contentHash":"705111ce11d2b0570d78bedbfc0b3aeab0cc88997cad8994be063eb2dc5a2943","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} diff --git a/docs/.generated/plugin-sdk-api-baseline/core.json b/docs/.generated/plugin-sdk-api-baseline/core.json index 7a4fc83b4b38..92ae9226aa64 100644 --- a/docs/.generated/plugin-sdk-api-baseline/core.json +++ b/docs/.generated/plugin-sdk-api-baseline/core.json @@ -1 +1 @@ -{"contentHash":"117093ae03e310c9ce4455f24e43fc678b8737af600eea1d0066137c2fa4651c","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} +{"contentHash":"7a35b9299874b2cd93188087a3eb2b4c40d349a8dfc33100704e4876531a9a16","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/discord.json b/docs/.generated/plugin-sdk-api-baseline/discord.json index 2fe6b5157a12..dd600c919aef 100644 --- a/docs/.generated/plugin-sdk-api-baseline/discord.json +++ b/docs/.generated/plugin-sdk-api-baseline/discord.json @@ -1 +1 @@ -{"contentHash":"b5d69672e4813d9de0f99bf51d1baedd73b167a54126a0c984f65dd84945699c","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} +{"contentHash":"7f7ad827672514d0d732fb17891851102a49d6b45904ee0b4815d5873cc88f2b","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} diff --git a/docs/.generated/plugin-sdk-api-baseline/gateway-runtime.json b/docs/.generated/plugin-sdk-api-baseline/gateway-runtime.json index a464d55adb0e..d22a92da58c1 100644 --- a/docs/.generated/plugin-sdk-api-baseline/gateway-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/gateway-runtime.json @@ -1 +1 @@ -{"contentHash":"096282e453532f80d63ac296d2cc53ad65eb38f23703ea27e359cc215c7adfe4","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"} +{"contentHash":"75dfa879ff76c5637813215d3ed4ff774f7bf5bd4998c2e74b504db1d48eea72","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json index d5adfaf16c24..42e4ff10f452 100644 --- a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json +++ b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json @@ -1 +1 @@ -{"contentHash":"d93e8de2beeea1902998af40aae725e6d6318cb011810d3478a776a016f62723","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} +{"contentHash":"8d9b1851d9d4bf3bc2273e5cf9b9071bedd205be3864d6fd4f88acf5f4097e27","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} diff --git a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json index af1a417186a4..a0a4c3ca6eec 100644 --- a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json @@ -1 +1 @@ -{"contentHash":"60a4f0f0501fb64019a7aa9c3e66f64ce9c388fb38dadd69d71b64a467ecbcbd","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} +{"contentHash":"3751be3212fc4f4d78555a72245a8492e0e5dfdfe30e7794cc20c22dd894e9df","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json index 07d8803b10f4..8dc798f7f6e3 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json @@ -1 +1 @@ -{"contentHash":"283ada2bb6c4499207b08f1d23c80919daa4fd139c6e02810c2c15f7a90fec58","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} +{"contentHash":"91cf8791fe2555a36a25bb359d1f14ef4ec2e61e25eb88af17187572ba431d50","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json index fc23cad8c835..a1a046744de0 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json @@ -1 +1 @@ -{"contentHash":"e5b10b255ad2bd9ed54f10017e1e09440962ed846ba885059e13dab10b27811f","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} +{"contentHash":"4a6d7f93ea9d3a0bdb207e007aef6edc12d095d57d5b01a72020c3f4bf2f2a96","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json index 68d9b93b88f5..e125d81714dc 100644 --- a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json @@ -1 +1 @@ -{"contentHash":"5055e33c4e0ffe0cb368d7a8e7970d5e97fd7d90913485e8a2bcecb1f97f92ef","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} +{"contentHash":"3087ba183395692801b60510757304c2da517299748c10625ecc3321cbc0afdb","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json index 790a9c522529..9f0f2eda6779 100644 --- a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json +++ b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json @@ -1 +1 @@ -{"contentHash":"7f5f39112df30ff5d32513258437bef16e8304e01e1d61a50f754c8e26831cbc","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} +{"contentHash":"694748a04e198f48a8e848d39b3c43942f8041f6353281d10d35f73f8d10f61b","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} diff --git a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json index 815557058094..ef221865071f 100644 --- a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json +++ b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json @@ -1 +1 @@ -{"contentHash":"983f60fcee19de0cc99dd2db216bea32e45054bffc50cfe3d6d382a7f51c7a41","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} +{"contentHash":"749f32ab3cde36b570b13173a9286da5d9453bd3ae08f81087ff66fd834fe6cb","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} diff --git a/packages/gateway-protocol/CHANGELOG.md b/packages/gateway-protocol/CHANGELOG.md index 7eb102a62c15..a3b0b4b98cfb 100644 --- a/packages/gateway-protocol/CHANGELOG.md +++ b/packages/gateway-protocol/CHANGELOG.md @@ -15,7 +15,7 @@ version and the additive schema surface. Dates are authoring dates (2026). - Remove dead protocol surfaces and add since-vintage metadata to retained schemas and methods. - Add optional `step` on `SystemAgentChatResult` carrying the full awaited wizard step. - Add owner-bound active-wizard snapshots to Custodian chat history for reload recovery. -- Add capability-gated accepted-action outcomes and receipt prompts to Custodian chat turns. +- Add capability-gated, owner-sanitized action receipts to Custodian chat turns. ## Protocol v4 (current) diff --git a/packages/gateway-protocol/src/schema/openclaw.test.ts b/packages/gateway-protocol/src/schema/openclaw.test.ts index 90dd14ca2dec..331f12cb8224 100644 --- a/packages/gateway-protocol/src/schema/openclaw.test.ts +++ b/packages/gateway-protocol/src/schema/openclaw.test.ts @@ -92,16 +92,19 @@ describe("OpenClaw chat question protocol", () => { describe("OpenClaw chat result protocol", () => { const result = { sessionId: "session-1", reply: "Choose again.", action: "none" }; - it("accepts explicit typed wizard-action outcomes", () => { + it("accepts only sanitized accepted wizard-action metadata", () => { expect(Value.Check(SystemAgentChatResultSchema, result)).toBe(true); expect( - Value.Check(SystemAgentChatResultSchema, { ...result, wizardActionAccepted: true }), + Value.Check(SystemAgentChatResultSchema, { + ...result, + wizardAction: { kind: "answer", prompt: "Choose a channel" }, + }), ).toBe(true); expect( - Value.Check(SystemAgentChatResultSchema, { ...result, wizardActionAccepted: false }), - ).toBe(true); - expect( - Value.Check(SystemAgentChatResultSchema, { ...result, wizardActionAccepted: "yes" }), + Value.Check(SystemAgentChatResultSchema, { + ...result, + wizardAction: { kind: "answer", value: "secret" }, + }), ).toBe(false); }); }); diff --git a/packages/gateway-protocol/src/schema/openclaw.ts b/packages/gateway-protocol/src/schema/openclaw.ts index c76408ac44bc..16f9ebde0cea 100644 --- a/packages/gateway-protocol/src/schema/openclaw.ts +++ b/packages/gateway-protocol/src/schema/openclaw.ts @@ -78,6 +78,12 @@ export const SystemAgentChatQuestionSchema = closedObject({ skipAction: Type.Optional(Type.Literal("exit")), }); +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. */ export const SystemAgentChatResultSchema = closedObject({ sessionId: NonEmptyString, @@ -86,8 +92,8 @@ export const SystemAgentChatResultSchema = closedObject({ sensitive: Type.Optional(Type.Boolean()), /** The hosted wizard will consume the next message as its current step answer. */ wizardInputPending: Type.Optional(Type.Boolean()), - /** Whether the submitted typed wizard action passed owner-side validation. */ - wizardActionAccepted: Type.Optional(Type.Boolean()), + /** Sanitized receipt metadata for an accepted typed wizard action. */ + wizardAction: Type.Optional(SystemAgentWizardActionReceiptSchema), action: Type.Union([ Type.Literal("none"), // The user asked to talk to their agent; clients should move to their @@ -115,18 +121,12 @@ export const SystemAgentChatHistoryParamsSchema = closedObject({ sessionId: Type.Optional(NonEmptyString), }); -export const SystemAgentChatHistoryWizardActionSchema = 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()), -}); - export const SystemAgentChatHistoryTurnSchema = closedObject({ role: Type.Union([Type.Literal("user"), Type.Literal("assistant")]), text: Type.String(), at: Type.Number(), /** Present only on accepted typed controls; values and sensitive prompts are excluded. */ - wizardAction: Type.Optional(SystemAgentChatHistoryWizardActionSchema), + wizardAction: Type.Optional(SystemAgentWizardActionReceiptSchema), }); export const SystemAgentChatHistoryResultSchema = closedObject({ @@ -389,9 +389,7 @@ export type SystemAgentWizardCancel = Static; export type SystemAgentChatResult = Static; export type SystemAgentChatHistoryParams = Static; -export type SystemAgentChatHistoryWizardAction = Static< - typeof SystemAgentChatHistoryWizardActionSchema ->; +export type SystemAgentWizardActionReceipt = Static; export type SystemAgentChatHistoryTurn = Static; export type SystemAgentChatHistoryResult = Static; export type SystemChangeEntry = Static; diff --git a/src/gateway/server-methods/system-agent-chat-history.ts b/src/gateway/server-methods/system-agent-chat-history.ts index 46e1a58acf1f..b5d169c38942 100644 --- a/src/gateway/server-methods/system-agent-chat-history.ts +++ b/src/gateway/server-methods/system-agent-chat-history.ts @@ -1,7 +1,7 @@ import { validateSystemAgentChatHistoryParams, type SystemAgentChatHistoryTurn, - type SystemAgentChatHistoryWizardAction, + type SystemAgentWizardActionReceipt, } from "../../../packages/gateway-protocol/src/index.js"; import type { SystemAgentChatEngine } from "../../system-agent/chat-engine.js"; import { resolveSystemAgentDelegationKey } from "../../system-agent/delegation-session.js"; @@ -41,7 +41,7 @@ export function persistSystemAgentEngineHistory( engine: Pick, startIndex: number, params: { - wizardAction?: SystemAgentChatHistoryWizardAction; + wizardAction?: SystemAgentWizardActionReceipt; wizardActionAccepted?: boolean; } = {}, ): SystemAgentChatHistoryTurn[] { diff --git a/src/gateway/server-methods/system-agent-chat-turn.test.ts b/src/gateway/server-methods/system-agent-chat-turn.test.ts index 9add1c1b663f..734ab159dd59 100644 --- a/src/gateway/server-methods/system-agent-chat-turn.test.ts +++ b/src/gateway/server-methods/system-agent-chat-turn.test.ts @@ -116,28 +116,40 @@ describe("system-agent chat input", () => { expect(handle).not.toHaveBeenCalled(); }); - it("preserves the enriched wizard step in the gateway result", () => { + it("forwards only owner-accepted wizard receipt metadata", () => { + const rejected = buildSystemAgentChatResult({ + sessionId: "s1", + reply: { + text: "Choose a channel.", + action: "none", + wizardActionAccepted: false, + wizardAction: { kind: "answer", prompt: "Channel" }, + step: { + id: "channel", + type: "select", + message: "Channel", + options: [{ label: "Twitch", value: "twitch" }], + }, + }, + }); + expect(rejected).toMatchObject({ + sessionId: "s1", + reply: "Choose a channel.", + action: "none", + step: { id: "channel", type: "select" }, + }); + expect(rejected).not.toHaveProperty("wizardAction"); + expect( buildSystemAgentChatResult({ sessionId: "s1", reply: { - text: "Choose a channel.", + text: "Next step.", action: "none", - wizardActionAccepted: false, - step: { - id: "channel", - type: "select", - message: "Channel", - options: [{ label: "Twitch", value: "twitch" }], - }, + wizardActionAccepted: true, + wizardAction: { kind: "answer", prompt: "Channel" }, }, - }), - ).toMatchObject({ - sessionId: "s1", - reply: "Choose a channel.", - action: "none", - wizardActionAccepted: false, - step: { id: "channel", type: "select" }, - }); + }).wizardAction, + ).toEqual({ kind: "answer", prompt: "Channel" }); }); }); diff --git a/src/gateway/server-methods/system-agent-chat-turn.ts b/src/gateway/server-methods/system-agent-chat-turn.ts index 02f8b45e1bc2..a59d34cc253a 100644 --- a/src/gateway/server-methods/system-agent-chat-turn.ts +++ b/src/gateway/server-methods/system-agent-chat-turn.ts @@ -82,8 +82,8 @@ export function buildSystemAgentChatResult(params: { : {}), ...(params.reply.sensitive === true ? { sensitive: true } : {}), ...(params.reply.wizardInputPending === true ? { wizardInputPending: true } : {}), - ...(params.reply.wizardActionAccepted !== undefined - ? { wizardActionAccepted: params.reply.wizardActionAccepted } + ...(params.reply.wizardActionAccepted === true && params.reply.wizardAction + ? { wizardAction: params.reply.wizardAction } : {}), ...(params.reply.question ? { question: params.reply.question } : {}), ...(params.reply.step ? { step: params.reply.step } : {}), diff --git a/src/gateway/server-methods/system-agent-reset-boundary.test.ts b/src/gateway/server-methods/system-agent-reset-boundary.test.ts index 94d3923f1c61..715fca2a84fe 100644 --- a/src/gateway/server-methods/system-agent-reset-boundary.test.ts +++ b/src/gateway/server-methods/system-agent-reset-boundary.test.ts @@ -243,18 +243,18 @@ describe("openclaw.chat reset boundary", () => { sessionId: "recover-session", wizardAnswer: { stepId: portStepId, value: "banana" }, }); - expect(rejected.wizardActionAccepted).toBe(false); + expect(rejected).not.toHaveProperty("wizardAction"); const accepted = await sendChat({ sessionId: "recover-session", wizardAnswer: { stepId: portStepId, value: "18789" }, }); - expect(accepted.wizardActionAccepted).toBe(true); + expect(accepted.wizardAction).toEqual({ kind: "answer", prompt: "Port" }); const secretStepId = expectDefined(accepted.step?.id, "expected sensitive step"); const cancelled = await sendChat({ sessionId: "recover-session", wizardCancel: { stepId: secretStepId }, }); - expect(cancelled.wizardActionAccepted).toBe(true); + expect(cancelled.wizardAction).toEqual({ kind: "cancel" }); closeOpenClawStateDatabase(); sessions.clear(); diff --git a/src/system-agent/chat-wizard-host.ts b/src/system-agent/chat-wizard-host.ts index 26087e6c6108..b77e7b0e7c64 100644 --- a/src/system-agent/chat-wizard-host.ts +++ b/src/system-agent/chat-wizard-host.ts @@ -1,6 +1,6 @@ import type { SystemAgentChatQuestion, - SystemAgentChatHistoryWizardAction, + SystemAgentWizardActionReceipt, SystemAgentWizardCancel, WizardAnswer, } from "../../packages/gateway-protocol/src/index.js"; @@ -33,7 +33,7 @@ export type SystemAgentChatReply = { wizardInputPending?: boolean; /** The submitted typed wizard action passed or failed owner-side validation. */ wizardActionAccepted?: boolean; - wizardAction?: SystemAgentChatHistoryWizardAction; + wizardAction?: SystemAgentWizardActionReceipt; handoff?: SystemAgentOperation; question?: SystemAgentChatQuestion; step?: WizardStep; @@ -48,13 +48,13 @@ export type ChatWizardResult = { export type ChatWizardAnswerResult = ChatWizardResult & { accepted: boolean; userHistoryText: string; - wizardAction: SystemAgentChatHistoryWizardAction; + wizardAction: SystemAgentWizardActionReceipt; }; function wizardActionReceipt( step: WizardStep, - kind: SystemAgentChatHistoryWizardAction["kind"], -): SystemAgentChatHistoryWizardAction { + kind: SystemAgentWizardActionReceipt["kind"], +): SystemAgentWizardActionReceipt { const prompt = !step.sensitive && !step.deviceCode && !step.externalUrl ? (step.title ?? step.message) diff --git a/ui/src/e2e/custodian-event-nudge.e2e.test.ts b/ui/src/e2e/custodian-event-nudge.e2e.test.ts index 8d9c65942372..93cbb58e26bd 100644 --- a/ui/src/e2e/custodian-event-nudge.e2e.test.ts +++ b/ui/src/e2e/custodian-event-nudge.e2e.test.ts @@ -442,7 +442,7 @@ suite.define(() => { sessionId: "e2e-rich-wizard", reply: "Choose features.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "answer", prompt: "Which channel?" }, wizardInputPending: true, step: { id: "features", @@ -466,7 +466,7 @@ suite.define(() => { sessionId: "e2e-rich-wizard", reply: "Enter the secret.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "answer", prompt: "Which features?" }, sensitive: true, wizardInputPending: true, step: { @@ -493,7 +493,7 @@ suite.define(() => { sessionId: "e2e-rich-wizard", reply: "Name this connection.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "answer" }, wizardInputPending: true, step: { id: "label", @@ -528,7 +528,7 @@ suite.define(() => { sessionId: "e2e-rich-wizard", reply: "Confirm setup.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "answer", prompt: "Connection name" }, wizardInputPending: true, step: { id: "confirm", @@ -563,7 +563,7 @@ suite.define(() => { sessionId: "e2e-rich-wizard", reply: "Setup complete.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "answer", prompt: "Connect Twitch now?" }, }); await page.getByText("Setup complete.").waitFor(); expect( @@ -706,7 +706,7 @@ suite.define(() => { sessionId: "e2e-reload-wizard", reply: "Twitch setup cancelled.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "cancel" }, }); await page.getByRole("button", { name: "Cancel", exact: true }).click(); await page.getByText("Twitch setup cancelled.").waitFor(); diff --git a/ui/src/pages/custodian/custodian-page.reload-recovery.test.ts b/ui/src/pages/custodian/custodian-page.reload-recovery.test.ts index e285f72a5c3f..724c55a09958 100644 --- a/ui/src/pages/custodian/custodian-page.reload-recovery.test.ts +++ b/ui/src/pages/custodian/custodian-page.reload-recovery.test.ts @@ -98,7 +98,7 @@ describe("Custodian wizard reload recovery", () => { sessionId: "live-wizard", reply: "Twitch setup cancelled.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "cancel" }, }; } freshChatCount += 1; diff --git a/ui/src/pages/custodian/custodian-page.structured-wizard.test.ts b/ui/src/pages/custodian/custodian-page.structured-wizard.test.ts index b7ee6dc88120..f63cec860a22 100644 --- a/ui/src/pages/custodian/custodian-page.structured-wizard.test.ts +++ b/ui/src/pages/custodian/custodian-page.structured-wizard.test.ts @@ -19,10 +19,7 @@ describe("custodian structured wizard", () => { vi.restoreAllMocks(); }); - it.each([ - { accepted: false, result: "rejected" }, - { accepted: undefined, result: "ambiguous" }, - ])("keeps a $result typed answer active as a plain user turn", async ({ accepted }) => { + it("keeps a rejected typed answer active as a plain user turn", async () => { const step = { id: "port", type: "text" as const, @@ -41,7 +38,6 @@ describe("custodian structured wizard", () => { sessionId: "validation-session", reply: "Enter port 18789.", action: "none", - ...(accepted === undefined ? {} : { wizardActionAccepted: accepted }), wizardInputPending: true, step, }); @@ -131,7 +127,7 @@ describe("custodian structured wizard", () => { sessionId: string; reply: string; action: "none"; - wizardActionAccepted: boolean; + wizardAction: { kind: "answer"; prompt: string }; }>(); const step = { id: "port", @@ -185,7 +181,7 @@ describe("custodian structured wizard", () => { sessionId: "rotation-session", reply: "Accepted by the retired client.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "answer", prompt: "Gateway port" }, }); await waitForFast(() => expect(page.textContent).toContain("Gateway connection changed")); diff --git a/ui/src/pages/custodian/custodian-page.test.ts b/ui/src/pages/custodian/custodian-page.test.ts index e35ca1a95c1e..44f30a2cc3b5 100644 --- a/ui/src/pages/custodian/custodian-page.test.ts +++ b/ui/src/pages/custodian/custodian-page.test.ts @@ -100,7 +100,7 @@ describe("custodian page", () => { sessionId: "rich-wizard-session", reply: "Choose features.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "answer", prompt: "Which channel?" }, wizardInputPending: true, step: { id: "features", @@ -117,7 +117,7 @@ describe("custodian page", () => { sessionId: "rich-wizard-session", reply: "Enter the secret.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "answer", prompt: "Which features?" }, sensitive: true, wizardInputPending: true, step: { @@ -131,7 +131,7 @@ describe("custodian page", () => { sessionId: "rich-wizard-session", reply: "Setup complete.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "answer" }, }); const { context } = createContext(request); const { page } = await mountPage(context); @@ -206,6 +206,11 @@ describe("custodian page", () => { expect(page.textContent).toContain("Chat, Announcements"); expect(page.textContent).toContain("Sensitive reply sent"); expect(page.querySelectorAll(".custodian__structured-response")).toHaveLength(3); + expect( + [...page.querySelectorAll(".custodian__structured-response-prompt")].map( + (element) => element.textContent, + ), + ).toEqual(["Which channel?", "Which features?", "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(); @@ -231,7 +236,7 @@ describe("custodian page", () => { sessionId: "cancel-wizard-session", reply: "Channel setup cancelled.", action: "none", - wizardActionAccepted: true, + wizardAction: { kind: "cancel" }, }); const { context } = createContext(request); const { page } = await mountPage(context); @@ -256,6 +261,9 @@ describe("custodian page", () => { expect(page.querySelector(".custodian__wizard-step")).toBeNull(); expect(page.querySelector(".chat-group.user")).toBeNull(); expect(page.querySelector(".custodian__structured-response")?.textContent).toContain("Cancel"); + expect(page.querySelector(".custodian__structured-response-prompt")?.textContent).toBe( + "Setup answer", + ); expect(page.querySelector(".custodian__structured-response")?.textContent).toContain( "Setup cancelled", ); diff --git a/ui/src/pages/custodian/custodian-session-store.ts b/ui/src/pages/custodian/custodian-session-store.ts index 01edfb10ddf9..5b8c2cdeeaf7 100644 --- a/ui/src/pages/custodian/custodian-session-store.ts +++ b/ui/src/pages/custodian/custodian-session-store.ts @@ -3,6 +3,7 @@ import { readSystemAgentInferenceUnavailableErrorDetails, type SystemAgentChatParams, type SystemAgentChatResult, + type SystemAgentWizardActionReceipt, } from "@openclaw/gateway-protocol"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; import { selectApplicationSession } from "../../app/agent-selection.ts"; @@ -55,6 +56,10 @@ function hasCustodianWizardAction(params: SystemAgentChatParams): boolean { type StoreListener = () => void; type ConfiguredInferenceState = "unresolved" | "required" | "ready"; type CustodianSetupIssue = "missing" | "unavailable"; +type CustodianSendResult = { + outcome: eventNudgeState.CustodianSendOutcome; + wizardAction?: SystemAgentWizardActionReceipt; +}; /** One process-local conversation owner shared by the full page and dock surface. */ export class CustodianSessionStore extends CustodianTranscriptState { @@ -228,7 +233,7 @@ export class CustodianSessionStore extends CustodianTranscriptState { return "rejected"; } const displayText = this.sensitive ? t("custodian.sensitiveReply") : (display ?? message); - return await this.sendUserTurn( + const result = await this.sendUserTurn( client, { sessionId: this.sessionId, @@ -237,6 +242,7 @@ export class CustodianSessionStore extends CustodianTranscriptState { displayText, questionReply, ); + return result.outcome; } private async sendUserTurn( @@ -245,7 +251,7 @@ export class CustodianSessionStore extends CustodianTranscriptState { displayText: string, questionReply: boolean, userTurnProjection: "always" | "unless-accepted" = "always", - ): Promise { + ): Promise { const questionState = [this.answeredQuestions, this.questionReplyUncertain] as const; if (questionReply) { this.questionReplyUncertain = true; @@ -269,7 +275,8 @@ export class CustodianSessionStore extends CustodianTranscriptState { this.emit(); const reply = this.requestReply(client, params); const replyEpoch = this.requestEpoch; - const outcome = await reply; + const result = await reply; + const outcome = result.outcome; if (userTurnProjection === "unless-accepted" && outcome !== "accepted") { // Owner-confirmed acceptance replaces the attempted turn with a receipt. Every other // outcome keeps the masked display in its original transcript position. @@ -285,7 +292,7 @@ export class CustodianSessionStore extends CustodianTranscriptState { } this.emit(); } - return outcome; + return result; } async sendEventNudge(): Promise { @@ -637,17 +644,17 @@ export class CustodianSessionStore extends CustodianTranscriptState { private async requestReply( client: GatewayBrowserClient, params: SystemAgentChatParams, - ): Promise { + ): Promise { const context = this.context; if (!context) { - return "rejected"; + return { outcome: "rejected" }; } const snapshot = context.gateway.snapshot; if ( snapshot.client !== client || !canCallGatewayMethod(snapshot, "openclaw.chat", "operator.admin") ) { - return "rejected"; + return { outcome: "rejected" }; } this.requestAbort?.abort(); const requestAbort = new AbortController(); @@ -669,7 +676,7 @@ export class CustodianSessionStore extends CustodianTranscriptState { }); delivery = "received"; if (epoch !== this.requestEpoch || client !== this.activeClient) { - return "sent"; + return { outcome: "sent" }; } this.sessionId = result.sessionId; this.sensitive = result.sensitive === true; @@ -690,7 +697,7 @@ export class CustodianSessionStore extends CustodianTranscriptState { if (result.agentId) { const roster = await context.agents.refreshList(); if (epoch !== this.requestEpoch || client !== this.activeClient) { - return "sent"; + return { outcome: "sent" }; } sessionKey = buildAgentMainSessionKey({ agentId: result.agentId, @@ -715,9 +722,11 @@ export class CustodianSessionStore extends CustodianTranscriptState { this.exitSetup(); } if (!hasCustodianWizardAction(params) || !this.wizardActionReceiptsAvailable) { - return "sent"; + return { outcome: "sent" }; } - return result.wizardActionAccepted === true ? "accepted" : "rejected"; + return result.wizardAction + ? { outcome: "accepted", wizardAction: result.wizardAction } + : { outcome: "rejected" }; } catch (error) { if (epoch === this.requestEpoch && client === this.activeClient) { this.error = custodianErrorMessage(error); @@ -739,7 +748,7 @@ export class CustodianSessionStore extends CustodianTranscriptState { // User turns have no idempotency key and are never replayed after an ambiguous failure. this.retryParams = null; } - return eventNudgeState.classifyCustodianSendFailure(error, delivery); + return { outcome: eventNudgeState.classifyCustodianSendFailure(error, delivery) }; } finally { if (this.requestAbort === requestAbort) { this.requestAbort = null; diff --git a/ui/src/pages/custodian/custodian-structured-interaction.ts b/ui/src/pages/custodian/custodian-structured-interaction.ts index 8ee871834068..306bfdd6d943 100644 --- a/ui/src/pages/custodian/custodian-structured-interaction.ts +++ b/ui/src/pages/custodian/custodian-structured-interaction.ts @@ -1,4 +1,7 @@ -import type { SystemAgentChatParams } from "@openclaw/gateway-protocol"; +import type { + SystemAgentChatParams, + SystemAgentWizardActionReceipt, +} from "@openclaw/gateway-protocol"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; import { t } from "../../i18n/index.ts"; import { custodianWizardSubmission } from "./custodian-wizard-step.ts"; @@ -26,7 +29,10 @@ type StructuredInteractionHost = { params: SystemAgentChatParams, display: string, userTurnProjection: "always" | "unless-accepted", - ) => Promise; + ) => Promise<{ + outcome: eventNudgeState.CustodianSendOutcome; + wizardAction?: SystemAgentWizardActionReceipt; + }>; }; function withResponse( @@ -49,7 +55,6 @@ export function createCustodianStructuredInteraction(host: StructuredInteraction request: SystemAgentChatParams; display: string; kind: CustodianStructuredResponse["kind"]; - prompt?: string; }): Promise => { const state = host.state(); if ( @@ -67,36 +72,35 @@ export function createCustodianStructuredInteraction(host: StructuredInteraction display: params.display, kind: params.kind, state: "submitting", - ...(params.prompt ? { prompt: params.prompt } : {}), }) ?? [...state.messages], ); host.emit(); } - const outcome = await host.sendUserTurn( + const result = await host.sendUserTurn( params.client, params.request, params.display, state.wizardActionReceiptsAvailable ? "unless-accepted" : "always", ); if (!state.wizardActionReceiptsAvailable) { - return outcome; + return result.outcome; } const current = host.state(); const response: CustodianStructuredResponse | null = - outcome !== "accepted" + result.outcome !== "accepted" || !result.wizardAction ? null : { display: params.display, - kind: params.kind, + kind: result.wizardAction.kind, state: "submitted", - ...(params.prompt ? { prompt: params.prompt } : {}), + ...(result.wizardAction?.prompt ? { prompt: result.wizardAction.prompt } : {}), }; const messages = withResponse(current.messages, params.message.id, response); if (messages) { host.replaceMessages(messages); host.emit(); } - return outcome; + return result.outcome; }; return { @@ -113,7 +117,6 @@ export function createCustodianStructuredInteraction(host: StructuredInteraction request: { sessionId: state.sessionId, wizardAnswer: submission.answer }, display: message.step?.sensitive ? t("custodian.sensitiveReply") : submission.display, kind: "answer", - prompt: message.step?.title ?? message.step?.message, }); }, @@ -137,7 +140,6 @@ export function createCustodianStructuredInteraction(host: StructuredInteraction request: { sessionId: state.sessionId, wizardCancel: { stepId: step.id } }, display: t("custodian.cancel"), kind: "cancel", - prompt: step.title ?? step.message, }); }, }; diff --git a/ui/src/pages/custodian/transcript.ts b/ui/src/pages/custodian/transcript.ts index 2fe959c44523..c0dcd90e4231 100644 --- a/ui/src/pages/custodian/transcript.ts +++ b/ui/src/pages/custodian/transcript.ts @@ -215,13 +215,7 @@ function renderCustodianEarlierDivider(message: CustodianMessage, boundaryAfterI } function structuredPrompt(message: CustodianMessage): string { - return ( - message.structuredResponse?.prompt ?? - message.step?.title ?? - message.step?.message ?? - message.question?.question ?? - t("custodian.structured.response") - ); + return message.structuredResponse?.prompt ?? t("custodian.structured.response"); } function renderStructuredResponse(message: CustodianMessage) {