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

  1. Log the raw evaluate result to see what the follow endpoint actually returned.
  2. Handle the already-following case separately — look the channel id up via thread-list instead.
  3. Update the CLI/server to matching versions so the response includes threadChannelId.
  4. 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

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


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/d6b5a1a4543a2105. Report an issue: GitHub.