#23431: feat(cron): add deferWhileActive to skip jobs during active sessions
gateway
size: S
Cluster:
Cron Job Enhancements
## 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
#16511: feat(cron): support custom session IDs and auto-bind to current ses...
by kkhomej33-netizen · 2026-02-14
76.9%
#23562: feat: add sessionFreshness config for isolated cron jobs (#23539)
by MunemHashmi · 2026-02-22
76.0%
#11657: fix(cron): treat skipped heartbeat as ok for one-shot jobs
by DukeDeSouth · 2026-02-08
75.2%
#18925: fix(cron): stagger missed jobs on restart to prevent gateway overload
by rexlunae · 2026-02-17
75.0%
#17064: fix(cron): prevent control-plane starvation during startup catch-up...
by donggyu9208 · 2026-02-15
74.2%
#20521: feat(heartbeat): inject active cron job summary into heartbeat prompt
by maximalmargin · 2026-02-19
73.8%
#14430: Cron: anti-zombie scheduler recovery and in-flight job persistence
by philga7 · 2026-02-12
73.6%
#5428: fix(Cron): prevent one-shot loop on skip
by imshrishk · 2026-01-31
73.6%
#14667: fix: preserve missed cron runs when updating job schedule
by WalterSumbon · 2026-02-12
73.4%
#17561: fix(cron): add runtime staleness guard for runningAtMs (#17554)
by robbyczgw-cla · 2026-02-15
73.3%