jackwener/OpenCLI · error · CommandExecutionError
${context} target is missing a stable channel/thread id.
Error message
${context} target is missing a stable channel/thread id. What it means
assertDiscordMessageRowsBelongToTarget verifies that fetched message rows came from the intended channel. It first requires the target object itself to carry a stable identifier (thread_id or channel_id); if both are empty, it throws CommandExecutionError because verification is impossible.
Source
Thrown at clis/discord-app/utils.js:596
}
export async function readDiscordMessages(page, count) {
return requireArrayEvaluateResult(await page.evaluate(buildReadMessagesScript(count)), 'Discord message list');
}
export async function listDiscordThreads(page, limit) {
return requireRowsWithFields(
requireArrayEvaluateResult(await page.evaluate(buildListThreadsScript(limit)), 'Discord thread list'),
['Thread', 'guild_id', 'channel_id', 'thread_id', 'url'],
'Discord thread list',
);
}
export function assertDiscordMessageRowsBelongToTarget(rows, target, context = 'Discord read') {
if (!target) return rows;
const expectedChannelId = String(target.thread_id || target.channel_id || '');
if (!expectedChannelId) {
throw new CommandExecutionError(`${context} target is missing a stable channel/thread id.`);
}
for (const row of rows) {
if (!row || typeof row !== 'object' || Array.isArray(row)) {
throw new CommandExecutionError(`${context} returned malformed message rows.`);
}
const actualChannelId = row.channel_id == null || row.channel_id === '' ? '' : String(row.channel_id);
if (!actualChannelId) {
throw new CommandExecutionError(
`${context} could not verify returned message target.`,
`Expected channel/thread id ${expectedChannelId}, but the message row did not include channel_id.`,
);
}
if (actualChannelId && actualChannelId !== expectedChannelId) {
throw new CommandExecutionError(
`${context} returned messages from the wrong Discord target.`,
`Expected channel/thread id ${expectedChannelId}, saw ${actualChannelId}.`,
);
}View on GitHub (pinned to 49907e53dc)
Solutions
- Resolve the target first with resolveDiscordChannelTarget (or thread target) so channel_id/thread_id is populated.
- Pass options.required: true to the resolver so it throws early instead of returning null.
- Construct the target from the numeric ids shown by "opencli discord-app channels -f json".
- Skip the assertion only if you truly have no target (the function returns rows unchanged when target is falsy).
Example fix
// before
const target = await resolveDiscordChannelTarget(page, kwargs); // may return null/partial
assertDiscordMessageRowsBelongToTarget(rows, target); // throws: no stable id
// after
const target = await resolveDiscordChannelTarget(page, kwargs, { required: true });
assertDiscordMessageRowsBelongToTarget(rows, target); Defensive patterns
Strategy: type-guard
Validate before calling
if (!target || !(String(target.thread_id || '') || String(target.channel_id || ''))) throw new Error('Target lacks thread_id/channel_id; resolve it first'); Type guard
function hasStableTargetId(t) { return Boolean(t) && Boolean(String(t.thread_id || t.channel_id || '').trim()); } Try / catch
try { assertDiscordMessageRowsBelongToTarget(rows, target, ctx); } catch (e) { if (String(e.message).includes('missing a stable channel/thread id')) { // re-resolve target with required:true before proceeding
} else throw e; } Prevention
- Always produce targets via resolveDiscordChannelTarget/resolveDiscordThreadTarget with { required: true }.
- Never hand-build target objects from raw user args.
- Persist the full resolved target (with ids), not just the label.
When it happens
Trigger: Calling assertDiscordMessageRowsBelongToTarget with a target object whose thread_id and channel_id are both missing/empty — typically a target produced without full resolution (e.g. resolveDiscordChannelTarget returned null because it was not required).
Common situations: Passing a hand-built { } or partially populated target; using a null-returning optional resolution result downstream; code that builds target from user args without running the resolver.
Related errors
- --url must be a Discord channel URL like https://discord.com
- Pass --url or --guild/--channel to choose a Discord channel.
- Pass --channel with --guild, or use --url.
- Could not resolve Discord channel "${channelArg}". Use "open
- ${context} returned malformed message rows.
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/73a65bab54524786.
Report an issue: GitHub.