Skip to content

feat(session): support multiple PRs per session#230

Open
Pritom14 wants to merge 1 commit into
mainfrom
feat/multi-pr-per-session
Open

feat(session): support multiple PRs per session#230
Pritom14 wants to merge 1 commit into
mainfrom
feat/multi-pr-per-session

Conversation

@Pritom14

@Pritom14 Pritom14 commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Lets a single session own several pull requests (a root plus stacked children) instead of being capped at one. The SQLite schema was already 1-session→many-PR (pr.url PK, session_id a plain FK with idx_pr_session, no UNIQUE on session_id), so this is a behavioural change across the observe → persist → derive → react pipeline, not a migration.

State & user-action flow

The session's display status is never stored — it is derived at read time from durable PR facts. One full loop:

  1. Agent opens PR(s). A root PR on the session branch (feat/x), optionally stacked children on the feat/x/... convention.
  2. OBSERVE (observe/scm): each poll lists the repo's open PRs, attributes every PR whose source branch equals the session branch or descends from it to the owning session (longest-prefix wins), and persists the pr row including the source/target branch pair.
  3. DERIVE (service/session/status.go, read-time): session status = worst-wins aggregate over the open PRs, by severity ci_failed > changes_requested > draft > review_pending > pr_open > approved > mergeable. A stacked child whose parent is still open is excluded from the aggregate (it cannot merge until the parent does). The stack model + per-PR facts are surfaced as prs[] on every session DTO, so the dashboard shows each PR's state.
  4. ACT (lifecycle/reactions.go): per-PR nudges to the agent — CI failing → "fix CI" (with log tail); review changes/unresolved comments → "address feedback"; merge conflict → "rebase", but only the bottom of the stack; a stacked child blocked by an open parent is suppressed. Dedup signature is persisted per PR so a daemon restart never re-fires.
  5. User action closes the loop. From the derived status + prs[] the user/agent fixes CI, addresses review, or rebases and pushes; the next poll re-observes and re-derives. No status is ever written back — only PR facts are.
  6. Completion. The session terminates (→ status merged) only when no PR is open and at least one merged. A single PR merging while another stays open keeps the session alive.

What changed, by layer

  • Observe (observe/scm/observer.go): one refresh subject per open tracked PR; discoverNewPRs + matchSession do the branch-prefix attribution (longest match wins so a child session claims its own stacked PRs).
  • Derive (service/session/stack.go, status.go): worst-wins aggregation + stack model (B is a child of A iff B.target_branch == A.source_branch and A is open), surfaced as prs[].
  • React (lifecycle/reactions.go): per-PR reactions, stacked-child nudge suppression, and sessionComplete (all closed/merged + ≥1 merged).
  • API (httpd/controllers/dto.go, sessions.go, regenerated openapi.yaml + frontend/src/api/schema.ts): prs[] on the session views via SessionView.

Tests

  • Unit coverage across stack, status, observer multi-subject discovery, and lifecycle per-PR reactions/completion.
  • A real-SQLite store test (store/pr_facts_test.go) for the stacked-PR read path: ListPRFactsForSession returns every owned PR newest-first with state flags and branch pair preserved (the data the stack aggregator builds on).
  • Functional end-to-end (integration/scm_observer_test.go, TestSCMObserverMultiPREndToEnd) drives the real sqlite.Store + lifecycle.Manager + observe/scm.Observer (canned provider only) through: (1) one session owning its root and stacked child from a single repo list with branch pairs persisted; (2) staying alive while a stacked PR is open and terminating only once all merge; (3) a stacked child blocked by an open parent suppressed while the stack bottom is nudged (asserted down to the persisted dedup signature).

Test plan

  • go build ./... clean
  • go vet ./... clean
  • go test ./... green (53 packages, exit 0)
  • TestSCMObserverMultiPREndToEnd passes (attribution / completion / nudge suppression)
  • Live agent-driven run: spawning an agent that opens multiple real GitHub PRs on one session. Needs a real GitHub repo + agent credentials/environment not available in this workspace, so it is not exercised here — the functional proof above drives the real store/lifecycle/observer against a canned SCM provider instead.

🤖 Generated with Claude Code

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

A session can now own several pull requests (a root plus stacked
children) instead of being capped at one. The SQLite schema was already
1-session->many-PR (pr.url PK, session_id a plain FK), so this is a
behavioural change across the observe -> persist -> derive -> react
pipeline, not a migration.

- observe: the SCM observer discovers every open PR whose source branch
  matches a session branch or descends from it ("branch/..." stacking),
  attributing each to the owning session; the longest matching branch
  wins so a child session claims its own stacked PRs.
- derive: session status is a worst-wins aggregate over all owned PRs,
  with a stack model (B is a child of A iff B.target == A.source and A is
  open) exposed via prs[] on every session read DTO.
- react: per-PR reactions; a stacked child blocked by an open parent is
  exempt from the rebase/merge-conflict nudge (only the bottom of the
  stack is eligible), and the session completes only when no PR is open
  and at least one merged.
- tests: unit coverage across stack/status/observer/lifecycle, a
  real-SQLite ListPRFactsForSession test for the stacked-PR read path,
  and a functional end-to-end integration test driving the real store +
  lifecycle + observer through attribution, completion, and stacked-child
  nudge suppression.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Pritom14 Pritom14 force-pushed the feat/multi-pr-per-session branch from f866099 to 0005563 Compare June 14, 2026 15:04

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

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