Replace provider session reaper polling with deadline-driven scheduling - #2351
Replace provider session reaper polling with deadline-driven scheduling#2351crafael23 wants to merge 9 commits into
Conversation
- Introduce ProviderSessionDirectoryEvents pub/sub for directory change notifications - Refactor ProviderSessionReaper from sweep-based to deadline-based reaping - Extract reaperDeadlines module for computing inactivity deadlines from read model - Add reaper observability metrics (wakeups, candidates, reap lag, schedule size) - Wire ProviderSessionDirectoryEvents through all provider layer compositions
…ssion-reaper-deadline
- Remove polling and deadline-shadow modes; hardcode deadline scheduling - Expose inactivity threshold and fallback reconcile interval via env vars (T3CODE_PROVIDER_SESSION_REAPER_INACTIVITY_THRESHOLD_MS, T3CODE_PROVIDER_SESSION_REAPER_FALLBACK_RECONCILE_INTERVAL_MS) - Add OpenTelemetry spans and attributes across reaper lifecycle - Signal reconcile-all after a reap so future deadlines fire without requiring a directory wake - Add test covering post-stop rescheduling of future deadlines
|
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 This PR replaces the provider session reaper's polling mechanism with a deadline-driven event scheduler, introducing new service abstractions, scheduling logic, and metrics. This is a major architectural refactor of core background processing that warrants human review. You can customize Macroscope's approvability policy. Learn more. |
- Schedule retry deadline when stopSession fails instead of silently dropping - Merge retry deadline with next future session deadline via earliestDeadline helper - Default retry interval: 5s, configurable via stopFailureRetryIntervalMs - Add timing test covering retry interleaved with future deadlines
- Track signal feed failures via providerSessionReaperSignalFeedRestartsTotal counter - Mark reaper start/iteration spans as root to decouple from caller traces - Strip parent span context from forked deadline scheduler - Add tests for root span isolation and feed-restart metric emission
- Signal feeds now retry after transient failures instead of stopping - forkFeed accepts a thunk to re-subscribe on each restart attempt - Add 1s delay between restart attempts to avoid tight loops - Add test covering feed failure, restart, and subsequent event processing
- Re-raise interrupt causes instead of swallowing them in catchCause - Add test verifying upsert propagates interruption from publishChanged - Remove unused makeProviderSessionDirectoryEventsLive function
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 13ecfe6. Configure here.
- Delete ProviderSessionReaperLive default-options export - Make resolveInactivityAnchor module-private

