Conversation
Implements ADR 004: Stateless Daemon Core. ## Phase 1: Fix session accumulation bug - reconcileAndEnrich() detects sessions that disappear from adapter discover() results and marks them stopped + autoUnlock - 30-second grace period for recently-launched sessions to avoid false positives from adapter discovery latency ## Phase 2: Make session.list adapter-first - session.list handler now fans out discover() to all adapters in parallel with 5s per-adapter timeouts - Merges results and enriches with daemon launch metadata (prompt, group, spec, cwd) - session.status also fans out to adapters for fresh data - Graceful degradation: failed adapters are skipped, partial results returned. Sessions from failed adapters fall back to launch metadata. ## Phase 3: Clean up - Removed SessionTracker.poll(), reapStaleEntries(), validateAllSessions(), pruneDeadSessions(), pruneOldSessions(), listSessions(), activeCount(), startPolling(), stopPolling() - Removed 5-second polling interval and all background state reconciliation - Simplified StateManager usage to only persist launch metadata, locks, and fuses - Added lightweight 30s PID liveness check for lock cleanup (startLaunchCleanup) — much cheaper than full adapter fan-out - MetricsRegistry decoupled from SessionTracker; active session count updated on session.list calls - session.prune kept for backward compat but now just runs PID liveness cleanup ## Key design decisions - Adapters own session truth. Daemon owns what it launched. - session.list = fan out adapter.discover() → merge → return - No daemon-side session registry for listing - Handle adapter failures gracefully (partial results, not errors) Fixes #51 Co-Authored-By: Charlie Hulcher <charlie@kindo.ai>
Collaborator
Author
|
Reviewed — clean implementation of ADR 004. Fan-out + reconcile is much simpler than the polling/pruning tower. Grace period for recently-launched sessions is a nice touch. Tests comprehensive. ⚡ |
Merged
This was referenced Mar 6, 2026
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
Implements ADR 004: Stateless Daemon Core — the daemon stops being a session database and becomes a stateless multiplexer.
Root cause: The daemon maintained a
state.jsonsession registry that mirrored every session discovered by every adapter. Over 26 hours, this accumulated 394 "active" sessions because OpenClaw sessions (no PID) had no exit path. Pruning was a band-aid on a fundamental design flaw.Fix: Adapters own session truth. Daemon owns what it launched.
Changes
Phase 1: Fix session accumulation bug
reconcileAndEnrich()detects sessions that disappear from adapterdiscover()results and marks them stopped + autoUnlockPhase 2: Make session.list adapter-first
session.listhandler fans outdiscover()to all adapters in parallel with 5s per-adapter timeoutssession.statusalso fans out to adapters for fresh dataPhase 3: Clean up
SessionTracker.poll(),reapStaleEntries(),validateAllSessions(),pruneDeadSessions(),pruneOldSessions(),listSessions(),activeCount(),startPolling(),stopPolling()startLaunchCleanup)MetricsRegistryfromSessionTracker; active session count updated onsession.listcallssession.prunekept for backward compat, now runs PID liveness cleanupArchitecture
Testing
npm test,npm run typecheck,npx biome checkall cleanFixes #51