You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
refactor(server): extract IdleReaper; one owner for the close deadline
Quality audit of my own feature. Four structural problems, all self-inflicted.
1. The idle sweeper was inlined into SessionManager: +204 lines onto an
already-1472-line file, plus 4 constructor options (idleCloseMs, idleSweepMs,
setTimer, clearTimer), 6 private fields and its own re-entrancy flag. It is a
periodic policy collaborator, which this codebase already has a shape for —
MetricsCollector and the ports service both own their window, clock and
injected timer and are wired in server.ts. Now `idle_reaper.ts` does the same,
reaching the session world through a two-function seam. manager.ts drops back
to +59 over base, ManagerOpts loses 4 options, and serve.ts returns to
untouched (the env parsing lives with the module that owns the policy).
The tests got the bigger win: the reaper reads a handful of session fields, so
they now use plain object doubles instead of standing up a SessionManager,
SQLite store and temp dir per case. Each case states exactly the session shape
it is about and nothing else can affect the outcome.
2. Two nested close deadlines with one caller between them. The adapter bounded
its own `session/close` AND the manager bounded `adapter.close()` — which is
how the previous commit shipped them inverted, and why it needed an invariant
test to hold them in order. Defence in depth against nobody: `closeSession` is
the only caller. Deleted the adapter-level deadline, the constant, the option,
the invariant test, and `bestEffort()` (a wrapper with one call site once the
redundancy was gone). Adapters now issue the plain request and MAY reject or
hang; the manager bounds and swallows it in one place, for every back end, and
reaps either way. The contract is stated on `AgentAdapter.close` instead of
half-implemented in three adapters.
3. `deadline.ts` was created as the canonical home for bounded agent waits and
then acp.ts's three pre-existing ad-hoc timeouts were left in place — four ways
to do one thing. Converged onto `withDeadline`, deleting the local `withTimeout`
closure and two raw `Promise.race` blocks; acp.ts shrinks despite gaining the
close path.
4. A triple-negative status check (`!== "running" && !== "awaiting-input" &&
!== "awaiting-approval"`) was the only place asking "is this agent mid-flight?"
— a missing model, not a formatting problem. Now `isBusy(status)` next to the
SessionStatus type it interrogates, and `Session.cold` replaces two
`instanceof DetachedAdapter` checks.
Also caught by the type checker mid-refactor: I had added a second `allSessions()`
returning Iterable when a canonical one returning Session[] already existed, which
broke its existing callers. Removed; the reaper reuses the canonical accessor.
No behaviour change. 1297 server tests green, and the stub e2e still proves
close -> not active -> in the Closed list -> a message transparently reopens.
0 commit comments