Summary
ProviderSessionReaperwith an event-driven, deadline-based scheduler that sleeps until the nearest inactivity deadline and wakes early on relevant state changes.reaperDeadlines.ts— deadlines are computed, never persisted.ProviderSessionDirectoryEvents, a local PubSub that publishes afterProviderSessionDirectory.upsert(...)writes.Motivation
The previous reaper was correct but coarse: it swept every 5 minutes regardless of state changes, meaning sessions could linger up to one full sweep interval past their true inactivity deadline. The new scheduler matches product intent more directly — if a session becomes inactive at time
T, it is stopped close toT + threshold, notT + threshold + [0, sweepInterval).Architecture
Scheduler loop
sequenceDiagram participant Reaper as DeadlineScheduler participant Wake as CoalescedWake (cap-1 queue) participant Reconcile as reconcileAuthoritativeState() participant Directory as ProviderSessionDirectory participant Orchestration as OrchestrationEngineService participant Stop as ProviderService.stopSession() Note over Reaper: startup signal enqueued loop Each iteration alt No pending deadline Reaper->>Wake: await (block indefinitely) else Deadline scheduled Reaper->>Wake: await with timeout(deadlineAtMs − now) end Wake-->>Reaper: signal | timeout Reaper->>Reconcile: reconcile from authoritative state Reconcile->>Directory: listBindings() Reconcile->>Orchestration: getReadModel() Reconcile-->>Reaper: ReconcileSnapshot {entries[], skipped, invalid} Reaper->>Reaper: partition entries → due (deadline ≤ now) / future opt due entries exist loop Each due entry (sequential) Reaper->>Stop: stopSession(threadId) Stop-->>Reaper: reaped | failed end opt reaped > 0 AND future entries remain Reaper->>Wake: post-stop reconcile signal end end Reaper->>Reaper: sleep until earliest future deadline endWake signal sources
sequenceDiagram participant DirEvents as ProviderSessionDirectoryEvents participant OrchEvents as OrchestrationEngine.streamDomainEvents participant Fallback as Fallback timer (30m default) participant Startup as startup signal participant PostStop as post-stop reconcile participant Wake as CoalescedWake DirEvents->>Wake: runtime-binding-changed(threadId) Note right of DirEvents: on every Directory.upsert() OrchEvents->>Wake: orchestration-thread-changed(threadId) Note right of OrchEvents: thread.session-set, thread.turn-diff-completed, thread.reverted OrchEvents->>Wake: thread-deleted(threadId) Fallback->>Wake: reconcile-all("fallback-tick") Note right of Fallback: backstop for missed signals / clock drift Startup->>Wake: startup Note right of Startup: initial reconcile on boot PostStop->>Wake: reconcile-all("post-stop") Note right of PostStop: only when ≥1 stop succeeded AND future deadlines existDeadline derivation (
reaperDeadlines.ts)sequenceDiagram participant Derive as deriveReapEntries() participant Binding as ProviderRuntimeBinding participant Thread as OrchestrationReadModel thread Derive->>Binding: for each binding alt binding.status === "stopped" Derive->>Derive: skip (skippedStopped++) else Derive->>Thread: lookup thread by binding.threadId Derive->>Derive: resolveInactivityAnchor() Note right of Derive: priority: completedAt > startedAt > requestedAt > lastSeenAt alt anchor parses to NaN Derive->>Derive: push to invalidAnchors[], skip else thread.session.activeTurnId != null Derive->>Derive: skip (skippedActiveTurn++) else Derive->>Binding: read runtimePayload.lastRuntimeEvent alt lastRuntimeEvent === "provider.sendTurn" AND eventAt > anchor Derive->>Derive: deadlineBasis = sendTurnAt (floor) else Derive->>Derive: deadlineBasis = inactivityAnchor end Derive->>Derive: deadlineAtMs = deadlineBasis + inactivityThreshold end end Derive->>Derive: sort entries by deadlineAtMs, then threadIdSafety invariant
Every wake — startup, directory event, orchestration event, timeout, fallback tick, post-stop — leads to the same
reconcileAuthoritativeState()path that readslistBindings()+getReadModel()before any stop decision. Wake signals are hints only.Additional guards:
activeTurnId != null) → skippedprovider.sendTurndeadline floor → can only delay a deadline, never advance itreapedCount > 0 AND futureEntries > 0What changed
ProviderSessionReaper.tsSchedule.spacedpolling with deadline scheduler loop: startup reconcile → event/timeout wake → reconcile → stop due → sleep to next deadlinereaperDeadlines.ts(new)ReapScheduleEntry[]from bindings + read model; anchor priority,sendTurnfloor, invalid anchor detectionProviderSessionDirectoryEvents.ts(new)PubSub<{threadId}>published on everyDirectory.upsert(); consumed as a wake signal streamProviderSessionDirectory.tspublishChanged()call (best-effort, failure logged at debug)Metrics.tsconfig.ts/cli.ts/server.tsT3CODE_PROVIDER_SESSION_REAPER_INACTIVITY_THRESHOLD_MSandT3CODE_PROVIDER_SESSION_REAPER_FALLBACK_RECONCILE_INTERVAL_MSServices/ProviderSessionReaper.tssweepIntervalMsoptionObservability
Metrics:
t3_provider_session_reaper_wakeups_total,_due_candidates_total,_reaped_total,_reap_lag,_schedule_size,_reconcile_duration,_wake_coalesced_totalSpans:
provider.session.reaper.start,.iteration,.reconcile,.stop_due,.stop_sessionTest plan
provider.sendTurndeadline floorTestClockbun run test src/provider/Layers/ProviderSessionReaper.timing.test.ts \ src/provider/Layers/ProviderSessionReaper.test.ts \ src/provider/Layers/reaperDeadlines.test.ts \ src/provider/Layers/ProviderSessionDirectory.test.ts \ src/provider/Layers/ProviderSessionDirectoryEvents.test.ts \ src/provider/Layers/ProviderService.test.ts6 files, 50 tests — all passing locally.
bun fmt,bun lint,bun typecheckexit clean.🤖 Generated with Claude Code
Note
Medium Risk
Touches session lifecycle management by changing when/why
stopSessionis invoked and adds new wake-signal plumbing; correctness depends on deadline derivation and event-stream behavior under failure/retry.Overview
Replaces
ProviderSessionReaper’s fixed-interval polling sweep with a deadline-driven scheduler that sleeps until the next inactivity deadline and wakes early on signals fromProviderSessionDirectorychanges and orchestration domain events, plus a configurable fallback reconcile tick and stop-failure retry.Adds
ProviderSessionDirectoryEvents(PubSub-backed) and wires it through server/runtime layers and tests;ProviderSessionDirectory.upsertnow publishes best-effort change notifications after persistence writes. Deadline derivation is extracted into newreaperDeadlines.ts(anchor selection + optionalprovider.sendTurnfloor), alongside substantial new reaper metrics/spans and new config/env tuning for inactivity threshold and fallback reconcile interval (propagated throughconfig.ts,cli.ts,server.ts, and test fixtures).Reviewed by Cursor Bugbot for commit 09d9116. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Replace
ProviderSessionReaperperiodic sweep with deadline-driven schedulingmakeProviderSessionReaperinProviderSessionReaper.tsto compute per-thread inactivity deadlines and sleep until the earliest one, instead of running a fixed-interval sweep.ProviderSessionDirectoryEventsPubSub service that emits{threadId}change events;ProviderSessionDirectory.upsertnow publishes to it after successful persistence.orchestrationEngine.streamDomainEventsas wake signals, and uses the newderiveReapEntriesfunction inreaperDeadlines.tsto build and sort the reap schedule.fallbackReconcileIntervalMsandstopFailureRetryIntervalMsoptions (replacingsweepIntervalMs), configurable viaT3CODE_PROVIDER_SESSION_REAPER_INACTIVITY_THRESHOLD_MSandT3CODE_PROVIDER_SESSION_REAPER_FALLBACK_RECONCILE_INTERVAL_MSenv vars.ProviderSessionReaperLiveconstant is no longer exported; callers must usemakeProviderSessionReaperLive()directly.Macroscope summarized 21e0d37.