openclaw - ✅(Solved) Fix [Bug]: resolveAcpTargetSessionKey short-circuits on token resolution failure instead of falling back to thread-bound resolution [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

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#66299Fetched 2026-04-15 06:26:44
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Author
Participants
Timeline (top)
commented ×1cross-referenced ×1mentioned ×1referenced ×1

When an /acp session-targeting command (e.g. /acp close) is dispatched with a non-empty restTokens that does not resolve to a session (for example, a Discord thread ID populated by the slash command interaction layer), resolveAcpTargetSessionKey returns Unable to resolve session target: <token> without attempting the in-thread binding lookup. The thread-bound fallback is only reachable when restTokens is strictly empty, which misses the real-world case of "user ran /acp close while sitting inside a bound thread and the dispatcher passed the thread ID as an arg".

Error Message

async function resolveAcpTargetSessionKey(params) { const token = normalizeOptionalString(params.token) ?? ""; if (token) { const resolved = await resolveSessionKeyByToken(token); if (!resolved) return { ok: false, error: Unable to resolve session target: ${token} // ← early return, thread-bound never tried }; return { ok: true, sessionKey: resolved }; } const threadBound = resolveBoundAcpThreadSessionKey(params.commandParams); if (threadBound) return { ok: true, sessionKey: threadBound }; ... }

Root Cause

When an /acp session-targeting command (e.g. /acp close) is dispatched with a non-empty restTokens that does not resolve to a session (for example, a Discord thread ID populated by the slash command interaction layer), resolveAcpTargetSessionKey returns Unable to resolve session target: <token> without attempting the in-thread binding lookup. The thread-bound fallback is only reachable when restTokens is strictly empty, which misses the real-world case of "user ran /acp close while sitting inside a bound thread and the dispatcher passed the thread ID as an arg".

Fix Action

Fix / Workaround

When an /acp session-targeting command (e.g. /acp close) is dispatched with a non-empty restTokens that does not resolve to a session (for example, a Discord thread ID populated by the slash command interaction layer), resolveAcpTargetSessionKey returns Unable to resolve session target: <token> without attempting the in-thread binding lookup. The thread-bound fallback is only reachable when restTokens is strictly empty, which misses the real-world case of "user ran /acp close while sitting inside a bound thread and the dispatcher passed the thread ID as an arg".

The if (token) branch short-circuits. When the dispatch layer supplies a token that cannot be mapped to a session (as happens when Discord passes the thread ID as a slash command option), the function errors out even though the command was issued from inside a bound thread whose binding unambiguously identifies the intended target.

After this patch plus the sibling dispatch bypass fix, /acp close inside a bound thread correctly routes to handleAcpCloseAction, resolves the session via in-thread binding lookup, calls closeSession + unbind, and the thread-bindings.json count drops from 2 → 1 as expected (verified on 2026.4.11, aarch64, Ubuntu 22.04).

PR fix notes

PR #66338: fix(acp): fall through to thread-binding when token doesn't resolve

Description (problem / solution / changelog)

Summary

`resolveAcpTargetSessionKey` returned an error immediately when the token was non-empty but didn't resolve as a session key — blocking the thread-binding fallback from ever being reached. This means `/acp close` inside a bound Discord thread fails when the slash command layer auto-fills the thread ID as a positional arg.

Fixes #66299

Root cause

```ts // targets.ts:62-69 (before fix) if (token) { const resolved = await resolveSessionKeyByToken(token); if (!resolved) { return { ok: false, error: `Unable to resolve session target: ${token}` }; // ← blocks fallback } return { ok: true, sessionKey: resolved }; } // Line 73: resolveBoundAcpThreadSessionKey — UNREACHABLE when token is non-empty ```

Fix

When `resolveSessionKeyByToken` returns null, fall through to the thread-binding lookup:

```ts if (token) { const resolved = await resolveSessionKeyByToken(token); if (resolved) { return { ok: true, sessionKey: resolved }; } // Fall through to thread-binding lookup } const threadBound = resolveBoundAcpThreadSessionKey(params.commandParams); ```

Scope

  • 1 file: `src/auto-reply/reply/commands-acp/targets.ts` (+5/-5)
  • oxlint clean
  • Zero competing PRs

Credit to @kindomLee for the exact RCA in #66299.

Changed files

  • src/auto-reply/reply/commands-acp/targets.ts (modified, +6/-6)

Code Example

[ws] ⇄ res ✗ sessions.resolve 77ms errorCode=INVALID_REQUEST errorMessage=No session found: 1493429188642996327 conn=992c9a41…a5de
[ws] ⇄ res ✗ sessions.resolve 47ms errorCode=INVALID_REQUEST errorMessage=No session found with label: 1493429188642996327 conn=99d76f45…f370

---

async function resolveAcpTargetSessionKey(params) {
    const token = normalizeOptionalString(params.token) ?? "";
    if (token) {
        const resolved = await resolveSessionKeyByToken(token);
        if (!resolved) return {
            ok: false,
            error: `Unable to resolve session target: ${token}`   // ← early return, thread-bound never tried
        };
        return {
            ok: true,
            sessionKey: resolved
        };
    }
    const threadBound = resolveBoundAcpThreadSessionKey(params.commandParams);
    if (threadBound) return { ok: true, sessionKey: threadBound };
    ...
}

---

async function resolveAcpTargetSessionKey(params) {
     const token = normalizeOptionalString(params.token) ?? "";
     if (token) {
         const resolved = await resolveSessionKeyByToken(token);
-        if (!resolved) return {
-            ok: false,
-            error: `Unable to resolve session target: ${token}`
-        };
-        return {
-            ok: true,
-            sessionKey: resolved
-        };
+        if (resolved) return {
+            ok: true,
+            sessionKey: resolved
+        };
+        // fall through to thread-bound resolution when the explicit token does not resolve
     }
     const threadBound = resolveBoundAcpThreadSessionKey(params.commandParams);
     if (threadBound) return {
         ok: true,
         sessionKey: threadBound
     };
+    if (token) return {
+        ok: false,
+        error: `Unable to resolve session target: ${token}`
+    };
     const fallback = resolveRequesterSessionKey(params.commandParams, { preferCommandTarget: true });
     ...
 }
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

When an /acp session-targeting command (e.g. /acp close) is dispatched with a non-empty restTokens that does not resolve to a session (for example, a Discord thread ID populated by the slash command interaction layer), resolveAcpTargetSessionKey returns Unable to resolve session target: <token> without attempting the in-thread binding lookup. The thread-bound fallback is only reachable when restTokens is strictly empty, which misses the real-world case of "user ran /acp close while sitting inside a bound thread and the dispatcher passed the thread ID as an arg".

Steps to reproduce

  1. Apply the sibling fix so /acp text commands inside bound threads reach handleAcpCommand (otherwise this code path is never exercised from a bound Discord thread — see related issue).
  2. Inside a bound Discord thread, invoke /acp close via a code path that populates restTokens with something that is not a session key / session id / session label — for example, Discord slash command auto-filling the current thread ID as a positional option.
  3. Observe gateway WS logs and the resulting bot reply.

Expected behavior

Handler falls through to resolveBoundAcpThreadSessionKey when token-based resolution fails, uses the current conversation context (channel + account + conversationId) to locate the bound session key from thread-bindings.json, and proceeds with closeSession + unbind. The thread-bound context in which the command was issued is unambiguous.

Actual behavior

Gateway logs show two back-to-back WS sessions.resolve failures using the thread ID first as key, then as label:

[ws] ⇄ res ✗ sessions.resolve 77ms errorCode=INVALID_REQUEST errorMessage=No session found: 1493429188642996327 conn=992c9a41…a5de
[ws] ⇄ res ✗ sessions.resolve 47ms errorCode=INVALID_REQUEST errorMessage=No session found with label: 1493429188642996327 conn=99d76f45…f370

resolveSessionKeyByToken catches both internally, returns null, and resolveAcpTargetSessionKey then returns { ok: false, error: 'Unable to resolve session target: 1493429188642996327' }. The user sees ⚠️ Unable to resolve session target: 1493429188642996327 in Discord. No close, no unbind, binding entry persists in thread-bindings.json.

OpenClaw version

2026.4.11

Operating system

Ubuntu 22.04.5 LTS (aarch64, Oracle Cloud Always Free VM)

Install method

npm global (/usr/lib/node_modules/[email protected], launched by systemd openclaw.service)

Model

Not model-specific. This is a resolver-layer bug that fires before the session handler interacts with any model.

Provider / routing chain

Discord guild text channel with thread bindings enabled → spawned thread bound via spawnAcpSessions: true → embedded acpx runtime backend.

Logs, screenshots, and evidence

Relevant code in dist/targets-CeL3E4gZ.js::resolveAcpTargetSessionKey:

async function resolveAcpTargetSessionKey(params) {
    const token = normalizeOptionalString(params.token) ?? "";
    if (token) {
        const resolved = await resolveSessionKeyByToken(token);
        if (!resolved) return {
            ok: false,
            error: `Unable to resolve session target: ${token}`   // ← early return, thread-bound never tried
        };
        return {
            ok: true,
            sessionKey: resolved
        };
    }
    const threadBound = resolveBoundAcpThreadSessionKey(params.commandParams);
    if (threadBound) return { ok: true, sessionKey: threadBound };
    ...
}

The if (token) branch short-circuits. When the dispatch layer supplies a token that cannot be mapped to a session (as happens when Discord passes the thread ID as a slash command option), the function errors out even though the command was issued from inside a bound thread whose binding unambiguously identifies the intended target.

Impact and severity

Medium. /acp session-targeting commands (close / cancel / steer / status / set-mode / …) cannot succeed from a client that passes a thread ID (or any other non-session identifier) as a positional token. Users see Unable to resolve session target: <id> despite being inside a valid bound thread with a clear binding record in thread-bindings.json. The common in-thread resolution path is blocked by an explicit token that shouldn't take precedence when it doesn't match any known session.

Additional information

Confirmed the fix locally by letting token-resolution failure fall through to thread-bound resolution, while preserving the original error for the "explicit token + no thread context" case:

 async function resolveAcpTargetSessionKey(params) {
     const token = normalizeOptionalString(params.token) ?? "";
     if (token) {
         const resolved = await resolveSessionKeyByToken(token);
-        if (!resolved) return {
-            ok: false,
-            error: `Unable to resolve session target: ${token}`
-        };
-        return {
-            ok: true,
-            sessionKey: resolved
-        };
+        if (resolved) return {
+            ok: true,
+            sessionKey: resolved
+        };
+        // fall through to thread-bound resolution when the explicit token does not resolve
     }
     const threadBound = resolveBoundAcpThreadSessionKey(params.commandParams);
     if (threadBound) return {
         ok: true,
         sessionKey: threadBound
     };
+    if (token) return {
+        ok: false,
+        error: `Unable to resolve session target: ${token}`
+    };
     const fallback = resolveRequesterSessionKey(params.commandParams, { preferCommandTarget: true });
     ...
 }

After this patch plus the sibling dispatch bypass fix, /acp close inside a bound thread correctly routes to handleAcpCloseAction, resolves the session via in-thread binding lookup, calls closeSession + unbind, and the thread-bindings.json count drops from 2 → 1 as expected (verified on 2026.4.11, aarch64, Ubuntu 22.04).

I am happy to submit a PR for this and the sibling issue as two small separate commits if the fix direction is acceptable.

extent analysis

TL;DR

The issue can be fixed by modifying the resolveAcpTargetSessionKey function to fall through to thread-bound resolution when the provided token does not resolve to a session.

Guidance

  • The current implementation of resolveAcpTargetSessionKey returns an error when the token does not resolve to a session, without attempting thread-bound resolution.
  • To fix this, the function should be modified to attempt thread-bound resolution when the token resolution fails.
  • The provided diff shows the necessary changes to achieve this.
  • The fix should be applied to the resolveAcpTargetSessionKey function in the dist/targets-CeL3E4gZ.js file.
  • After applying the fix, the /acp close command should correctly resolve the session via in-thread binding lookup and call closeSession + unbind.

Example

The provided diff shows the necessary changes to the resolveAcpTargetSessionKey function:

 async function resolveAcpTargetSessionKey(params) {
     const token = normalizeOptionalString(params.token) ?? "";
     if (token) {
         const resolved = await resolveSessionKeyByToken(token);
+        if (resolved) return {
+            ok: true,
+            sessionKey: resolved
+        };
+        // fall through to thread-bound resolution when the explicit token does not resolve
     }
     const threadBound = resolveBoundAcpThreadSessionKey(params.commandParams);
     if (threadBound) return {
         ok: true,
         sessionKey: threadBound
     };
+    if (token) return {
+        ok: false,
+        error: `Unable to resolve session target: ${token}`
+    };
     const fallback = resolveRequesterSessionKey(params.commandParams, { preferCommandTarget: true });
     ...
 }

Notes

The fix assumes that the resolveBoundAcpThreadSessionKey function is correctly implemented and can resolve the session key from the thread binding.

Recommendation

Apply the workaround by modifying the resolveAcpTargetSessionKey function as shown in the provided diff. This fix allows the function to fall through to thread-bound resolution when the token does not resolve to a session, enabling the /acp close command to work correctly inside a bound thread.

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…

FAQ

Expected behavior

Handler falls through to resolveBoundAcpThreadSessionKey when token-based resolution fails, uses the current conversation context (channel + account + conversationId) to locate the bound session key from thread-bindings.json, and proceeds with closeSession + unbind. The thread-bound context in which the command was issued is unambiguous.

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 - ✅(Solved) Fix [Bug]: resolveAcpTargetSessionKey short-circuits on token resolution failure instead of falling back to thread-bound resolution [1 pull requests, 1 comments, 2 participants]