{"record":{"id":"41409b51be6c433b","repo":"odysseus-dev/odysseus","slug":"preset-chosen-get-name-r-has-no-launchable-cm","errorCode":null,"errorMessage":"Preset {chosen.get('name')!r} has no launchable cmd (adopted from external launch). Use POST /cookbook/serve with the actual cmd instead.","messagePattern":"Preset (.+?) has no launchable cmd \\(adopted from external launch\\)\\. Use POST /cookbook/serve with the actual cmd instead\\.","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/codex_routes.py","lineNumber":801,"sourceCode":"        state = _read_cookbook_state()\n        presets = state.get(\"presets\") or []\n        lname = name.lower().strip()\n        chosen = next(\n            (p for p in presets if isinstance(p, dict) and (p.get(\"name\") or \"\").lower() == lname),\n            None,\n        )\n        if chosen is None:\n            chosen = next(\n                (p for p in presets if isinstance(p, dict) and lname in (p.get(\"name\") or \"\").lower()),\n                None,\n            )\n        if chosen is None:\n            raise HTTPException(404, f\"No preset matching {name!r}\")\n        repo_id = chosen.get(\"model\") or chosen.get(\"modelId\") or \"\"\n        cmd = (chosen.get(\"cmd\") or \"\").strip()\n        host = chosen.get(\"host\") or chosen.get(\"remoteHost\") or \"\"\n        if not repo_id or not cmd or cmd.startswith(\"(adopted\"):\n            raise HTTPException(400, f\"Preset {chosen.get('name')!r} has no launchable cmd \"\n                                     \"(adopted from external launch). Use POST /cookbook/serve \"\n                                     \"with the actual cmd instead.\")\n        # Reuse the serve handler we already validated.\n        from routes.cookbook_helpers import ServeRequest\n        body = {\"repo_id\": repo_id, \"cmd\": cmd}\n        if host:\n            body[\"remote_host\"] = host\n        try:\n            req = ServeRequest(**body)\n        except Exception as exc:\n            raise HTTPException(400, f\"Preset payload invalid: {exc}\")\n        serve_endpoint = _find_endpoint(None, \"POST\", \"/api/model/serve\")\n        if serve_endpoint is None:\n            from fastapi import FastAPI\n            app: FastAPI = request.app\n            for route in app.routes:\n                if getattr(route, \"path\", None) == \"/api/model/serve\" and \"POST\" in getattr(route, \"methods\", set()):\n                    serve_endpoint = route.endpoint","sourceCodeStart":783,"sourceCodeEnd":819,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/codex_routes.py#L783-L819","documentation":"Raised when the matched preset exists but has no usable launch command: repo_id/cmd is empty, or the cmd starts with '(adopted', marking it as a session adopted from an external ssh launch. Adopted presets store no real command, so relaunching them is impossible and the API points the caller to POST /cookbook/serve.","triggerScenarios":"POST /cookbook/preset/{name} where the chosen preset was created by POST /cookbook/adopt (cmd begins with '(adopted'), or the stored preset has model/cmd fields missing/blank.","commonSituations":"User adopted a manually-started tmux session, then tries to 'relaunch' it later after it died; preset JSON was hand-edited and cmd removed.","solutions":["Re-launch via POST /cookbook/serve with the actual serving command, which saves a real preset.","If you know the real command, edit the preset's cmd field in cookbook_state.json so it no longer starts with '(adopted'.","Re-run the model manually and re-adopt only for tracking; use /cookbook/serve for relaunchable presets."],"exampleFix":"// before\nawait post(`/cookbook/preset/${name}`)  // preset cmd == '(adopted ...)'\n\n// after\nawait post('/cookbook/serve', {repo_id, cmd: 'python -m vllm.entrypoints... ', remote_host})","handlingStrategy":"validation","validationCode":"p = find_preset(name)\nlaunchable = p and p.get('cmd') and not p['cmd'].startswith('(adopted') and p.get('model')\nif not launchable: use POST /cookbook/serve instead","typeGuard":"const isLaunchable = (p?: {cmd?:string; model?:string}) =>\n  !!p?.model && !!p?.cmd?.trim() && !p.cmd.trim().startsWith('(adopted')","tryCatchPattern":null,"preventionTips":["Treat adopted presets as track-only; never offer a 'relaunch' button for them.","Save launchable presets via /cookbook/serve so a real cmd is stored."],"tags":["preset","adopted-session","http-400"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}