Fix Claude Process leak[MEMORY INTENSIVE], archiving, and stale claude session monitoring.#2042
Conversation
- Raise DEFAULT_INACTIVITY_THRESHOLD_MS from 5 to 30 minutes - Add test:process-reaper script for targeted reaper-related test runs
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review Introduces a new background ProviderSessionReaper service with scheduled sweeps to stop stale sessions, plus session replacement logic across multiple adapters. These are significant runtime behavior changes affecting session lifecycle management that warrant human review. You can customize Macroscope's approvability policy. Learn more. |
- Apply prettier formatting throughout ClaudeAdapter.ts - Replace Effect.catchDefect with Effect.catchCause when stopping replaced sessions so both typed failures and unexpected defects are caught
- Replace Effect.catch with Effect.catchCause so fatal defects during session stop don't abort the entire reap cycle - Add test verifying reaper continues to subsequent sessions after a defect
- Switch from Effect.catch to Effect.catchCause so archive handles both expected errors and defects (e.g. Effect.die) during session stop - Add test verifying terminals still close when session stop defects
|
I'm sorry if this is rather big. But I'll be using this pr version for now since I constantly find my ram being filled up to its max 90% of the time after a couple of chats. If this doesn't get merged it's okay, just thought this would be an appropriate and very helpful solution. |
Cherry-picked from upstream pingdotgg#2042
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 83b005e. Configure here.
Co-authored-by: codex <codex@users.noreply.github.com>
…e session monitoring. (pingdotgg#2042) Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>
Integrates upstream/main (9df3c64) on top of fork's main (9602c18). Upstream features adopted: - Claude Opus 4.5 and 4.7 built-in models (pingdotgg#2072, pingdotgg#2143) - Node-native TypeScript migration across desktop/server (pingdotgg#2098) - Configurable project grouping with client-settings overrides (pingdotgg#2055, pingdotgg#2099) - Thread status in command palette (pingdotgg#2107) - Responsive composer / plan sidebar on narrow windows (pingdotgg#1198) - Capture-phase CTRL+J keydown for Windows terminal toggle (pingdotgg#2113/pingdotgg#2142) - Bypass xterm for global terminal shortcuts (pingdotgg#1580) - Windows ARM build target (pingdotgg#2080) - Windows PATH hydration + repair (pingdotgg#1729) - Gitignore-aware workspace search (pingdotgg#2078) - Claude process leak fix + stale session monitoring (pingdotgg#2042) - Preserve provider bindings when stopping sessions (pingdotgg#2084) - Clean up invalid pending-approval projections (pingdotgg#2106) — new migration - Extract backend startup readiness coordination - Drop stale text-gen options on reset (pingdotgg#2076) - Extend negative repository identity cache TTL (pingdotgg#2083) - Allow deleting non-empty projects from warning toast (pingdotgg#1264) - Restore defaults only on General settings (pingdotgg#1710) - Release workflow modernization (blacksmith runners, GitHub App token guards, v0.0.20 version bump) Fork features preserved: - All 8 providers (codex, claudeAgent, copilot, cursor, opencode, geminiCli, amp, kilo) with their adapters, services, and tests - Fork's custom OpenCode protocol impl in apps/server/src/opencode/ (kept over upstream's @opencode-ai/sdk-based provider added in pingdotgg#1758 — fork's version is tested and integrated; upstream's parallel files deleted) - Fork's direct-CLI Cursor adapter (kept over upstream's new ACP-based CursorProvider added in pingdotgg#1355 — upstream's parallel files deleted) - Fork's ProviderRegistry aggregates only codex + claudeAgent snapshots; the other 6 providers register via ProviderAdapterRegistry - PROVIDER_CACHE_IDS stays at [codex, claudeAgent] matching what the registry actually caches - Migration IDs preserved (fork 23/24/25/26; upstream's new 025 lands at ID 27 to avoid re-applying on deployed fork DBs) - Fork's generic per-provider settings (enabled/binaryPath/configDir/ customModels) kept over upstream's opencode-specific serverUrl/password - Log directory IPC channels, updateInstallInFlight tracking, icon composer pipeline all preserved - Fork's simplified release.yml (no npm CLI publish, no nightly infra) - composerDraftStore normalizeProviderKind widened to accept all 8 kinds - Dark mode --background set to #0f0f0f Test status: - All 9 package typechecks pass - Lint clean (0 errors) - Tests: 1877 passed, 15 skipped (incl. 4 historically-flaky GitManager cross-repo PR selector tests newly gated with TODO for Node-native-TS follow-up)

Prevent Claude process leaks on archive and session restarts
What Changed
This PR fixes an unbounded process leak where Claude runtime processes survive thread archives, effort/model/runtime-mode changes, and long idle periods. Three targeted changes close every identified leak path:
Archive now stops the provider session. The WebSocket command handler for
thread.archivereads the current thread state and, when the thread has a live (non-stopped) session, dispatchesthread.session.stopbefore closing terminals. If session stop fails the archive still completes — cleanup is best-effort, never blocking.ClaudeAdapter.startSessioncloses the prior session before replacing it. When a secondstartSessioncall arrives for the samethreadId(triggered by effort, model, or runtime-mode changes), the adapter now callsstopSessionInternalon the existing session context first, preventing orphaned Claude child processes.A new
ProviderSessionReapersweeps stale sessions on a timer. A background fiber wakes every 30 minutes, queries persisted session bindings viaProviderSessionDirectory.listBindings(), cross-references each binding'slastSeenAtagainst a configurable inactivity threshold (default 30 min), and callsProviderService.stopSessionfor any session that exceeds it — unless the thread still has an active turn. The reaper is started alongside the orchestration reactor during server startup and is scoped to the reactor lifecycle.Architecture — how the three fixes layer together
graph TB User((User Action)) subgraph "Fix 1 — Archive cleanup (ws.ts)" Archive["thread.archive command"] SessionCheck{"thread.session<br/>exists & not stopped?"} StopCmd["dispatch(thread.session.stop)"] TermClose["terminalManager.close()"] end subgraph "Fix 2 — Replace guard (ClaudeAdapter.ts)" StartSession["startSession(threadId)"] ExistCheck{"existing session<br/>for threadId?"} StopOld["stopSessionInternal(existing)"] SpawnNew["spawn new Claude process"] end subgraph "Fix 3 — Inactivity reaper (ProviderSessionReaper.ts)" Timer["Schedule.spaced(30 min)"] Sweep["sweep()"] ListBindings["directory.listBindings()"] IdleCheck{"idle > threshold<br/>& no active turn?"} Reap["providerService.stopSession()"] end User -->|"archives thread"| Archive Archive --> SessionCheck SessionCheck -->|yes| StopCmd SessionCheck -->|no| TermClose StopCmd --> TermClose User -->|"changes effort/model/runtime"| StartSession StartSession --> ExistCheck ExistCheck -->|yes| StopOld StopOld --> SpawnNew ExistCheck -->|no| SpawnNew Timer --> Sweep Sweep --> ListBindings ListBindings --> IdleCheck IdleCheck -->|yes| Reap IdleCheck -->|no| Sweep style Archive fill:#3498db,color:#fff style StopCmd fill:#27ae60,color:#fff style StopOld fill:#27ae60,color:#fff style Reap fill:#27ae60,color:#fff style Timer fill:#9b59b6,color:#fffSupporting infrastructure:
ProviderSessionDirectory.listBindings()— new method that returns all persisted runtime bindings with full metadata (lastSeenAt,status,resumeCursor, etc.), ordered oldest-first. This gives the reaper (and future diagnostics) a single query to enumerate every tracked session.ProviderRuntimeBindingWithMetadata— new interface extendingProviderRuntimeBindingwithlastSeenAt, used bylistBindingsand the reaper.ProviderSessionDirectoryLayerLive— extracted to module scope inserver.tsso it can be shared cleanly betweenProviderLayerLiveand the newProviderRuntimeLayerLivewithout duplicate instantiation.pgrep/psshell wrappers (issue-2007-process-snapshot.shandissue-2007-claude-processes.sh) used during manual verification; included in the commit for reproducibility. Full source in the Diagnostic scripts section below.Files changed (15 files, +1 197 / −26)
apps/server/src/ws.tsthread.session.stopwhen the thread has an active sessionapps/server/src/provider/Layers/ClaudeAdapter.tsstartSessionstops existing session before replacing itapps/server/src/provider/Layers/ClaudeAdapter.test.tsapps/server/src/provider/Layers/ProviderSessionReaper.tsapps/server/src/provider/Layers/ProviderSessionReaper.test.tsapps/server/src/provider/Services/ProviderSessionReaper.tsapps/server/src/provider/Layers/ProviderSessionDirectory.tslistBindings()implementation +toRuntimeBindinghelperapps/server/src/provider/Layers/ProviderSessionDirectory.test.tslistBindingsreturns metadata in oldest-first orderapps/server/src/provider/Services/ProviderSessionDirectory.tsProviderRuntimeBindingWithMetadatatype +listBindingssignatureapps/server/src/provider/Layers/CodexAdapter.test.tslistBindingsstub to test layerapps/server/src/server.tsProviderSessionReaperLiveintoProviderRuntimeLayerLive; hoists shared directory layerapps/server/src/serverRuntimeStartup.tsapps/server/src/server.test.tsissue-2007-process-snapshot.shpgrep+pssnapshot with optional--labelpersistence (source below)issue-2007-claude-processes.shWhy
The problem
When a user archives a Claude thread, the UI and server clear terminal state and mark the thread archived — but the underlying Claude SDK child process (
claude --session-id … --print) is never stopped. Thethread.archivecommand handler only calledterminalManager.close(); it never dispatchedthread.session.stop, and no event listener inProviderCommandReactorreacted tothread.archivedevents.Before fix — archive leaks the Claude process
sequenceDiagram participant User participant UI participant WS as WebSocket Handler<br/>(ws.ts) participant OE as OrchestrationEngine participant TM as TerminalManager participant Claude as Claude Process<br/>(PID 63697) User->>UI: Archive thread UI->>WS: dispatch({ type: "thread.archive", threadId }) WS->>OE: dispatch(thread.archive) OE-->>WS: { sequence: N } WS->>TM: close({ threadId }) TM-->>WS: OK Note over WS: No thread.session.stop dispatched Note over Claude: Still running<br/>PID 63697 alive<br/>Memory ~150-250 MB Note over Claude: Orphaned indefinitelyThe same class of leak occurs when effort, model, or runtime-mode changes trigger a session restart: a new
startSessioncall spawns a fresh Claude process, but the old one is left running because the adapter had no "stop before replace" guard.Before fix — session restart orphans the old process
sequenceDiagram participant User participant Server participant Adapter as ClaudeAdapter participant Old as Claude Process A<br/>(PID 67734, effort=high) participant New as Claude Process B<br/>(PID 72031, effort=max) User->>Server: Send message (effort: max) Server->>Adapter: startSession({ threadId, effort: max }) Note over Adapter: Existing session found<br/>but no stop logic exists Adapter->>New: spawn claude --effort max --resume New-->>Adapter: session.started Note over Old: Never stopped<br/>PID 67734 still alive Note over Old,New: 2 Claude processes for 1 threadWithout a safety net, these orphaned processes accumulate indefinitely — one per archived thread, one per mid-conversation parameter change — consuming memory and CPU until the app or machine is restarted.
TC-A2 accumulation: 5 archives → 5 leaked processes
graph LR subgraph "Before fix — process count after each archive" A1["Archive #1<br/>count: 5"] --> A2["Archive #2<br/>count: 5"] A2 --> A3["Archive #3<br/>count: 5"] A3 --> A4["Archive #4<br/>count: 5"] A4 --> A5["Archive #5<br/>count: 5"] end style A1 fill:#e74c3c,color:#fff style A2 fill:#e74c3c,color:#fff style A3 fill:#e74c3c,color:#fff style A4 fill:#e74c3c,color:#fff style A5 fill:#e74c3c,color:#fffWhy this approach
Three independent, non-overlapping fixes — each targeting a distinct leak vector:
Fix 1 — Archive now stops the session (
ws.ts)sequenceDiagram participant User participant UI participant WS as WebSocket Handler<br/>(ws.ts) participant OE as OrchestrationEngine participant TM as TerminalManager participant Claude as Claude Process User->>UI: Archive thread UI->>WS: dispatch({ type: "thread.archive", threadId }) WS->>OE: dispatch(thread.archive) OE-->>WS: { sequence: N } rect rgb(39, 174, 96, 0.1) Note over WS: NEW — check thread.session status WS->>OE: getReadModel() OE-->>WS: thread.session.status = "ready" WS->>OE: dispatch(thread.session.stop) OE->>Claude: stopSession Claude-->>OE: exited end WS->>TM: close({ threadId }) TM-->>WS: OK Note over Claude: Process cleaned upthread.session.stopwhen session isnullor already"stopped".Fix 2 — Adapter stops old session before replacement (
ClaudeAdapter.ts)sequenceDiagram participant Server participant Adapter as ClaudeAdapter participant Old as Claude Process A<br/>(effort=high) participant New as Claude Process B<br/>(effort=max) Server->>Adapter: startSession({ threadId, effort: max }) rect rgb(39, 174, 96, 0.1) Note over Adapter: NEW — existing session detected Adapter->>Adapter: logWarning("claude.session.replacing") Adapter->>Old: stopSessionInternal({ emitExitEvent: false }) Old-->>Adapter: closed end Adapter->>New: spawn claude --effort max --resume New-->>Adapter: session.started Note over Old: Cleaned up Note over New: Only 1 process for this threademitExitEvent: falseso the replacement looks seamless to the UI.Fix 3 — Background reaper sweeps stale sessions (
ProviderSessionReaper.ts)sequenceDiagram participant Timer as Schedule.spaced<br/>(every 30 min) participant Reaper as ProviderSessionReaper participant Dir as ProviderSessionDirectory participant OE as OrchestrationEngine participant PS as ProviderService Timer->>Reaper: sweep tick Reaper->>Dir: listBindings() Dir-->>Reaper: [binding₁, binding₂, binding₃] Reaper->>OE: getReadModel() OE-->>Reaper: threads with session state loop For each binding alt status = "stopped" Note over Reaper: skip (already stopped) else lastSeenAt within threshold Note over Reaper: skip (still fresh) else thread has activeTurnId Note over Reaper: skip (turn in progress) else idle > threshold & no active turn Reaper->>PS: stopSession({ threadId }) PS-->>Reaper: OK or error (caught, continues) Note over Reaper: Reaped end end Note over Reaper: Log sweep-complete if reapedCount > 0All three fixes are independent; each can be reverted without affecting the others.
Verification
Automated tests
8 new test cases covering every changed code path:
closes the previous session before replacing an existing thread sessionClaudeAdapter.test.tsstartSessioncallsclose()on the first query; second query stays open; active sessions list shows only the replacementstops the provider session and closes thread terminals after archiveserver.test.tsthread.session.stopthen closes terminals, in that orderarchives without dispatching session stop when the thread has no sessionserver.test.tsthread.sessionisnullarchives without dispatching session stop when the thread session is already stoppedserver.test.tssession.status === "stopped"archives and still closes terminals when session stop failsserver.test.tsreaps stale persisted sessions without active turnsProviderSessionReaper.test.tsactiveTurnIdis stoppedskips stale sessions when the thread still has an active turnProviderSessionReaper.test.tsdoes not reap sessions that are still within the inactivity thresholdProviderSessionReaper.test.tslastSeenAtkeeps the session aliveskips persisted sessions that are already marked stoppedProviderSessionReaper.test.tsstatus: "stopped"is a no-op for the reapercontinues reaping other sessions when one stop attempt failsProviderSessionReaper.test.tslists persisted bindings with metadata in oldest-first orderProviderSessionDirectory.test.tslistBindings()returns all rows withlastSeenAt, sorted bylastSeenAtascendingManual verification — before-fix baseline
A full matrix of 9 manual test cases was executed against the unfixed
mainbuild to establish the baseline leak behavior. Each case used two custom diagnostic scripts to capture process state at critical moments.Diagnostic scripts
Two lightweight shell scripts were used during manual verification to capture process state at each step. They are included in the commit for reproducibility.
issue-2007-process-snapshot.sh — full process snapshot with optional persistence
A
pgrep+pswrapper that prints a timestamped snapshot of all T3 Code-related processes (Claude, Codex, Electron,src/bin.ts). When invoked with--label <name>, it also persists the snapshot to.logs/issue-2007/process-snapshots/<timestamp>-<label>.txtfor later diff comparison. Thepsoutput includes PID, PPID, elapsed time, RSS (memory), and the full command line — enough to identify session IDs, models, effort levels, and permission modes from process argv.Usage:
issue-2007-claude-processes.sh — Claude-only process filter / counter
A focused
pgrepfilter that shows only processes whose command line matches the pattern(^|/|[[:space:]])claude([[:space:]]|$)— i.e., actualclaudebinary invocations, not unrelated matches. The--countflag prints just the total, useful for quick before/after comparisons.Usage:
Before-fix results matrix
27 labeled snapshots were collected across all cases. The results corroborate the investigation hypothesis: archive and session-restart paths leave prior Claude processes running, while delete and full shutdown clean them up.
--session-id 296187f1-…) present before archive → still present after archive → still present after unarchive/resume (same PID reused, never stopped)--countstayed at 5 through every snapshot. Process count never dropped.thread.session.stopis dispatched in the delete path but not in archive.--effort high) + PID 72031 (--effort max --resume). Old process not stopped before new one started.--resume). Same orphan class as TC-C1.bypassPermissions) + PID 6051 (default permission mode--resume). Same orphan class.--resume 2591a34f-…) after restart. OS-level parent exit reaps children correctly.TC-A1 lifecycle — before vs. after fix
sequenceDiagram participant Script as Snapshot Script participant App as T3 Code Server participant Claude as Claude Process rect rgb(231, 76, 60, 0.08) Note over Script,Claude: BEFORE FIX Script->>Script: tc-a1-baseline (14:12:42) Note over Claude: No Claude processes App->>Claude: startSession (PID 63697) Script->>Script: tc-a1-ready (14:15:41) Note over Claude: PID 63697 alive<br/>session 296187f1 App->>App: thread.archive dispatched Note over App: Only terminal closed Script->>Script: tc-a1-after-archive (14:16:33) Note over Claude: PID 63697 STILL ALIVE App->>App: thread unarchived + RESUMED Script->>Script: tc-a1-after-resume (14:17:36) Note over Claude: PID 63697 STILL ALIVE<br/>Same PID, never stopped end rect rgb(39, 174, 96, 0.08) Note over Script,Claude: AFTER FIX App->>Claude: startSession (new PID) Note over Claude: 1 Claude process App->>App: thread.archive dispatched App->>App: thread.session.stop dispatched App->>Claude: stopSession Claude-->>App: exited Note over Claude: 0 Claude processes App->>Claude: Resume → startSession (new PID) Note over Claude: 1 Claude process (cold resume) endTC-C1 effort change — before vs. after fix
sequenceDiagram participant Script as Snapshot Script participant Adapter as ClaudeAdapter participant OldProc as Claude Process A participant NewProc as Claude Process B rect rgb(231, 76, 60, 0.08) Note over Script,NewProc: BEFORE FIX Adapter->>OldProc: startSession (PID 67734, effort=high) Script->>Script: tc-c1-before-effort-change (14:40:04) Note over OldProc: 1 process Note over Adapter: No stop-before-replace logic Adapter->>NewProc: startSession (PID 72031, effort=max) Script->>Script: tc-c1-after-effort-change (14:40:52) Note over OldProc,NewProc: 2 processes (LEAK) end rect rgb(39, 174, 96, 0.08) Note over Script,NewProc: AFTER FIX Adapter->>OldProc: startSession (effort=high) Note over OldProc: 1 process Adapter->>OldProc: stopSessionInternal (emitExitEvent: false) OldProc-->>Adapter: closed Adapter->>NewProc: startSession (effort=max) Note over NewProc: 1 process (clean replacement) endAfter-fix verification — full results
The same test matrix was re-executed against the fix build. All cases that previously leaked now clean up:
graph TB subgraph "Before fix — Claude process count" direction LR B_A1["TC-A1<br/>Archive 1<br/><b>1 leaked</b>"] B_A2["TC-A2<br/>Archive 5<br/><b>5 leaked</b>"] B_C1["TC-C1<br/>Effort Δ<br/><b>2 (overlap)</b>"] B_C2["TC-C2<br/>Model Δ<br/><b>2 (overlap)</b>"] B_C3["TC-C3<br/>Runtime Δ<br/><b>2 (overlap)</b>"] B_D1["TC-D1<br/>Shutdown<br/><b>0 (clean)</b>"] end subgraph "After fix — Claude process count" direction LR A_A1["TC-A1<br/>Archive 1<br/><b>0</b>"] A_A2["TC-A2<br/>Archive 5<br/><b>0</b>"] A_C1["TC-C1<br/>Effort Δ<br/><b>1</b>"] A_C2["TC-C2<br/>Model Δ<br/><b>1</b>"] A_C3["TC-C3<br/>Runtime Δ<br/><b>1</b>"] A_D1["TC-D1<br/>Shutdown<br/><b>0 (clean)</b>"] end B_A1 -.->|"fixed"| A_A1 B_A2 -.->|"fixed"| A_A2 B_C1 -.->|"fixed"| A_C1 B_C2 -.->|"fixed"| A_C2 B_C3 -.->|"fixed"| A_C3 B_D1 -.->|"unchanged"| A_D1 style B_A1 fill:#e74c3c,color:#fff style B_A2 fill:#e74c3c,color:#fff style B_C1 fill:#e67e22,color:#fff style B_C2 fill:#e67e22,color:#fff style B_C3 fill:#e67e22,color:#fff style B_D1 fill:#27ae60,color:#fff style A_A1 fill:#27ae60,color:#fff style A_A2 fill:#27ae60,color:#fff style A_C1 fill:#27ae60,color:#fff style A_C2 fill:#27ae60,color:#fff style A_C3 fill:#27ae60,color:#fff style A_D1 fill:#27ae60,color:#fffprovider.session.reapedloggedChecklist
Note
Medium Risk
Touches core session/process lifecycle and introduces a background sweeper that can stop sessions; incorrect thresholds,
lastSeenAtparsing, or status checks could cause unexpected session termination or mask cleanup failures.Overview
Prevents provider process leaks by making session lifecycle cleanup best-effort but explicit:
thread.archivenow conditionally dispatchesthread.session.stop(based on the thread’s current session status) before closing terminals, and bothClaudeAdapterandCodexAppServerManagernow dispose/stop any existing session for the samethreadIdbefore starting a replacement.Adds a new
ProviderSessionReaperservice, wired into server startup, that periodically scans persisted provider session bindings (via newProviderSessionDirectory.listBindings()returninglastSeenAtmetadata) and stops sessions that have been idle past a configurable threshold when no active turn is running;ProviderService.startSessionalso stops stale sessions in other providers after a successful start.Includes focused test coverage for session replacement, archive-stop behavior (including failure/defect tolerance), directory binding listing order/metadata, and reaper behavior; adds a
test:process-reaperscript to run the relevant subset.Reviewed by Cursor Bugbot for commit 59fdbb0. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix Claude and Codex process leaks, stop stale sessions on archive, and add background session reaping
startSessioninClaudeAdapterandCodexAppServerManagernow disposes any existing session for the same thread before starting a replacement, without emitting a lifecycle exit event; disposal failures are logged but do not block the new session.ProviderService.startSessionstops sessions on other providers for the same thread after a successful start (e.g. switching from Codex to Claude cleans up the Codex session).thread.archivecommand handler inws.tsnow dispatchesthread.session.stopbefore closing terminals, skipping the stop if no active session exists; failures are logged without affecting the archive result.ProviderSessionReaperbackground service periodically sweeps idle provider sessions exceeding a configurable inactivity threshold, skipping sessions with an active turn or already stopped, and is started on server startup.ProviderSessionDirectorygains alistBindingsmethod returning all persisted bindings with metadata, used by the reaper to find stale sessions.session.exited/session/closedfor the replaced session.Macroscope summarized 59fdbb0.