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
- Set the block's connection to a remote SSH profile in block settings before using durable shell
- Use a regular (non-durable) shell controller for local terminals
- 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
- Pick controller type based on meta.connection before starting
- Don't convert remote terminal blocks to local while durable controllers are active
- Document that durable shell is remote-only
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
- No tab view found for the given webContents id
- cannot call ${methodName}: no web endpoint
- ai:model is required
- chatOpts.ClientId is required
- ai:endpoint is required
AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01).
Data as JSON: /api/errors/f5870756050d6e65.
Report an issue: GitHub.