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 onView on GitHub (pinned to 0c42157ee5)
Solutions
- Verify the command runs and stays alive when launched manually.
- Check the executable path/arguments for the persistent service.
- 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
- Only mark a command persist_pending if it is a long-lived service; short-lived commands belong in normal background/sync mode.
- Verify the executable path resolves inside the workspace before spawning.
- Smoke-test the service command in the foreground once before promoting it to a persistent service.
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
- read-only executable path is not valid UTF-8
- Failed to capture stdout
- Failed to capture stderr
- tmux new-session failed with {status}
- MCP server '{server_name}' has no command configured
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/71bb25fb61e13619.
Report an issue: GitHub.