{"record":{"id":"1eae38de87d01141","repo":"wavetermdev/waveterm","slug":"job-already-started","errorCode":null,"errorMessage":"job already started","messagePattern":"job already started","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/jobmanager/jobmanager.go","lineNumber":228,"sourceCode":"\nfunc (jm *JobManager) SetAttachedClient(msc *MainServerConn) {\n\tjm.lock.Lock()\n\tdefer jm.lock.Unlock()\n\n\tif jm.attachedClient != nil {\n\t\tlog.Printf(\"SetAttachedClient: kicking out existing client\\n\")\n\t\tjm.attachedClient.Close()\n\t}\n\tjm.attachedClient = msc\n}\n\nfunc (jm *JobManager) StartJob(msc *MainServerConn, data wshrpc.CommandStartJobData) (*wshrpc.CommandStartJobRtnData, error) {\n\tjm.lock.Lock()\n\tdefer jm.lock.Unlock()\n\n\tif jm.Cmd != nil {\n\t\tlog.Printf(\"StartJob: job already started\")\n\t\treturn nil, fmt.Errorf(\"job already started\")\n\t}\n\n\tcmdDef := CmdDef{\n\t\tCmd:      data.Cmd,\n\t\tArgs:     data.Args,\n\t\tEnv:      data.Env,\n\t\tTermSize: data.TermSize,\n\t}\n\tlog.Printf(\"StartJob: creating job cmd for jobid=%s\", jm.JobId)\n\tjobCmd, err := MakeJobCmd(jm.JobId, cmdDef)\n\tif err != nil {\n\t\tlog.Printf(\"StartJob: failed to make job cmd: %v\", err)\n\t\treturn nil, fmt.Errorf(\"failed to start job: %w\", err)\n\t}\n\tjm.Cmd = jobCmd\n\tlog.Printf(\"StartJob: job cmd created successfully\")\n\n\tif data.StreamMeta != nil {","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/jobmanager/jobmanager.go#L210-L246","documentation":"StartJob enforces that a JobManager can own only one running process; if jm.Cmd is already set, it refuses to start another and returns 'job already started'. This guards against double-spawning the job's command on repeated start requests.","triggerScenarios":"Calling StartJob twice for the same jobId — e.g. a client retry after a slow first response, two panes starting the same job, or a stale client re-sending start after reconnect while the old process still runs.","commonSituations":"User clicks 'start job' twice quickly; an automatic reconnect logic re-issues StartJob; orchestration script assumes start is idempotent.","solutions":["Check job status first (jm.Cmd != nil equivalent via the job status API) and skip StartJob if already running","Treat this error as success/idempotent: fetch the existing job handle instead of starting a new one","Use a distinct jobId for each new job instance","If the old job is defunct, stop/kill it and wait for the process to be reaped before calling StartJob again"],"exampleFix":"// before\njm.StartJob(msc, startData) // may error if already started\n// after\nstatus := jm.GetJobStatus()\nif status.Running {\n    return existingHandle, nil // idempotent\n}\nreturn jm.StartJob(msc, startData)","handlingStrategy":"validation","validationCode":"if jobStatus(jobId).Running {\n    return existingJobHandle, nil // already started; skip StartJob\n}","typeGuard":null,"tryCatchPattern":"rtn, err := jm.StartJob(msc, startData)\nif err != nil && err.Error() == \"job already started\" {\n    // idempotent success: reuse existing job handle\n    return jm.GetExistingJobHandle(), nil\n}\nreturn rtn, err","preventionTips":["Make start flows idempotent (query status first)","Use a unique jobId per job instance","Debounce duplicate start requests in the UI","On reconnect, check running state before re-issuing StartJob"],"tags":["jobmanager","state-conflict","idempotency"],"backgroundTag":"already-started","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}