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

  1. Call Start (and wait for it to succeed) before sending input.
  2. Check the controller status/state before writing input.
  3. If Start failed, retry the start flow to attach a new job.
  4. 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

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


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