fix(agent): unblock watch queue after ACP peer disconnect #26

Merged
frank merged 3 commits from agent/issue-24-acp-peer-connection-closed-beim-job-ende into main 2026-07-12 12:57:52 +02:00
Owner

Summary

  • Treat ACP peer disconnect after agent output as a successful prompt completion instead of hanging forever on session/prompt.
  • Shut down ACP sessions and agent subprocesses cleanly via CloseSession and stdin close.
  • Stop clearing in-flight active runs on poll/wait transitions so runner state stays consistent until UntrackRunning.

Test plan

  • go test ./internal/agent/...
  • forge agent watch --pr --agent cursor-agent --tui — after a job finishes with peer connection closed, the next matching issue should be picked up on the following poll

Closes #24

## Summary - Treat ACP peer disconnect after agent output as a successful prompt completion instead of hanging forever on `session/prompt`. - Shut down ACP sessions and agent subprocesses cleanly via `CloseSession` and stdin close. - Stop clearing in-flight `active` runs on poll/wait transitions so runner state stays consistent until `UntrackRunning`. ## Test plan - [x] `go test ./internal/agent/...` - [ ] `forge agent watch --pr --agent cursor-agent --tui` — after a job finishes with `peer connection closed`, the next matching issue should be picked up on the following poll Closes #24
fix(agent): unblock watch queue after ACP peer disconnect
Some checks failed
CI / test (pull_request) Successful in 20s
CI / race (pull_request) Failing after 20s
8f3f70aed0
When cursor-agent closes the ACP connection at job end without a final
prompt response, treat received output as success and shut down the agent
process cleanly so the watch loop can enqueue the next issue.

Co-authored-by: Cursor <cursoragent@cursor.com>
Author
Owner

🔗 Linked to issue #24. Babysitting CI until green…

🔗 Linked to issue #24. Babysitting CI until green…
Author
Owner

forge agent finished implementation

  • Commit: fix(agent): unblock watch queue after ACP peer disconnect
  • Pull request: #26
  • Next: waiting for CI / babysitting fixes if needed
internal/agent/acp_client.go    | 111 +++++++++++++++++++++++++++++++++++-----
 internal/agent/acp_mock_test.go | 110 +++++++++++++++++++++++++++++++++++++++
 internal/agent/status.go        |   3 --
 3 files changed, 209 insertions(+), 15 deletions(-)
Agent output (tail)
## Zusammenfassung
Nach Job-Ende schließt `cursor-agent` die ACP-Verbindung oft mit `peer connection closed`, ohne eine finale `session/prompt`-Antwort zu senden. `conn.Prompt()` wartete dann unbegrenzt (bei `--timeout 0`), der Watch-Loop blieb in `dispatch` hängen und konnte keine weiteren Issues starten.
**Änderungen:**
1. **`internal/agent/acp_client.go`**
   - `runACPPrompt`: wartet parallel auf `conn.Done()` und wertet Peer-Disconnect mit vorhandenem Agent-Output als erfolgreichen Lauf
   - `shutdownACPAgent`: sauberes Aufräumen via `CloseSession`, stdin schließen, Prozess beenden
   - Hilfsfunktionen `acpPeerDisconnect`, `acpHasAgentOutput`, `acpCapturedOutput`
2. **`internal/agent/status.go`**
   - `OnPollStart` / `OnWaiting` / `OnUsageLimited` leeren `active` nicht mehr — laufende Jobs bleiben bis `UntrackRunning` sichtbar
3. **`internal/agent/acp_mock_test.go`**
   - Test für Disconnect nach Output ohne Prompt-Response
