{"record":{"id":"4027b088ef86c81f","repo":"wavetermdev/waveterm","slug":"no-job-attached-to-controller","errorCode":null,"errorMessage":"no job attached to controller","messagePattern":"no job attached to controller","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/blockcontroller/durableshellcontroller.go","lineNumber":211,"sourceCode":"\tif !destroy {\n\t\treturn\n\t}\n\tjobId := dsc.getJobId()\n\tif jobId == \"\" {\n\t\treturn\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tjobcontroller.TerminateAndDetachJob(ctx, jobId)\n}\n\nfunc (dsc *DurableShellController) SendInput(inputUnion *BlockInputUnion) error {\n\tif inputUnion == nil {\n\t\treturn nil\n\t}\n\tjobId := dsc.getJobId()\n\tif jobId == \"\" {\n\t\treturn fmt.Errorf(\"no job attached to controller\")\n\t}\n\tinputSessionId, seqNum := dsc.getNextInputSeq()\n\tdata := wshrpc.CommandJobInputData{\n\t\tJobId:          jobId,\n\t\tInputSessionId: inputSessionId,\n\t\tSeqNum:         seqNum,\n\t\tTermSize:       inputUnion.TermSize,\n\t\tSigName:        inputUnion.SigName,\n\t}\n\tif len(inputUnion.InputData) > 0 {\n\t\tdata.InputData64 = base64.StdEncoding.EncodeToString(inputUnion.InputData)\n\t}\n\treturn jobcontroller.SendInput(context.Background(), data)\n}\n\nfunc (dsc *DurableShellController) startNewJob(ctx context.Context, blockMeta waveobj.MetaMapType, connName string, rtOpts *waveobj.RuntimeOpts) (string, error) {\n\ttermSize := waveobj.TermSize{\n\t\tRows: shellutil.DefaultTermRows,","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/blockcontroller/durableshellcontroller.go#L193-L229","documentation":"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.","triggerScenarios":"Calling SendInput before Start succeeds, after Stop is invoked (job detached), or after startNewJob/ReconnectJob failed leaving dsc.JobId empty.","commonSituations":"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.","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."],"exampleFix":"// before\ndsc.SendInput(inputUnion) // panics-level error: no job attached\n// after\nif dsc.getJobId() == \"\" {\n    if err := dsc.Start(ctx, blockMeta, rtOpts, false); err != nil {\n        return err\n    }\n}\nreturn dsc.SendInput(inputUnion)","handlingStrategy":"validation","validationCode":"jobId := dsc.getJobId()\nif jobId == \"\" {\n    return fmt.Errorf(\"cannot send input: durable shell not started\")\n}\nerr := dsc.SendInput(union)","typeGuard":"func (dsc *DurableShellController) HasJob() bool {\n    return dsc.getJobId() != \"\"\n}","tryCatchPattern":"if err := dsc.SendInput(union); err != nil && strings.Contains(err.Error(), \"no job attached\") {\n    // recover: start the job, then retry once\n    if serr := dsc.Start(ctx, meta, rtOpts, false); serr == nil {\n        err = dsc.SendInput(union)\n    }\n    return err\n}","preventionTips":["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"],"tags":["state","durable-shell","input","lifecycle"],"backgroundTag":"controller-not-started","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}