From fc3f402ee95efcde54106386e84c48881fdb978a Mon Sep 17 00:00:00 2001 From: onevcat Date: Tue, 21 Jul 2026 22:31:28 +0900 Subject: [PATCH] Make resume-fork side-effect-free for the source session Claude Code's `--resume` defaults to continuing the same session ID, so a headless preparation appended turns to the transcript of a session still live in the pane. `--fork-session` forks the preparation turn onto a new ID; Codex `--ephemeral` keeps it out of ~/.codex/sessions entirely. Claude-Session: https://claude.ai/code/session_01EtBU9JYAcRp3nUuPfGH3n8 --- .../AgentRuntime/AgentRuntimeAdapter.swift | 17 ++++++++++++----- supacodeTests/AgentRuntimeAdapterTests.swift | 4 +++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/supacode/Domain/AgentRuntime/AgentRuntimeAdapter.swift b/supacode/Domain/AgentRuntime/AgentRuntimeAdapter.swift index 48b55164..39ed6d0d 100644 --- a/supacode/Domain/AgentRuntime/AgentRuntimeAdapter.swift +++ b/supacode/Domain/AgentRuntime/AgentRuntimeAdapter.swift @@ -7,9 +7,11 @@ nonisolated protocol AgentRuntimeAdapter: Sendable { func observe(arguments: [String]) -> AgentLaunchObservation func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation - /// Resume is read-only by design: the invocation never renders execution-mode - /// flags. The resumed agent replies with content; Prowl persists any artifact. - /// `replyFile`, when supported, asks the CLI to write its final message there. + /// Resume is side-effect-free by design: the invocation never renders + /// execution-mode flags, and it must not mutate the source session's recorded + /// state (fork/ephemeral variants only). The resumed agent replies with + /// content; Prowl persists any artifact. `replyFile`, when supported, asks the + /// CLI to write its final message there. func makeResumeInvocation(_ request: AgentResumeRequest, replyFile: URL?) throws -> AgentInvocation } @@ -178,7 +180,9 @@ nonisolated private struct CodexRuntimeAdapter: AgentRuntimeAdapter { } func makeResumeInvocation(_ request: AgentResumeRequest, replyFile: URL?) throws -> AgentInvocation { - var arguments = ["exec", "resume"] + // `--ephemeral` keeps the preparation turn out of `~/.codex/sessions`, so a + // resume never mutates the recorded state of the live source session. + var arguments = ["exec", "resume", "--ephemeral"] if let model = request.model { arguments += ["--model", model] } @@ -223,7 +227,10 @@ nonisolated private struct ClaudeCodeRuntimeAdapter: AgentRuntimeAdapter { func makeResumeInvocation(_ request: AgentResumeRequest, replyFile: URL?) throws -> AgentInvocation { // `claude -p` prints only the final reply on stdout, so no reply file is needed. - var arguments = ["-p", "--resume", request.session.id] + // `--fork-session` is load-bearing: without it `--resume` continues the same + // session ID and appends the preparation turn to the transcript of a session + // that is usually still live in the pane (dual-writer on one JSONL). + var arguments = ["-p", "--fork-session", "--resume", request.session.id] if let model = request.model { arguments += ["--model", model] } diff --git a/supacodeTests/AgentRuntimeAdapterTests.swift b/supacodeTests/AgentRuntimeAdapterTests.swift index 8a993c10..7f5b525f 100644 --- a/supacodeTests/AgentRuntimeAdapterTests.swift +++ b/supacodeTests/AgentRuntimeAdapterTests.swift @@ -43,6 +43,7 @@ struct AgentRuntimeAdapterTests { invocation.arguments == [ "-p", + "--fork-session", "--resume", "9B0E3B0E-67B3-4D45-A3A0-7DD9BC713711", "--model", @@ -76,6 +77,7 @@ struct AgentRuntimeAdapterTests { == [ "exec", "resume", + "--ephemeral", "--model", "gpt-5.4", "--output-last-message", @@ -187,7 +189,7 @@ struct AgentRuntimeAdapterTests { #expect(reply == "## Objective\nreply") #expect(recordedExecutable.value?.path == "/usr/bin/env") let arguments = recordedArguments.value - #expect(arguments.prefix(3) == ["codex", "exec", "resume"]) + #expect(arguments.prefix(4) == ["codex", "exec", "resume", "--ephemeral"]) #expect(arguments.contains("--output-last-message")) #expect(!arguments.contains("--dangerously-bypass-approvals-and-sandbox")) #expect( -- 2.51.2