{"record":{"id":"d14715170c4deb77","repo":"jamiepine/voicebox","slug":"offset-must-be-0","errorCode":null,"errorMessage":"`offset` must be >= 0.","messagePattern":"`offset` must be >= 0\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/mcp_server/tools.py","lineNumber":186,"sourceCode":"        try:\n            return await _transcribe_file(tmp_path, language, model)\n        finally:\n            tmp_path.unlink(missing_ok=True)\n\n    @mcp.tool(\n        name=\"voicebox.list_captures\",\n        description=(\n            \"List recent voice captures (dictations, recordings, uploads) \"\n            \"with their transcripts. Most-recent first.\"\n        ),\n    )\n    async def voicebox_list_captures(\n        limit: int = 20, offset: int = 0\n    ) -> dict[str, Any]:\n        if not (1 <= limit <= 200):\n            raise ValueError(\"`limit` must be between 1 and 200.\")\n        if offset < 0:\n            raise ValueError(\"`offset` must be >= 0.\")\n        db = next(get_db())\n        try:\n            items, total = captures_service.list_captures(\n                db, limit=limit, offset=offset\n            )\n            return {\n                \"captures\": [\n                    item.model_dump(mode=\"json\") for item in items\n                ],\n                \"total\": total,\n            }\n        finally:\n            db.close()\n\n    @mcp.tool(\n        name=\"voicebox.list_profiles\",\n        description=(\n            \"List available voice profiles (both cloned voices and presets). \"","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/mcp_server/tools.py#L168-L204","documentation":"Raised by voicebox.list_captures when offset < 0. Default offset is 0. Negative offsets are rejected because they have no meaningful SQL/offset semantics.","triggerScenarios":"Calling voicebox.list_captures(offset=-1) or a decremented page index that underflows below zero.","commonSituations":"A 'Previous page' handler computing offset = current - limit without clamping when already on the first page; arithmetic underflow on page 0.","solutions":["Pass a non-negative integer offset (0 is the first page).","Clamp on the client: offset = max(0, current_offset - limit).","Treat 'previous on first page' as a no-op rather than sending offset=-limit."],"exampleFix":"// before\nvoicebox.list_captures(offset=-20)\n// after\nvoicebox.list_captures(offset=0)","handlingStrategy":"validation","validationCode":"if not isinstance(offset, int) or offset < 0:\n    offset = max(0, int(offset) if isinstance(offset, int) else 0)\nawait voicebox.list_captures(limit=limit, offset=offset)","typeGuard":"def is_valid_list_offset(value: int) -> bool:\n    return isinstance(value, int) and value >= 0","tryCatchPattern":"try:\n    await voicebox.list_captures(limit=limit, offset=offset)\nexcept ValueError as exc:\n    if \"offset\" in str(exc):\n        await voicebox.list_captures(limit=limit, offset=max(0, offset))\n    else:\n        raise","preventionTips":["Clamp 'previous page' offsets to >= 0 client-side.","Treat first-page 'previous' as a no-op.","Compute offset as page_index * limit, never negative."],"tags":["mcp","pagination","validation","captures"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}