← Back to PRs

#23431: feat(cron): add deferWhileActive to skip jobs during active sessions

by Dave-Pataky open 2026-02-22 09:42 View on GitHub →
gateway size: S
## Summary Add session-aware cron suppression that silently skips main-session cron jobs when the user is actively chatting with the agent. This reduces wasted API calls during active conversations by ~80%. ## Problem Cron jobs (email checks, handoff checkpoints, etc.) fire on fixed schedules regardless of whether the user is mid-conversation. During active sessions, these jobs load full context, trigger agent responses (often just `NO_REPLY`), and waste tokens. On a typical day this produces 25+ unnecessary API calls. ## Solution A new `deferWhileActive` option that tells the scheduler to **silently skip** main-session jobs when the session has recent human activity. The job simply does not fire — no error, no backoff, no state pollution. It fires at its next scheduled time as normal. ### Key design decisions - **Skipped status, not error** — deferred jobs return `{ status: "skipped", error: "session-active" }` with no consecutive error tracking or exponential backoff - **Per-job overrides global config** — a job with `deferWhileActive: false` always fires even if the global setting is on - **Only applies to `sessionTarget: "main"` jobs** — isolated agent jobs are never deferred - **Dedicated `lastHumanInboundAt` timestamp** — tracks actual human messages separately from `updatedAt` (which gets bumped by heartbeat/cron responses too), preventing false positives that would defer jobs indefinitely - **Default quiet window: 5 minutes** — configurable via `quietMs` ## Usage **Global config:** ```yaml cron: deferWhileActive: quietMs: 300000 # 5 minutes ``` **Per-job:** ```json { "deferWhileActive": { "quietMs": 600000 } } ``` **Disable for a specific job (when global is on):** ```json { "deferWhileActive": false } ``` ## Files changed (8 files, 118 lines added, 0 deleted) | File | Change | |------|--------| | `src/cron/types.ts` | New `CronDeferWhileActive` type + field on `CronJob` | | `src/config/types.cron.ts` | Global `deferWhileActive` on `CronConfig` | | `src/config/zod-schema.ts` | Zod schema validation for new config key | | `src/cron/service/state.ts` | `getLastInboundAtMs` callback in `CronServiceDeps` | | `src/cron/service/timer.ts` | Skip logic at top of `executeJobCore()` | | `src/gateway/server-cron.ts` | Wires callback using session store | | `src/config/sessions/types.ts` | `lastHumanInboundAt` field on `SessionEntry` | | `src/auto-reply/reply/get-reply.ts` | Sets `lastHumanInboundAt` for non-heartbeat inbound messages | ## Testing Tested manually on a live instance: - ✅ Jobs deferred correctly during active conversation - ✅ Jobs resumed after quiet window elapsed - ✅ Heartbeat/cron responses do NOT reset the activity timer (via `lastHumanInboundAt`) - ✅ Per-job override works (email-check defers, handoff checkpoint always fires) - ✅ TypeScript strict mode — compiles clean with `tsc --noEmit` <!-- greptile_comment --> <h3>Greptile Summary</h3> Implements session-aware cron job deferral to skip jobs when users are actively chatting. Prevents wasteful API calls by tracking human message timestamps separately from automated heartbeat/cron updates. **Key changes:** - Adds `lastHumanInboundAt` timestamp field to track real human activity (separate from `updatedAt` which gets bumped by heartbeats) - New `deferWhileActive` option with configurable quiet window (default 5 min) at both global and per-job levels - Jobs return `{ status: "skipped", error: "session-active" }` without triggering consecutive error tracking or backoff - Only applies to `sessionTarget: "main"` jobs; isolated agent jobs always fire - Timestamps updated via locked `updateSessionStore` to prevent race conditions **Issue found:** - Type mismatch: `CronDeferWhileActive` in `src/cron/types.ts:117` doesn't allow `false` value, but PR description and implementation expect per-job `deferWhileActive: false` to disable when global config is on. The code uses `as unknown` type assertion as a workaround at `src/cron/service/timer.ts:585`. <h3>Confidence Score: 4/5</h3> - Safe to merge after fixing type definition - Implementation is solid with proper locking, good separation of concerns, and comprehensive timestamp tracking. The type mismatch is a minor issue that requires a one-line fix but doesn't affect runtime correctness since the code handles `false` values properly via type assertion. No logical errors, race conditions, or security issues found. - Fix type definition in `src/cron/types.ts:117` to allow `| false` <sub>Last reviewed commit: f7a766a</sub> <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs