Skip to content

bug(scm): PR auto-attach fails — session branch frozen at spawn, branch-sync mechanism disconnected #284

@AgentWrapper

Description

@AgentWrapper

Bug

A PR opened from a session's worktree never auto-attaches to that session unless its head branch is exactly the spawn branch (ao/<name>). The SCM observer discovers a session's PR by querying gh pr list --head <session.branch>, but session.branch is frozen at the value set at spawn time and is never updated when the agent/user checks out a feature branch. Since the normal workflow is to do work on a feature branch (e.g. fix/...) and open a PR from it, the pr row is never created, and every downstream PR surface (sidebar, Session Inspector, /prs, kanban badge) shows "no PR".

The branch shouldn't have to equal the spawn branch — the system is designed to track the agent's actual branch (a git/gh wrapper exists for exactly this). That tracking mechanism is fully disconnected in the Go rewrite.

Source: surfaced while triaging why PR #281 (session reverbcode-3, branch ao/reverbcode-3, PR head fix/bug-triage-reverbcode-stack) showed no attached PR in the sidebar. | Analyzed against: 6ac65d2
Confidence: High — traced the full data path and confirmed with live daemon logs + DB state.

Reproduction

  1. Spawn a worker session (gets branch ao/<name>).
  2. In its worktree, create a feature branch: git checkout -b fix/something.
  3. Commit, push, and open a PR from fix/something (whether via raw gh pr create or any flow).
  4. Expected: the PR attaches to the session (a pr row is written; sidebar/inspector show it).
  5. Actual: nothing attaches. ~/.ao/data/ao.db pr table stays empty; daemon.log logs scm observer: no PR detected for branch ... branch=ao/<name>.

Verified live: pr table empty across all sessions; daemon.log shows
"scm observer: no PR detected for branch" session=reverbcode-1 branch=ao/reverbcode-orchestrator.

Root Cause

The observer keys PR discovery on the session's stored branch, read from SQLite, set at spawn and never refreshed:

  • backend/internal/observe/scm/observer.go:419branch := strings.TrimSpace(sess.Metadata.Branch)
  • backend/internal/observe/scm/observer.go:526pr, err := o.provider.DetectPRByBranch(ctx, s.repo, s.branch)

The mechanism that is supposed to keep that branch in sync with the worktree's real branch is a git/gh wrapper (~/.ao/bin/git, ~/.ao/bin/ao-metadata-helper.sh) ported verbatim from the upstream TypeScript app. It calls update_ao_metadata branch "$3" on git checkout -b / git switch -c. In the Go rewrite this is triply disconnected:

  1. Not on the session PATH. The session PATH pin (HookPATH in backend/internal/session_manager/manager.go) prepends only the daemon binary's own directory so bare ao resolves to the daemon. It does not add ~/.ao/bin, so the git/gh shims never run inside spawned sessions. Nothing in backend/ references ~/.ao/bin — those shims appear to be orphaned artifacts of an older (npm) AO install.
  2. Writes to a store the daemon doesn't use. Even if invoked, update_ao_metadata writes to a per-session file $AO_DATA_DIR/<session>.json and first guards on that file already existing ([[ -f "$metadata_file" ]] || return 0). ReverbCode's source of truth is SQLite (~/.ao/data/ao.db) and never creates those per-session JSON files — confirmed: ~/.ao/data/reverbcode-3.json does not exist. So the call is a silent no-op.
  3. No daemon path to update the branch. There is no hook event (backend/internal/cli/hooks.go has no branch handling) and no endpoint that writes sessions.branch from the agent's actual branch. The one place that could (backend/internal/service/session/claim_pr.go:126) is stubbed: // TODO: implement workspace branch checkout with BranchChanged: false hard-coded.

Net effect: sessions.branch is permanently ao/<name>, so DetectPRByBranch only ever matches a PR pushed on that exact branch.

Why upstream doesn't hit this

AgentWrapper/agent-orchestrator also matches by branch (gh pr list --head <session.branch> in packages/plugins/scm-github/src/index.ts:detectPR), but its git/gh wrapper (packages/core/src/agent-workspace-hooks.ts) writes to the file-based session metadata that is its source of truth, and the lifecycle manager reads it back. So when the agent runs git checkout -b fix/..., metadata.branch follows, and detection finds the PR. ReverbCode ported the wrapper but moved the source of truth to SQLite and never reconnected the sync path.

Fix (options, not prescriptive)

  • Track the real branch (matches upstream intent): add a workspace/git hook that reports branch changes to the daemon, and a daemon path that updates sessions.branch (then the existing DetectPRByBranch works). This is the branch-self-discovery assumption Wire PR engine into the daemon (built but unwired) #86 already bakes in — it needs the branch to actually be live.
  • Don't depend solely on branch name: also resolve a session's PR by the worktree's pushed head SHA / commits, or by gh pr list --head $(git -C <worktree> rev-parse --abbrev-ref HEAD) evaluated against the live worktree rather than the stored spawn branch.
  • Finish manual claim as the documented fallback: implement the claim_pr.go:126 TODO so ao session claim-pr <id> <pr> is a reliable escape hatch (it currently attaches the pr row but never adopts the branch).
  • Remove the orphaned ~/.ao/bin git/gh shims (or wire them) so the dead branch-sync path isn't mistaken for working.

Impact

  • PR auto-attachment silently fails for the default agent workflow (work on a feature branch). Every PR surface in the app stays empty even though the daemon's observer is running correctly.
  • Workaround exists but is manual/non-obvious: push on the spawn branch ao/<name>, or run ao session claim-pr <session-id> <pr-number>.

Related

  • #86 — Wire PR engine into the daemon; its "branch self-discovery" approach assumes SessionMetadata.Branch is live, which this bug shows it isn't.
  • #240 / #251 — frontend never renders PR facts. Those are downstream of this: they assume the pr row exists; this bug is why it often doesn't.
  • #111 — surface PR observation in ao session get.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcoding-agentsRuntime + Workspace + Agent adapters lanepriority: mediumFix when convenientscmSCM observer/notifier lane

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions