pingdotgg / pingdotgg/t3code

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

Open
#11,805 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug via-triage
Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

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 @ 9d4bb550. 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 @ 9d4bb550: 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
  • #11796 - the stop failure that surfaces after a session is unbound. This issue is one trigger for reaching that state; that issue is about the stop path not settling afterwards.
  • #11202 - guarded idle session stops, same subsystem.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with ProviderSessionReaper.ts and trace HostPowerMonitor's suspended flag through HostPowerSnapshot and BackgroundPolicy.ts:147; check the ProviderSessionReaperLive wiring in server.ts:445. Compare the reaper's persisted timestamps with resume behavior and determine how suspended intervals should be handled. Done means host sleep no longer causes eligible sessions to be reaped immediately after resume, while genuine inactivity still reaches the threshold.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
62/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.