wavetermdev/waveterm · error
no job attached to controller
Error message
no job attached to controller
What it means
SendInput on DurableShellController requires an attached durable job to route input to. If the controller has no JobId (job never started, was stopped, or Start failed), there is no target session and input is rejected. It is a state precondition check, not a transport error.
Source
Thrown at pkg/blockcontroller/durableshellcontroller.go:211
if !destroy {
return
}
jobId := dsc.getJobId()
if jobId == "" {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
jobcontroller.TerminateAndDetachJob(ctx, jobId)
}
func (dsc *DurableShellController) SendInput(inputUnion *BlockInputUnion) error {
if inputUnion == nil {
return nil
}
jobId := dsc.getJobId()
if jobId == "" {
return fmt.Errorf("no job attached to controller")
}
inputSessionId, seqNum := dsc.getNextInputSeq()
data := wshrpc.CommandJobInputData{
JobId: jobId,
InputSessionId: inputSessionId,
SeqNum: seqNum,
TermSize: inputUnion.TermSize,
SigName: inputUnion.SigName,
}
if len(inputUnion.InputData) > 0 {
data.InputData64 = base64.StdEncoding.EncodeToString(inputUnion.InputData)
}
return jobcontroller.SendInput(context.Background(), data)
}
func (dsc *DurableShellController) startNewJob(ctx context.Context, blockMeta waveobj.MetaMapType, connName string, rtOpts *waveobj.RuntimeOpts) (string, error) {
termSize := waveobj.TermSize{
Rows: shellutil.DefaultTermRows,View on GitHub (pinned to a4447c1563)
Solutions
- Call Start (and wait for it to succeed) before sending input.
- Check the controller status/state before writing input.
- If Start failed, retry the start flow to attach a new job.
- Gate the input UI on a 'connected' signal rather than block visibility.
Example fix
// before
dsc.SendInput(inputUnion) // panics-level error: no job attached
// after
if dsc.getJobId() == "" {
if err := dsc.Start(ctx, blockMeta, rtOpts, false); err != nil {
return err
}
}
return dsc.SendInput(inputUnion) Defensive patterns
Strategy: validation
Validate before calling
jobId := dsc.getJobId()
if jobId == "" {
return fmt.Errorf("cannot send input: durable shell not started")
}
err := dsc.SendInput(union) Type guard
func (dsc *DurableShellController) HasJob() bool {
return dsc.getJobId() != ""
} Try / catch
if err := dsc.SendInput(union); err != nil && strings.Contains(err.Error(), "no job attached") {
// recover: start the job, then retry once
if serr := dsc.Start(ctx, meta, rtOpts, false); serr == nil {
err = dsc.SendInput(union)
}
return err
} Prevention
- Only enable the terminal input UI after Start succeeds
- Reset input availability on Stop or failed reconnect
- Track job lifecycle state explicitly in calling code
When it happens
Trigger: Calling SendInput before Start succeeds, after Stop is invoked (job detached), or after startNewJob/ReconnectJob failed leaving dsc.JobId empty.
Common situations: UI race where the user types into a terminal block before the remote shell connects; a failed reconnect left the block alive but job-less; block restored from saved state without its job.
Related errors
- no shell input chan
- no controller found for block %s
- error getting block: %w
- no vdom context block id
- client is done
AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01).
Data as JSON: /api/errors/4027b088ef86c81f.
Report an issue: GitHub.