Skip to content

fix: preserve ACP stream disconnect recovery - #319

Merged
pedronauck merged 2 commits into
mainfrom
codex/issue-315-acp-stream-disconnect
Aug 6, 2026
Merged

pedronauck merged 2 commits into
mainfrom
codex/issue-315-acp-stream-disconnect

Conversation

@pedronauck

@pedronauck pedronauck commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

Context and reproduction

A Codex ACP subprocess could disconnect while Compozy was still streaming the final response. The session manager persisted the chunks it had already received, but its fatal-path cleanup raced the subprocess' natural exit. That race replaced the real process result with a generic wait override, discarded useful exit diagnostics, and left stream consumers unable to distinguish a complete stream from a disconnected or crashed process.

The visible symptoms were:

  • already-emitted response chunks could exist without a trustworthy terminal outcome;
  • CLI JSONL consumers could reach EOF and report success even though no completion event arrived;
  • the native compozy__session_prompt tool could drain a terminal runtime error and still return success;
  • crash bundles preserved stderr but not the subprocess exit code or signal;
  • the next action did not have a clearly documented recovery contract.

Proven root cause

There were four connected causes:

  1. manager_prompt_runtime_loop.go installed a wait override and immediately stopped the ACP process, racing the operating-system wait path.
  2. The session AgentProcess.Wait wrapper returned the override without waiting for the underlying process, so it lost the subprocess exit result and diagnostics.
  3. Crash bundle schema v1 had no structured exit-code or signal fields.
  4. The CLI and native-tool stream consumers treated transport exhaustion as sufficient success instead of requiring an explicit terminal completion event.

The regression fixture reproduces a subprocess that emits partial output and then exits with code 23. The integration test proves the previous ambiguity across HTTP/SSE, persisted transcript, CLI JSONL, crash evidence, native tool invocation, and same-session recovery.

Solution design

This change uses a fail-closed stream contract:

  • persisted chunks remain committed and visible;
  • success requires an explicit completion event (done, finish, prompt_result, or [DONE]);
  • disconnect, terminal error, and process_exit remain distinct failure outcomes;
  • fatal prompt cleanup gives the subprocess a bounded grace period to exit naturally before force-stopping it;
  • the session wait path always observes the underlying subprocess and joins the runtime override with the real wait error;
  • subprocess exit code and signal are captured immediately after cmd.Wait and propagated into crash evidence;
  • Compozy does not automatically replay a prompt after a disconnect, because replay can repeat external side effects;
  • a later explicit prompt restarts the ACP subprocess while retaining the same Compozy session and persisted transcript.

Changes by surface

Runtime and process lifecycle

  • Added cross-platform subprocess exit-status capture with Unix signal support and a Windows-safe implementation.
  • Exposed optional exit status through the ACP process boundary.
  • Removed the fatal-path stop/wait race and added a bounded natural-exit grace period.
  • Changed the session wait wrapper to observe both the underlying process and the higher-level runtime failure.
  • Hard-cut the crash bundle contract to compozy.session_crash_bundle.v2 with structured exit_code and signal.

HTTP/SSE and CLI

  • CLI prompt streaming now requires an explicit terminal completion marker.
  • A stream ending after partial frames without completion returns a clear non-zero failure.
  • Terminal error frames are forwarded before the CLI returns the error, preserving machine-readable diagnostics and already-received content.

UDS/native tools

  • compozy__session_prompt now classifies terminal session errors instead of treating a drained stream as success.
  • A subprocess process_exit becomes tool_backend_failed with backend_dead, while partial events remain available in the persisted session transcript.

Documentation and operator workflow

  • Updated session lifecycle documentation with the disconnect/recovery contract.
  • Updated failed-session debugging guidance for exit code, signal, persisted partial output, and explicit retry.
  • Updated the official Compozy skill runtime operations reference.
  • Added the tracked QA scenario, charter, journey coverage, and dated execution report.

Invariants and canonical tests

  • Session manager invariant: partial chunks persist, no completion is synthesized, the failure is explicit, and the next explicit prompt recovers the same session. Covered in the existing session manager prompt contract suite.
  • Subprocess invariant: exit code/signal survive process wait and remain queryable. Covered in the existing subprocess process suite.
  • CLI invariant: prior JSONL frames are emitted and incomplete/error streams exit non-zero. Covered in the existing CLI client/session suites.
  • Native-tool invariant: a terminal runtime failure cannot become tool success. Covered in the existing daemon native tools suite.
  • Public-surface invariant: a real daemon with the ACP fault fixture preserves partial transcript, emits the HTTP terminal error, fails CLI JSONL, records exit 23, fails the native tool clearly, and recovers the same session on the next explicit prompt. Covered by TestDaemonE2EACPmockCrashMidStreamProjectsRuntimeFailure.

