From 024d8906aa831375b79936e2f7682b9e56ea8e62 Mon Sep 17 00:00:00 2001 From: Nitay Rabinovich Date: Wed, 13 May 2026 14:13:17 +0200 Subject: [PATCH] recover openclaw prompt metadata sessions --- docs/past-failures.md | 2 +- internal/acpx/runner.go | 19 +++++++-- internal/acpx/runner_test.go | 78 ++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/docs/past-failures.md b/docs/past-failures.md index 93fabe0..c4f5cae 100644 --- a/docs/past-failures.md +++ b/docs/past-failures.md @@ -4,7 +4,7 @@ - Symptom: production OpenClaw sessions failed with `ACP_SESSION_INIT_FAILED` and `ACP metadata is missing for agent:main:acp:`, instructing the operator to recreate the ACP session with `/acp spawn`. - Cause: the node daemon reused the server session id as the ACPX session name, but a stale ACPX session could exist without the backing ACP metadata OpenClaw needs to initialize. -- Mitigation: the Go ACPX runner now treats that exact metadata-missing failure as recoverable, recreates the named ACPX session once with `sessions new --name `, and retries `sessions ensure`. A regression test covers the failure, recreate, and retry sequence. +- Mitigation: the Go ACPX runner now treats that exact metadata-missing failure as recoverable both during `sessions ensure` and during the first prompt run, recreates the named ACPX session once with `sessions new --name `, and retries. Regression tests cover the ensure path and the OpenClaw prompt-time failure shape. ## 2026-05-12: Installer rejected valid Node 24 runtimes diff --git a/internal/acpx/runner.go b/internal/acpx/runner.go index 1be1e5c..5296247 100644 --- a/internal/acpx/runner.go +++ b/internal/acpx/runner.go @@ -74,6 +74,13 @@ func (Runner) Run( if err := (Runner{}).Ensure(ctx, request); err != nil { return nil, fmt.Errorf("ensure acpx session: %w", err) } + output, err := runStreamingCommand(ctx, command, buildPromptArgs(request), request.WorkingDir, request.Env, request.Stdin, onStdoutLine) + if err == nil || request.Session == "" || !isMissingACPMetadataError(err) { + return output, err + } + if newErr := runCommand(ctx, command, buildSessionNewArgs(request), request.WorkingDir, request.Env, ""); newErr != nil { + return output, fmt.Errorf("%w; recreate acpx session after missing ACP metadata: %v", err, newErr) + } return runStreamingCommand(ctx, command, buildPromptArgs(request), request.WorkingDir, request.Env, request.Stdin, onStdoutLine) } @@ -223,6 +230,7 @@ func runStreamingCommand( var ( stdoutBuf bytes.Buffer + stderrBuf bytes.Buffer mu sync.Mutex wg sync.WaitGroup ) @@ -246,16 +254,19 @@ func runStreamingCommand( go func() { defer wg.Done() // Drain stderr so the child process never blocks on a full pipe. - // We intentionally discard the contents: acpx stderr is operator noise - // (banners, [acpx] tokens summary, http debug) and never carries protocol - // state. Surfacing it would let it leak into chat transcripts again. - _, _ = io.Copy(io.Discard, stderr) + // Keep it out of stdout/chat events, but retain it for errors because + // provider init failures such as OpenClaw's ACP metadata error arrive + // there. + _, _ = io.Copy(&stderrBuf, stderr) }() err = cmd.Wait() wg.Wait() output := stdoutBuf.Bytes() if err != nil { + if stderrText := strings.TrimSpace(stderrBuf.String()); stderrText != "" { + return output, fmt.Errorf("run acpx: %w: %s", err, stderrText) + } return output, fmt.Errorf("run acpx: %w", err) } return output, nil diff --git a/internal/acpx/runner_test.go b/internal/acpx/runner_test.go index a381806..1d5f18c 100644 --- a/internal/acpx/runner_test.go +++ b/internal/acpx/runner_test.go @@ -65,6 +65,38 @@ func TestRunnerRecreatesSessionWhenACPMetadataIsMissing(t *testing.T) { } } +func TestRunnerRecreatesSessionWhenOpenClawPromptACPMetadataIsMissing(t *testing.T) { + t.Parallel() + + statePath := filepath.Join(t.TempDir(), "state") + command, args := helperCommand(t, "recover-openclaw-prompt-missing-acp-metadata") + output, err := (Runner{}).Run(context.Background(), RunRequest{ + Command: command, + Args: args, + Agent: "openclaw", + Session: "agent-main-acp-d9d29d47", + Stdin: "review this", + Env: []string{ + "GO_WANT_HELPER_PROCESS=1", + "ACP_METADATA_RECOVERY_STATE=" + statePath, + }, + }, nil) + if err != nil { + t.Fatalf("run: %v", err) + } + if !strings.Contains(string(output), "prompt-ok") { + t.Fatalf("output = %q, want prompt-ok", output) + } + + bytes, err := os.ReadFile(statePath) + if err != nil { + t.Fatal(err) + } + if got, want := strings.TrimSpace(string(bytes)), "ensure\nprompt\nnew\nprompt"; got != want { + t.Fatalf("commands = %q, want %q", got, want) + } +} + func TestRunnerCancelsProcess(t *testing.T) { t.Parallel() @@ -146,6 +178,8 @@ func TestRunnerHelperProcess(t *testing.T) { os.Exit(0) case "recover-missing-acp-metadata": helperRecoverMissingACPMetadata() + case "recover-openclaw-prompt-missing-acp-metadata": + helperRecoverOpenClawPromptMissingACPMetadata() default: fmt.Fprintf(os.Stderr, "unknown helper mode %q", mode) os.Exit(2) @@ -185,6 +219,50 @@ func helperRecoverMissingACPMetadata() { os.Exit(0) } +func helperRecoverOpenClawPromptMissingACPMetadata() { + statePath := os.Getenv("ACP_METADATA_RECOVERY_STATE") + if statePath == "" { + fmt.Fprintln(os.Stderr, "missing ACP_METADATA_RECOVERY_STATE") + os.Exit(2) + } + + args := os.Args + command := "" + for index := 0; index+2 < len(args); index++ { + if args[index] != "openclaw" { + continue + } + switch { + case args[index+1] == "sessions": + command = args[index+2] + case args[index+1] == "--session" || (index+2 < len(args) && args[index+1] == "agent-main-acp-d9d29d47"): + command = "prompt" + default: + command = "prompt" + } + break + } + if command == "" { + fmt.Fprintf(os.Stderr, "missing openclaw command in %q", args) + os.Exit(2) + } + + previous, _ := os.ReadFile(statePath) + if err := os.WriteFile(statePath, append(previous, []byte(command+"\n")...), 0o600); err != nil { + fmt.Fprintf(os.Stderr, "write state: %v", err) + os.Exit(2) + } + + if command == "prompt" && !strings.Contains(string(previous), "new\n") { + fmt.Fprintln(os.Stderr, "ACP error (ACP_SESSION_INIT_FAILED): ACP metadata is missing for agent:main:acp:d9d29d47-c7c5-40d0-9773-e464d4430352. Recreate this ACP session with /acp spawn and rebind the thread. next: If this session is stale, recreate it with /acp spawn and rebind the thread.") + os.Exit(1) + } + if command == "prompt" { + fmt.Fprintln(os.Stdout, "prompt-ok") + } + os.Exit(0) +} + func helperCommand(t *testing.T, mode string) (string, []string) { t.Helper()