fix(flowing): close sessions a flow can no longer reach - #82
Merged
Merged
Conversation
The flow API has no `Session.close`, and the engine closed a session only as the flow call that opened it ended, holding every view strongly until then. A loop opening a fresh session a round -- ralph_loop, flame_chase, rlar's reviewer, lane runtimes, for days -- kept every one of them open, and a stream harness keeps a CLI process per session. - The engine now holds a SessionView only weakly, through an `Opened` record (a `weakref.ref` subclass) the call's cleanup and the agent's hooks keep instead. A view let go of has its session closed on the run's loop -- marshalled there from whichever thread let go of it or a collection found it -- in an eagerly started task the call's and the run's cleanup wait for; the next spawn drains what was let go of first, so a flow that never yields holds few open too. A session is closed exactly once whichever of that and its call's end comes first, and one handed up to a caller still closes as the call that opened it ends. `Call.res` is keyed by id, so a session closed early leaves it. - A hook heard for a session let go of gets a closed stand-in; a fork keeps the session it was cut from open until its own first turn succeeds; a hook failing between turns no longer ties its session into a reference cycle through its own frame; `steer` refuses a session that is over. - A turn's hard deadline is kept when a sooner graceful one is what its driver is told: `Call.limits` also answers the hard deadline, and the engine interrupts the turn there and raises `DurationExceeded`; a turn hard for its cost or tokens is told the hard deadline, not the graceful one. A graceful deadline's cancel is sent from the loop, so a flow that returns as its last turn ends no longer leaks a bare CancelledError to its caller. - Fakes: `FakeAgentDriver.live` and `.peak` count open sessions; a fake turn held open by its reply is cut at a hard deadline, as a real one is; `FakeEnvDriver` lets go of temporary-copy holds as the run that took them closes (or as the root fake closes), so a run resumed on the same fake takes its copy again instead of raising TempCloneBusy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
futrime
added a commit
to humanfia/flowverse
that referenced
this pull request
Sep 25, 2026
…t go of humanize now closes a session a flow can no longer reach instead of holding it until the call that opened it ends (humanfia/humanize#82), so the compliance check's reviewer session is gone by the second round too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
futrime
pushed a commit
that referenced
this pull request
Sep 25, 2026
fix(flowing): close sessions a flow can no longer reach
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
The flow API has no
Session.close, and the engine closed a session only as the flow call that opened it ended, holding everySessionViewstrongly until then. A loop that opens a fresh session a round (ralph_loop, flame_chase, rlar's reviewer, lane runtimes, which can run for days) kept all of them open. With stream harnesses that is one CLI process per round.Session lifetime (
viewing.py,engine.py)SessionViewonly weakly, through anOpenedrecord (aweakref.refsubclass). The call's cleanup and the agent's hooks keep the record instead of the view.spawnfirst closes whatever was let go of, so a flow that never yields to the loop (a fake one, say) also keeps few sessions open.Call.resis keyed by id, so a session that closes early leaves it.steernow refuses a session that is closed.Hard deadlines under a sooner graceful one (a follow-up bug from the coordinator)
Call.limitsnow also returns the hard deadline that a sooner graceful deadline hides from the driver.AgentView.runarms a timer for it that interrupts the turn and raisesDurationExceeded. Only aSessionErrorfrom the interrupted turn becomesDurationExceeded; a turn that answered in time keeps its answer.CancelledErrorup to its caller.Fakes (
fakes.py)FakeAgentDriver.liveand.peakcount how many sessions are open now, and the most open at once.until_steered()) is cut off at a hard deadline, as a real turn is.FakeEnvDriverreleases temporary-copy holds when the run that took them closes (or when the root fake closes), and keeps the copies. A run resumed on the same fake gets its copy back instead ofTempCloneBusy.The spec (
specs/runtime/flowing.md, the cleanup section only), theSessiondocstring, and the docs (reference/flows.md,features/budgets.md,weaver/testing-flows.md) are updated to match.Tests
tests/unit/flows/test_engine_sessions.py(new):run_flow.test_engine_budget.py:test_engine_resume.pyandtest_fakes.py: a resumed run takes its copy again, and fake holds are released when the fake closes.test_engine_scale.py: a new micro-benchmark compares spawn + turn + close per round, letting go of each session versus keeping all of them. Result: 20.8 µs vs 20.0 µs (1.04×). The existing thresholds are unchanged and still pass. Per-call engine cost is the same as the base (1.88 µs vs 1.89 µs).uv run pre-commit run --all-filespasses.uv run pytestpasses (4149 passed, 110 skipped). The system tier ran without--run-agents, so no real harnesses were driven.E2E: an on-disk flow run through
run_flowon the fake kit, 1,000 rounds with a fresh session each: at most 2 sessions open at once, 0 open at the end, all 1,000 closed, in about 38 ms. On the base branch the peak is 1,000.🤖 Generated with Claude Code