QA and evidence

  • Scenario: RT-acp-stream-disconnect-recovery — pass
  • Surfaces walked: runtime, HTTP/SSE, CLI JSONL, persisted session transcript, crash bundle, native tool, explicit same-session recovery
  • Strict QA audit: pass, no blockers or warnings
  • Runtime teardown: clean, no surviving processes
  • Full repository gate: pass/current
    • fingerprint: 77bfa676781096cad214652973d291c4acd1073e
    • completed: 2026-08-05T21:18:18Z
    • 20,695 tests passed with 2 expected skips
    • Go/Bun lint, typecheck, tests, web build, code generation checks, Go build, and boundaries passed

Web/Docs impact

The web application needed no code change: it already renders persisted session events and explicit runtime error events from the daemon. The public contract changed at the runtime/consumer boundary, so session lifecycle and failure-debugging docs were updated. The QA scenario exercises the API and agent-manageable surfaces; there is no new UI control or client-side state.

Compozy Impact Audit

  • Native tools: compozy__session_prompt terminal classification changed so process_exit returns tool_backend_failed / backend_dead; tool ID, descriptor, input/output schema, digest, risk flags, and capability gates are unchanged. Existing native-tool and daemon integration suites cover the new failure result.
  • Extensibility and hooks: no new extension, hook, registry, bridge SDK, MCP sidecar, capability, or config lifecycle surface. Checked ACP launcher/process boundaries, session event streaming, native tool dispatch, HTTP/SSE, CLI, and official runtime operations guidance. Extensions continue to observe the same persisted events; only false success at terminal disconnect is removed.
  • Workspace data isolation: changed data is session-scoped process-failure evidence. Existing workspace_id propagation through session manager, store, HTTP/SSE, CLI, native tools, event persistence, and transcript reads is unchanged. The integration test creates and reads the affected session through one workspace and verifies recovery by the same session ID; no global cache or cross-workspace list/read path was added.
  • Official Compozy skill: updated skills/compozy/references/runtime-operations.md to document explicit terminal completion, retained partial events, process diagnostics, and safe explicit retry.

Compatibility and risks

This is a greenfield hard cut: crash bundles now use schema v2, with no v1 compatibility branch. Stream consumers that previously relied on bare EOF as success will now receive the correct failure unless the daemon emitted an explicit completion event.

Automatic retry is intentionally excluded because a prompt may already have caused external side effects before the process disconnected. Recovery is explicit and reuses the same session. Signal diagnostics are present when the operating system exposes a signal; otherwise the exit code, stderr tail, runtime error, and persisted events remain available.

The deterministic ACP fault fixture exercises the exact process boundary without depending on an unreliable live-provider crash. The remaining risk is provider-specific behavior outside that boundary, mitigated by the common subprocess/ACP lifecycle and public-surface integration coverage.

Scope relative to #306

This PR does not change ACP protocol-version negotiation, initialize-handshake compatibility, provider presets, or Windows executable discovery. The only conceptual overlap is the shared ACP subprocess failure boundary; this diff is limited to stream disconnect classification, process-exit diagnostics, safe recovery, and client-visible failure semantics.

Fixes #315

Summary by CodeRabbit

  • Bug Fixes
    • Improved session prompt streaming reliability by detecting missing completion events and reporting terminal failures clearly.
    • Partial responses and error details are now preserved when streams fail.
    • Native tool failures now provide clearer backend status and process-related diagnostics.
    • Sessions can recover more reliably after an agent process crash.
    • Crash diagnostics now include available exit codes, termination signals, and process stderr for easier troubleshooting.

@vercel

vercel Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
compozy-site Ready Ready Preview Aug 5, 2026 9:23pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds portable process exit diagnostics, enforces terminal events for prompt streams, propagates fatal failures through session lifecycle handling, preserves partial events, maps native tool failures, and records process metadata in version 2 crash bundles.

Changes

Session failure handling

