Before submitting
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
- Start a turn on a thread and let it settle, so the session is
running with activeTurnId null and no background liveness.
- Sleep or hibernate the host for more than 30 minutes.
- Wake it.
- 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:
- 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.
- 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
Before submitting
Area
apps/server
Summary
ProviderSessionReapermeasures 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.
HostPowerMonitorcarries asuspendedflag in itsHostPowerSnapshot, but the only consumer isBackgroundPolicy(BackgroundPolicy.ts:147). The reaper never consults it.ProviderSessionReaper.ts:Both inputs are persisted wall-clock timestamps compared against current wall-clock time.
lastSeenAtis only refreshed by provider activity (lastSeenAt: nowondirectory.upsert), and nothing re-baselines it when the host wakes. A suspended process generates no activity, so the full sleep duration lands inidleDurationMs.ProviderSessionReaperLiveis wired with no option overrides (server.ts:445), so the 30-minute threshold and 5-minute sweep are what ships.Steps to reproduce
runningwithactiveTurnIdnull and no background liveness.reason: "inactivity_threshold", andprovider.session.reapedis 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-baselinelastSeenAt/ 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
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:
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, thatlastSeenAtis only bumped bydirectory.upserton provider activity, that nothing re-baselines it on resume, and thatHostPowerMonitor.suspendedhasBackgroundPolicyas 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-spanrecords with no log lines, soprovider.session.reapedwould 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