feat(sessions): unify Activity and ChatThread into a Sessions app#1374
Merged
Conversation
- sandbox_envs migration test now restores migrations to head: reversing sandbox_envs.0006 cascade-reverses agent_sessions.0001 (drops its tables), which left later Session/Run tests failing with 'no such table'. - test_by_owner_admin_sees_all uses a leak-immune subset assertion: async ORM writes commit to the shared in-memory SQLite DB and escape rollback. - Drop the earlier threaded cleanup fixture (wrong approach: a worker thread hit a separate empty in-memory DB and corrupted connection state).
…_envs to Session/Run
…ff Run - Replace MemoryObservation.activity FK (→ activity.Activity) with MemoryObservation.run FK (→ agent_sessions.Run); migration 0002 adds the new column, copies activity_id values whose PKs exist in Run, then drops the old column. - Switch memory.signals receiver from activity_finished to run_finished; add CHAT-trigger guard so chat turns are never mined for observations. - Update extract_observations_task to accept run_id, look up Run, and use run.session_id as the LangGraph thread_id. - notifications.signals: on_run_finished receiver (already scaffolded) now fully wired with CHAT- and webhook-trigger guards, schedule resolution via run.session.scheduled_job, and a top-level try/except so errors never crash the run lifecycle. - Update memory/detail.html template to reference obs.run instead of obs.activity.
Complete the FK swap: memory models, signals, tasks, views and templates now reference sessions.Run instead of activity.Activity. Tests updated to use Run/Session fixtures. Notifications on_run_finished receiver completed with chat-skip guard and run-based helpers.
…un form move - Replace activity/chat URL mounts with permanent redirect patterns (301) via sessions.urls_legacy; LegacyActivityDetailRedirectView resolves Run by pk → session_detail#run-<id> - Move AgentRunCreateView, AgentRunCreateForm, RepoListField, AgentRunFieldsMixin from activity app into sessions app; run-form templates copied to sessions/templates/sessions/ - Switch runs include from activity.urls_runs to sessions.urls_runs (app_name="runs", route name "agent_run_new" preserved) - Sidebar: two items (Activity + Chat) → one "Sessions" item with running-jobs badge; "New chat" CTA → session_new - context_processors: replace "activity"/"chat" SECTION_URL_NAMES with "sessions"; running_jobs_count now queries Run instead of Activity - accounts/views.py dashboard tiles: all segment URLs → session_list - schedules/forms.py: import AgentRunFieldsMixin/RepoListField from sessions.forms (not activity.forms) - Fix all live stragglers: schedules/views.py, schedules/_schedule_row.html, memory/detail.html, notifications/signals.py, dashboard.html - Tests: new test_redirects.py (8 tests); update accounts/sidebar, breadcrumbs, context_processors and schedules tests to use session routes
…t become stubs - Empty activity/models.py and chat/models.py to module docstrings; both apps stay in INSTALLED_APPS with migration history intact (other apps' historical migrations reference them) - Delete dead files from activity app: views, urls, urls_runs, filters, forms, services, signals, management commands, static assets, and most templates; keep _agent_run_fields.html and _copy_markdown_button.html (still included by schedules and sessions templates respectively), and the activity_tags templatetag library (still loaded by notification email templates and sessions/_prompt_disclosure.html) with Activity-model-dependent tags removed - Delete dead files from chat app: views, urls, managers, chat_list.html, chat_detail.html; keep api/, turns.py, repo_state.py, static/, templatetags/, and _composer.html (included by sessions/session_detail.html) - Generate DeleteModel migrations for Activity (activity/0016) and ChatThread (chat/0004) with explicit dependencies on agent_sessions.0002 and memory.0002 so tables are never dropped before data is copied - Remove on_activity_finished receiver and all Activity-based helpers from notifications/signals.py; keep on_run_finished and all _run helpers - Drop legacy chat_thread/activity branches from generate_title_task in automation/titling/tasks.py; narrow Literal to ["session", "run"] - Delete all tests/unit_tests/activity/ and dead chat test files (test_models, test_views, test_composer_agent_picker, test_composer_env_select, test_sandbox_env_link); delete sessions/test_data_migration.py which depended on live Activity/ChatThread model access - Update notifications/test_signals.py to remove Activity-based test classes; keep TestUserBindingSeeder and TestOnRunFinished - Update automation/titling/test_tasks.py to use Session/Run entities instead of Activity; preserve all behavioural coverage - Fix global conftest mock_generate_title_task to remove the now-dead patches on activity.services and chat.models
…hedule forms load it
Follow-up hardening for the Session/Run unification based on review: - SessionLock.try_claim logs stale takeovers and splits the free-slot fast path from the takeover path (one query in the common case); heartbeat reports lost ownership and run_job_task awaits the cancelled heartbeat before releasing the slot. - Backfill wraps bulk_create with a contextual error plus summary logs and corrects the merge-precedence comment (user is first-wins). - Add DB CheckConstraints pinning Session.origin, Run.trigger_type and Run.status to their enums (new migration 0003). - Surface the orphan-task failure mode in error_message via a shared LINK_FAILED_PREFIX constant. - SSE stream guards malformed frames, auto-reconnects, and re-subscribes on timeout; the turns poller caps consecutive failures. - hydration returns a HydratedThread NamedTuple; rename _mark_failed_and_release -> _mark_failed_and_advance. Tests: backfill migration (historical registry), run-form view, run sync/usage parsing, sandbox-env propagation, templatetags, forms, enum-drift and effective_notify_on.
- sync_stuck_runs now fails chat runs (which have no DBTaskResult, so the task-result sync can never reconcile one whose streamer finally never ran) once their session heartbeat goes stale — the same staleness signal SessionLock uses to declare a holder dead — so a genuinely long-running chat turn is never reaped. A direct .update() (no run_finished emit) mirrors finalize_chat_run: chat runs stay out of the notification / memory / dispatch receivers. - notifications batch payload renders the human-readable status via RunStatus(agg_status).label instead of the raw enum value. - webhook callbacks log "Failed to create run ..." (was "activity").
_run_timeline.html included _status_pill.html without the pill's required variant/label args, so every run's timeline pill rendered with no color and no status text. Pass variant=run.status|status_variant and label=run.status, matching the session list page.
Fill 705 previously untranslated or fuzzy msgstr entries across 12 Django apps' Portuguese catalogs (European Portuguese), keeping terminology consistent with the existing glossary and preserving all format placeholders, HTML markup, and product/code names.
Follow-up refinements to the Activity/ChatThread → Sessions unification (callbacks, signals, views, managers, MCP, email templates) together with fixes from a code review of the change: - streaming: detect the agent's RUN_ERROR event (ag_ui surfaces it without raising) so an errored chat turn is finalized FAILED with an error_message instead of a silent SUCCESSFUL that also fed memory extraction - notifications: serialize Notification.context via DjangoJSONEncoder so lazy translation proxies no longer crash the save — the batch-rollup payload was silently failing, so batch notifications were never delivered - models: add DB CheckConstraints for agent_thinking_level (Session, Run) and Run.notify_on, consistent with the existing enum-constraint pattern - run timeline: render human-readable status labels and non-empty API/MCP origin badges instead of raw enum values / empty pills - sidebar: highlight the Sessions section on the namespaced "Start a run" route - remove dead code and stale comments, fix a sandbox docs link Add coverage for the chat-run failure paths, the notification batch rollup + fanout suite (restoring what the rename dropped), the new enum constraints, RunManager.by_owner, the API auth/ownership gates, and context serialization.
Reconcile main's per-user repository authorization (#1365, #1366) with this branch's activity+chat -> sessions unification. Conflict resolution highlights: - Ported the authorization wiring into the sessions app: assert_can_run in sessions/forms.py, aassert_can_run in sessions/services.py, chat/jobs/mcp submission gates, and the RepositoryAccessDenied handling in sessions views. - Split the run/session manager: by_owner stays ownership-only (async-safe, used by thread-continuation lookups in chat/jobs/mcp/sessions APIs) and a new visible_to adds the repo-read visibility clause for the sync viewing surfaces (list/detail/download/stream, dashboard, nav badge), matching main's own ownership-vs-visibility split. The stream view wraps visible_to in sync_to_async since identity resolution does a sync DB read. - Repointed the conftest allow-all authorization fixture to sessions.* targets (activity.services/forms were removed) and adapted the auto-merged authorization tests that still referenced the deleted Activity model to Run/Session (context-processors, schedules dispatch, batch-submit authz). - Kept the retired activity app as a migration-only stub; removed the leftover activity views/templates/tests carried in by main. Verified: make test (3549 passed), ruff check/format, makemigrations --check.
…fill The 0002 backfill copied text columns verbatim from historical Activity/ChatThread rows into Session/Run, whose text columns (repo_id, ref, title, prompt, agent_model, external_username, ...) are NOT NULL. Legacy rows can carry NULL on any of them (the columns pre-date their NOT NULL / default="" tightening on some deployments) — both a NULL repo_id and a NULL ref were hit in production — aborting the whole atomic migration with an IntegrityError. Coalesce NULL -> "" on every non-nullable text column via model introspection (``_blank_null_text``) for the activity Session/Run copy, and inline on the chat get_or_create defaults. Discriminator columns (origin/trigger_type/status) are deliberately excluded — a NULL there is corruption, and "" would violate their enum check constraints. Regression tests drive run_backfill with stubbed Activity/ChatThread iterators yielding NULL-text rows (SQLite enforces NOT NULL on the historical tables, so such rows can't be seeded directly).
…of schema drift activity.0016 / chat.0004 dropped the source tables by first issuing an individual RemoveIndex/RemoveConstraint per object and then DeleteModel. On deployments whose tables drifted from migration history — e.g. the activity_thread_id_nonempty check constraint (added in activity.0009) that was never actually created in production — RemoveConstraint aborts the migration with "constraint does not exist". Since both migrations drop the table wholesale, wrap the operations in SeparateDatabaseAndState: the database side is now a single DeleteModel (DROP TABLE CASCADE on PostgreSQL), which removes every index/constraint at once regardless of which ones are actually present, while the state side keeps the exact original operations so Django's migration bookkeeping and reversibility are unchanged. End state and behavior on a clean database are identical (verified: full suite + makemigrations --check green; backfill tests exercise both directions on a fresh DB).
- _run_timeline.html applied the duration filter to the Run object instead of its duration property, raising a TypeError whenever a session had a finished run. - session_list.html dropped the stretched-link anchor during the sessions unification, leaving rows styled as clickable (hover, chevron) but not actually linking to session_detail.
Post-review cleanup of the sessions list/filter UI:
- Drive the Time presets from a `ranges` context var sourced from
RANGE_CHOICES (matching the origins/statuses loop pattern) and replace
the int(token.rstrip("d")) parse with a _RANGE_WINDOWS map, so the
presets/window sizes live in one place instead of three.
- Narrow the runs prefetch with .only(...) so list rows skip the fat
prompt/result_summary/error_message/usage_by_model columns; verified no
per-run deferred-field query.
- Add a clearable repository filter chip for the deep-link-only ?repo
param (previously filtered invisibly), with pt-PT translation.
- Remove the dead setRange() JS helper and tighten now-stale comments.
- Expand tests: day_bucket None/edge windows, session_cost mixed null
costs, filter_range 2d/30d + inclusive boundary, filter_q dual-column
match; narrow the brittle day-group header assertion.
The detail view suppressed the "expired" state whenever any run was non-terminal, so a crashed or orphaned background run (stuck in QUEUED/READY/RUNNING) pinned the session on a permanent "working" spinner with no error surfaced. Gate the suppression on heartbeat freshness: a holder that hasn't bumped last_active_at within STALE_RUN_MINUTES is treated as dead, so the session falls through to the expired banner. Extract the shared staleness threshold into sessions.locks.stale_cutoff() so the detail view, SessionLock takeover, and sync_stuck_runs compute the window one way. Add regression tests: a stale in-flight run renders as expired, and the split-out session_new_chat route stays login-gated.
- Show the repository on session rows and drop the run-count pill; add a .session-repo style for it. - Rename the SUCCESSFUL run status label "Successful" -> "Done" (model, migration 0005, session-stream.js, pt catalog). - Read `breadcrumbs` directly in accounts/_breadcrumb.html and drop the redundant `with crumbs=breadcrumbs` from every include site. - Harden session-list view tests: anchor the New CTA assertion and cover the repository row.
Butt folded untracked-file diff sections directly together with a single newline boundary instead of prefixing each with "\n", matching what one git diff invocation emits. A blank line after a hunkless section (an empty-file addition with no @@) made unidiff abort the whole parse, which silently bypassed redact_diff_content's omit-pattern redaction and leaked excluded file content to the diff-to-metadata model. Add regression tests covering the empty-then-real fold ordering, the empty-base leading-newline case, and redaction alongside a hunkless empty-file section.
Run sync_stuck_runs every 5 minutes as a crash-recovery backstop instead of relying on manual operator invocation. The new cron task reuses the shared locked_task guard (non-blocking) so an overrunning pass skips the tick rather than double-dispatching. Switch the command's output from stdout to the daiv.sessions logger so it is observable when run headless via the cron task, and include the failed run ids in the error summary so the failed DBTaskResult is self-contained. The command still raises CommandError on per-row failures, which fails the task's DBTaskResult and surfaces errors to monitoring.
Fold the enum check constraints (0003, 0004) and the run status label change (0005) into 0001_initial, since none of these were applied on prod. The 0002 data backfill is kept as-is so the cross-app dependency anchors in activity/0016, chat/0004 and memory/0002 stay valid.
Robustness and cleanup on the client-driven sessions listing: - Guard overlapping swaps with a monotonic token, commit the URL to history only after a real swap fires, and toast on a failed swap so the address bar never describes results the user isn't seeing. - Preserve modifier/middle clicks on swap-marked pagination links. - Derive the Type/Time button labels from the rendered menu instead of storing them, dropping the label args and the escapejs threading. - Extract a shared core.utils.is_htmx() and reuse it in both sessions and sandbox_envs; skip the schedule-name query on the HTMX fragment path (the filter-bar chip only renders on the full page). - Warn instead of silently degrading when the SSE re-arm can't find its in-flight marker or a filter value has no menu item. - Expand list-view tests for the fragment path and pagination markers.
Replace the per-turn error hover popup (which rendered the raw traceback into the page) with a boolean `errored` flag that only drives the icon + red border; the message stays developer-only in the logs. Drop the now-redundant `_run_timeline.html` partial, since per-turn errors already surface run failures. Also persist the resolved agent model/thinking level onto Run + Session right after resolution in `run_job_task`, so the locked "Auto" pill on session detail reflects what actually ran instead of the requested-override placeholder.
srtab
added a commit
that referenced
this pull request
Jul 10, 2026
Revising the merge-conflict resolution: main's #1374 is the reviewed successor of this branch's own sessions foundation, so the session-UI files should match main exactly and PR #1378 should add only delegate_jobs (the DELEGATED_JOB origin flows into the filter UI automatically via SessionOrigin.choices — no bespoke UI needed). Reverts the two hand-merge grafts that re-introduced branch-only UI main's review had dropped: - run-timeline rail + _run_timeline.html partial (deleted) - the "N runs" pill in _session_row.html - the 3 run-timeline tests spliced into test_views_detail.py _session_row.html, session_detail.html and test_views_detail.py are now identical to origin/main.
srtab
added a commit
that referenced
this pull request
Jul 10, 2026
Reconcile the parallel sessions-app fork (main's PR #1374 vs this branch): - Keep this branch's RunRelay/EventSource run streaming; drop the superseded session_status/thread_status probe endpoints. - Integrate main's _persist_resolved_agent (records the resolved model on Run/Session) plus the "Auto" locked-pill fallback. - Adopt main's failed-run design: icon + red border only, with no raw error text on the turn, fallback banner, or run timeline. - Remove the run timeline entirely, matching main.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Unifies
activity.Activityandchat.ChatThreadinto a newdaiv/sessions/app (Django app labelagent_sessions), built around two models:Session— one per LangGraph thread (PK =thread_id= checkpoint key)Run— one per execution, including chat turns, with denormalized usage/costThe result is one unified data model, one transcript-centric UI, and one execution lock across every surface (chat, background jobs, webhooks, schedules).
How it was built
The app was built additively, then consumers switched one surface at a time, then the unified UI landed, then the old models/pages were deleted:
Session/Runmodels, unifiedSessionLock(claim/heartbeat/release with stale takeover), submit services + FIFO dispatcher + task-sync/backfill signals, and a data migration that copies every historicalActivity/ChatThreadrow intoSession/Run(Run.pkis preserved fromActivity.pkso external job IDs keep resolving).run_job_taskclaims the session lock;/api/jobsand the MCPsubmit_job/get_job_status/list_jobstools are backed bySession/Runwith byte-identical contracts; webhooks/schedules/dashboard/sandbox_envs switched; chat turns now createRuns with usage tracking; memory observations and notifications hang offRun(both skip chat-origin runs)./dashboard/activity/and/dashboard/chat/URL.ActivityandChatThreadmodels dropped; theactivity/chatapps remain as migration-only stubs (their migration history is referenced by other apps).Deploy note
This branch is safe to deploy only at completion (this state). Migration ordering guarantees no data loss: the backfill (
agent_sessions.0002) and the memory FK-swap (memory.0002) run before either old table is dropped (theDeleteModelmigrations explicitly depend on them). On a populated DB, everyActivity→Run(same UUID) andChatThread→Sessionis copied, and memory observations re-point to the equal-PKRun, before the drops.External contracts
Unchanged:
/api/jobsrequest/response shapes, MCP tool names/wording/JSON keys,job_id == Run.pk, and the MCPmcp_jobtrigger are all preserved.Verification
make test— 3352 passed, 0 failedmakemigrations --check --dry-run— clean ("No changes detected")make lint/make lint-typing— clean / no new error classesmkdocs build --strict— cleanfindstatic— confirms the relocated run-form JS resolves under manifest static storageEach of the 16 plan tasks was implemented and reviewed independently; a final whole-branch review verified cross-cutting coherence, migration-chain safety, contract preservation, and spec fidelity.
Notes for the reviewer
chore(migrations): add missing migrations for pre-existing thinking-level field driftaddresses pre-existingcore/schedulesmodel-vs-migration drift (unrelated to this change) so the branch'smakemigrations --checkgate is clean. It can be split into its own PR if preferred.activity/templates+activity_tags.py(duplicated bysession_tags.py) into the sessions app soactivitybecomes a true stub; useSessionOriginmembers instead of raw strings innotificationsEXCLUDED_RUN_TRIGGERS; a few minor test-coverage additions.