You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug] Recurring reminders claim one extra turn per occurrence while the session is busy
#12991
A recurring reminder (every_seconds / daily / weekly / cron) is delivered with agent.followup(), which requests a new turn. That path never reads the agent's busy state, so every occurrence that comes due while a turn is already running becomes a queued turn, and each queued turn replays the whole request context.
For long-running work this dominates the cost of the reminder itself. A 1-minute reminder that fires 7 times during a 7-minute turn costs 7 extra turns on top of the work the user asked for. @deepseek-ai/dsh-tool-jobs already avoids exactly this for background jobs by branching on owner.status.
Reproduction
No tools required beyond bash and schedule_create:
Start a turn that stays busy for ~7 minutes — a foreground command is enough.
While it runs: schedule_create { title: "poll", prompt: "check state", every_seconds: 60 } 60 is the floor: MIN_EVERY_INTERVAL_SECONDS (packages/schedule/schedule/src/domain.ts:51).
Let the turn finish.
Observed: one additional turn per occurrence that fell due during the busy turn, each replaying the full context. Expected: at most the one turn that was already running, carrying the current reminder.
Where the behavior comes from
packages/schedule/schedule/src/runtime.ts:126 — delivery is agent.followup(message), after admission at :110. Neither line reads agent.status.
packages/core/agent-loop/src/agent.ts:230 — wakeDriver() returns immediately when this.phase.kind !== 'idle'; a busy agent is not woken, the message stays queued.
packages/core/agent-loop/src/inbox.ts:110 — claim() removes onenext-turn message per turn (this.mutate('next-turn', 0, 1, [], false)), and agent.ts:427 re-opens the driver while this.inbox.hasPending.
followup + busy agent = one queued turn per occurrence; the queue then drains one turn at a time.
What is already bounded
The recurrence machinery reduces some of this and is not the problem:
resolveEveryOccurrence (packages/schedule/schedule/src/domain.ts:755) folds a backlog to a single latest occurrence per record.
renderRecurringReminderBatchFraming merges records due at the same instant into one message.
Neither reduces occurrences that fall at different instants during one busy turn, which is the reproduction above.
Suggested direction
Deliver as background jobs already do:
packages/jobs/tool-jobs/src/index.ts:398 — owner.followup(message) only when delivery === 'wakeup' && owner.status === 'idle'; otherwise owner.inject(message) at :410, which the open turn consumes without opening a new one.
The same package bounds the self-exciting variant with maxConsecutiveWakes (:48-58).
Applying that idle/busy split to reminder delivery would collapse the queued turns into the turn that was already running. A related options-only question: whether a busy-time delivery should also compress several occurrences into one message the way latest-only already does per record.
Related
Automation tasks: support holiday-aware scheduling via an optional pre-delivery condition (skip) hook #9850 asks for a pre-delivery condition hook so a rule can skip occurrences (holiday-aware scheduling). That is a different axis — skip before delivering versus coalesce when busy — and the two would compose: the skip hook decides whether an occurrence matters, the busy-aware delivery decides how much it costs to deliver it.
[Bug] Follow-up can remain indefinitely queued during driver retirement #274 is adjacent but distinct: there a waking followup() is lost (accepted into next-turn, never run, agent sits idle with pending work). Here nothing is lost — every occurrence is delivered, in order, one turn each. The cost is the multiplicity, not a stranded message.
Two adjacent gaps (product decisions, not filed separately)
No interval guidance in the prompt.@deepseek-ai/dsh-tool-schedule registers no systemPrompt.section, so the model gets four reminder schemas and no rule for choosing among them; @deepseek-ai/dsh-tool-jobs does register one (packages/jobs/tool-jobs/src/index.ts:316). Moot if delivery became busy-aware, since a wrongly short interval would stop being expensive.
No bounded recurrence.EveryScheduleRecord (packages/schedule/schedule/src/types.ts:47-59) carries no maxOccurrences / expiresAt / untilAt, so "stop after N" has to live in the reminder text and be enforced by the woken turn. A host-side cap would let the scheduler retire a reminder whose condition has passed.
Environment
deepseek-ai/deepseek-harness at local origin checkout.
Presets mounting @deepseek-ai/dsh-tool-schedule in the shipped Web composition: standard, cordis, ptc (e.g. packages/bundle/web-app/presets/standard.patch.yml:35).
Verified against which revision
Every line reference above was opened and re-read individually on checkout a22477f3bcf1ebf2f7f65b9c75f1e9880999b505 (0.2.2-alpha.2), not inferred from memory or from an index. These numbers do drift — #857 cites runtime.ts:275, while that file is 169 lines on this checkout — so treat the three-file story (runtime.ts delivers, agent.ts wakes, inbox.ts claims) as the invariant to re-locate rather than the numbers themselves.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
A recurring reminder (
every_seconds/daily/weekly/cron) is delivered withagent.followup(), which requests a new turn. That path never reads the agent's busy state, so every occurrence that comes due while a turn is already running becomes a queued turn, and each queued turn replays the whole request context.For long-running work this dominates the cost of the reminder itself. A 1-minute reminder that fires 7 times during a 7-minute turn costs 7 extra turns on top of the work the user asked for.
@deepseek-ai/dsh-tool-jobsalready avoids exactly this for background jobs by branching onowner.status.Reproduction
No tools required beyond
bashandschedule_create:schedule_create { title: "poll", prompt: "check state", every_seconds: 60 }60is the floor:MIN_EVERY_INTERVAL_SECONDS(packages/schedule/schedule/src/domain.ts:51).Observed: one additional turn per occurrence that fell due during the busy turn, each replaying the full context. Expected: at most the one turn that was already running, carrying the current reminder.
Where the behavior comes from
packages/schedule/schedule/src/runtime.ts:126— delivery isagent.followup(message), after admission at:110. Neither line readsagent.status.packages/core/agent-loop/src/agent.ts:230—wakeDriver()returns immediately whenthis.phase.kind !== 'idle'; a busy agent is not woken, the message stays queued.packages/core/agent-loop/src/inbox.ts:110—claim()removes onenext-turnmessage per turn (this.mutate('next-turn', 0, 1, [], false)), andagent.ts:427re-opens the driver whilethis.inbox.hasPending.followup+ busy agent = one queued turn per occurrence; the queue then drains one turn at a time.What is already bounded
The recurrence machinery reduces some of this and is not the problem:
resolveEveryOccurrence(packages/schedule/schedule/src/domain.ts:755) folds a backlog to a single latest occurrence per record.renderRecurringReminderBatchFramingmerges records due at the same instant into one message.Neither reduces occurrences that fall at different instants during one busy turn, which is the reproduction above.
Suggested direction
Deliver as background jobs already do:
packages/jobs/tool-jobs/src/index.ts:398—owner.followup(message)only whendelivery === 'wakeup' && owner.status === 'idle'; otherwiseowner.inject(message)at:410, which the open turn consumes without opening a new one.maxConsecutiveWakes(:48-58).Applying that idle/busy split to reminder delivery would collapse the queued turns into the turn that was already running. A related options-only question: whether a busy-time delivery should also compress several occurrences into one message the way
latest-onlyalready does per record.Related
followup()is lost (accepted intonext-turn, never run, agent sitsidlewith pending work). Here nothing is lost — every occurrence is delivered, in order, one turn each. The cost is the multiplicity, not a stranded message.Two adjacent gaps (product decisions, not filed separately)
@deepseek-ai/dsh-tool-scheduleregisters nosystemPrompt.section, so the model gets four reminder schemas and no rule for choosing among them;@deepseek-ai/dsh-tool-jobsdoes register one (packages/jobs/tool-jobs/src/index.ts:316). Moot if delivery became busy-aware, since a wrongly short interval would stop being expensive.EveryScheduleRecord(packages/schedule/schedule/src/types.ts:47-59) carries nomaxOccurrences/expiresAt/untilAt, so "stop after N" has to live in the reminder text and be enforced by the woken turn. A host-side cap would let the scheduler retire a reminder whose condition has passed.Environment
deepseek-ai/deepseek-harnessat localorigincheckout.@deepseek-ai/dsh-schedule(delivery),@deepseek-ai/dsh-agent-loop(inbox),@deepseek-ai/dsh-tool-jobs(contrast implementation).@deepseek-ai/dsh-tool-schedulein the shipped Web composition:standard,cordis,ptc(e.g.packages/bundle/web-app/presets/standard.patch.yml:35).Verified against which revision
Every line reference above was opened and re-read individually on checkout
a22477f3bcf1ebf2f7f65b9c75f1e9880999b505(0.2.2-alpha.2), not inferred from memory or from an index. These numbers do drift — #857 citesruntime.ts:275, while that file is 169 lines on this checkout — so treat the three-file story (runtime.tsdelivers,agent.tswakes,inbox.tsclaims) as the invariant to re-locate rather than the numbers themselves.All reactions