Hmbown/CodeWhale · error · anyhow::Error

Persistent service has no process group id

Error message

Persistent service has no process group id

What it means

For background shells spawned with persist_pending on Unix, the child must expose a process id so its process group can be registered (register_pending_persistent_process_group) for later lifecycle management/cleanup. If the child handle yields no pid — the process exited and was reaped before registration, or the spawn variant does not track a pid — registration fails with this error before stdin/lifecycle publishing.

Source

Thrown at crates/tui/src/tools/shell.rs:2610

            stdin,
            child: Some(child),
            #[cfg(windows)]
            windows_job,
            stdout_thread,
            stderr_thread,
            work_lifecycle,
            lifecycle_seq: 0,
            last_lifecycle_status: None,
            last_lifecycle_bytes: 0,
        };

        #[cfg(unix)]
        if persist_pending {
            let process_group_id = bg_shell
                .child
                .as_ref()
                .and_then(ShellChild::process_id)
                .ok_or_else(|| anyhow!("Persistent service has no process group id"))?;
            register_pending_persistent_process_group(process_group_id);
        }

        if let Some(input) = stdin_data
            && let Err(err) = bg_shell.write_stdin(input, false)
        {
            let _ = bg_shell.kill();
            return Err(err);
        }

        if let Err(err) = bg_shell.publish_lifecycle() {
            let _ = bg_shell.kill();
            return Err(err);
        }

        self.processes.insert(task_id.clone(), bg_shell);
        spawn_guard.disarm();
        // Evict here, not only from `list_jobs()`: retention must not depend on

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Verify the command runs and stays alive when launched manually.
  2. Check the executable path/arguments for the persistent service.
  3. If the service legitimately exits quickly, run it as a normal (non-persistent) background or sync command instead of persist_pending.

Example fix

# before
exec_shell("./missing-binary --serve", { background: true, persist: true })

# after
exec_shell("./existing-binary --serve", { background: true, persist: true })
Defensive patterns

Strategy: retry

Validate before calling

// Pre-check the service binary exists and is executable before persist-spawn.
let program = command.split_whitespace().next().unwrap_or_default();
let resolved = resolve_in(workspace, program);
anyhow::ensure!(
    resolved.map(|p| p.is_file()).unwrap_or(false),
    "persistent service program '{program}' not found in workspace"
);

Try / catch

match spawn_persistent(command).await {
    Ok(task) => task,
    Err(e) if e.to_string().contains("no process group id") => {
        // Process died before registration: verify the command, then retry once.
        verify_program_exists(&command)?;
        spawn_persistent(command).await?
    }
    Err(e) => return Err(e),
};

Prevention

When it happens

Trigger: Registering a persistent service whose process dies instantly (bad executable path, immediate exit) so the pid is already gone; or a child wrapper on this path lacking process-id tracking.

Common situations: persist: true service pointing at a nonexistent binary or one that exits immediately; race between spawn and pid registration for very short-lived 'services'.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/71bb25fb61e13619. Report an issue: GitHub.