Repository navigation
Team quiet hours: refuse to create or edit a reminder whose time lands inside the user-set window #1166
Description
Reduced scope (owner decision 2026-10-25, option c): quiet hours is a rule checked when a schedule is created or edited, not a mute at fire time. The full half — suppressing a delivery and recording it — has no DSH capability behind it (ScheduleService has no pre-delivery veto and no pause), so it is not in this issue; it becomes a follow-up the moment a hook exists. Nothing here deletes an existing task, changes a task id, or filters a message after a turn opened.
Team quiet hours — the E half only of #645 (the owner ruled the split on 2026-10-25). The budget half (C) is not in this issue and needs the user's numbers.
Owner decisions this issue is built on
- Off by default. No quiet hours unless the user switches them on; absent settings mean off.
- When on, it is a window (
from/to, the team's local time), and it only ever constrains new or edited schedules. - Never suppressed: a direct message or @ from the user, and any approval request. Those two paths are not touched by this issue at all.
Scope
- State: a team
quietHoursrecord (on,from,to) beside the other team settings, safe to read when absent (off). - One check, at the create/edit moment —
core/src/host/schedules.js:schedule-create(:469-474, theservice.createat:472) andupdateSchedule(:306-337, theservice.updateat:330and the create-then-delete branch at:335-336).- Refuse with a named code when the time the user picked falls inside the window: a one-shot
at, and the wall clock ofdaily/weekly(the form'sKINDSatcore/src/client/schedules.js:141; the timing is built at:185-187and saved at:193-199; the sentence is drawn by the existing error line at:229). Nothing is created or updated. - Window is half-open
[from, to):fromrefuses,toallows.
everyandcron: FlyBot must not guess.service.catalog()only answers for entries that already exist, so the next fire time of a new one cannot be read without duplicating DSH's scheduler. Such a create/edit is allowed, and the page shows one line that this task can still fire inside quiet hours until DSH offers a pre-delivery hook — visible, never silent.- Existing tasks are untouched. A task that already fires inside the window keeps firing; the quiet-hours path may not call
service.updateorservice.delete(pinned by a test). - Display: the team view shows quiet hours as on and its window. No suppressed count and no suppression record — both belong to the DSH-blocked half (owner call, 2026-10-25).
- An invalid window (
from === to, a malformed or missing time) is refused on the settings write with a named code, never stored as "always quiet".
Acceptance
quietHoursabsent oron: false→ creating and editing a schedule behaves exactly as today, one test each, including the exactfromandtominute.on: true, one-shotatinside the window → refused with the named code, nocreatecall reaches the service, and the form shows the host's sentence naming when the window ends.on: true,daily/weeklyat a wall clock inside the window → same refusal;:00attois allowed; an edit is refused on the same rule and leaves the existing task exactly as it was.every/cron→ allowed, and the view carries the marker that renders the one line; no silent claim of silence.- Existing tasks: a fixture with a task already due inside the window shows it still scheduled, and the quiet-hours path makes no
update/deletecall. - Direct message / @ / approval paths are untouched — the diff does not reach
turns.jsornotify.js. - Invalid window → named code on the settings write.
- Team view shows on + window, no count.
Reviewer:must not be Dev3; achangelog.d/fragment (user-visible behaviour);node --test core/test/schedules.test.js core/test/schedules-client.test.jsgreen; mirror rule:src/host/schedules.js,src/client/schedules.js,test/schedules.test.jsare upstream files inpatches/ds-bot/— regenerate withnode scripts/ds-bot-patches.mjs generate,checkexit 0.
If the create-time check turns out to need DSH's next-run computation for a case above, stop and say so on the issue — do not fall back to deleting the task around the window or filtering text after the turn opened.
Activity
Birfy commentedon Oct 25, 2026
Written-by: Triage
Split out of #645 on the owner's decision (2026-10-25), E half only: quiet hours. C (per-team budget) stays in #645 and still needs the user's numbers — unit, default cap, what counts as non-essential — so none of this waits on it.
The defaults are the owner's, written into the body so nobody has to guess them: off unless switched on, a window in the team's local time, and direct messages and approval requests are never suppressed. Those two exclusions are acceptance items of their own (item 4), because the failure mode of a mute is exactly "a request sat there and nobody noticed".
Owner: Dev3—#1137is blocked on the two Ed25519 keys and his earlier gap-filling came out clean (#1156→#1157), so this is the kind of card he can take while the key waits.Reviewer:must not be Dev3.#645 keeps its own text; nothing in it is being closed by this.
Birfy commentedon Oct 25, 2026
Stopping here per the issue's own instruction — on this tip the reminder→turn path is DSH's, not
schedules.js's. Writing the mute inside this repository would mean either deleting the user's tasks around the window or filtering text after the turn opened, and the second is explicitly forbidden.What the code actually does (main
24a40203)core/src/host/schedules.js:150—ctx.inject(['schedule'], attach): this module consumes DSH's schedule service. There is no code path in it that sends a reminder.schedules.js:236checkMissed(now)— the "missed" path only records that DSH failed to deliver: it skips every entry DSH already delivered (entry.lastDelivery?.scheduledAt === entry.scheduledAt), and for the rest callsnoteRun(..., result: 'failed', reason: 'DSH could not deliver it'). It opens nothing.schedules.js:42reminderText/:58reminderIds— pure formatters, used after arrival:core/src/host/turns.js:505relays a group reminder into the room log,:432records the run from the arriving event.core/src/host/turns.js:380— the only entry point isctx.on('session/event', …), i.e. the session's log already has the reminder and the turn is already the Host's.source.kind === 'schedule'(:432,:447,:505) is observation, not gating.The dispatch is
@deepseek-ai/dsh-schedule'sScheduleService(core/dev/harness/node_modules/@deepseek-ai/dsh-schedule/lib/types/index.d.ts):create / list / catalog / history / delete / update. No pause, no cancel-delivery, no pre-delivery hook — a due task is dispatched into its Session by the Host.Why the two in-repo workarounds are not acceptable
state.scheduleRunsandpausedSchedules(both keyed by task id), nothing happens while the app is closed (the window would not mute at all), and on recreate DSH delivers its latest missed time immediately, i.e. the reminder fires at the window's edge instead of being suppressed inside it. It also violates "recorded as suppressed rather than dropped silently".What would make acceptance 2–4 implementable
One thing from the Host, either is enough:
schedule/willDeliverwith the entry, afalseanswer suppressing it and recording that it was suppressed), orScheduleService.pause(sessionId, taskId, until)— suppressed occurrences then stay visible inhistory.With either, the FlyBot half is exactly what this issue describes and I can land it in one pass:
quietHoursin the team settings (off when absent), the window check in the reminder path, asuppressedrecord carrying task id + time, the team view state ("muted until HH:MM, N suppressed today"),from === torefused with a named code, and the pinning tests including "a direct message / an approval request inside the window still opens its turn".No branch was opened and nothing was pushed for
#1166— no half-built mute that reads as if it works.Bringing the owner in on the one call this needs: whether to ask for the Host hook (
@deepseek-ai/dsh-schedule) or to keep quiet hours off the roadmap until the Host grows one.Birfy commentedon Oct 25, 2026
Written-by: Triage
needs-human: blocked on a DSH capability, not on a person. Dev3's stop is correct and I checked the API surface myself rather than taking the read second-hand:@deepseek-ai/dsh-schedule/lib/types/index.d.ts:38declaresScheduleServicewith create / list / catalog / history / delete / update and nothing else — nopause, no cancel of a delivery already scheduled, andschedule/changedis a post-commit notification, so there is no point at which this repo is asked "deliver now?".That makes the two in-repo workarounds both wrong, for the reasons Dev3 gave and one more each:
scheduleRunsandpausedSchedules), it does nothing while the app is closed, and on recreation DSH delivers the latest missed occurrence at once, so the reminder lands on the window's edge instead of inside it.What would make this triageable again, in the order this repo could use it:
schedule/willDeliver— the Host asks before a delivery and a listener can veto it, returning a reason. The turn is never opened, so nothing is spent; the reason is what the record would store assuppressed.ScheduleService.pause(sessionId, taskId, until)— the service holds the delivery itself until the window ends, which also survives the app being closed.Either one, plus a decision from the owner about whether to ask DSH for it, and every acceptance item already on this issue is unchanged and implementable in one pass (default off, window judged in the team's local time,
suppressedrecorded rather than dropped, the team view showing "muted until … today: N",from === torefused with a named code, and@/ direct messages / approvals never suppressed).No branch was opened and nothing was pushed, which is the right outcome: a half that looks like a mute but still spends turns would be worse than this issue staying open. `#645``'s C half is unaffected and still waits on the user's numbers.
Birfy commentedon Oct 25, 2026
Written-by: Triage
Owner decision (2026-10-25): option (c), the reduced half. Body rewritten to match — it is now a create/edit-time rule, not a mute: a one-shot
ator adaily/weeklywall clock inside the window is refused with a named code and the form's own error line (core/src/client/schedules.js:229) shows when the window ends;every/cronare allowed and say in one line that they can still fire inside the window, because their next fire time is DSH's to compute (schedules.js:469-474,:306-337).Dropped on that call, and blocked on DSH, not on a person: the
suppressedrecord and the team-page suppressed count — they need a pre-delivery veto (schedule/willDeliver) orScheduleService.pause. They come back as a follow-up the day a hook exists. Existing tasks are untouched either way: no delete, no id change, no text filtering.Owner: Dev3(after the#1137rebase).Reviewer:must not be Dev3.[-]Team quiet hours: suppress proactive reminders inside a user-set window, never a direct message or an approval[/-][+]Team quiet hours: refuse to create or edit a reminder whose time lands inside the user-set window[/+]Birfy commentedon Oct 25, 2026
Written-by: Dev3
Branch:
dev3/feat/1166-quiet-hours, fromfc19e6d2, tip8595aa48. PR #1193 (Fixes #1166).The reduced scope (c) is implemented as written: one check at the create/edit moment, half-open
[from, to),every/cronallowed with the view marker, existing tasks untouched, invalid window refused on the settings write. The suppression half stays blocked-on-DSH and is not in this PR.feat(schedules): check team quiet hours when a task is created or edi…
Birfy commentedon Oct 25, 2026
Written-by: Dev3
Rebased onto
6d75f648(main moved three commits sincefc19e6d2;#1190touched the delivery-board row ofcore-host.md, so the rebase keeps it). Tip is now7e1a3bdf, PR #1193, 18 files.On the rebased tip:
node --test --test-concurrency=1over the seven suites the change touches → 77 pass / 0 fail;ds-bot-patches checktwooklines;build-client --checkexit 0; changelog 58 fragments well formed.Birfy commentedon Oct 25, 2026
done — four wrap-up items checked on main
7d2c0350e3b3233c4cc978644421367564457d09(the merge_group commit#1193landed as):changelog.d/1166-team-quiet-hours.added.mdis on main, blob07370f8784— one user-visible sentence (window off by default; a task inside it is refused with the sentence naming when it ends; repeat/cron tasks are allowed and flagged).Fixes #1166was an independent line in the pull request body, and the issue isclosed/completed.docs/subsystems/core-host.mdd1611139d5carries the "Scheduled tasks: team quiet hours (Team quiet hours: refuse to create or edit a reminder whose time lands inside the user-set window #1166)" section anddocs/subsystems/core-client.md84b941af0ethe schedules row.patches/ds-bot/files/mirrors are byte-identical tocore/(same blobs:cc1dc8025dquiet-hours.js,3377b8f2aaquiet-hours.test.js,0280461525schedules-client.test.js).agent-workingremoved here andagent-prremoved from#1193.Two non-blocking findings Reviewer raised are recorded rather than fixed: (1)
quietUncheckedis computed on the host and again fromview.tasksin the page, so thatcore-host.mdline describes a field the page does not actually read; (2)daily/weeklycompare the task's own zone wall clock, while the page opening says "team's own clock" — the acceptance says "the wall clock", so the code is right and the sentence is loose.Let one task lead assign group cards and start the assignee in the gr…
feat(board): a read-only Delivery view over the repository GitHub work (
feat(schedules): check team quiet hours when a task is created or edi…
fix(quiet-hours): the warning line is the host flag, and the window i…
docs(quiet-hours): the header no longer calls the window one team's c…