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
- 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).
- 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.
- 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.
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
When an
/acpsession-targeting command (e.g./acp close) is dispatched with a non-emptyrestTokensthat does not resolve to a session (for example, a Discord thread ID populated by the slash command interaction layer),resolveAcpTargetSessionKeyreturnsUnable to resolve session target: <token>without attempting the in-thread binding lookup. The thread-bound fallback is only reachable whenrestTokensis 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
/acptext commands inside bound threads reachhandleAcpCommand(otherwise this code path is never exercised from a bound Discord thread — see related issue)./acp closevia a code path that populatesrestTokenswith 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.Expected behavior
Handler falls through to
resolveBoundAcpThreadSessionKeywhen token-based resolution fails, uses the current conversation context (channel + account + conversationId) to locate the bound session key fromthread-bindings.json, and proceeds withcloseSession+unbind. The thread-bound context in which the command was issued is unambiguous.Actual behavior
Gateway logs show two back-to-back WS
sessions.resolvefailures using the thread ID first askey, then aslabel:resolveSessionKeyByTokencatches both internally, returnsnull, andresolveAcpTargetSessionKeythen returns{ ok: false, error: 'Unable to resolve session target: 1493429188642996327' }. The user sees⚠️ Unable to resolve session target: 1493429188642996327in Discord. No close, no unbind, binding entry persists inthread-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 systemdopenclaw.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: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.
/acpsession-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 seeUnable to resolve session target: <id>despite being inside a valid bound thread with a clear binding record inthread-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 closeinside a bound thread correctly routes tohandleAcpCloseAction, resolves the session via in-thread binding lookup, callscloseSession+unbind, and thethread-bindings.jsoncount 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.