jackwener/OpenCLI · error · CommandExecutionError
Slock thread-follow succeeded without returning a thread cha
Error message
Slock thread-follow succeeded without returning a thread channel id for parent message ${id}. What it means
After following a thread, thread-follow reads `threadChannelId ?? channelId ?? id` from the API response; if none is present it throws this CommandExecutionError because the whole point of the command is to report the created/found thread channel id. Success without an identifiable channel id is treated as contract drift.
Source
Thrown at clis/slock/thread-follow.js:42
],
columns: ['parentMessageId', 'threadChannelId', 'result'],
func: async (page, kwargs) => {
let id;
try { id = assertMessageIdShape(String(kwargs.parentMessageId ?? '')); }
catch (e) { throw new ArgumentError(e.message); }
await page.goto(SLOCK_HOME_URL);
const snippet = buildFetchSnippet({
method: 'POST',
path: '/channels/threads/follow',
body: { parentMessageId: id },
serverScoped: true,
serverIdOverride: kwargs.server,
});
const result = await page.evaluate(`(async () => { ${snippet} })()`);
const data = dispatchEvaluateResult(result);
const threadChannelId = data?.threadChannelId ?? data?.channelId ?? data?.id;
if (!threadChannelId) {
throw new CommandExecutionError(`Slock thread-follow succeeded without returning a thread channel id for parent message ${id}.`);
}
return [{ parentMessageId: id, threadChannelId, result: 'followed' }];
},
});
View on GitHub (pinned to 49907e53dc)
Solutions
- Log the raw evaluate result to see what the follow endpoint actually returned.
- Handle the already-following case separately — look the channel id up via thread-list instead.
- Update the CLI/server to matching versions so the response includes threadChannelId.
- If the field is nested, unwrap it in dispatchEvaluateResult before the check.
Example fix
// before const data = dispatchEvaluateResult(result); const threadChannelId = data?.threadChannelId ?? data?.channelId ?? data?.id; // after const raw = dispatchEvaluateResult(result); const data = raw?.thread ?? raw; const threadChannelId = data?.threadChannelId ?? data?.channelId ?? data?.id;
Defensive patterns
Strategy: fallback
Type guard
function hasThreadChannelId(d) { return !!(d?.threadChannelId ?? d?.channelId ?? d?.id); } Try / catch
try {
const r = await cli.run(['thread-follow', '--parent-message-id', id]);
} catch (e) {
if (String(e.message).includes('without returning a thread channel id')) {
// likely already-following: look it up
const threads = await cli.run(['thread-list']);
const hit = threads.find(t => t.parentMessageId === id);
} else throw e;
} Prevention
- Check thread-list first — the thread may already exist
- Log raw follow responses when upgrading the server
- Unwrap nested payloads before validating
When it happens
Trigger: POST /channels/threads/follow returns 2xx but the body lacks threadChannelId/channelId/id — e.g. the endpoint returns {kind:'ok'} with no payload, an already-following response with an empty body, or a renamed field in a newer API version.
Common situations: Following a thread you already follow (API short-circuits with no payload); server version older/newer than the CLI expects; proxies stripping response bodies.
Related errors
- Slock task-status succeeded without returning task id ${expe
- Slock task-status returned task ${expectedId} without taskSt
- expected threads array, got ${typeof threads} (contract drif
- ${label} did not include a stable ${field}.
- Bilibili comments reply ${index + 1} was missing rpid
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/d6b5a1a4543a2105.
Report an issue: GitHub.