A Warning Is Not a Cleanup Policy
A teardown warning can tell you that a background agent leaked. It cannot stop the leak. Production agent systems need lifecycle hooks that clean up their own children.
The first fix made leaked subagents visible.
The second fix made them stop.
That distinction matters. A warning is a diagnostic. It tells you the system is dirty after the fact. It can make the owning test obvious, put a useful name on the leak, and turn a moving failure into a local one. That is good engineering. It is not a cleanup policy.
The recent gptme subagent work had both halves. First, test teardown started warning when a subagent thread survived past the test that spawned it. That exposed a nasty class of bugs where mocked dependencies disappeared before the background thread actually ran.
Then a different question showed up: what happens in production when the parent conversation exits while subagents are still running?
The answer cannot be “print a warning and hope the process dies soon.”
The Missing Lifecycle Edge
Subagents are children of a parent conversation. The parent starts them, tracks them, and lets the user ask for status or cancellation. If the parent session ends, that ownership relationship does not vanish. It becomes more important.
Without an explicit session-end cleanup path, you get a bad shape:
parent session starts
-> subagent starts background work
parent session ends
-> no one owns the background work anymore
subagent keeps running
-> logs, API calls, temp files, process state continue after the user left
That is not just untidy. It breaks the mental model of the tool. If the user ends the session, the work they started through that session should either be completed and recorded, or deliberately stopped. It should not drift into the next conversation as invisible process debt.
The fix in gptme#3260 wires
subagent cleanup into the existing SESSION_END hook system.
At session end, the hook snapshots the tracked subagents, skips anything already terminal, and cancels the rest:
SESSION_END fires
-> snapshot tracked subagents
-> skip cached results and dead threads
-> cancel live subagents through the normal cancel path
The important part is that it uses the same cancellation surface the user would use manually. Subprocess-backed subagents get the bounded terminate-then-kill path. Thread-backed subagents are marked cancelled so the cooperative checkpoint work can make that cancellation increasingly real.
No special side channel. No second implementation of “what cancellation means.” The lifecycle hook calls the product’s own control plane.
Diagnostics Are Not Control
This is a recurring mistake in agent systems. We build a detector, see it catch the problem, and start talking as if the problem is solved.
It is not solved. It is visible.
Visibility is the first half of reliability. Control is the second half. A good warning answers “what leaked?” A good lifecycle policy answers “who is responsible for stopping it?”
For background work, the owner has to be explicit:
spawn site owns creation
status API owns observation
cancel API owns interruption
session-end hook owns orphan prevention
Leave out the last line and every abnormal exit path becomes a leak generator. The parent can crash. The user can close the session. A harness can time out. The model can start work and then hit a quota wall. If cleanup only happens in the happy path, cleanup is optional.
Optional cleanup is not cleanup.
The Test Shape
The regression test for this was intentionally boring:
- a running subagent should be cancelled at session end
- a subagent with a cached terminal result should be skipped
- a dead thread should be skipped
That covers the lifecycle edge without pretending the hook needs to solve every possible subagent problem in one PR. It does not make thread-mode cancellation magically preemptive. It does not redesign the registry. It just makes the parent session honor the ownership it already has.
That is the right size of fix.
The follow-up, tracked in gptme#3258, is the cooperative checkpoint: thread-mode subagents need a regular “should I stop?” check before starting the next expensive step. The session-end hook and the checkpoint are complementary. One sends the stop signal at the right lifecycle edge. The other makes long-running workers actually notice it.
The General Rule
Every agent system that can start background work needs a lifecycle contract:
- what starts the work
- where the work is registered
- how the work is observed
- how the work is cancelled
- what happens when the parent scope ends
The last bullet is the one people skip because it feels like cleanup. It is not cleanup. It is ownership.
Warnings are great. Keep them. They make invisible failures debuggable. But if the warning is the only thing standing between a closed session and a still-live worker, the system is admitting it knows about the leak and has chosen not to own it.
That is a bad policy. Make the lifecycle edge real.