Skip to content

[Bug]: Session reaper counts host sleep as idle time, so sleeping over 30 min kills every provider session on resume #11805

Description

@supermicro-danem

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Summary

ProviderSessionReaper measures session idleness in wall-clock time, so time the host spends suspended counts as idle time. Sleeping a laptop for longer than the inactivity threshold therefore reaps every live provider session on the first sweep after resume, even though the user's actual interaction gap was "closed the lid, opened the lid".

The server already has the signal needed to exclude suspended time. HostPowerMonitor carries a suspended flag in its HostPowerSnapshot, but the only consumer is BackgroundPolicy (BackgroundPolicy.ts:147). The reaper never consults it.

ProviderSessionReaper.ts:

const DEFAULT_INACTIVITY_THRESHOLD_MS = 30 * 60 * 1000;
const DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1000;
...
const now = yield* Clock.currentTimeMillis;
...
const lastSeenMs = Date.parse(binding.lastSeenAt);
if (now - lastSeenMs < inactivityThresholdMs) continue;
...
const lastActivityMs = Math.max(lastSeenMs, Date.parse(thread?.session?.updatedAt ?? binding.lastSeenAt));
const idleDurationMs = now - lastActivityMs;
if (idleDurationMs < inactivityThresholdMs) continue;
...
yield* providerService.stopSession({ threadId: binding.threadId })

Both inputs are persisted wall-clock timestamps compared against current wall-clock time. lastSeenAt is only refreshed by provider activity (lastSeenAt: now on directory.upsert), and nothing re-baselines it when the host wakes. A suspended process generates no activity, so the full sleep duration lands in idleDurationMs.

ProviderSessionReaperLive is wired with no option overrides (server.ts:445), so the 30-minute threshold and 5-minute sweep are what ships.

Steps to reproduce

  1. Start a turn on a thread and let it settle, so the session is running with activeTurnId null and no background liveness.
  2. Sleep or hibernate the host for more than 30 minutes.
  3. Wake it.
  4. Within one sweep interval, the session is stopped with reason: "inactivity_threshold", and provider.session.reaped is logged. The thread survives but has no bound provider session.

Expected behavior

Time the host spent suspended should not count toward the inactivity threshold. Either exclude suspended intervals from idleDurationMs, or re-baseline lastSeenAt / skip one sweep on resume, so that a sleep is not equivalent to the user walking away for the same duration.

Actual behavior

Every non-stopped session whose last activity predates the sleep by more than the threshold is reaped on resume. A laptop closed overnight therefore comes back with all of its provider sessions stopped.

Impact

Minor bug or occasional failure

Version or commit

main @ 9d4bb55. Symptom observed on desktop 0.0.40.

Environment

T3 Code desktop 0.0.40, Windows 11, provider Claude Code. Reported symptom that led here: after the host slept, pressing stop/close on a thread produced No active provider session is bound to this thread. (the failing stop path itself is #11796).

Logs or stack traces

provider.session.reaped
  threadId: <id>
  idleDurationMs: <at least the sleep duration>
  reason: "inactivity_threshold"

Workaround

Send a new message to the thread; the session resumes from resumeCursor. The reap is not data-destructive, it just unbinds the session.

The counter-argument, and why this still seems wrong

Reaping a session that has genuinely been idle for 30 minutes is the intended policy, and by wall-clock measurement a slept host qualifies. Two things make the suspended case different:

  1. The rationale for reaping is resource reclamation, and a suspended provider process is already consuming nothing. Nothing is reclaimed by killing it at the moment the host wakes.
  2. The threshold is calibrated against user inattention. Sleep is not inattention of the same kind: from the user's point of view they resumed immediately, and the sessions they left open are gone.

The reaper's two existing skip conditions show the same intent already applies elsewhere: it deliberately refuses to reap a thread mid-turn (skipped-active-turn) or with background work in flight (skipped-background-work), precisely because wall-clock idleness misrepresents those states too. Suspended time looks like a third case of the same kind.

What I verified and what I did not

Verified by reading main @ 9d4bb55: the thresholds and that no options override them, that both comparison inputs are persisted wall-clock timestamps, that lastSeenAt is only bumped by directory.upsert on provider activity, that nothing re-baselines it on resume, and that HostPowerMonitor.suspended has BackgroundPolicy as its only consumer.

Not verified: a captured reap-after-sleep log. The affected thread was on another machine, and on the machine I had access to the trace files contain only effect-span records with no log lines, so provider.session.reaped would not appear there even if it had fired. The sequence above is derived from the code, not from a captured instance. Happy to attach a log if you want it confirmed first.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions