openclaw - ✅(Solved) Fix Discord: ACP thread spawn fails with thread_binding_invalid after 2026.4.5 (channel: prefix not stripped in resolveChannelIdForBinding) [4 pull requests, 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#63329Fetched 2026-04-09 07:55:15
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
cross-referenced ×2referenced ×2

Spawning an ACP session with thread: true from a Discord guild channel fails with:

{
  "status": "error",
  "errorCode": "thread_binding_invalid",
  "error": "Session binding adapter failed to bind target conversation"
}

This worked on 2026.4.1 and regressed in 2026.4.5.

Error Message

"status": "error", "error": "Session binding adapter failed to bind target conversation" Routes.channel() URL-encodes the colon in channel:, producing /channels/channel%3A1490136404385075452. Discord returns an error for this path, which is caught silently, causing resolveChannelIdForBinding to return null. With no channelId, bindTarget returns null, triggering BINDING_CREATE_FAILEDthread_binding_invalid. 3. Observe thread_binding_invalid error

Root Cause

In thread-bindings.discord-api-CL8HMdV4.js, the resolveChannelIdForBinding function receives the conversation ID in OpenClaw's internal prefixed format (channel:1490136404385075452) and passes it directly to Routes.channel():

// Current (broken) code in resolveChannelIdForBinding
const channel = await createDiscordRestClient({
    accountId: params.accountId,
    token: params.token
}, params.cfg).rest.get(Routes.channel(params.threadId));
// Routes.channel("channel:1490136404385075452")
// → /channels/channel%3A1490136404385075452  ← invalid Discord API path

Routes.channel() URL-encodes the colon in channel:, producing /channels/channel%3A1490136404385075452. Discord returns an error for this path, which is caught silently, causing resolveChannelIdForBinding to return null. With no channelId, bindTarget returns null, triggering BINDING_CREATE_FAILEDthread_binding_invalid.

Fix Action

Workaround

None available via config. The only workaround is to spawn without thread: true and lose the thread-binding behavior.

PR fix notes

PR #63354: fix(discord): strip channel: prefix in resolveChannelIdForBinding to fix thread_binding_invalid on ACP spawn

Description (problem / solution / changelog)

Summary

Fixes the thread_binding_invalid error when spawning ACP sessions with thread: true from Discord guild channels after upgrading to 2026.4.5+.

Fixes #63329

Root Cause

In resolveChannelIdForBinding, the params.threadId is passed directly to Routes.channel(). After the ACPX runtime refactor in #61319 (2026.4.5), the conversation ID is now passed in OpenClaw's internal prefixed format (channel:1490136404385075452) rather than as a raw numeric Discord ID.

Routes.channel() (from discord-api-types) URL-encodes the colon in the channel: prefix, producing /channels/channel%3A1490136404385075452. Discord returns an error for this invalid path. The error is caught and swallowed silently, causing resolveChannelIdForBinding to return null, bindTarget to return null, and the spawn to fail with thread_binding_invalid.

Fix

Strip the channel: or user: prefix from params.threadId before passing it to Routes.channel(), so the Discord REST API always receives a valid numeric channel ID.

- const channel = (await rest.get(Routes.channel(params.threadId))) as {
+ const channel = (await rest.get(Routes.channel(params.threadId.replace(/^(channel:|user:)/i, '')))) as {

Testing

Verified locally by applying the equivalent patch to the built bundle (/app/dist/thread-bindings.discord-api-CL8HMdV4.js). After the patch, sessions_spawn with thread: true from a Discord guild text channel succeeds and creates a thread as expected.

Notes

  • Regression introduced in 2026.4.5 by the ACPX runtime embed refactor (#61319)
  • Worked correctly on 2026.4.1 and earlier
  • An alternative fix would be to use the existing resolveDiscordChannelId() helper which already handles prefix stripping

Changed files

  • extensions/discord/src/monitor/thread-bindings.discord-api.ts (modified, +2/-1)

PR #63574: fix(acp): strip discord channel prefix for thread-bound spawns

Description (problem / solution / changelog)

Summary

Fix Discord thread-bound ACP spawns when the resolved conversation id is returned in OpenClaw's internal channel:<id> format.

This normalizes Discord plugin-resolved conversation ids before handing them to the thread binding flow.

Why

Issue #63329 reports a regression where Discord ACP thread spawns fail with thread_binding_invalid because channel:<id> is passed into the binding layer instead of the raw Discord channel id.

The binding path expects the bare Discord id, but resolveConversationIdForThreadBinding() could return the prefixed internal form unchanged.

Changes

  • Updated src/agents/acp-spawn.ts
  • When channel === "discord" and the plugin-resolved conversation id starts with channel:, strip the prefix before binding
  • Added a regression assertion in src/agents/acp-spawn.test.ts to verify the binding layer receives the raw channel id

Scope

This change is intentionally narrow:

  • only affects Discord
  • only applies when the plugin-resolved conversation id is prefixed with channel:
  • leaves all non-Discord channels unchanged
  • does not alter the broader thread-binding flow

Validation

  • Added a focused regression assertion covering the Discord thread-binding path
  • Change is limited to a single normalization step before binding

Changed files

  • docs/concepts/session-tool.md (modified, +5/-0)
  • src/agents/acp-spawn.test.ts (modified, +20/-0)
  • src/agents/acp-spawn.ts (modified, +5/-1)

PR #68034: fix(discord): strip channel: prefix from conversationId in thread binding adapter

Description (problem / solution / changelog)

Summary

When sessions_spawn({ thread: true }) is called from a Discord channel, the channel: prefix on the conversation ID survives into the Discord thread binding adapter's bind() function, causing GET /channels/channel:<id> — an invalid Discord API path. The error is swallowed by logVerbose (no-op in production), bind() returns null, and the spawn fails with thread_binding_invalid / BINDING_CREATE_FAILED.

Fixes #62685, #63329.

Root cause

resolveConversationRefForThreadBinding calls resolvePluginConversationRefForThreadBinding first. For Discord, this calls resolveDiscordCurrentConversationIdentity({ originatingTo: "channel:<id>", chatType: "group" }), which returns "channel:<id>" (prefix preserved by normalizeDiscordTarget). The non-null result triggers an early return, bypassing resolveConversationIdFromTargets — the existing fix that strips the prefix — entirely.

Fix

Strip channel: at the bind() entry point, before conversationId propagates downstream:

-      const conversationId = normalizeOptionalString(input.conversation.conversationId) ?? "";
+      // Strip "channel:" prefix — must not appear in raw Discord API calls. It survives
+      // into bind() via resolvePluginConversationRefForThreadBinding's early return,
+      // bypassing the resolveConversationIdFromTargets stripping step.
+      const conversationId = (normalizeOptionalString(input.conversation.conversationId) ?? "").replace(/^channel:/i, "");

Why bind() rather than resolveChannelIdForBinding

The fix suggested in #63329 targets resolveChannelIdForBinding. That covers placement === "child" but not placement === "current", where conversationId is used directly as threadId without going through resolveChannelIdForBinding. Fixing at the bind() entry covers both paths.

Testing

Confirmed working on OC 2026.4.15 with patch applied to dist:

sessions_spawn({
  runtime: "acp",
  thread: true,
  agentId: "claude",
  mode: "session",
  task: "...",
  cwd: "/path/to/workspace"  // explicit cwd required
})

Result: thread created in Discord channel, ACP session bound (agent:claude:acp:176fb9e6).

Note: explicit cwd is required — default cwd resolution from a Discord message turn may exit with cwd_resolution_failed before thread binding is attempted.

Changed files

  • extensions/discord/src/monitor/thread-bindings.manager.bind.test.ts (added, +184/-0)
  • extensions/discord/src/monitor/thread-bindings.manager.ts (modified, +5/-1)

PR #68063: fix: apply canonical-to-binding normalization on provider-resolved ACP command contexts

Description (problem / solution / changelog)

AI-assisted PR. Targeted local tests run and manual Discord repro/verification performed.

Summary

  • Problem: Discord ACP thread-bound spawn could fail because the provider-resolved command path passed canonical conversation ids like channel:<id> through unchanged, eventually reaching Discord binding/channel resolution where a raw platform id was expected.
  • Why it matters: /acp ... --thread auto and sessions_spawn(... thread: true ...) could fail with thread_binding_invalid, preventing ACP sessions from creating/binding their Discord thread.
  • What changed: added a shared normalizeProviderConversationId() helper next to the existing outbound conversation-id normalization logic, then used it in both resolveConversationBindingContext() and the sessions_spawn plugin-resolved path.
  • What did NOT change (scope boundary): no plugin output format changes, no Discord adapter-specific patching, no change to DM user:<id> semantics, no unrelated thread-binding refactor.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #64017
  • Related #63329
  • Related #63686
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: the provider-resolved branch in resolveConversationBindingContext() skipped the same canonical-to-binding normalization that the fallback path already relied on, so plugin-resolved ids like channel:<id> could flow unchanged into Discord thread binding.
  • Missing detection / guardrail: there was no unit coverage asserting provider-resolved command contexts normalize canonical channel: ids while preserving DM user: identities.
  • Contributing context (if known): the same normalization gap also existed in the sessions_spawn plugin-resolved path. Existing code already had the right normalization convention in shared conversation-id utilities, but these two entry points were not using it.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • src/channels/conversation-binding-context.test.ts
    • src/agents/acp-spawn.test.ts
  • Scenario the test should lock in: provider-resolved Discord guild conversation ids normalize channel:<id> to <id>, preserve user:<id> for DM identity, reject prefix-only inputs like channel:, and apply the same rules on the sessions_spawn plugin-resolved path.
  • Why this is the smallest reliable guardrail: the bug is a normalization gap at the command/plugin boundary, so focused unit coverage on that boundary catches the regression without requiring live Discord setup.
  • Existing test that already covers this (if any): none for the provider-resolved branch; one existing expectation was updated from room:<id> to <id> because room: is already part of the shared strip convention and the old expectation encoded the missing normalization.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Discord ACP thread-bound spawn no longer passes canonical channel:<id> values into downstream binding/channel resolution on the provider-resolved command path.
  • sessions_spawn(... runtime: "acp", thread: true ...) now applies the same normalization semantics.
  • DM user:<id> conversation identities remain unchanged.
  • Degenerate prefix-only values like channel: are now rejected instead of being passed through.

Diagram (if applicable)

Before:
[user runs ACP thread spawn]
  -> [provider resolves conversationId = "channel:<id>"]
  -> [provider branch passes value through unchanged]
  -> [Discord binding/channel lookup expects raw id]
  -> [bind fails]
  -> [thread_binding_invalid]

After:
[user runs ACP thread spawn]
  -> [provider resolves conversationId = "channel:<id>"]
  -> [shared normalization converts to "<id>"]
  -> [Discord binding/channel lookup receives raw id]
  -> [thread bind succeeds]

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: macOS (local dev machine)
  • Runtime/container: local OpenClaw install + source checkout
  • Model/provider: N/A for the bug itself; local testing performed from existing OpenClaw sessions
  • Integration/channel (if any): Discord guild text channel with ACP thread binding enabled
  • Relevant config (redacted):
    • channels.discord.threadBindings.enabled = true
    • channels.discord.threadBindings.spawnAcpSessions = true
    • session.threadBindings.enabled = true

Steps

  1. In a Discord guild text channel with ACP/thread binding enabled, run /acp action:spawn value:claude --thread auto.
  2. Reproduce the same path through runtime tools with sessions_spawn({ runtime: "acp", agentId: "claude", thread: true, mode: "session" }).
  3. Repeat for codex and gemini.

Expected

  • ACP thread-bound session is created and bound successfully.

Actual

  • Received:
    {
      "status": "error",
      "errorCode": "thread_binding_invalid",
      "error": "Session binding adapter failed to bind target conversation"
    }

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Sanitized evidence:

  • Before fix, local session transcripts captured repeated thread_binding_invalid / Session binding adapter failed to bind target conversation failures for claude, codex, and gemini.
  • After fix, targeted source tests passed:
    • vitest run src/channels/conversation-binding-context.test.ts
    • vitest run src/agents/acp-spawn.test.ts
  • After applying the same fix locally for runtime verification, ACP spawn completed normally instead of failing at thread binding.

Human Verification (required)

  • Verified scenarios:
    • reproduced failure before fix for ACP thread-bound spawn
    • confirmed fix normalizes provider-resolved channel:<id> to raw id
    • confirmed DM user:<id> is preserved
    • confirmed prefix-only values like channel: are rejected
    • confirmed sessions_spawn path uses the same helper/semantics
  • Edge cases checked:
    • raw unprefixed ids remain unchanged
    • room: / conversation: / group: / dm: follow existing shared strip semantics
  • What you did not verify:
    • did not add or run a live Discord end-to-end test in repo CI
    • did not broaden scope into unrelated pre-existing test failures already present on main

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk:

    • normalization changes a second entry point (sessions_spawn) in addition to the primary command-context path.
  • Mitigation:

    • both sites now share one helper with explicit unit coverage instead of maintaining separate prefix logic.
  • Risk:

    • over-normalizing DM identities would regress user:<id> routing.
  • Mitigation:

    • the helper reuses existing conversation-id normalization semantics and intentionally preserves user:<id>.
  • Risk:

    • malformed canonical prefix-only inputs could silently leak through.
  • Mitigation:

    • the helper rejects degenerate values like channel: / dm: by returning undefined.

Changed files

  • src/agents/acp-spawn.test.ts (modified, +54/-0)
  • src/agents/acp-spawn.ts (modified, +8/-3)
  • src/channels/conversation-binding-context.test.ts (modified, +104/-1)
  • src/channels/conversation-binding-context.ts (modified, +8/-3)
  • src/infra/outbound/conversation-id.ts (modified, +20/-0)

Code Example

{
  "status": "error",
  "errorCode": "thread_binding_invalid",
  "error": "Session binding adapter failed to bind target conversation"
}

---

// Current (broken) code in resolveChannelIdForBinding
const channel = await createDiscordRestClient({
    accountId: params.accountId,
    token: params.token
}, params.cfg).rest.get(Routes.channel(params.threadId));
// Routes.channel("channel:1490136404385075452")
// → /channels/channel%3A1490136404385075452  ← invalid Discord API path

---

- const channel = await createDiscordRestClient({
-     accountId: params.accountId,
-     token: params.token
- }, params.cfg).rest.get(Routes.channel(params.threadId));
+ const rawChannelId = params.threadId.replace(/^(channel:|user:)/i, "");
+ const channel = await createDiscordRestClient({
+     accountId: params.accountId,
+     token: params.token
+ }, params.cfg).rest.get(Routes.channel(rawChannelId));

---

- const channel = await createDiscordRestClient({...}, params.cfg).rest.get(Routes.channel(params.threadId));
+ const channel = await createDiscordRestClient({...}, params.cfg).rest.get(Routes.channel(resolveDiscordChannelId(params.threadId)));
RAW_BUFFERClick to expand / collapse

Bug Report: Discord ACP thread spawn fails with thread_binding_invalid after upgrade to 2026.4.5+

Summary

Spawning an ACP session with thread: true from a Discord guild channel fails with:

{
  "status": "error",
  "errorCode": "thread_binding_invalid",
  "error": "Session binding adapter failed to bind target conversation"
}

This worked on 2026.4.1 and regressed in 2026.4.5.

Environment

  • OpenClaw version: 2026.4.8 (regression introduced in 2026.4.5)
  • Channel: Discord guild text channel
  • Config: channels.discord.threadBindings.enabled: true, spawnAcpSessions: true
  • Bot permissions: Bot can create threads (verified via direct Discord REST API call)

Root Cause

In thread-bindings.discord-api-CL8HMdV4.js, the resolveChannelIdForBinding function receives the conversation ID in OpenClaw's internal prefixed format (channel:1490136404385075452) and passes it directly to Routes.channel():

// Current (broken) code in resolveChannelIdForBinding
const channel = await createDiscordRestClient({
    accountId: params.accountId,
    token: params.token
}, params.cfg).rest.get(Routes.channel(params.threadId));
// Routes.channel("channel:1490136404385075452")
// → /channels/channel%3A1490136404385075452  ← invalid Discord API path

Routes.channel() URL-encodes the colon in channel:, producing /channels/channel%3A1490136404385075452. Discord returns an error for this path, which is caught silently, causing resolveChannelIdForBinding to return null. With no channelId, bindTarget returns null, triggering BINDING_CREATE_FAILEDthread_binding_invalid.

Why it worked before 2026.4.5

The regression was introduced by #61319 (ACPX/runtime: embed the ACP runtime directly in the bundled acpx plugin, remove the extra external ACP CLI hop). Prior to this refactor, the ACP session binding code path passed the raw numeric Discord channel ID into the thread binding adapter. After the refactor, it passes the full channel:-prefixed internal conversation ID, which resolveChannelIdForBinding was not written to handle.

Suggested Fix

Strip the OpenClaw-internal channel/user prefix before passing the ID to the Discord REST API in resolveChannelIdForBinding:

- const channel = await createDiscordRestClient({
-     accountId: params.accountId,
-     token: params.token
- }, params.cfg).rest.get(Routes.channel(params.threadId));
+ const rawChannelId = params.threadId.replace(/^(channel:|user:)/i, "");
+ const channel = await createDiscordRestClient({
+     accountId: params.accountId,
+     token: params.token
+ }, params.cfg).rest.get(Routes.channel(rawChannelId));

Alternatively, use the existing resolveDiscordChannelId() helper (from target-parsing-wpRt6Oe8.js) which already handles the channel: prefix stripping and would be the idiomatic fix:

- const channel = await createDiscordRestClient({...}, params.cfg).rest.get(Routes.channel(params.threadId));
+ const channel = await createDiscordRestClient({...}, params.cfg).rest.get(Routes.channel(resolveDiscordChannelId(params.threadId)));

Reproduction Steps

  1. Configure Discord with channels.discord.threadBindings.enabled: true and spawnAcpSessions: true
  2. From a Discord guild text channel, call sessions_spawn with runtime: "acp", thread: true, mode: "session"
  3. Observe thread_binding_invalid error

Workaround

None available via config. The only workaround is to spawn without thread: true and lose the thread-binding behavior.

extent analysis

TL;DR

Strip the OpenClaw-internal channel prefix before passing the ID to the Discord REST API in resolveChannelIdForBinding to fix the thread_binding_invalid error.

Guidance

  • Identify the resolveChannelIdForBinding function in thread-bindings.discord-api-CL8HMdV4.js and modify it to remove the channel: prefix from the threadId before passing it to Routes.channel().
  • Alternatively, use the existing resolveDiscordChannelId() helper from target-parsing-wpRt6Oe8.js to handle the prefix stripping.
  • Verify the fix by spawning an ACP session with thread: true from a Discord guild channel and checking for the absence of the thread_binding_invalid error.
  • If the issue persists, review the Discord API documentation to ensure that the Routes.channel() path is correctly formatted.

Example

- const channel = await createDiscordRestClient({
-     accountId: params.accountId,
-     token: params.token
- }, params.cfg).rest.get(Routes.channel(params.threadId));
+ const rawChannelId = params.threadId.replace(/^(channel:|user:)/i, "");
+ const channel = await createDiscordRestClient({
+     accountId: params.accountId,
+     token: params.token
+ }, params.cfg).rest.get(Routes.channel(rawChannelId));

Notes

The provided fix assumes that the resolveChannelIdForBinding function is the root cause of the issue. If the problem persists after applying the fix, further investigation may be necessary to identify the underlying cause.

Recommendation

Apply the suggested fix by modifying the resolveChannelIdForBinding function to strip the OpenClaw-internal channel prefix, as this is the most direct and efficient solution to resolve the thread_binding_invalid error.

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