Layer / File(s) Summary
Prompt stream terminal contract
internal/acp/prompt_stream_terminal.go, internal/api/core/prompt_stream_*, internal/cli/client_session_prompt.go, internal/cli/*_test.go
Prompt streams now emit or require terminal events. Buffered and streaming clients preserve prior events and return terminal or incomplete-stream errors.
Process exit diagnostics
internal/subprocess/*, internal/acp/launcher.go, internal/acp/agent_process.go, internal/sandbox/daytona/*
Processes capture exit codes and signals. Local, ACP, and Daytona handles expose exit status when available.
Session failure finalization
internal/session/interfaces.go, internal/session/session*.go, internal/session/manager_*.go, internal/session/crash_bundle*
Session handling coordinates prompt completion, cancellation, process exit, fatal finalization, recovery, and crash-bundle persistence. Crash bundles store exit code, signal, and completed-process stderr.
Native failure mapping and integration coverage
internal/daemon/native_session_*.go, internal/daemon/native_tools_test.go, internal/daemon/daemon_acpmock_faults_integration_test.go
Native prompts map process and incomplete-stream failures to structured backend errors. Tests validate ordering, partial output, recovery, transcript continuity, and diagnostics.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PromptStream
  participant SessionManager
  participant ACPProcess
  participant CrashBundle
  Client->>PromptStream: submit session prompt
  PromptStream->>SessionManager: forward prompt events
  ACPProcess-->>SessionManager: emit partial events and failure
  SessionManager->>ACPProcess: wait for process exit
  ACPProcess-->>SessionManager: return exit status
  SessionManager->>CrashBundle: persist failure diagnostics
  SessionManager-->>Client: return partial output and failure
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes preserving recovery for ACP stream disconnects, which is the primary change.
Linked Issues check ✅ Passed The changes preserve partial output, require terminal events, capture process diagnostics, return failures, and support session recovery as required by issue #315.
Out of Scope Changes check ✅ Passed The reviewed changes support ACP disconnect handling, failure diagnostics, recovery, lifecycle behavior, and related test coverage.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-315-acp-stream-disconnect

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Greptile Summary

The PR makes ACP prompt streams fail closed after a provider disconnect while retaining partial output and enabling explicit same-session recovery.

  • Coordinates fatal prompt cleanup with subprocess exit and session finalization.
  • Carries exit codes and available signals into crash bundle v2 diagnostics.
  • Requires explicit terminal stream events in HTTP/SSE, CLI, and native-tool consumers.
  • Adds focused lifecycle, transport, subprocess, CLI, native-tool, and end-to-end coverage.

Confidence Score: 5/5

The PR appears safe to merge because no eligible new blocking failure or outstanding prior finding was established.

No blocking failure remains.

Important Files Changed

Filename Overview
internal/session/manager_prompt_failure_barrier.go Adds the process-evidence barrier and coordinates fatal prompt failures with normal session-stop finalization.
internal/session/manager_prompt_runtime_loop.go Routes fatal stream outcomes through the new cleanup and finalization path without synthesizing completion.
internal/subprocess/exit_status.go Introduces synchronized subprocess exit-status capture and filters signal sentinel values from numeric exit-code output.
internal/session/crash_bundle.go Hard-cuts crash evidence to schema v2 and records available exit code and signal facts.
internal/api/core/prompt_stream_delivery.go Emits a terminal transport failure for incomplete upstream streams and stops after semantic terminal events.
internal/cli/client_session_prompt.go Preserves streamed frames while rejecting terminal errors and EOF without an explicit completion marker.
internal/daemon/native_session_prompt_stream.go Classifies drained native-tool prompt streams so terminal runtime failures cannot become successful tool results.
internal/sandbox/daytona/launcher.go Exposes numeric remote-process exit status through the ACP process boundary.

Sequence Diagram

sequenceDiagram
    participant User
    participant Consumer as CLI / Native Tool / HTTP
    participant Manager as Session Manager
    participant ACP as ACP Subprocess
    participant Store as Transcript / Crash Bundle

    User->>Consumer: Send prompt
    Consumer->>Manager: Start prompt stream
    Manager->>ACP: Submit prompt
    ACP-->>Manager: Partial response chunks
    Manager->>Store: Persist delivered chunks
    Manager-->>Consumer: Stream partial frames
    ACP--xManager: Disconnect / exit
    Manager->>ACP: Wait for bounded natural exit
    Manager->>Store: Record process exit diagnostics
    Manager-->>Consumer: Emit explicit terminal error
    Consumer-->>User: Return failure, preserving prior frames
    User->>Consumer: Send new explicit prompt
    Consumer->>Manager: Resume same session
    Manager->>ACP: Start replacement process
    ACP-->>Manager: Complete response
    Manager-->>Consumer: Finish + [DONE]
Loading

Reviews (2): Last reviewed commit: "fix: harden ACP stream failure finalizat..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/session/manager_prompt_contract_test.go`:
- Around line 375-405: Wrap the changed scenario in
TestPromptFatalProcessFailureStopsSessionAndAllowsFreshResumeFallback with
t.Run("Should stop after a fatal process failure and recover with an explicit
prompt", ...) and call t.Parallel() at the start of that subtest. Include all
related setup, hooks, assertions, and cleanup for the scenario within the
subtest, including the additional ranges noted in the review.

In `@internal/subprocess/exit_status_unix.go`:
- Around line 1-15: Restrict the build constraint for processStateSignal to
supported Unix targets rather than using only !windows, so Plan 9 does not
compile this syscall.WaitStatus implementation. Update the build tag on
exit_status_unix.go while preserving the existing processStateSignal behavior
for Unix platforms.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ad682d63-36d8-4635-95d5-a1e1a572a0fc

📥 Commits

Reviewing files that changed from the base of the PR and between a76a7c8 and 4bc0329.

⛔ Files ignored due to path filters (8)
  • docs/qa/charters/CH-acp-stream-disconnect-recovery.md is excluded by !**/*.md
  • docs/qa/journeys/J-15-operate-session-via-cli-api.md is excluded by !**/*.md
  • docs/qa/reports/2026-08-05-acp-stream-disconnect.md is excluded by !**/*.md
  • docs/qa/scenarios/RT-acp-stream-disconnect-recovery.md is excluded by !**/*.md
  • internal/testutil/acpmock/testdata/driver_fault_fixture.json is excluded by !**/*.json
  • packages/site/content/docs/guides/debug-a-failed-session.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/sessions/lifecycle.mdx is excluded by !**/*.mdx
  • skills/compozy/references/runtime-operations.md is excluded by !**/*.md
