Conversation
Signed-off-by: yaron2 <schneider.yaron@live.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a new @dapr/openclaw extension plus an @dapr/openclaw-plugin package to add Dapr Workflow-backed durability to OpenClaw agent runs, and adds a dedicated GitHub Actions workflow to build/publish those packages. It fits into the repo as a new extension module that bridges OpenClaw agent execution with the existing Dapr workflow runtime.
Changes:
- Adds the OpenClaw runtime extension (
extensions/openclaw/src/*) that patches agent execution to run through Dapr Workflows and activities. - Adds the distributable OpenClaw plugin package, metadata, docs, and local crash-recovery demo assets.
- Adds a dedicated GitHub Actions workflow to build and publish the OpenClaw runtime/plugin packages.
Reviewed changes
Copilot reviewed 21 out of 23 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
extensions/openclaw/tsconfig.json |
TypeScript build config for the new runtime package. |
extensions/openclaw/test/plugins/crasher-tool/package.json |
Defines the test-only crash/retry plugin package. |
extensions/openclaw/test/plugins/crasher-tool/openclaw.plugin.json |
Plugin metadata for the crash-recovery demo tool. |
extensions/openclaw/test/plugins/crasher-tool/index.js |
Implements demo tools used to force and verify crash recovery. |
extensions/openclaw/test/e2e/run-cli-crash-e2e.sh |
Adds a two-phase CLI crash-recovery demo harness. |
extensions/openclaw/test/components/statestore.yaml |
Provides Redis-backed Dapr state store config for workflows. |
extensions/openclaw/src/workflow.ts |
Defines the durable agent-loop workflow orchestration. |
extensions/openclaw/src/types.ts |
Declares workflow/activity/runtime types for the extension. |
extensions/openclaw/src/registry.ts |
Adds in-memory workflow-to-agent runtime context registry. |
extensions/openclaw/src/patch.ts |
Patches OpenClaw agent execution to schedule/resume workflows. |
extensions/openclaw/src/index.ts |
Exposes enable/disable entry points and starts the workflow runtime. |
extensions/openclaw/src/activities.ts |
Implements LLM, tool, and event workflow activities. |
extensions/openclaw/plugin/package.json |
Defines the publishable OpenClaw plugin package. |
extensions/openclaw/plugin/package-lock.json |
Locks plugin package dependencies for local/plugin packaging. |
extensions/openclaw/plugin/openclaw.plugin.json |
Plugin manifest for OpenClaw discovery. |
extensions/openclaw/plugin/index.js |
Plugin entry point that resolves Agent and enables durable execution. |
extensions/openclaw/plugin/README.md |
Usage docs for installing/enabling the plugin package. |
extensions/openclaw/plugin/LICENSE |
License file for the plugin package. |
extensions/openclaw/package.json |
Defines the publishable OpenClaw runtime package. |
extensions/openclaw/README.md |
Main extension documentation and crash-recovery walkthrough. |
extensions/openclaw/LICENSE |
License file for the runtime package. |
.github/workflows/openclaw.yml |
Adds CI/publish automation for the new OpenClaw packages. |
Files not reviewed (1)
- extensions/openclaw/plugin/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Drain the Agent's steering queue (messages injected while the loop runs) | ||
| if (rt.config.getSteeringMessages) { | ||
| const steering: any[] = await rt.config.getSteeringMessages(); | ||
| if (steering?.length) { | ||
| for (const msg of steering) { | ||
| await rt.emit({ type: "message_start", message: msg }); | ||
| await rt.emit({ type: "message_end", message: msg }); | ||
| messages.push(msg); | ||
| } | ||
| } |
| - name: Install runtime dependencies | ||
| working-directory: extensions/openclaw | ||
| run: npm install | ||
|
|
||
| - name: Build runtime | ||
| working-directory: extensions/openclaw | ||
| run: npm run build | ||
|
|
||
| - name: Pack smoke test (@dapr/openclaw) | ||
| working-directory: extensions/openclaw | ||
| run: npm pack --dry-run | ||
|
|
||
| - name: Pack smoke test (@dapr/openclaw-plugin) | ||
| working-directory: extensions/openclaw/plugin | ||
| run: npm pack --dry-run |
| const sessionId = this.sessionId || "default"; | ||
| const workflowId = `openclaw-${sessionId}`; |
There was a problem hiding this comment.
Pushed a per-prompt workflow ID, but couldn't use idempotencyKey directl since it doesn't reach the Agent in current OpenClaw.
Agent.prompt(input, images) doesn't forward options (pi-agent-core@0.65.0/agent.js:213), and hookCtx exposes runId but not idempotencyKey (and runId is randomUUID() per attempt, so it'd break resume).
Used a SHA-256 over {messages, systemPrompt, toolNames} instead (with timestamp stripped). Same content + retry → same workflow (resume works); different prompts → different workflows. Two distinct calls with an identical content requires an upstream Openclaw cooperation which is a no-go
| // No tool calls → done | ||
| if (!llmResult.toolCalls.length || llmResult.error) { | ||
| yield ctx.callActivity(emitEvent, { | ||
| workflowId, | ||
| event: { | ||
| type: "turn_end", | ||
| message: llmResult.assistantMessage, | ||
| toolResults: [], | ||
| }, | ||
| } as EmitEventInput); | ||
| break; | ||
| } |
| while (iteration < input.maxIterations) { | ||
| // --- turn_start --- | ||
| yield ctx.callActivity(emitEvent, { | ||
| workflowId, | ||
| event: { type: "turn_start" }, | ||
| } as EmitEventInput); |
| // Deterministic per-prompt workflow ID — stable for retries of this | ||
| // exact prompt, distinct from other prompts in the same session. | ||
| const sessionId = this.sessionId || "default"; | ||
| const workflowId = buildWorkflowId(sessionId, messages, context.systemPrompt, toolNames); | ||
|
|
| function buildWorkflowId(sessionId: string, messages: any[], systemPrompt: string, toolNames: string[]): string { | ||
| // Strip volatile fields before hashing. Agent.normalizePromptInput in | ||
| // pi-agent-core stamps `timestamp: Date.now()` on every wrapped user | ||
| // message — including this in the hash would make every retry produce a | ||
| // different workflow ID, breaking crash recovery. The content + role | ||
| // identify the prompt; the timestamp does not. | ||
| const stable = JSON.stringify({ messages, systemPrompt, toolNames }, (key, value) => | ||
| key === "timestamp" ? undefined : value, | ||
| ); | ||
| const fingerprint = createHash("sha256").update(stable).digest("hex").slice(0, 16); | ||
| return `openclaw-${sessionId}-${fingerprint}`; | ||
| } |
| } | ||
|
|
||
| proto.runPromptMessages = async function runPromptMessagesDapr(messages: any[], options: any = {}): Promise<void> { | ||
| console.log("[dapr] runPromptMessages intercepted — routing through Dapr Workflow"); |
| @@ -0,0 +1,129 @@ | |||
| /* | |||
| Copyright 2025 The Dapr Authors | |||
| * and tool call becomes a durable activity with automatic crash recovery. | ||
| * | ||
| * Installation (canonical): | ||
| * openclaw plugin install @dapr/openclaw-plugin |
| try { | ||
| // Check if a previous run left a workflow in RUNNING state (crash recovery). | ||
| let instanceId = workflowId; | ||
| let isResume = false; | ||
| try { | ||
| const existing = await workflowClient.getWorkflowState(workflowId, true); | ||
| if (existing && existing.runtimeStatus === WorkflowRuntimeStatus.RUNNING) { | ||
| console.log(`[dapr] Resuming existing workflow ${workflowId} (crash recovery)`); | ||
| isResume = true; |
WhitWaldo
left a comment
There was a problem hiding this comment.
@yaron2 I'm not familiar with OpenClaw extensions, so I assume the integration there is handled properly. There looks to be an e2e test in extensions/openclaw/test/e2e/run-cli-crash-e2e.sh, but I don't see that it's ever invoked and evaluated by the GitHub action. Should it be?
|
@yaron2 I can merge this as-is as I don't see that it has any impact on the JS SDK with Dapr itself and it just providing a separate extension for using Dapr with OpenClaw. However, it does look like there is some sort of e2e test that's been written and nothing is actually testing it. I'm hesitant to put this out there as "we're so sure this will work that we published it to our repository" when we have zero testing done to validate that. I would much rather see that the e2e tests run against a Testcontainer wrapping the OpenClaw functionality so we can at least be confident that publishing this will do what we claim it does. Happy to discuss further, but this is my primary objection to merging as-is. |
This PR adds a Dapr Workflow integration to OpenClaw - allowing users to make their openclaw agents immortal and recover from failures. In addition, this enables cross-device work which OpenClaw doesn't natively support today.
Added here is a GitHub workflow to publish to
dapr/openclawon NPM.cc @WhitWaldo