Skip to content

fix(opencode): connection drops when background subagent dispatched - #1647

Open
zmlgit wants to merge 9 commits into
chenhg5:mainfrom
zmlgit:opencode-resume-v2
Open

fix(opencode): connection drops when background subagent dispatched#1647
zmlgit wants to merge 9 commits into
chenhg5:mainfrom
zmlgit:opencode-resume-v2

Conversation

@zmlgit

@zmlgit zmlgit commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Problem

When cc-connect talks to official opencode serve and the LLM dispatches a background subagent (task(background=true)), the connection drops — the user sees no response to the background completion.

Symptom reported: "现在官方的cc-connect和官方的opencode配合使用时,一旦派发后台任务就断了".

Root cause

agent/opencode/session_http.go had two load-bearing failure modes:

1. Blocking POST /session/{id}/message

The POST held the connection open for the entire agent turn. When the LLM dispatched a background subagent, the parent turn ended immediately (the tool returns "Background task started" → parent loop goes idle), so the blocking POST returned. cc-connect then treated the turn as done — but the actual work (background completion, parent-session injection) was still pending.

2. Fatal GET /event SSE EOF

Any /event SSE stream termination (server restart, network blip, keepalive expiry) marked the session dead:

s.alive.Store(false)
s.emitHTTPEvent(core.Event{Type: core.EventError, Error: ...})

The engine received the error → tore down the session → cancelled s.ctx → killed the in-flight turn. No reconnect logic existed.

Fix — align with opencode's official streaming pattern

The official TUI client (packages/opencode/src/cli/cmd/run/stream.transport.ts) uses four rules. This PR adopts the two cc-connect was missing:

# Official pattern This PR
1 One long-lived SSE subscription for the whole session readHTTPEventsLoop reconnects with exponential backoff (500ms → 30s cap) on stream EOF; only exits when session ctx is cancelled
2 Fire-and-forget prompt POSTs sendHTTP switched from POST /message (blocking) to POST /prompt_async (204 No Content)
3 session.status: idle events as turn-completion signal Already in place — unchanged
4 Track child sessions Analyzed and intentionally omitted — opencode injects background completion into the parent sessionID, so the existing filter is correct (see TestOpencodeHTTPMode_StreamsBeforePromptReturnsAndKeepsBackgroundEvents). Child session events are intermediate noise the user doesn't need.

Server compatibility

Verified against opencode serve v1.18.14 (current release):

  • POST /session/{id}/prompt_async ✓ exists, returns 204 fire-and-forget
  • GET /event ✓ exists, real SSE global stream
  • /api/session/{id}/event?after= ✗ dev-branch only, not on this version (so no cursor/replay; reconnect is blind but sufficient because the SSE stays open across background runs)

Verification

Unit tests

=== RUN   TestOpencodeHTTPMode_SSEReconnectsAfterEOF
2026/08/06 15:20:42 INFO opencode: /event stream ended, reconnecting backoff=500ms
--- PASS: TestOpencodeHTTPMode_SSEReconnectsAfterEOF (0.00s)

ok  github.com/chenhg5/cc-connect/agent/opencode  8.828s  # all 6 HTTP-mode tests pass, race-clean
ok  github.com/chenhg5/cc-connect/core            3.014s  # CUJ tests pass

Live end-to-end against opencode serve v1.18.14

TestLivePromptAsync:
  session id: ses_02a0a1c81ffe4wAkMHMC1f6ebc
  Send returned in 3.370805ms (prompt_async 204 ack)   # OLD: would block ~38s
  event: type="text" content="PONG"
  event: type="result" content=""
  PASS (37.78s — full turn duration, all via SSE)

TestLiveSSEReconnectAfterServerRestart:
  INFO opencode: /event stream ended, reconnecting backoff=500ms
  PASS: SSE reconnected after forced EOF (killCount=2)

Test changes

  • NEW TestOpencodeHTTPMode_SSEReconnectsAfterEOF — regression for the central fix
  • NEW TestLivePromptAsync, TestLiveSSEReconnectAfterServerRestart — env-gated live tests
  • UPDATED TestOpencodeHTTPMode_StreamsBeforePromptReturnsAndKeepsBackgroundEvents — adapted for prompt_async (Send returns immediately; no more releasePrompt synchronization)
  • RENAMED TestOpencodeHTTPMode_SendReturnsSSEErrorWhenMessageEndpointFails...PromptAsyncFails — endpoint follow rename

