wavetermdev/waveterm · error

durable shell controller requires a remote connection

Error message

durable shell controller requires a remote connection

What it means

Durable shells rely on remote job management for persistence across restarts, so Start refuses to run when dsc.ConnName is a local connection. This error is a guard against starting a durable shell controller on a local terminal block.

Source

Thrown at pkg/blockcontroller/durableshellcontroller.go:146

}

// Start initializes or reconnects to a durable shell for the block.
// Logic:
// - If block has no existing jobId: starts a new job and attaches it
// - If block has existing jobId with running job manager: reconnects to existing job
// - If block has existing jobId with non-running job manager:
//   - force=true: detaches old job and starts new one
//   - force=false: returns without starting (leaves block unstarted)
//
// After establishing jobId, ensures job connection is active (reconnects if needed)
func (dsc *DurableShellController) Start(ctx context.Context, blockMeta waveobj.MetaMapType, rtOpts *waveobj.RuntimeOpts, force bool) error {
	blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, dsc.BlockId)
	if err != nil {
		return fmt.Errorf("error getting block: %w", err)
	}

	if conncontroller.IsLocalConnName(dsc.ConnName) {
		return fmt.Errorf("durable shell controller requires a remote connection")
	}

	var jobId string
	if blockData.JobId != "" {
		status, err := jobcontroller.GetJobManagerStatus(ctx, blockData.JobId)
		if err != nil {
			return fmt.Errorf("error getting job manager status: %w", err)
		}
		if status == jobcontroller.JobManagerStatus_Running {
			jobId = blockData.JobId
		} else if !force {
			log.Printf("block %q has jobId %s but manager is not running (status: %s), not starting (force=false)\n", dsc.BlockId, blockData.JobId, status)
			return nil
		} else {
			log.Printf("block %q has jobId %s but manager is not running (status: %s), starting new job (force=true)\n", dsc.BlockId, blockData.JobId, status)
			// intentionally leave jobId empty to trigger starting a new job below
		}
	}

View on GitHub (pinned to a4447c1563)

Solutions

  1. Set the block's connection to a remote SSH profile in block settings before using durable shell
  2. Use a regular (non-durable) shell controller for local terminals
  3. Verify meta.connection is populated with a remote conn name at controller construction time

Example fix

// before
"meta": { "connection": "" } // local -> durable shell refuses
// after
"meta": { "connection": "conn:myprofile" } // remote profile
Defensive patterns

Strategy: validation

Validate before calling

if conncontroller.IsLocalConnName(connName) {
    // use a plain shell controller, not DurableShellController
}

Try / catch

if err := dsc.Start(ctx, meta, rtOpts, true); err != nil {
    if strings.Contains(err.Error(), "requires a remote connection") { /* fall back to local controller */ }
}

Prevention

When it happens

Trigger: Start is invoked with the controller constructed with ConnName set to a local connection name (IsLocalConnName true), typically because the block's meta connection was cleared or set to local.

Common situations: User switched a terminal block from a remote connection back to local while a durable shell controller was still wired up; block created locally but durable-shell code path triggered by config; meta.connection removed.

Related errors


AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01). Data as JSON: /api/errors/f5870756050d6e65. Report an issue: GitHub.