PR_TITLE: fix(agent): unblock watch queue after ACP peer disconnect
PR_BODY:
## Summary
- Treat ACP peer disconnect after agent output as a successful prompt completion instead of hanging forever on `session/prompt`.
- Shut down ACP sessions and agent subprocesses cleanly via `CloseSession` and stdin close.
- Stop clearing in-flight `active` runs on poll/wait transitions so runner state stays consistent until `UntrackRunning`.
## Test plan
- [x] `go test ./internal/agent/...`
- [ ] `forge agent watch --pr --agent cursor-agent --tui` — after a job finishes with `peer connection closed`, the next matching issue should be picked up on the following poll
✅ **forge agent finished implementation** - Commit: `fix(agent): unblock watch queue after ACP peer disconnect` - Pull request: https://repository.hildebrandt.io/frank/forgecli/pulls/26 - Next: waiting for CI / babysitting fixes if needed ``` internal/agent/acp_client.go | 111 +++++++++++++++++++++++++++++++++++----- internal/agent/acp_mock_test.go | 110 +++++++++++++++++++++++++++++++++++++++ internal/agent/status.go | 3 -- 3 files changed, 209 insertions(+), 15 deletions(-) ``` <details><summary>Agent output (tail)</summary> ``` ## Zusammenfassung Nach Job-Ende schließt `cursor-agent` die ACP-Verbindung oft mit `peer connection closed`, ohne eine finale `session/prompt`-Antwort zu senden. `conn.Prompt()` wartete dann unbegrenzt (bei `--timeout 0`), der Watch-Loop blieb in `dispatch` hängen und konnte keine weiteren Issues starten. **Änderungen:** 1. **`internal/agent/acp_client.go`** - `runACPPrompt`: wartet parallel auf `conn.Done()` und wertet Peer-Disconnect mit vorhandenem Agent-Output als erfolgreichen Lauf - `shutdownACPAgent`: sauberes Aufräumen via `CloseSession`, stdin schließen, Prozess beenden - Hilfsfunktionen `acpPeerDisconnect`, `acpHasAgentOutput`, `acpCapturedOutput` 2. **`internal/agent/status.go`** - `OnPollStart` / `OnWaiting` / `OnUsageLimited` leeren `active` nicht mehr — laufende Jobs bleiben bis `UntrackRunning` sichtbar 3. **`internal/agent/acp_mock_test.go`** - Test für Disconnect nach Output ohne Prompt-Response PR_TITLE: fix(agent): unblock watch queue after ACP peer disconnect PR_BODY: ## Summary - Treat ACP peer disconnect after agent output as a successful prompt completion instead of hanging forever on `session/prompt`. - Shut down ACP sessions and agent subprocesses cleanly via `CloseSession` and stdin close. - Stop clearing in-flight `active` runs on poll/wait transitions so runner state stays consistent until `UntrackRunning`. ## Test plan - [x] `go test ./internal/agent/...` - [ ] `forge agent watch --pr --agent cursor-agent --tui` — after a job finishes with `peer connection closed`, the next matching issue should be picked up on the following poll ``` </details>
Author
Owner

🔴 Watch: CI failed — prioritizing repair…

CI run #91 () finished with failure
URL: https://repository.hildebrandt.io/frank/forgecli/actions/runs/62
🔴 **Watch: CI failed** — prioritizing repair… ``` CI run #91 () finished with failure URL: https://repository.hildebrandt.io/frank/forgecli/actions/runs/62 ```
Author
Owner

🛠️ Pushed CI fix (attempt 1/3). Waiting for CI again…

Der CI-Lauf race (go test -race ./...) ist fehlgeschlagen. In TestRunACPPromptPeerDisconnectAfterOutput schrieb SessionUpdate parallel in forgeACPClient.output (strings.Builder), während acpCapturedOutput gleichzeitig las — ein Data Race.
make ci (test/vet/build) lief lokal durch; nur der Race-Job war betroffen.

Fix

forgeACPClient hat jetzt ein sync.Mutex für den Output-Buffer, mit den Hilfsmethoden appendOutput() und capturedOutput(). So sind Schreibzugriffe aus ACP-Goroutines und Lesezugriffe aus runACPPrompt / Tests synchronisiert.

Verifikation

  • go test -race ./internal/agent/... — grün
  • make ci — grün

Commit

8133d16 fix(agent): synchronize ACP captured output for race detector

Nicht gepusht — der Agent-Watcher übernimmt Push und erneuten CI-Check.