Commit-by-commit

  1. 23036102 — fix opencode background streaming over HTTP (baseline)
  2. 5145e134 — fix opencode HTTP agent error reporting (baseline)
  3. 9787dab0 — fix opencode HTTP lint errors (baseline)
  4. 7204f7d0 — surface opencode HTTP retry limit errors (baseline)
  5. 5e14fe5e — reset agent session after idle timeout (baseline)
  6. 3f081e26 — split HTTP client (60s API) from streamClient (no-timeout SSE)
  7. 165743b9core fix: prompt_async + SSE auto-reconnect

Out of scope (YAGNI)

  • dev-branch ?after=<seq> cursor replay — server v1.18.14 doesn't expose it
  • Child session event forwarding — opencode injects background results into the parent session; existing filter is correct
  • CLI subprocess mode — only HTTP/SSE mode touches this issue

Pre-merge checklist

  • Build passes: go build ./...
  • Tests pass: go test ./agent/opencode/ -race
  • CUJ tests pass: go test ./core/ -run TestCUJ
  • Bug fix has regression test: TestOpencodeHTTPMode_SSEReconnectsAfterEOF
  • No new hardcoded platform/agent names in core
  • No new user-facing strings
  • No secrets in code

@zmlgit
zmlgit requested a review from chenhg5 as a code owner August 6, 2026 08:00
张满良 added 8 commits August 7, 2026 18:45
/message is non-idempotent and the previous 60s timeout killed legit
long-running turns. Move /message and GET /event onto a separate
no-timeout client; keep the 60s client for short API calls.
Two related changes that together align cc-connect with opencode's
official streaming pattern (packages/opencode/src/cli/cmd/run/stream.
transport.ts): one long-lived SSE subscription + fire-and-forget
prompts + session.status idle as turn-completion signal.

1. POST /message (blocking, holds conn for whole turn) -> POST /
   prompt_async (204 fire-and-forget). The whole turn output now flows
   via the /event SSE subscription, so a long turn (incl. background
   subagent dispatch) can no longer break the POST and vice versa.

2. De-fatalize /event stream EOF. Previously any stream end (server
   restart, network blip, keepalive expiry) marked the session dead
   and emitted EventError, tearing down the in-flight turn. The new
   readHTTPEventsLoop reconnects with exponential backoff (500ms ->
   30s cap) and only exits when the session ctx is cancelled.

Verified end-to-end against opencode-server v1.18.14: prompt_async
returns in ~3ms (vs ~38s for the old blocking /message on the same
turn), SSE delivers all turn output, and the SSE reconnects after a
forced EOF without losing the session.

Adds regression test TestOpencodeHTTPMode_SSEReconnectsAfterEOF and
updates the existing /message-based tests for the new prompt_async
semantics.
…henhg5#1557

Upstream PR chenhg5#1557 added a messageID parameter to core.AgentSession.Send
(prompt, messageID, images, files) so attachments are scoped per-message.
Rebasing opencode-resume-v2 onto the new main exposed the stale 3-arg
calls in the opencode tests, failing CI lint with
"not enough arguments in call to session.Send".

Thread an empty messageID through the four opencode test call sites
(no platform message id in test context); SaveFilesToDisk falls back to
its flat layout when messageID is empty.
@zmlgit
zmlgit force-pushed the opencode-resume-v2 branch from 165743b to 3f05967 Compare August 7, 2026 10:45
CI surfaced five lint violations on the rebased branch:
- live_resume_test.go: unchecked resp.Body.Close / w.Write (errcheck) and
  numeric 502/500 literals instead of http.Status* constants (staticcheck ST1013)
- session_http.go: doHTTPRequest and httpStatusError became dead code after
  the streamClient split and were flagged as unused.

Thread ignored-return markers (`_, _ =`), use http.Status* constants, and
remove the two unused helpers. Validated locally with golangci-lint v2.11.4
matching CI both with and without the no_web build tag.
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.

1 participant