Skip to content

fix(staged): resolve project session state from backend instead of content heuristics#726

Merged
matt2e merged 10 commits into
mainfrom
project-sessions-not-updating-state
May 14, 2026
Merged

fix(staged): resolve project session state from backend instead of content heuristics#726
matt2e merged 10 commits into
mainfrom
project-sessions-not-updating-state

Conversation

@matt2e
Copy link
Copy Markdown
Contributor

@matt2e matt2e commented May 14, 2026

Summary

  • Backend-resolved session status: Project notes now carry sessionStatus and completionReason fields populated at query time from the sessions table, replacing the fragile frontend heuristic that inferred "generating" from empty title+content.
  • Propagate branch_id/project_id on all session events: SessionConfig and PipelineConfig now carry ownership context so terminal events (completed, error, cancelled) include project_id/branch_id — eliminating the DB lookup race where emit_status previously sent null for these fields.
  • Unified delete-with-cancel helper: Extracted deleteSessionLinkedItem() on the frontend and made delete_project_note return+delete the linked session atomically (using DELETE ... RETURNING) to eliminate TOCTOU races.
  • Resilient hashtag item building: buildProjectHashtagItems now uses Promise.allSettled so a single branch timeline failure doesn't break the entire hashtag list.
  • Misc fixes: isSessionActive helper consolidates active-status checks, stale comment updated, isSessionActive accepts undefined.

Test plan

  • crates-test passing
  • staged-ci passing
  • Verify project notes show spinner while generating and transition to content on completion
  • Verify cancelling/deleting a generating project note cleans up the session
  • Verify session status updates propagate to the project sidebar badge

🤖 Generated with Claude Code

matt2e and others added 8 commits May 14, 2026 11:28
Project notes used a content-based heuristic (empty title + content = running)
to determine active state, causing two bugs:
1. Replying to a completed note didn't show the spinner due to a race
   condition in the isKnownNoteSession gate
2. Cancelled sessions left empty stubs that still appeared as "generating"

This unifies project notes with the branch timeline approach by:
- Adding sessionStatus and completionReason fields to ProjectNote, resolved
  at query time via the same resolve_session_status() used by timeline items
- Extracting a shared isSessionActive() helper used by ProjectSection,
  BranchCard, and BranchCardSessionManager
- Simplifying the ProjectSection event handler to always refresh from the
  backend (removing the racy isKnownNoteSession check)
- Filtering out empty stubs from cancelled/errored sessions in the backend

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…e helpers

Project note deletion didn't cancel running sessions or clean up session
records, leaving orphaned sessions running to completion. Branch notes
handled this correctly via a cancel → delete → cleanup flow.

This fix:
- Adds delete_session handling to delete_project_note (Tauri command +
  web server), mirroring the existing delete_note pattern
- Extracts a shared deleteSessionLinkedItem() frontend helper that
  encapsulates cancel → delete → registry cleanup, used by both
  ProjectSection and BranchCard (note, review, pending commit deletion)
- Fixes project note sorting to use isSessionActive(sessionStatus)
  instead of the stale content-based heuristic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…backend

The backend filter removed project notes with empty content whose session
was cancelled or errored. This caused notes to vanish entirely from the
UI — the frontend already handles these via the "failed-note" display
type ("Session finished — no note created"), so the backend filter was
redundant and overly aggressive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…failures

buildProjectHashtagItems used Promise.all for branch timeline fetches,
so a single failing branch caused the entire operation to reject silently
(the $effect had no .catch handler), leaving hashtagItems as [] and the
# dropdown permanently empty.

This fix:
- Switches to Promise.allSettled so individual branch timeline failures
  are skipped rather than aborting all hashtag item loading
- Adds .catch() to the $effect to log errors instead of swallowing them
- Adds a stale flag to prevent race conditions when dependencies change
  rapidly (matching the pattern used in NewSessionModal)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…events

Session completion/error/cancelled events emitted by emit_status() had
project_id and branch_id hardcoded to None, causing ProjectSection's
listener to silently drop them (it filters on projectId === project.id).
This left the spinner stuck until the user opened and closed the session
dialog, which triggered an unconditional loadProjectNotes().

Fix by piping branch_id and project_id through SessionConfig and
PipelineConfig from callers who already have the context, so emit_status
can include them in terminal events without a DB lookup. This makes the
data flow explicit from session creation to completion.

Changes:
- Add branch_id/project_id fields to SessionConfig and PipelineConfig
- All ~12 callers of start_session/start_pipeline_session now populate
  these fields from the branch/project context they already hold
- emit_status() accepts and forwards the piped IDs instead of hardcoding
  None
- Pipeline→AI handoff copies IDs from PipelineConfig to SessionConfig
- Pipeline terminal paths use piped branch_id for queue draining instead
  of a DB lookup via get_branch_id_for_session
- recover_orphaned_sessions (app-quit recovery) falls back to DB lookup
  via new Store::get_project_id_for_session() since it has no caller
  context
- cancel_session command also looks up IDs from DB for its inline event

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…euristic

The comment on the project note stub creation still referenced the old
!title && !content frontend check, which was replaced by backend-resolved
sessionStatus in 0fbd80a.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
delete_project_note used separate get + delete calls, creating a window
where the note could be deleted by another caller between reading its
session_id and performing the delete. Fix by using DELETE ... RETURNING
session_id to atomically delete the note and retrieve its session_id in
a single query. Both the Tauri command and web server handler now use
this simplified path consistently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Some callers (e.g. BranchCard timeline items) may pass undefined for
sessionStatus. The function already returns false for undefined, but
the type signature didn't reflect this, masking potential missing-data
bugs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e matt2e requested review from baxen and wesbillman as code owners May 14, 2026 05:16
matt2e and others added 2 commits May 14, 2026 15:40
…events

ProjectSection's session-status-changed handler called loadProjectNotes()
on every event (including running), reloading all notes when only one
changed. This is wasteful since a session maps 1:1 to a single note.

Fix by:
- Adding get_project_note_by_session command (Tauri + web server) that
  returns a single note with resolved session status
- Skipping backend fetch on 'running' events (the stub is already loaded
  by startProjectSession)
- On terminal events (completed/error/cancelled), fetching only the
  affected note by session ID and patching it into the local array

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…timeline invalidation

The # hashtag reference dropdown in project prompts showed stale data
after sessions created commits, because nothing triggered a rebuild of
hashtagItems. Two contributing factors:

1. The hashtag $effect only depended on project.id, branches, and
   reposById — none of which change when a session creates a commit
2. The timeline cache wasn't invalidated by project session completion,
   so even a forced re-run would return stale data

Fix by adding a hashtagVersion counter as a reactive dependency in the
hashtag $effect. The counter is bumped in two places:
- On project session terminal events (completed/error/cancelled), after
  invalidating all branch timeline caches
- On timeline-invalidated custom events, catching branch-level session
  completions that BranchCard already fires

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e matt2e merged commit 1d2f7c9 into main May 14, 2026
5 checks passed
@matt2e matt2e deleted the project-sessions-not-updating-state branch May 14, 2026 07:30
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.

1 participant