Skip to content

feat(db): add study_session and study_turn persistence model - #312

Open
longobucco wants to merge 1 commit into
masterfrom
feat/study-session-data-model
Open

longobucco wants to merge 1 commit into
masterfrom
feat/study-session-data-model

Conversation

@longobucco

Copy link
Copy Markdown
Contributor

What changed and why

First of five PRs implementing #136 (server-side persistence of study sessions,
replacing browser localStorage). This PR adds only the data-model layer:

  • study_session / study_turn SQLAlchemy models (app/db/models.py)
  • Alembic migration 0009_study_sessions.py
  • Function-based repository (app/repositories/study_session_repo.py),
    following the existing progress_repo.py pattern — no class-based
    unit-of-work exists in this codebase, so this matches what's actually used.
  • Service layer (app/services/study_session_service.py) converting ORM
    rows to Pydantic DTOs (app/schemas/study_session_schemas.py), scoped by
    user_id so cross-user access returns None rather than leaking existence.

No API endpoints and no dispatch-path hook yet — those are PR2 and PR3. This
PR only lays the DB layer so each later PR stays small and reviewable.

Design decisions (made and documented in
docs/superpowers/specs/2026-11-06-study-session-persistence-design.md):

  • Sessions are discrete, explicitly created (not one continuous history
    per course) — needed for "session list + resume + delete" from the issue.
  • Citations are stored as resolvable anchors (doc_id, doc_name,
    section, page/slide, chunk_id, chunk_type) plus a short snippet —
    not the full evidence-chunk text, to bound storage growth given the
    no-automatic-expiry decision.
  • study_turn cascades on study_session deletion (ON DELETE CASCADE).
  • This is a study-specific schema, not the generic session_event sketched
    in docs/agent-memory-plan.md Phase 4 — PR5 will add a note there
    reconciling the two instead of leaving them as parallel designs.

How it was verified

cd services/ai && uv run --no-sync pytest   # 1094 passed
cd services/ai && uv run --no-sync mypy app # Success: no issues found in 90 source files

New unit tests in tests/unit/test_study_session_service.py cover: session
creation, per-user/per-course scoping, turn append + citation round-trip,
cross-user access returning None, and cascade delete of turns.

tests/integration/test_migrations.py::test_alembic_head_matches_models
(schema parity between alembic upgrade head and Base.metadata.create_all)
passes — the migration was adjusted to not add indexes the models don't
declare.

Things reviewers should watch for

  • No API surface yet — this PR is deliberately inert from the outside
    (no new endpoints, no behavior change for existing users).
  • study_turn.citations_json is a JSON text column rather than a normalized
    table, matching how course_document and other models already store
    variable-shaped JSON in this codebase.
  • Full implementation plan for the remaining 4 PRs (sessions API, dispatch
    hook + feedback linkage, frontend, retention/docs) is in the linked design
    doc.

Refs #136

Add the data-model layer for server-side study session persistence
(study_session, study_turn tables + Alembic migration + repository +
service) following the existing function-based repository pattern.
Citations are stored as resolvable anchors (doc/page/slide/chunk_id),
not the lossy HTTP-facing DTO. No API or dispatch-path integration yet
— that lands in follow-up PRs per the design doc.

Refs #136

@lucaosti lucaosti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Luca, splitting #136 into small PRs is the right call, and the design decisions (discrete sessions, anchor-only citations, cascading turns) make sense to me. Since this layer is what the next four PRs build on, a few things are worth settling now:

  • append_turn doesn't take a user_id, so it can add a turn to any user's session. With a made-up session_id it creates an orphan row on SQLite and would be an IntegrityError on Postgres. Could it take user_id, check ownership, and return None like get_session does?
  • Timestamps: the repo writes naive local time (datetime.now()), while the model's onupdate=func.now() writes UTC in another format. Renaming a session currently pushes it down the list. Using datetime.now(timezone.utc).isoformat() everywhere, as #311 does, and dropping the func.now() default and onupdate would fix it.
  • Indexes: could you declare (user_id, course_id, last_active_at) on study_session and (session_id, created_at) on study_turn, in both the model and the migration? The parity test will still pass.
  • SQLite doesn't enforce ON DELETE CASCADE here because foreign keys aren't turned on. The ORM cascade covers it today, but a comment or a test that says so would help. Course deletion doesn't remove sessions either; fine to decide in PR2/PR3.
  • As we asked on #309, please move the design doc out of docs/superpowers/, for example to docs/design/, and keep only the decisions rather than the step-by-step plan.
  • Migration numbering: #311 also adds 0009 on top of 0008. Whichever lands second becomes 0010 with down_revision = "0009".

Smaller: list_sessions runs a separate count query per session, and a stored citation without doc_id makes get_session raise, which will matter for the localStorage import in PR4.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants