Skip to content

[Bug]: resolveAcpTargetSessionKey short-circuits on token resolution failure instead of falling back to thread-bound resolution #66299

Description

@kindomLee

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions