GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix(harness): close the turn's event source when delivery stops early by michaelxu2288 · Pull Request #541 · scaleapi/scale-agentex-python

Conversation

@michaelxu2288

@michaelxu2288 michaelxu2288 commented Oct 1, 2026 •
edited by greptile-apps Bot
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Problem

The harness delivery adapters never close the event source they iterate:

  • auto_send, the async push path behind UnifiedEmitter.auto_send_turn;
  • yield_events and UnifiedEmitter.yield_turn, the sync HTTP yield path.

Their finally blocks close streaming contexts and flush span derivation, but leave turn.events alone. Every CLI tap depends on being closed. The Claude Code and Codex taps close their raw line iterator in a finally, and that close is what terminates the CLI subprocess in the scaffolds.

It goes wrong in two cases:

  • a turn is cancelled (an interrupt, or an activity cancel) while delivery is awaiting the backend (ctx.stream_update or a context close) rather than the source;
  • a sync client disconnects mid-stream.

In both, the source stays suspended at a yield. The turn object keeps a reference to its event generator, and agents hold the turn to read turn.session_id. So garbage collection never finalizes it, and the CLI subprocess and its stdout pipe leak.

Fix

  • auto_send and yield_events close events in their finally, after the existing cleanup. The close is guarded with contextlib.suppress(Exception), so a failing aclose cannot mask the original exception.
  • UnifiedEmitter.yield_turn closes its delivery generator explicitly instead of leaving it to async-generator finalization.
  • Plain async iterators without aclose, and generators that are already exhausted, behave exactly as before.

Verification

  • New tests:
    • test_auto_send.py: cancel auto_send while stream_update blocks, with the source held by a local and no gc.collect();
    • test_yield_delivery.py: close yield_events early;
    • test_emitter.py: close yield_turn early, with a turn that pins its generator the way the CLI taps do.
  • All three fail on main with assert [] == [True] (the source's finally never ran) and pass here.
  • An extra test checks that a source without aclose still delivers normally.
  • uv run pytest -n 0 tests/lib/core/harness tests/lib/adk: 571 passed, 1 skipped.
  • ruff check and pyright (1.1.399) are clean.

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no actionable issue remains established.

Summary

Harness delivery now closes event sources when delivery stops early, so CLI tap cleanup runs on cancellation and client disconnects. The ACP response stream also closes its handler generator, carrying disconnect cleanup upstream.

  • Async push delivery closes its source after its existing cleanup.
  • Sync harness delivery passes early closure through to the event source.
  • ACP response streaming closes the handler generator when the stream ends.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[ACP response body closes] --> B[Handler generator closes]
  B --> C[yield_turn closes delivery]
  C --> D[yield_events closes turn events]
Loading

Reviews (2) · Last reviewed commit: "fix(acp): close the handler generator wh..."

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

greptile-apps[bot] reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
@michaelxu2288
fix(harness): close the turn's event source when delivery stops early
88bf5b9
The delivery adapters (auto_send for the async push path, yield_events
and UnifiedEmitter.yield_turn for the sync yield path) closed their
streaming contexts and flushed span derivation in `finally`, but never
closed the event source they were iterating. Every tap relies on that
close: the Claude Code and Codex taps close their raw line iterator in a
`finally`, which is what terminates the CLI subprocess in the scaffolds.

When a turn is cancelled while delivery is awaiting the backend
(stream_update or a context close) rather than the source, or a sync
client disconnects mid-stream, the source stays suspended at a yield.
The turn object keeps a reference to its event generator, so garbage
collection does not finalize it while the caller still holds the turn,
and the CLI subprocess and its stdout handle leak.

Both adapters now close the source in `finally`, after the existing
cleanup and guarded so a failing aclose cannot mask the original
exception. yield_turn closes its delivery generator explicitly instead
of leaving it to async-generator finalization. Plain async iterators
without aclose and already-exhausted generators are unaffected.

Three new tests (cancel during a blocked stream_update; early close of
yield_events; early close of yield_turn with a turn that pins its
generator) fail on main with `assert [] == [True]` and pass here.
tests/lib/core/harness and tests/lib/adk: 571 passed, 1 skipped.
greptile-apps[bot]
greptile-apps Bot reviewed Oct 1, 2026
View reviewed changes
Comment thread src/agentex/lib/core/harness/emitter.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@michaelxu2288
fix(acp): close the handler generator when a streamed response ends
953c110
_handle_streaming_response iterated the handler's async generator inside
generate_json_rpc_stream but never closed it. When a client disconnects,
Starlette closes the body iterator, and the handler (with the harness
turn and CLI subprocess under it) stayed suspended until garbage
collection. Close the handler generator in a finally, guarded so a
failing aclose cannot mask the original error.

New test: take one chunk from the streaming response, close the body
iterator, and assert the handler generator's finally ran. It fails on
main (assert [] == [True]) and passes here.

This branch has not been deployed

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

Reviewers

@greptile-apps greptile-apps[bot] greptile-apps[bot] left review comments

At least 1 approving review is required to merge this pull request.

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

1 participant

@michaelxu2288
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.