diff --git a/CHANGELOG.md b/CHANGELOG.md index 377e63f8fb8..61ea035c2ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai ### Fixes - OpenAI/Responses: resolve `/think` levels against each GPT model's supported reasoning efforts so `/think off` no longer becomes high reasoning or sends unsupported `reasoning.effort: "none"` payloads. +- Lobster/TaskFlow: allow managed approval resumes to use `approvalId` without a resume token, and persist that id in approval wait state. (#69559) Thanks @kirkluokun. - Setup/TUI: relaunch the setup hatch TUI in a fresh process while preserving the configured gateway target and auth source, so onboarding recovers terminal state cleanly without exposing gateway secrets on command-line args. (#69524) Thanks @shakkernerd. - Codex: avoid re-exposing the image-generation tool on native vision turns with inbound images, and keep bare image-model overrides on the configured image provider. (#65061) Thanks @zhulijin1991. - Sessions/reset: clear auto-sourced model, provider, and auth-profile overrides on `/new` and `/reset` while preserving explicit user selections, so channel sessions stop staying pinned to runtime fallback choices. (#69419) Thanks @sk7n4k3d. diff --git a/extensions/lobster/src/lobster-taskflow.ts b/extensions/lobster/src/lobster-taskflow.ts index 1dfd2f8e488..a46e6be5f42 100644 --- a/extensions/lobster/src/lobster-taskflow.ts +++ b/extensions/lobster/src/lobster-taskflow.ts @@ -23,6 +23,7 @@ export type LobsterApprovalWaitState = { prompt: string; items: JsonLike[]; resumeToken?: string; + approvalId?: string; }; export type RunManagedLobsterFlowParams = { @@ -41,9 +42,8 @@ export type ResumeManagedLobsterFlowParams = { runner: LobsterRunner; runnerParams: LobsterRunnerParams & { action: "resume"; - token: string; approve: boolean; - }; + } & ({ token: string } | { approvalId: string }); flowId: string; expectedRevision: number; currentStep?: string; @@ -120,6 +120,9 @@ function buildApprovalWaitState(envelope: Extract ...(envelope.requiresApproval.resumeToken ? { resumeToken: envelope.requiresApproval.resumeToken } : {}), + ...(envelope.requiresApproval.approvalId + ? { approvalId: envelope.requiresApproval.approvalId } + : {}), } satisfies LobsterApprovalWaitState; } diff --git a/extensions/lobster/src/lobster-tool.test.ts b/extensions/lobster/src/lobster-tool.test.ts index eefc13a25ab..5fe715051d0 100644 --- a/extensions/lobster/src/lobster-tool.test.ts +++ b/extensions/lobster/src/lobster-tool.test.ts @@ -137,6 +137,7 @@ describe("lobster plugin tool", () => { prompt: "Approve this?", items: [{ id: "item-1" }], resumeToken: "resume-1", + approvalId: "approval-1", }, }), }; @@ -168,6 +169,7 @@ describe("lobster plugin tool", () => { prompt: "Approve this?", items: [{ id: "item-1" }], resumeToken: "resume-1", + approvalId: "approval-1", }, }); expect(res.details).toMatchObject({ @@ -214,7 +216,51 @@ describe("lobster plugin tool", () => { ).rejects.toThrow(/flowStateJson must be valid JSON/); }); - it("rejects managed TaskFlow resume mode without a token", async () => { + it("can resume managed TaskFlow mode with only approvalId", async () => { + const runner = { + run: vi.fn().mockResolvedValue({ + ok: true, + status: "ok", + output: [], + requiresApproval: null, + }), + }; + const taskFlow = createFakeTaskFlow(); + const tool = createLobsterTool(fakeApi(), { runner, taskFlow }); + + const res = await tool.execute("call-managed-resume-approval-id", { + action: "resume", + approvalId: "approval-1", + approve: true, + flowId: "flow-1", + flowExpectedRevision: 1, + flowCurrentStep: "resume_lobster", + }); + + expect(taskFlow.resume).toHaveBeenCalledWith({ + flowId: "flow-1", + expectedRevision: 1, + status: "running", + currentStep: "resume_lobster", + }); + expect(runner.run).toHaveBeenCalledWith({ + action: "resume", + approvalId: "approval-1", + approve: true, + cwd: process.cwd(), + timeoutMs: 20_000, + maxStdoutBytes: 512_000, + }); + expect(res.details).toMatchObject({ + ok: true, + status: "ok", + mutation: { + applied: true, + }, + }); + }); + + it("rejects managed TaskFlow resume mode without a token or approvalId", async () => { const tool = createLobsterTool(fakeApi(), { runner: { run: vi.fn() }, taskFlow: createFakeTaskFlow(), @@ -227,7 +273,7 @@ describe("lobster plugin tool", () => { flowExpectedRevision: 1, approve: true, }), - ).rejects.toThrow(/token required when using managed TaskFlow resume mode/); + ).rejects.toThrow(/token or approvalId required when using managed TaskFlow resume mode/); }); it("rejects managed TaskFlow resume mode without approve", async () => { diff --git a/extensions/lobster/src/lobster-tool.ts b/extensions/lobster/src/lobster-tool.ts index 8dfba439c54..6c2a6b99077 100644 --- a/extensions/lobster/src/lobster-tool.ts +++ b/extensions/lobster/src/lobster-tool.ts @@ -141,6 +141,7 @@ function parseResumeFlowParams(params: Record): ManagedFlowResu const currentStep = readOptionalTrimmedString(params.flowCurrentStep, "flowCurrentStep"); const waitingStep = readOptionalTrimmedString(params.flowWaitingStep, "flowWaitingStep"); const token = readOptionalTrimmedString(params.token, "token"); + const approvalId = readOptionalTrimmedString(params.approvalId, "approvalId"); const approve = readOptionalBoolean(params.approve, "approve"); const runControllerId = readOptionalTrimmedString(params.flowControllerId, "flowControllerId"); const runGoal = readOptionalTrimmedString(params.flowGoal, "flowGoal"); @@ -164,8 +165,8 @@ function parseResumeFlowParams(params: Record): ManagedFlowResu if (expectedRevision === undefined) { throw new Error("flowExpectedRevision required when using managed TaskFlow resume mode"); } - if (!token) { - throw new Error("token required when using managed TaskFlow resume mode"); + if (!token && !approvalId) { + throw new Error("token or approvalId required when using managed TaskFlow resume mode"); } if (approve === undefined) { throw new Error("approve required when using managed TaskFlow resume mode"); @@ -295,9 +296,8 @@ export function createLobsterTool(api: OpenClawPluginApi, options?: LobsterToolO runner, runnerParams: runnerParams as LobsterRunnerParams & { action: "resume"; - token: string; approve: boolean; - }, + } & ({ token: string } | { approvalId: string }), flowId: flowParams.flowId, expectedRevision: flowParams.expectedRevision, ...(flowParams.currentStep ? { currentStep: flowParams.currentStep } : {}),