jackwener/OpenCLI · error · CommandExecutionError

expected array of rows from server, got ${typeof rows} (cont

Error message

expected array of rows from server, got ${typeof rows} (contract drift?)

What it means

Same guard as task-list-server: after the in-page fetch resolves, task-list asserts the unwrapped result is an array of task rows. A non-array indicates the server response no longer matches the shape the snippet expects (`data.tasks` array), so the command throws a CommandExecutionError instead of mapping garbage rows.

Source

Thrown at clis/slock/task-list.js:60

    await page.goto(SLOCK_HOME_URL);
    const snippet = `
      ${authHeadersFragment({ serverScoped: true, serverIdOverride: kwargs.server })}
      ${channelResolveFragment(channel)}
      const status = ${JSON.stringify(status)};
      const qs = status ? ('?status=' + encodeURIComponent(status)) : '';
      const tres = await fetch('${SLOCK_API_BASE}/tasks/channel/' + encodeURIComponent(channelId) + qs, { credentials:'include', headers });
      if (!tres.ok) return { kind: tres.status===401?'auth':'http', status: tres.status, where: '/tasks/channel/:id' };
      const data = await tres.json();
      // Server contract: { tasks: [...] }. Reject anything else as drift.
      if (!data || !Array.isArray(data.tasks)) {
        return { kind: 'http', status: 200, where: '/tasks/channel/:id (expected {tasks:[]}, got drift)' };
      }
      return { kind: 'ok', rows: data.tasks };
    `;
    const result = await page.evaluate(`(async () => { ${snippet} })()`);
    const rows = dispatchEvaluateResult(result);
    if (!Array.isArray(rows)) {
      throw new CommandExecutionError(`expected array of rows from server, got ${typeof rows} (contract drift?)`);
    }
    return rows.map((t) => ({
      id: t.id ?? '',
      taskNumber: t.taskNumber ?? null,
      title: t.content ?? t.title ?? '',
      taskStatus: t.taskStatus ?? t.status ?? '',
      assigneeId: t.claimedById ?? t.assigneeId ?? null,
    }));
  },
});

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Dump the raw page.evaluate result to see the real payload
  2. Update the snippet's row extraction path to match the current API (e.g. data.tasks → data.items)
  3. Verify auth/session validity; an error body with HTTP 200 commonly triggers this
  4. Align CLI and server versions, or add normalization before the Array.isArray assertion

Example fix

// before
return { kind: 'ok', rows: data.tasks };
// after
const list = Array.isArray(data?.tasks) ? data.tasks : (Array.isArray(data?.items) ? data.items : []);
return { kind: 'ok', rows: list };
Defensive patterns

Strategy: type-guard

Validate before calling

const payload = dispatchEvaluateResult(result);
if (!Array.isArray(payload) && payload?.tasks == null) {
  console.error('Unexpected channel-tasks payload:', payload);
}

Type guard

const isTaskRowArray = (v) => Array.isArray(v) && v.every((r) => r != null && typeof r === 'object');

Try / catch

try {
  const rows = await listChannelTasks(channel, opts);
} catch (e) {
  if (/expected array of rows/.test(e.message)) {
    // inspect raw payload; verify auth and API schema
  }
  throw e;
}

Prevention

When it happens

Trigger: The channel-tasks endpoint returns an error envelope, object, or paginated wrapper instead of an array under data.tasks; snippet and dispatchEvaluateResult expectations diverge; auth rejected with a 200 error body.

Common situations: Slock API/schema upgrade renaming or wrapping `tasks`; proxy returning HTML; channel name resolving but response contract changed; stale cached CLI version against a newer server.

Related errors


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