{"record":{"id":"678f33438f70180f","repo":"plandex-ai/plandex","slug":"stream-has-no-branch","errorCode":null,"errorMessage":"Stream has no branch","messagePattern":"Stream has no branch","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"app/server/handlers/plans_crud.go","lineNumber":518,"sourceCode":"\tvar apiPlansById = make(map[string]*shared.Plan)\n\tfor _, plan := range plans {\n\t\tapiPlan := plan.ToApi()\n\t\tapiPlansById[plan.Id] = apiPlan\n\t}\n\n\tvar apiBranchesByComposite = make(map[string]*shared.Branch)\n\tfor _, branch := range branches {\n\t\tapiBranch := branch.ToApi()\n\t\tapiBranchesByComposite[branch.PlanId+\"|\"+branch.Name] = apiBranch\n\t}\n\n\taddedBranches := make(map[string]bool)\n\tfor _, stream := range streams {\n\t\tbranchComposite := stream.PlanId + \"|\" + stream.Branch\n\t\tapiBranch, ok := apiBranchesByComposite[branchComposite]\n\t\tif !ok {\n\t\t\tlog.Printf(\"Stream %s has no branch\\n\", stream.Id)\n\t\t\thttp.Error(w, \"Stream has no branch\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tapiPlan, ok := apiPlansById[stream.PlanId]\n\t\tif !ok {\n\t\t\tlog.Printf(\"Stream %s has no plan\\n\", stream.Id)\n\t\t\thttp.Error(w, \"Stream has no plan\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif !addedBranches[branchComposite] {\n\t\t\tres.Branches = append(res.Branches, apiBranch)\n\t\t\taddedBranches[branchComposite] = true\n\t\t}\n\n\t\tres.StreamStartedAtByBranchId[apiBranch.Id] = stream.CreatedAt\n\t\tif stream.FinishedAt != nil {\n\t\t\tres.StreamFinishedAtByBranchId[apiBranch.Id] = *stream.FinishedAt","sourceCodeStart":500,"sourceCodeEnd":536,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/handlers/plans_crud.go#L500-L536","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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","Restore/recreate the missing branch for that planId with the exact name recorded on the stream","Check whether the stream is stale (still active but actually dead) and mark it finished/archived so GetActiveModelStreams stops returning it","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)","Code-level fix: skip orphaned streams with a log warning instead of aborting the whole listing"],"exampleFix":"// before\napiBranch, ok := apiBranchesByComposite[branchComposite]\nif !ok {\n    log.Printf(\"Stream %s has no branch\\n\", stream.Id)\n    http.Error(w, \"Stream has no branch\", http.StatusInternalServerError)\n    return\n}\n// after\napiBranch, ok := apiBranchesByComposite[branchComposite]\nif !ok {\n    log.Printf(\"Stream %s has no branch; skipping\\n\", stream.Id)\n    continue\n}","handlingStrategy":"type-guard","validationCode":"// Guard: only treat a stream as listable if its branch still exists\nfor _, s := range streams {\n    if !branchExists(branches, s.PlanId, s.Branch) {\n        log.Printf(\"skipping stream %s: branch %s missing\", s.Id, s.Branch)\n    }\n}","typeGuard":"func streamHasBranch(stream *db.ModelStream, branches []*db.Branch) bool {\n    for _, b := range branches {\n        if b.PlanId == stream.PlanId && b.Name == stream.Branch {\n            return true\n        }\n    }\n    return false\n}","tryCatchPattern":"// Go: client-side, detect the sentinel 500 body\nif resp.StatusCode == 500 {\n    body, _ := io.ReadAll(resp.Body)\n    if strings.Contains(string(body), \"Stream has no branch\") {\n        // refresh branch list / re-open session before retrying\n    }\n}","preventionTips":["Never hard-delete or rename a branch while a stream is active; mark streams finished first","Enforce FK or application-level check between model_streams.plan_id/branch and branches","Periodically sweep stale active streams whose branches no longer exist","When deleting/archiving branches, cascade-close their running streams"],"tags":["go","http","data-integrity","orphaned-record"],"backgroundTag":"orphaned-stream-reference","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}