🛠️ Pushed CI fix (attempt 1/3). Waiting for CI again… Der CI-Lauf `race` (`go test -race ./...`) ist fehlgeschlagen. In `TestRunACPPromptPeerDisconnectAfterOutput` schrieb `SessionUpdate` parallel in `forgeACPClient.output` (`strings.Builder`), während `acpCapturedOutput` gleichzeitig las — ein Data Race. `make ci` (test/vet/build) lief lokal durch; nur der Race-Job war betroffen. ## Fix `forgeACPClient` hat jetzt ein `sync.Mutex` für den Output-Buffer, mit den Hilfsmethoden `appendOutput()` und `capturedOutput()`. So sind Schreibzugriffe aus ACP-Goroutines und Lesezugriffe aus `runACPPrompt` / Tests synchronisiert. ## Verifikation - `go test -race ./internal/agent/...` — grün - `make ci` — grün ## Commit ``` 8133d16 fix(agent): synchronize ACP captured output for race detector ``` Nicht gepusht — der Agent-Watcher übernimmt Push und erneuten CI-Check.
fix(agent): synchronize ACP captured output for race detector
Some checks failed
CI / test (pull_request) Successful in 20s
CI / race (pull_request) Failing after 20s
8133d16bd6
Protect forgeACPClient output writes from SessionUpdate goroutines with a mutex so the race CI job passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Author
Owner

🔴 Watch: CI failed — prioritizing repair…

CI run #92 () finished with failure
URL: https://repository.hildebrandt.io/frank/forgecli/actions/runs/63
🔴 **Watch: CI failed** — prioritizing repair… ``` CI run #92 () finished with failure URL: https://repository.hildebrandt.io/frank/forgecli/actions/runs/63 ```
Author
Owner

🛠️ Pushed CI fix (attempt 1/3). Waiting for CI again…

CI läuft lokal durch — ich hole die Logs vom fehlgeschlagenen Run.
Flaky Race-Test gefunden — ich reproduziere den Fehler gezielt.
Race zwischen Peer-Disconnect und Session-Update — ich füge eine kurze Wartezeit für ausstehende Updates ein.
100 Durchläufe ohne Fehler. Ich erstelle jetzt den Commit.
Ursache: Der race-Job in CI ist an TestRunACPPromptPeerDisconnectAfterOutput gescheitert. Beim Peer-Disconnect kann session/update noch unterwegs sein, während runACPPrompt sofort prüft, ob Agent-Output da ist — ein klassischer Race.
Fix: Neue Hilfsfunktion acpPeerDisconnectResolved wartet bis zu 500 ms auf eintreffende Session-Updates, bevor ein Peer-Disconnect als Fehler gewertet wird.
Verifikation:

  • go test -race ./... — grün
  • make ci — grün
  • 100× Stress-Test von TestRunACPPromptPeerDisconnectAfterOutput — 0 Fehler
    Commit: e2aa08cfix(agent): wait for in-flight ACP output after peer disconnect
    Nicht gepusht — der Agent-Watcher übernimmt Push und CI-Neustart.
🛠️ Pushed CI fix (attempt 1/3). Waiting for CI again… CI läuft lokal durch — ich hole die Logs vom fehlgeschlagenen Run. Flaky Race-Test gefunden — ich reproduziere den Fehler gezielt. Race zwischen Peer-Disconnect und Session-Update — ich füge eine kurze Wartezeit für ausstehende Updates ein. 100 Durchläufe ohne Fehler. Ich erstelle jetzt den Commit. **Ursache:** Der `race`-Job in CI ist an `TestRunACPPromptPeerDisconnectAfterOutput` gescheitert. Beim Peer-Disconnect kann `session/update` noch unterwegs sein, während `runACPPrompt` sofort prüft, ob Agent-Output da ist — ein klassischer Race. **Fix:** Neue Hilfsfunktion `acpPeerDisconnectResolved` wartet bis zu 500 ms auf eintreffende Session-Updates, bevor ein Peer-Disconnect als Fehler gewertet wird. **Verifikation:** - `go test -race ./...` — grün - `make ci` — grün - 100× Stress-Test von `TestRunACPPromptPeerDisconnectAfterOutput` — 0 Fehler **Commit:** `e2aa08c` — `fix(agent): wait for in-flight ACP output after peer disconnect` Nicht gepusht — der Agent-Watcher übernimmt Push und CI-Neustart.
fix(agent): wait for in-flight ACP output after peer disconnect
All checks were successful
CI / test (pull_request) Successful in 22s
CI / race (pull_request) Successful in 22s
e2aa08ccca
Session updates can arrive after the connection closes, which made the
race CI job flake on TestRunACPPromptPeerDisconnectAfterOutput.

Co-authored-by: Cursor <cursoragent@cursor.com>
frank merged commit 17abcbbe3d into main 2026-07-12 12:57:52 +02:00
frank deleted branch agent/issue-24-acp-peer-connection-closed-beim-job-ende 2026-07-12 12:57:52 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
frank/forgecli!26
No description provided.