📒 Files selected for processing (19)
  • internal/acp/agent_process.go
  • internal/acp/launcher.go
  • internal/cli/client_session_prompt.go
  • internal/cli/client_test.go
  • internal/cli/session_test.go
  • internal/daemon/daemon_acpmock_faults_integration_test.go
  • internal/daemon/native_session_mutation.go
  • internal/daemon/native_session_prompt_stream.go
  • internal/daemon/native_tools_test.go
  • internal/session/crash_bundle.go
  • internal/session/interfaces.go
  • internal/session/manager_prompt_contract_test.go
  • internal/session/manager_prompt_runtime_loop.go
  • internal/subprocess/exit_status.go
  • internal/subprocess/exit_status_unix.go
  • internal/subprocess/exit_status_windows.go
  • internal/subprocess/process.go
  • internal/subprocess/process_lifecycle.go
  • internal/subprocess/process_test.go

Comment thread internal/session/manager_prompt_contract_test.go Outdated
Comment thread internal/subprocess/exit_status_unix.go Outdated
@pedronauck
pedronauck force-pushed the codex/issue-315-acp-stream-disconnect branch from 4bc0329 to 911f158 Compare August 6, 2026 00:59
@pedronauck

Copy link
Copy Markdown
Member Author

Publication update: pushed remediation commit 911f15881baccbb2409b7efe5f1369272f3c9920 to codex/issue-315-acp-stream-disconnect using --force-with-lease after the branch rebase. The in-progress make gate-full run was canceled by explicit user instruction and did not complete; this update does not claim a passing full gate. No merge was performed.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@pedronauck
pedronauck merged commit 5962808 into main Aug 6, 2026
15 of 17 checks passed
@pedronauck
pedronauck deleted the codex/issue-315-acp-stream-disconnect branch August 6, 2026 01:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
internal/session/manager_prompt_runtime_loop.go (1)

302-308: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the returned flag self-describing.

prepareFatalPromptFailureEvent returns a barrier outcome, and this code uses it directly as attachDiagnostics. The two meanings are not the same. When the barrier fails, this code also skips attachPromptFailureDiagnostics, which is the case where crash context is most useful. If that suppression is intentional, name the value for the decision it drives and add a short comment.

