Skip to content

fix(server): refresh provider session lastSeenAt on turn and task activity - #5524

Open
kraptor23 wants to merge 2 commits into
pingdotgg:mainfrom
kraptor23:fix/provider-session-activity-refresh
Open

fix(server): refresh provider session lastSeenAt on turn and task activity#5524
kraptor23 wants to merge 2 commits into
pingdotgg:mainfrom
kraptor23:fix/provider-session-activity-refresh

Conversation

@kraptor23

@kraptor23 kraptor23 commented Aug 6, 2026

Copy link
Copy Markdown

What

Refreshes provider_session_runtime.last_seen_at when the adapter emits turn or background-task lifecycle events, so the inactivity reaper measures idleness from the last completed work instead of the last explicit sendTurn.

Addresses the staleness half of #4198.

Why

last_seen_at is only written by startSession / recoverSessionForThread / sendTurn / stopSession. Turns delivered without a sendTurn call (queued follow-ups, plan-sourced turns) and background tasks never refresh it. Observed on 0.0.32-nightly.20260805.1006: one thread was reaped 6 times in ~12 hours, three of those 105 s - 4 m 53 s after a turn had actually completed — the reaper was measuring idleness from a last_seen_at several turns stale, exactly what the reporters in #4198 back-computed from their logs.

How

  • ProviderSessionDirectory.touch(threadId) updates only last_seen_at for an existing binding via a new repository touchByThreadId (UPDATE ... WHERE thread_id). It never creates rows, and preserves every other column.
  • ProviderService.processRuntimeEvent touches the binding on turn.started/completed/aborted and task.started/progress/completed, after event fan-out.
  • Touches are throttled to one per thread per 60 s (in-memory map, cleared on stopSession) because this runs on the event drain fiber and task.progress can fire many times per second in multi-agent sessions; reaper thresholds are minutes, so minute-level granularity is sufficient. The gate timestamp is set before the write so a failing database is not retried per event.
  • Touch failures log provider.session.activity-touch-failed and never disturb fan-out.

This deliberately does not add a busy-guard for in-flight background tasks (discussed in #4198) — that is a separate lifecycle change; this PR only fixes the idle clock.

Tests

  • ProviderService.test.ts: every activity event type refreshes the binding's lastSeenAt; content.delta does not; a second boundary inside the throttle window does not write again.
  • ProviderSessionDirectory.test.ts: touch refreshes lastSeenAt and leaves all other fields unchanged; touch on an unknown thread creates no row.
  • vp test run on the five touched test files (97 tests) and tsgo --noEmit pass.

Prior art and non-goals

This addresses the liveness-staleness half of #4198. Back-computing the reaper's logged idleDurationMs against turn timestamps shows every incident reported there is an event-emitting case this PR covers: ordinary provider-started turns that never refreshed last_seen_at (NoorChasib's table — turns at 06:08 and 06:17 with last_seen_at stuck at 05:49), and background tasks killed seconds after their foreground turn settled (Mooned8, Zeus-Deus — task.started/turn.completed both now touch the binding).

Earlier attempts in this area (#4199, #3856, #4994) were closed for orchestration-V2 roadmap reasons rather than on the mechanism, and #2829 is still open — this PR is deliberately the smallest reviewable slice of that idea: one new directory method, one call site on the existing event drain, throttled, with no lifecycle behavior changes.

Non-goals, intentionally out of scope here:

Written by Claude Fable 5 via Claude Code.


Note

Medium Risk
Changes when sessions are considered idle for reaping (core provider lifecycle), but scope is narrow—targeted DB updates with throttling and non-blocking failure handling.

Overview
Provider session lastSeenAt now advances when adapters emit turn or background-task lifecycle events, not only on startSession, sendTurn, or stopSession. That aligns the inactivity reaper with last completed work (queued turns, plan-driven work, long-running tasks) instead of the last explicit user send.

A touch path updates only last_seen_at via repository touchByThreadId (UPDATE … WHERE thread_id); it never inserts rows and leaves other binding fields unchanged. ProviderService.processRuntimeEvent calls it after fan-out for SESSION_ACTIVITY_EVENT_TYPES (turn.*, task.*), with a 60s per-thread throttle and throttle state cleared on stopSession. Streaming events like content.delta do not touch; touch failures are logged and do not affect event delivery.

Reviewed by Cursor Bugbot for commit 0fadabd. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Refresh provider session lastSeenAt on turn and task activity events

  • Adds a touch method to ProviderSessionDirectory and ProviderSessionDirectoryShape that updates only last_seen_at for an existing binding, with no-op behavior for unknown threads.
  • Extends ProviderSessionRuntimeRepository with touchByThreadId, which performs a targeted UPDATE on last_seen_at without affecting other fields.
  • In makeProviderService, emitting events from the new SESSION_ACTIVITY_EVENT_TYPES set (turn.started, turn.completed, turn.aborted, task.*) triggers directory.touch, throttled to at most once per 60 seconds per thread. The throttle map entry is cleared on stopSession.
  • Behavioral Change: provider session lastSeenAt now updates during active turns and tasks, where previously it only updated on session creation or explicit upsert.

Macroscope summarized 0fadabd.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b02ac06f-8f1a-4af5-ae12-8301f9034df2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 6, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding: the new activity-touch warning log serializes the raw cause into the log payload instead of a normalized error tag.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/provider/Layers/ProviderService.ts Outdated
Comment thread apps/server/src/provider/Layers/ProviderService.ts
@macroscopeapp

macroscopeapp Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session lifecycle behavior by updating lastSeenAt timestamps based on turn/task events, affecting how the inactivity reaper determines session idleness. While well-tested and throttled, this runtime behavior change to session management warrants human review.

You can customize Macroscope's approvability policy. Learn more.

kraptor23 and others added 2 commits August 6, 2026 15:44
…ivity

The inactivity reaper measures idleness from provider_session_runtime
.last_seen_at, but that column was only written by startSession /
recoverSessionForThread / sendTurn / stopSession. Turns delivered without
a sendTurn call (queued follow-ups, plan-sourced turns) and background
tasks never refreshed it, so the reaper kills sessions minutes after
real work finished (pingdotgg#4198).

Treat turn and task lifecycle runtime events as session activity: a new
ProviderSessionDirectory.touch updates only last_seen_at for an existing
binding (never creates rows), and ProviderService.processRuntimeEvent
touches the binding on turn.started/completed/aborted and
task.started/progress/completed. Touch failures log a warning and never
disrupt event fan-out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lures as error tags

Review feedback: Codex background tasks transition through task.updated
without necessarily emitting task.progress, so include it in the
activity set; log the normalized causeErrorTag instead of the raw cause,
matching the runStopAll finalizer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant