{"record":{"id":"cb6cb742e1f4abbe","repo":"odysseus-dev/odysseus","slug":"assistant-session-could-not-be-resolved","errorCode":null,"errorMessage":"Assistant session could not be resolved","messagePattern":"Assistant session could not be resolved","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"routes/assistant_routes.py","lineNumber":128,"sourceCode":"        # user — safe to call again, it's idempotent.\n        await task_scheduler.ensure_assistant_defaults(owner)\n        db = SessionLocal()\n        try:\n            crew = db.query(CrewMember).filter(\n                CrewMember.owner == owner,\n                CrewMember.is_default_assistant == True,  # noqa: E712\n            ).first()\n            return crew\n        finally:\n            db.close()\n\n    @router.get(\"/session\")\n    async def get_assistant_session(request: Request):\n        \"\"\"Resolve (or lazily create) the pinned Assistant session for this user.\"\"\"\n        owner = _owner(request)\n        crew = await _get_or_create(owner)\n        if not crew or not crew.session_id:\n            raise HTTPException(status_code=500, detail=\"Assistant session could not be resolved\")\n        return {\n            \"session_id\": crew.session_id,\n            \"crew_member_id\": crew.id,\n            \"name\": crew.name,\n        }\n\n    @router.get(\"/settings\")\n    async def get_assistant_settings(request: Request):\n        \"\"\"Return CrewMember fields + the three check-in task rows + task IDs for logs.\"\"\"\n        owner = _owner(request)\n        crew = await _get_or_create(owner)\n        if not crew:\n            raise HTTPException(status_code=500, detail=\"Assistant not available\")\n        db = SessionLocal()\n        try:\n            tasks = db.query(ScheduledTask).filter(\n                ScheduledTask.owner == owner,\n                ScheduledTask.crew_member_id == crew.id,","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/assistant_routes.py#L110-L146","documentation":"Raised by GET /api/assistant/session when the per-user assistant CrewMember cannot be resolved, or is resolved but its pinned session_id is NULL/empty. The route first lazily seeds the assistant via task_scheduler.ensure_assistant_defaults(owner); the 500 means seeding produced no CrewMember row, or produced one whose Session was never created or linked. It is a server-state defect, not a client payload problem.","triggerScenarios":"Calling GET /api/assistant/session on a fresh user whose startup seeding hook never ran and whose lazy ensure_assistant_defaults failed (e.g. DB write error, exception swallowed); a CrewMember row with is_default_assistant=1 exists but session_id is NULL because the Session insert was rolled back or a migration wiped it; DB recreated from a partial backup that kept crew rows but lost session rows.","commonSituations":"Fresh installs where the app was killed before the startup hook finished; SQLite file replaced/restored while the app runs; schema drift after upgrade where session_id column was added without backfill; concurrent first requests from the same new user racing the seed.","solutions":["Re-hit GET /api/assistant/session (or restart the app so the startup seeding hook runs for each user) — seeding is idempotent and will link a session on the next pass.","Inspect the CrewMember table: SELECT id, session_id, is_default_assistant FROM crew_members WHERE owner='<user>'; if session_id is NULL, delete the broken row so the next request reseeds it cleanly.","Check application logs for exceptions inside task_scheduler.ensure_assistant_defaults (DB locked, constraint violation, missing Session table).","If it persists after a clean reseed, verify the database schema is current (run the app's migration/init path) — a missing sessions table makes the seed silently skip session linking."],"exampleFix":"// before (client treats it as fatal):\nconst r = await fetch('/api/assistant/session');\nif (!r.ok) throw new Error('assistant broken');\n\n// after: retry once — lazy seeding is idempotent and usually succeeds on the 2nd pass:\nlet r = await fetch('/api/assistant/session');\nif (r.status === 500) {\n  await new Promise(res => setTimeout(res, 1000));\n  r = await fetch('/api/assistant/session');\n}\nif (!r.ok) throw new Error('Assistant session could not be resolved');","handlingStrategy":"retry","validationCode":"async function getAssistantSession(retries = 1) {\n  for (let i = 0; ; i++) {\n    const r = await fetch('/api/assistant/session', {credentials: 'include'});\n    if (r.ok) return r.json();\n    if (r.status === 500 && i < retries) {\n      await new Promise(res => setTimeout(res, 1000)); // lazy seed is idempotent\n      continue;\n    }\n    throw new Error(`Assistant session could not be resolved (HTTP ${r.status})`);\n  }\n}","typeGuard":"function isAssistantSession(v) {\n  return v != null && typeof v === 'object'\n    && typeof v.session_id === 'string' && v.session_id.length > 0\n    && typeof v.crew_member_id === 'string'\n    && typeof v.name === 'string';\n}","tryCatchPattern":"try { return await getAssistantSession(); } catch (e) { if (/could not be resolved/.test(e.message)) { await reloadAssistantState(); } throw e; }","preventionTips":["Ensure the app's startup seeding hook completes before serving traffic (wait for readiness on deploy).","Never manually NULL crew_members.session_id; if it is NULL, delete the row and let lazy seeding recreate it.","Back up the database as a whole — restoring crew_members without sessions produces exactly this 500.","Monitor logs for ensure_assistant_defaults exceptions on first request per user."],"tags":["fastapi","database","sqlalchemy","assistant","session","lazy-seeding"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}