alibaba/spring-ai-alibaba · warning

Shutdown command failed: {}

Error message

Shutdown command failed: {}

What it means

ShellSessionManager.cleanup runs configured shutdown commands (e.g. 'exit') through the still-open shell session before tearing it down. If any single shutdown command throws while executing, this warning is logged with the command and exception, and the loop continues to the next command; doCleanup then runs in the finally block regardless. Session teardown is never skipped because of a failed shutdown command.

Source

Thrown at spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/tools/ShellSessionManager.java:166

	/**
	 * Clean up shell session.
	 * This removes the session from both the context and the global registry.
	 */
	public void cleanup(RunnableConfig config) {
		try {
			// Try to get session from context first, then from registry
			ShellSession session = (ShellSession) config.context().get(SESSION_INSTANCE_CONTEXT_KEY);
			if (session == null) {
				session = getSessionFromRegistry(config);
			}
			if (session != null) {
				// Run shutdown commands
				for (String command : shutdownCommands) {
					try {
						session.execute(command, commandTimeout, maxOutputLines, maxOutputBytes);
					} catch (Exception e) {
						log.warn("Shutdown command failed: {}", command, e);
					}
				}
			}
		} finally {
			doCleanup(config);
		}
	}

	private void doCleanup(RunnableConfig config) {
		// Try to get session from context first
		ShellSession session = (ShellSession) config.context().get(SESSION_INSTANCE_CONTEXT_KEY);
		Path tempDir = (Path) config.context().get(SESSION_PATH_CONTEXT_KEY);

		// If not in context, try to get from registry (HITL resume scenario)
		SessionEntry registryEntry = null;
		if (session == null && config.threadId().isPresent()) {
			registryEntry = SESSION_REGISTRY.remove(config.threadId().get());
			if (registryEntry != null) {

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Order shutdownCommands so terminating commands ('exit') come last, or drop redundant exits.
  2. Increase commandTimeout if the shutdown script legitimately needs longer.
  3. Treat the warning as benign when it stems from an already-exited shell, or filter that case before executing.
  4. Ensure only one component owns session lifecycle to avoid concurrent cleanup.

Example fix

// before
shutdownCommands = List.of("exit", "sync"); // 'sync' runs after shell died
// after
shutdownCommands = List.of("sync", "exit");
Defensive patterns

Strategy: try-catch

Validate before calling

if (shutdownCommands != null) {
    for (int i = 0; i < shutdownCommands.size(); i++) {
        if ("exit".equalsIgnoreCase(shutdownCommands.get(i).trim()) && i < shutdownCommands.size() - 1) {
            log.warn("'exit' is not the last shutdown command; later commands will fail");
        }
    }
}

Try / catch

try {
    session.execute(cmd, commandTimeout, maxOutputLines, maxOutputBytes);
} catch (Exception e) {
    log.warn("Shutdown command failed: {}", cmd, e); // safe to ignore: session is being torn down
}

Prevention

When it happens

Trigger: cleanup's session.execute(command, commandTimeout, maxOutputLines, maxOutputBytes) throws for a shutdown command — typically because the shell already exited (e.g. an earlier 'exit' terminated it), the command timed out, or the session's streams are broken.

Common situations: 1) shutdownCommands containing 'exit' followed by another command, so the second command executes on a dead shell. 2) The child process crashed or was killed before cleanup. 3 commandTimeout too short for a slow teardown script. 4 Tests/agents closing sessions concurrently, racing the cleanup.

Related errors


AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09). Data as JSON: /api/errors/2733add119792aee. Report an issue: GitHub.