♻️ Suggested clarification
-	attachDiagnostics := true
-	if isFatalPromptFailureEvent(normalized) {
-		attachDiagnostics = m.prepareFatalPromptFailureEvent(ctx, session, normalized, fatal)
-	}
+	// Diagnostics require an observed process exit; the barrier guarantees it.
+	attachDiagnostics := true
+	if isFatalPromptFailureEvent(normalized) {
+		barrierOwned := m.prepareFatalPromptFailureEvent(ctx, session, normalized, fatal)
+		attachDiagnostics = barrierOwned
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/session/manager_prompt_runtime_loop.go` around lines 302 - 308,
Update the fatal-event handling around prepareFatalPromptFailureEvent so its
barrier outcome is stored in a self-describing variable that reflects the
decision it controls, not attachDiagnostics. Add a brief comment documenting the
intentional behavior when the barrier fails, and use that decision variable to
govern attachPromptFailureDiagnostics while preserving the existing logic.
internal/session/manager_synthetic_prompt_test.go (1)

166-174: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the released-prompt hook into a shared helper.

The same four-line pattern now appears in four hooks: buffer one event, wait for a release channel, emit EventTypeDone, close. Add a helper next to completedSyntheticPromptEvents to keep the fixtures consistent.

♻️ Suggested helper
func releasedPromptEvents(release <-chan struct{}, turnID string) <-chan acp.AgentEvent {
	events := make(chan acp.AgentEvent, 1)
	go func() {
		<-release
		events <- acp.AgentEvent{Type: acp.EventTypeDone, TurnID: turnID}
		close(events)
	}()
	return events
}

Also applies to: 272-280, 367-375, 457-465

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/session/manager_synthetic_prompt_test.go` around lines 166 - 174,
Add a shared releasedPromptEvents helper next to completedSyntheticPromptEvents
that creates the buffered event channel, waits on the provided release channel,
emits the completed event, and closes the channel. Replace the duplicated
goroutine patterns in all four hooks, including the locations around the
existing releaseFirstPrompt usages, with calls to this helper while preserving
each hook’s release channel and request TurnID.
internal/session/manager_prompt_failure_barrier.go (3)

85-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Do not return (error, error).

stopProcessForFatalPromptFailure returns two error values. The first is a payload passed to fatal.ownFinalization, and the second is a control-flow error. Callers cannot tell them apart from the signature, and this shape breaks the Go convention that a trailing error is the only error. Use named results or return the wait error in a small struct.

♻️ Suggested signature
 func (m *Manager) stopProcessForFatalPromptFailure(
 	ctx context.Context,
 	session *Session,
 	proc *AgentProcess,
 	failure *store.SessionFailure,
 	summary string,
-) (error, error) {
+) (processWaitErr error, err error) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/session/manager_prompt_failure_barrier.go` around lines 85 - 113,
Change stopProcessForFatalPromptFailure so it no longer returns two error
values; introduce a named result type or small struct that clearly separates the
fatal-finalization payload from the control-flow error, and update all callers
including fatal.ownFinalization to use the new result while preserving both
existing error semantics.

115-133: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard fatal.failure before the log dereference.

Line 129 reads fatal.failure.Kind. The function only checks fatal.finalizationOwned. Today finishPromptPump reaches this branch only when fatal.failure is non-nil, so no panic occurs. That invariant lives in another file. Add a local guard so a future caller cannot introduce a nil dereference.

🛡️ Suggested guard
-	if m == nil || session == nil || fatal == nil || !fatal.finalizationOwned {
+	if m == nil || session == nil || fatal == nil || fatal.failure == nil || !fatal.finalizationOwned {
 		return
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/session/manager_prompt_failure_barrier.go` around lines 115 - 133,
In finalizeSessionAfterFatalPromptFailure, guard fatal.failure before
dereferencing its Kind in the warning log. Preserve finalization behavior, and
use a nil-safe failure kind value or conditional logging so callers with a nil
failure cannot panic.

56-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why the finalization handle is discarded.

claimFinalization returns (bool, *sessionFinalization), not an error. Since this barrier only checks ownership and later calls finishFinalization(session.ID, err) regardless, add a short comment explaining that finalization is intentionally ignored in this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/session/manager_prompt_failure_barrier.go` around lines 56 - 61, In
the finalization-ownership branch around claimFinalization, add a short comment
documenting that the returned finalization handle is intentionally discarded
because this barrier only checks ownership and finishFinalization(session.ID,
err) is called later regardless. Preserve the existing ownership check and fatal
failure flow.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/api/core/prompt_stream_test.go`:
- Around line 45-52: Extend the test around the stream collected from
writer.String() to assert that the previously emitted “partial response” remains
present before the terminal error event, and verify that the terminal sentinel
“data: [DONE]” is emitted exactly once and after the error. Keep the existing
incomplete-stream failure assertions and use the test’s existing stream/event
symbols.

In `@internal/session/interfaces.go`:
- Around line 250-257: The process-exit error assigned to event.Error in
promptProcessFailureEvent must be bounded and normalized rather than using the
raw multi-line waitErr.Error() text. Reuse the existing error-summary
normalization or truncation helper before assigning event.Error, while
preserving the current normalized handling of event.Failure.Summary.

In `@internal/session/manager_prompt_process_exit.go`:
- Around line 67-75: Update the exit-status detail construction in the
process-exit summary to use status.ExitCodeValue() instead of the raw
status.ExitCode, while preserving the existing signal-detail override and
summary formatting. Keep the change scoped to the status handling in the
surrounding process-exit logic.

---

Nitpick comments:
In `@internal/session/manager_prompt_failure_barrier.go`:
- Around line 85-113: Change stopProcessForFatalPromptFailure so it no longer
returns two error values; introduce a named result type or small struct that
clearly separates the fatal-finalization payload from the control-flow error,
and update all callers including fatal.ownFinalization to use the new result
while preserving both existing error semantics.
- Around line 115-133: In finalizeSessionAfterFatalPromptFailure, guard
fatal.failure before dereferencing its Kind in the warning log. Preserve
finalization behavior, and use a nil-safe failure kind value or conditional
logging so callers with a nil failure cannot panic.
- Around line 56-61: In the finalization-ownership branch around
claimFinalization, add a short comment documenting that the returned
finalization handle is intentionally discarded because this barrier only checks
ownership and finishFinalization(session.ID, err) is called later regardless.
Preserve the existing ownership check and fatal failure flow.

In `@internal/session/manager_prompt_runtime_loop.go`:
- Around line 302-308: Update the fatal-event handling around
prepareFatalPromptFailureEvent so its barrier outcome is stored in a
self-describing variable that reflects the decision it controls, not
attachDiagnostics. Add a brief comment documenting the intentional behavior when
the barrier fails, and use that decision variable to govern
attachPromptFailureDiagnostics while preserving the existing logic.

In `@internal/session/manager_synthetic_prompt_test.go`:
- Around line 166-174: Add a shared releasedPromptEvents helper next to
completedSyntheticPromptEvents that creates the buffered event channel, waits on
the provided release channel, emits the completed event, and closes the channel.
Replace the duplicated goroutine patterns in all four hooks, including the
locations around the existing releaseFirstPrompt usages, with calls to this
helper while preserving each hook’s release channel and request TurnID.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b296898f-78d2-4e42-88f3-5e54ceed8253

📥 Commits

Reviewing files that changed from the base of the PR and between a76a7c8 and 911f158.

⛔ Files ignored due to path filters (9)
  • docs/qa/charters/CH-acp-stream-disconnect-recovery.md is excluded by !**/*.md
  • docs/qa/journeys/J-15-operate-session-via-cli-api.md is excluded by !**/*.md
  • docs/qa/reports/2026-08-05-acp-stream-disconnect.md is excluded by !**/*.md
  • docs/qa/reports/2026-08-05-pr319-acp-stream-remediation.md is excluded by !**/*.md
  • docs/qa/scenarios/RT-acp-stream-disconnect-recovery.md is excluded by !**/*.md
  • internal/testutil/acpmock/testdata/driver_fault_fixture.json is excluded by !**/*.json
  • packages/site/content/docs/guides/debug-a-failed-session.mdx is excluded by !**/*.mdx
  • packages/site/content/docs/sessions/lifecycle.mdx is excluded by !**/*.mdx
  • skills/compozy/references/runtime-operations.md is excluded by !**/*.md
📒 Files selected for processing (39)
  • internal/acp/agent_process.go
  • internal/acp/launcher.go
  • internal/acp/prompt_stream_terminal.go
  • internal/api/core/prompt_stream_delivery.go
  • internal/api/core/prompt_stream_test.go
  • internal/cli/client_session_prompt.go
  • internal/cli/client_test.go
  • internal/cli/session_test.go
  • internal/daemon/daemon_acpmock_faults_integration_test.go
  • internal/daemon/native_session_mutation.go
  • internal/daemon/native_session_prompt_stream.go
  • internal/daemon/native_tools_test.go
  • internal/sandbox/daytona/launcher.go
  • internal/sandbox/daytona/provider_test.go
  • internal/session/crash_bundle.go
  • internal/session/crash_bundle_test.go
  • internal/session/interfaces.go
  • internal/session/manager_hooks_lifecycle_test.go
  • internal/session/manager_lifecycle.go
  • internal/session/manager_managed_input_submit.go
  • internal/session/manager_prompt.go
  • internal/session/manager_prompt_batch.go
  • internal/session/manager_prompt_contract_test.go
  • internal/session/manager_prompt_failure_barrier.go
  • internal/session/manager_prompt_process_exit.go
  • internal/session/manager_prompt_pump.go
  • internal/session/manager_prompt_runtime_loop.go
  • internal/session/manager_prompt_submit.go
  • internal/session/manager_synthetic_prompt_test.go
  • internal/session/session.go
  • internal/session/session_prompt_state.go
  • internal/session/stop_reason.go
  • internal/subprocess/exit_status.go
  • internal/subprocess/exit_status_other.go
  • internal/subprocess/exit_status_unix.go
  • internal/subprocess/exit_status_windows.go
  • internal/subprocess/process.go
  • internal/subprocess/process_lifecycle.go
  • internal/subprocess/process_test.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • internal/acp/agent_process.go
  • internal/subprocess/exit_status_windows.go
  • internal/subprocess/exit_status_unix.go
  • internal/acp/launcher.go
  • internal/subprocess/process_lifecycle.go
  • internal/daemon/native_session_mutation.go
  • internal/daemon/native_tools_test.go
  • internal/cli/session_test.go
  • internal/cli/client_test.go
  • internal/cli/client_session_prompt.go
  • internal/subprocess/process.go
  • internal/daemon/daemon_acpmock_faults_integration_test.go

Comment on lines +45 to +52
stream := writer.String()
if !strings.Contains(stream, `"type":"error"`) ||
!strings.Contains(stream, "ended before a terminal event") {
t.Fatalf("stream = %q, want explicit incomplete-stream failure", stream)
}
if !strings.Contains(stream, "data: [DONE]") {
t.Fatalf("stream = %q, want a terminal sentinel after the failure", stream)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Verify partial-output retention and terminal order.

This test sends "partial response" before the channel closes. It does not assert that the response retains that chunk before the terminal error. It also does not assert that delivery emits one terminal sentinel.

Proposed test assertions
 		stream := writer.String()
 		if !strings.Contains(stream, `"type":"error"`) ||
 			!strings.Contains(stream, "ended before a terminal event") {
 			t.Fatalf("stream = %q, want explicit incomplete-stream failure", stream)
 		}
-		if !strings.Contains(stream, "data: [DONE]") {
-			t.Fatalf("stream = %q, want a terminal sentinel after the failure", stream)
+		partialAt := strings.Index(stream, "partial response")
+		errorAt := strings.Index(stream, `"type":"error"`)
+		if partialAt < 0 || partialAt > errorAt {
+			t.Fatalf("stream = %q, want retained partial output before terminal failure", stream)
+		}
+		if got := strings.Count(stream, "data: [DONE]"); got != 1 {
+			t.Fatalf("terminal sentinel count = %d, want 1; stream = %q", got, stream)
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stream := writer.String()
if !strings.Contains(stream, `"type":"error"`) ||
!strings.Contains(stream, "ended before a terminal event") {
t.Fatalf("stream = %q, want explicit incomplete-stream failure", stream)
}
if !strings.Contains(stream, "data: [DONE]") {
t.Fatalf("stream = %q, want a terminal sentinel after the failure", stream)
}
stream := writer.String()
if !strings.Contains(stream, `"type":"error"`) ||
!strings.Contains(stream, "ended before a terminal event") {
t.Fatalf("stream = %q, want explicit incomplete-stream failure", stream)
}
partialAt := strings.Index(stream, "partial response")
errorAt := strings.Index(stream, `"type":"error"`)
if partialAt < 0 || partialAt > errorAt {
t.Fatalf("stream = %q, want retained partial output before terminal failure", stream)
}
if got := strings.Count(stream, "data: [DONE]"); got != 1 {
t.Fatalf("terminal sentinel count = %d, want 1; stream = %q", got)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/core/prompt_stream_test.go` around lines 45 - 52, Extend the
test around the stream collected from writer.String() to assert that the
previously emitted “partial response” remains present before the terminal error
event, and verify that the terminal sentinel “data: [DONE]” is emitted exactly
once and after the error. Keep the existing incomplete-stream failure assertions
and use the test’s existing stream/event symbols.

Comment on lines 250 to 257
func (p *AgentProcess) Wait() error {
<-p.Done()
waitErr := p.waitFn()
p.waitOverrideMu.RLock()
override := p.waitErrOverride
p.waitOverrideMu.RUnlock()
if override != nil {
return override
}
return p.waitFn()
return errors.Join(waitErr, override)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Trace how wait errors become failure summaries and whether multi-line text is normalized.
set -euo pipefail

rg -n -C5 'waitErrOverride|firstTrimmedNonEmpty|func failureSummary' internal/session internal/acp

Repository: compozy/compozy

Length of output: 23267


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- internal/session/manager_prompt_process_exit.go ---\n'
sed -n '1,120p' internal/session/manager_prompt_process_exit.go | cat -n

printf '\n--- internal/acp/failure.go relevant sections ---\n'
sed -n '1,120p' internal/acp/failure.go | cat -n
sed -n '170,245p' internal/acp/failure.go | cat -n

printf '\n--- internal/session/diagnostics references ---\n'
fd -a 'diagnostics' internal | sed 's#^\./##'
rg -n 'func RedactAndBound|RedactAndBound|maxFailureSummaryBytes|maxSessionFailureSummaryBytes' internal

Repository: compozy/compozy

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- internal/session/manager_prompt_process_exit.go ---'
sed -n '1,120p' internal/session/manager_prompt_process_exit.go | cat -n

printf '%s\n' ''
printf '%s\n' '--- internal/acp/failure.go relevant sections ---'
sed -n '70,110p' internal/acp/failure.go | cat -n
sed -n '204,233p' internal/acp/failure.go | cat -n

printf '%s\n' ''
printf '%s\n' '--- diagnostics/bounds references ---'
rg -n 'func RedactAndBound|RedactAndBound|maxFailureSummaryBytes|maxSessionFailureSummaryBytes' internal

Repository: compozy/compozy

Length of output: 23494


Bound the process-exit summary before assigning it to event.Error.

promptProcessFailureEvent can leave waitErr.Error() as event.Error unbounded and multi-line. event.Error is written to the session event/crash bundle, while event.Failure.Summary already receives truncation through normalization, so use a bounded summary before assignment or otherwise normalize the multi-line joined text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/session/interfaces.go` around lines 250 - 257, The process-exit
error assigned to event.Error in promptProcessFailureEvent must be bounded and
normalized rather than using the raw multi-line waitErr.Error() text. Reuse the
existing error-summary normalization or truncation helper before assigning
event.Error, while preserving the current normalized handling of
event.Failure.Summary.

Comment on lines +67 to +75
if proc != nil {
if status, ok := proc.ExitStatus(); ok {
detail := fmt.Sprintf("exit code %d", status.ExitCode)
if status.Signal != "" {
detail = "signal " + status.Signal
}
summary = fmt.Sprintf("%s (%s)", summary, detail)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use ExitCodeValue() for the summary detail.

status.ExitCode is used raw. On Unix a signal termination reports -1, and the fallback processStateSignal returns an empty signal on platforms outside darwin, linux, and windows. In that combination the summary reads "exit code -1", which is not an exit code. writeCrashBundleAtPath in internal/session/crash_bundle.go (Lines 161-166) already suppresses the sentinel through ExitCodeValue(), so the event summary and the crash bundle disagree for the same failure.

🐛 Proposed fix for the exit-code detail
 	if proc != nil {
 		if status, ok := proc.ExitStatus(); ok {
-			detail := fmt.Sprintf("exit code %d", status.ExitCode)
-			if status.Signal != "" {
-				detail = "signal " + status.Signal
-			}
-			summary = fmt.Sprintf("%s (%s)", summary, detail)
+			detail := ""
+			switch {
+			case status.Signal != "":
+				detail = "signal " + status.Signal
+			default:
+				if code, available := status.ExitCodeValue(); available {
+					detail = fmt.Sprintf("exit code %d", code)
+				}
+			}
+			if detail != "" {
+				summary = fmt.Sprintf("%s (%s)", summary, detail)
+			}
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if proc != nil {
if status, ok := proc.ExitStatus(); ok {
detail := fmt.Sprintf("exit code %d", status.ExitCode)
if status.Signal != "" {
detail = "signal " + status.Signal
}
summary = fmt.Sprintf("%s (%s)", summary, detail)
}
}
if proc != nil {
if status, ok := proc.ExitStatus(); ok {
detail := ""
switch {
case status.Signal != "":
detail = "signal " + status.Signal
default:
if code, available := status.ExitCodeValue(); available {
detail = fmt.Sprintf("exit code %d", code)
}
}
if detail != "" {
summary = fmt.Sprintf("%s (%s)", summary, detail)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/session/manager_prompt_process_exit.go` around lines 67 - 75, Update
the exit-status detail construction in the process-exit summary to use
status.ExitCodeValue() instead of the raw status.ExitCode, while preserving the
existing signal-detail override and summary formatting. Keep the change scoped
to the status handling in the surrounding process-exit logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codex ACP process exits with peer disconnected before response while streaming final answer

1 participant