plandex-ai/plandex · error

Stream has no branch

Error message

Stream has no branch

What it means

ListPlansRunningHandler builds a map of branches keyed by PlanId|BranchName from db.ListBranchesForPlans, then iterates active/recent model streams. If a stream references a (planId, branch) pair not present among the fetched branches, the handler aborts with this 500 instead of returning partial data. It is a referential-integrity assertion: every stream is expected to have a matching non-archived branch row.

Source

Thrown at app/server/handlers/plans_crud.go:518

	var apiPlansById = make(map[string]*shared.Plan)
	for _, plan := range plans {
		apiPlan := plan.ToApi()
		apiPlansById[plan.Id] = apiPlan
	}

	var apiBranchesByComposite = make(map[string]*shared.Branch)
	for _, branch := range branches {
		apiBranch := branch.ToApi()
		apiBranchesByComposite[branch.PlanId+"|"+branch.Name] = apiBranch
	}

	addedBranches := make(map[string]bool)
	for _, stream := range streams {
		branchComposite := stream.PlanId + "|" + stream.Branch
		apiBranch, ok := apiBranchesByComposite[branchComposite]
		if !ok {
			log.Printf("Stream %s has no branch\n", stream.Id)
			http.Error(w, "Stream has no branch", http.StatusInternalServerError)
			return
		}

		apiPlan, ok := apiPlansById[stream.PlanId]
		if !ok {
			log.Printf("Stream %s has no plan\n", stream.Id)
			http.Error(w, "Stream has no plan", http.StatusInternalServerError)
			return
		}

		if !addedBranches[branchComposite] {
			res.Branches = append(res.Branches, apiBranch)
			addedBranches[branchComposite] = true
		}

		res.StreamStartedAtByBranchId[apiBranch.Id] = stream.CreatedAt
		if stream.FinishedAt != nil {
			res.StreamFinishedAtByBranchId[apiBranch.Id] = *stream.FinishedAt

View on GitHub (pinned to e2d772072e)

Solutions

  1. Find the orphaned stream: the server log line 'Stream <id> has no branch' gives the stream Id; delete or finish that stream row in the model_streams table
  2. Restore/recreate the missing branch for that planId with the exact name recorded on the stream
  3. Check whether the stream is stale (still active but actually dead) and mark it finished/archived so GetActiveModelStreams stops returning it
  4. Reproduce locally by comparing SELECT plan_id, branch FROM model_streams WHERE active against the branch list for those plans; align the two queries' filters (archived/deleted, planIds)
  5. Code-level fix: skip orphaned streams with a log warning instead of aborting the whole listing

Example fix

// before
apiBranch, ok := apiBranchesByComposite[branchComposite]
if !ok {
    log.Printf("Stream %s has no branch\n", stream.Id)
    http.Error(w, "Stream has no branch", http.StatusInternalServerError)
    return
}
// after
apiBranch, ok := apiBranchesByComposite[branchComposite]
if !ok {
    log.Printf("Stream %s has no branch; skipping\n", stream.Id)
    continue
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Guard: only treat a stream as listable if its branch still exists
for _, s := range streams {
    if !branchExists(branches, s.PlanId, s.Branch) {
        log.Printf("skipping stream %s: branch %s missing", s.Id, s.Branch)
    }
}

Type guard

func streamHasBranch(stream *db.ModelStream, branches []*db.Branch) bool {
    for _, b := range branches {
        if b.PlanId == stream.PlanId && b.Name == stream.Branch {
            return true
        }
    }
    return false
}

Try / catch

// Go: client-side, detect the sentinel 500 body
if resp.StatusCode == 500 {
    body, _ := io.ReadAll(resp.Body)
    if strings.Contains(string(body), "Stream has no branch") {
        // refresh branch list / re-open session before retrying
    }
}

Prevention

When it happens

Trigger: A model_stream row exists whose stream.Branch does not match any branch name for stream.PlanId returned by ListBranchesForPlans — e.g. the branch was renamed or deleted/archived while its stream is still marked active, or GetActiveModelStreams and ListBranchesForPlans were called with inconsistent planIds/filters.

Common situations: A branch was deleted (or archived — the branch query may exclude archived/deleted rows) while a stream on it was still running; branch rename during an active coding session; an org/plan scoping mismatch (branches fetched with auth.OrgId but streams not org-scoped); soft-deleted plan still having active streams.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/678f33438f70180f. Report an issue: GitHub.