{"record":{"id":"1cbae30301d6049a","repo":"plandex-ai/plandex","slug":"stream-has-no-plan","errorCode":null,"errorMessage":"Stream has no plan","messagePattern":"Stream has no plan","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"app/server/handlers/plans_crud.go","lineNumber":525,"sourceCode":"\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\n\t\t}\n\t\tres.StreamIdByBranchId[apiBranch.Id] = stream.Id\n\n\t\tres.PlansById[stream.PlanId] = apiPlan\n\t}\n\n\tsort.Slice(res.Branches, func(i, j int) bool {","sourceCodeStart":507,"sourceCodeEnd":543,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/handlers/plans_crud.go#L507-L543","documentation":"ListPlansRunningHandler builds apiPlansById from the plans list fetched earlier in the handler, then verifies every stream's PlanId exists in that map. If a stream references a plan that was not returned (deleted, not owned, or filtered out), the handler stops and returns this 500. It is a referential-integrity check: every active stream must belong to a plan visible to the caller.","triggerScenarios":"A model_stream exists for a planId that is absent from the handler's plan list — e.g. the plan was soft-deleted after the stream started, the plan belongs to a different project/owner than the one queried, or the plan query (ListOwnedPlans/GetPlansByIds) filters it out while GetActiveModelStreams still returns the stream.","commonSituations":"Deleting a plan while a coding session stream on it is still marked active; plan moved between projects; permission scoping where streams are fetched org-wide but plans only for the requesting user; stale streams never marked finished after a crash.","solutions":["Use the server log line 'Stream <id> has no plan' to identify the orphaned stream, then clean up or finish that stream row","Restore the missing plan or re-associate the stream with a valid planId","Align the plan fetch and stream fetch filters (same project, ownership, and soft-delete exclusions) so the two queries agree","Code-level fix: skip streams whose plan is missing, logging a warning, rather than failing the entire response"],"exampleFix":"// before\napiPlan, ok := apiPlansById[stream.PlanId]\nif !ok {\n    log.Printf(\"Stream %s has no plan\\n\", stream.Id)\n    http.Error(w, \"Stream has no plan\", http.StatusInternalServerError)\n    return\n}\n// after\napiPlan, ok := apiPlansById[stream.PlanId]\nif !ok {\n    log.Printf(\"Stream %s has no plan; skipping\\n\", stream.Id)\n    continue\n}","handlingStrategy":"type-guard","validationCode":"// Guard: filter streams to those whose plan is present before building the response\nvalid := streams[:0]\nfor _, s := range streams {\n    if _, ok := apiPlansById[s.PlanId]; ok {\n        valid = append(valid, s)\n    }\n}","typeGuard":"func streamHasPlan(stream *db.ModelStream, plansById map[string]*shared.Plan) bool {\n    _, ok := plansById[stream.PlanId]\n    return ok\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 plan\") {\n        // re-fetch plan list or clear local session state before retrying\n    }\n}","preventionTips":["When deleting a plan, finish/archive all its model streams in the same transaction","Fetch plans and streams with the same scoping filters (project, owner, org)","Sweep orphaned streams on a schedule","Never reuse planIds; keep streams pointing only at live plans"],"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"}