openclaw - 💡(How to fix) Fix RFC: sessions.patch plugin extension hook \u2014 let plugins handle their own patch payloads [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#71426Fetched 2026-04-26 05:12:52
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Participants
Timeline (top)
cross-referenced ×2

Error Message

// src/plugin-sdk/sessions-patch.ts (NEW or extend existing) export type SessionsPatchExtensionHandler = (input: { key: string; // sessionKey agentId: string; entry: SessionEntry; // current entry (post any non-extension patch fields) payload: unknown; // raw plugin payload }) => Promise< | { ok: true; entryPatch?: Partial<SessionEntry> } | { ok: false; error: string }

;

// Plugin manifest declares the extension handler: export const plugin = definePluginEntry({ // ...existing... sessionsPatchExtensions: { "approve": myApproveHandler, "reject": myRejectHandler, "answer": myAnswerHandler, }, });

Fix Action

Fix / Workaround

Third-party plugins that need to react to sessions.patch RPCs (UI inline-button events, /command-style mutations from any channel) currently have to patch the gateway's applySessionsPatchToStore handler at src/gateway/sessions-patch.ts:89 to add new branches. This is brittle:

  • Every host version bump requires re-baselining the patch
  • The patch grows monotonically as the plugin adds features (Smarter-Claw's plan-mode patch is now ~500 lines)
  • Multiple plugins patching the same handler will conflict
  • Schema changes require a parallel patch to src/gateway/protocol/schema/sessions.ts

Smarter-Claw is the proof case: we maintain installer/patches/core/sessions-patch-handler-plan-mode.diff (500 LOC) and installer/patches/core/sessions-patch-schema-plan-mode.diff (~70 LOC) just to wire the UI's Approve/Reject/Edit/Auto/Answer button payloads through to plugin state. A clean SDK seam would let us delete both patches.

Code Example

// src/gateway/protocol/schema/sessions.ts
export const sessionsPatchPayload = Type.Object({
  key: NonEmptyString,
  // ...existing fields...
  extension: Type.Optional(
    Type.Object(
      {
        plugin: NonEmptyString,                          // "smarter-claw"
        action: NonEmptyString,                          // "approve" / "reject" / "answer"
        payload: Type.Unknown(),                         // plugin-validated
      },
      { additionalProperties: false },
    ),
  ),
});

---

// src/plugin-sdk/sessions-patch.ts (NEW or extend existing)
export type SessionsPatchExtensionHandler = (input: {
  key: string;                                   // sessionKey
  agentId: string;
  entry: SessionEntry;                           // current entry (post any non-extension patch fields)
  payload: unknown;                              // raw plugin payload
}) => Promise<
  | { ok: true; entryPatch?: Partial<SessionEntry> }
  | { ok: false; error: string }
>;

// Plugin manifest declares the extension handler:
export const plugin = definePluginEntry({
  // ...existing...
  sessionsPatchExtensions: {
    "approve": myApproveHandler,
    "reject": myRejectHandler,
    "answer": myAnswerHandler,
  },
});

---

// src/gateway/sessions-patch.ts
if (patch.extension) {
  const plugin = pluginRegistry.get(patch.extension.plugin);
  if (!plugin?.sessionsPatchExtensions?.[patch.extension.action]) {
    return invalid(`unknown extension: ${patch.extension.plugin}.${patch.extension.action}`);
  }
  const result = await plugin.sessionsPatchExtensions[patch.extension.action]({
    key, agentId, entry: next, payload: patch.extension.payload,
  });
  if (!result.ok) return invalid(result.error);
  if (result.entryPatch) Object.assign(next, result.entryPatch);
}
RAW_BUFFERClick to expand / collapse

Problem statement

Third-party plugins that need to react to sessions.patch RPCs (UI inline-button events, /command-style mutations from any channel) currently have to patch the gateway's applySessionsPatchToStore handler at src/gateway/sessions-patch.ts:89 to add new branches. This is brittle:

  • Every host version bump requires re-baselining the patch
  • The patch grows monotonically as the plugin adds features (Smarter-Claw's plan-mode patch is now ~500 lines)
  • Multiple plugins patching the same handler will conflict
  • Schema changes require a parallel patch to src/gateway/protocol/schema/sessions.ts

Smarter-Claw is the proof case: we maintain installer/patches/core/sessions-patch-handler-plan-mode.diff (500 LOC) and installer/patches/core/sessions-patch-schema-plan-mode.diff (~70 LOC) just to wire the UI's Approve/Reject/Edit/Auto/Answer button payloads through to plugin state. A clean SDK seam would let us delete both patches.

Proposed change

Add an extension field to the sessions.patch payload + a plugin-SDK callback for handling it.

Schema (additive)

// src/gateway/protocol/schema/sessions.ts
export const sessionsPatchPayload = Type.Object({
  key: NonEmptyString,
  // ...existing fields...
  extension: Type.Optional(
    Type.Object(
      {
        plugin: NonEmptyString,                          // "smarter-claw"
        action: NonEmptyString,                          // "approve" / "reject" / "answer"
        payload: Type.Unknown(),                         // plugin-validated
      },
      { additionalProperties: false },
    ),
  ),
});

Plugin SDK seam (additive)

// src/plugin-sdk/sessions-patch.ts (NEW or extend existing)
export type SessionsPatchExtensionHandler = (input: {
  key: string;                                   // sessionKey
  agentId: string;
  entry: SessionEntry;                           // current entry (post any non-extension patch fields)
  payload: unknown;                              // raw plugin payload
}) => Promise<
  | { ok: true; entryPatch?: Partial<SessionEntry> }
  | { ok: false; error: string }
>;

// Plugin manifest declares the extension handler:
export const plugin = definePluginEntry({
  // ...existing...
  sessionsPatchExtensions: {
    "approve": myApproveHandler,
    "reject": myRejectHandler,
    "answer": myAnswerHandler,
  },
});

Handler dispatch (gateway-side, ~20 LOC)

// src/gateway/sessions-patch.ts
if (patch.extension) {
  const plugin = pluginRegistry.get(patch.extension.plugin);
  if (!plugin?.sessionsPatchExtensions?.[patch.extension.action]) {
    return invalid(`unknown extension: ${patch.extension.plugin}.${patch.extension.action}`);
  }
  const result = await plugin.sessionsPatchExtensions[patch.extension.action]({
    key, agentId, entry: next, payload: patch.extension.payload,
  });
  if (!result.ok) return invalid(result.error);
  if (result.entryPatch) Object.assign(next, result.entryPatch);
}

Backward compatibility

  • Fully additive — clients without extension field unaffected
  • Existing plugins that patch the handler directly continue to work
  • New plugins use the SDK seam instead

What this saves Smarter-Claw

  • Delete installer/patches/core/sessions-patch-handler-plan-mode.diff (500 LOC)
  • Delete installer/patches/core/sessions-patch-schema-plan-mode.diff (~70 LOC)
  • Move ~570 LOC from a brittle host patch into typed plugin code
  • Eliminate version-bump fragility (no more SHA pinning per host upgrade for these patches)

Generalizes to any plugin that wants to react to UI button events / channel-routed mutations.

Vocabulary alignment

The payload: Type.Unknown() shape lets each plugin own its own payload schema (validated inside the handler). This avoids leaking plugin-specific vocabulary like "approve"/"reject"/"answer" into the gateway protocol — keeps core extension-agnostic per the AGENTS.md architecture rule.

Tests proposed

  • Round-trip: client sends extension: { plugin: "test-plugin", action: "noop", payload: {x:1} } → plugin handler receives raw payload → returns ok with entryPatch → next entry contains patch
  • Unknown plugin → 400 with clear error
  • Unknown action for known plugin → 400
  • Plugin handler throws → 500 surfaced cleanly (not silent)
  • Plugin returns ok=false → mutation rejected, error surfaced

Cross-link

Smarter-Claw architecture impact: electricsheephq/Smarter-Claw#44, #45, #46. Once this RFC lands + Smarter-Claw migrates, the entire sessions-patch-handler-plan-mode.diff retires.

This is one of several RFCs Smarter-Claw is filing to push the plugin/host seam upstream. Sister RFC: openclaw/openclaw#71260 (mergeSessionEntryWithPolicy "merge-plugin-metadata").

extent analysis

TL;DR

Add an extension field to the sessions.patch payload and introduce a plugin-SDK callback for handling it to resolve the issue of third-party plugins needing to patch the gateway's applySessionsPatchToStore handler.

Guidance

  • Introduce the proposed extension field in the sessions.patch payload to allow plugins to react to UI button events and channel-routed mutations without patching the gateway handler directly.
  • Implement the plugin-SDK callback SessionsPatchExtensionHandler to handle the extension field and provide a way for plugins to validate and process their own payloads.
  • Update the gateway-side handler dispatch to call the plugin's extension handler when an extension field is present in the patch payload.
  • Ensure backward compatibility by making the changes fully additive and allowing existing plugins that patch the handler directly to continue working.

Example

// Example plugin manifest with extension handler
export const plugin = definePluginEntry({
  // ...existing...
  sessionsPatchExtensions: {
    "approve": myApproveHandler,
    "reject": myRejectHandler,
    "answer": myAnswerHandler,
  },
});

// Example extension handler implementation
const myApproveHandler: SessionsPatchExtensionHandler = async ({ key, agentId, entry, payload }) => {
  // Validate and process the payload
  // Return the result with an optional entry patch
  return { ok: true, entryPatch: { /* updated entry fields */ } };
};

Notes

The proposed solution assumes that the plugin-SDK callback and the gateway-side handler dispatch are implemented correctly and that the extension field is properly validated and processed by the plugins.

Recommendation

Apply the proposed changes to introduce the extension field and the plugin-SDK callback to resolve the issue and improve the plugin/host seam. This approach allows plugins to own their own payload schema and avoids leaking plugin-specific vocabulary into the gateway protocol.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix RFC: sessions.patch plugin extension hook \u2014 let plugins handle their own patch payloads [1 participants]