plandex-ai/plandex · warning

plan %s branch %s already has an active stream on this host

Error message

plan %s branch %s already has an active stream on this host

What it means

activatePlan enforces single-writer semantics per plan+branch on this host. After a short wait it checks GetActivePlan; if an active plan already exists for the same plan ID and branch, it refuses to start another stream and returns this error. This prevents two concurrent model streams from fighting over the same plan branch.

Source

Thrown at app/server/model/plan/activate.go:35

	plan *db.Plan,
	branch string,
	auth *types.ServerAuth,
	prompt string,
	buildOnly,
	autoContext bool,
	sessionId string,
) (*types.ActivePlan, error) {
	log.Printf("Activate plan: plan ID %s on branch %s\n", plan.Id, branch)

	// Just in case this request was made immediately after another stream finished, wait a little to allow for cleanup
	log.Println("Waiting 100ms before checking for active plan")
	time.Sleep(100 * time.Millisecond)
	log.Println("Done waiting, checking for active plan")

	active := GetActivePlan(plan.Id, branch)
	if active != nil {
		log.Printf("Tell: Active plan found for plan ID %s on branch %s\n", plan.Id, branch) // Log if an active plan is found
		return nil, fmt.Errorf("plan %s branch %s already has an active stream on this host", plan.Id, branch)
	}

	modelStream, err := db.GetActiveModelStream(plan.Id, branch)
	if err != nil {
		log.Printf("Error getting active model stream: %v\n", err)
		return nil, fmt.Errorf("error getting active model stream: %v", err)
	}

	if modelStream != nil {
		log.Printf("Tell: Active model stream found for plan ID %s on branch %s on host %s\n", plan.Id, branch, modelStream.InternalIp) // Log if an active model stream is found
		return nil, fmt.Errorf("plan %s branch %s already has an active stream on host %s", plan.Id, branch, modelStream.InternalIp)
	}

	active = CreateActivePlan(
		auth.OrgId,
		auth.User.Id,
		plan.Id,
		branch,

View on GitHub (pinned to e2d772072e)

Solutions

  1. Check if an existing stream is intentional; if it is stale, cancel/deregister it and retry.
  2. Wait for the current stream to finish (subscribe to its stream events) instead of starting a new one.
  3. Verify ActivePlan cleanup runs when a stream ends or the process crashes (crash-recovery sweep).
  4. Serialize client-side: guard against duplicate Tell calls for the same plan/branch.

Example fix

// before: blind retry
resp, err := Tell(ctx, req)
// after: treat 'already active' as expected
resp, err := Tell(ctx, req)
if err != nil && strings.Contains(err.Error(), "already has an active stream") {
    return subscribeExisting(ctx, req.PlanId, req.Branch)
}
Defensive patterns

Strategy: type-guard

Validate before calling

active := model.GetActivePlan(planId, branch)
if active != nil {
    return fmt.Errorf("stream already active for plan %s branch %s", planId, branch)
}
// safe to call Tell

Type guard

func isActiveStream(r *shared.StreamRes) bool { return r != nil }

Try / catch

resp, err := Tell(ctx, req)
if err != nil && strings.Contains(err.Error(), "already has an active stream on this host") {
    resp, err = subscribeToExistingStream(ctx, req.PlanId, req.Branch)
}

Prevention

When it happens

Trigger: Calling activatePlan (via Tell or loadPendingBuilds) while another stream for the same plan.Id + branch is still registered as active on this host.

Common situations: Double-submitting a Tell from a retried UI request, a previous stream that crashed without deregistering, or concurrent webhooks/CLI calls triggering the same plan branch.

Related errors


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