Skip to content

Team quiet hours: refuse to create or edit a reminder whose time lands inside the user-set window #1166

Closed

Description

@Birfy

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

  1. State: a team quietHours record (on, from, to) beside the other team settings, safe to read when absent (off).
  2. One check, at the create/edit moment — core/src/host/schedules.js:
    • schedule-create (:469-474, the service.create at :472) and updateSchedule (:306-337, the service.update at :330 and 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 of daily / weekly (the form's KINDS at core/src/client/schedules.js:141; the timing is built at :185-187 and 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): from refuses, to allows.
  3. every and cron: 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.
  4. Existing tasks are untouched. A task that already fires inside the window keeps firing; the quiet-hours path may not call service.update or service.delete (pinned by a test).
  5. 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).
  6. 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

  1. quietHours absent or on: false → creating and editing a schedule behaves exactly as today, one test each, including the exact from and to minute.
  2. on: true, one-shot at inside the window → refused with the named code, no create call reaches the service, and the form shows the host's sentence naming when the window ends.
  3. on: true, daily / weekly at a wall clock inside the window → same refusal; :00 at to is allowed; an edit is refused on the same rule and leaves the existing task exactly as it was.
  4. every / cron → allowed, and the view carries the marker that renders the one line; no silent claim of silence.
  5. Existing tasks: a fixture with a task already due inside the window shows it still scheduled, and the quiet-hours path makes no update/delete call.
  6. Direct message / @ / approval paths are untouched — the diff does not reach turns.js or notify.js.
  7. Invalid window → named code on the settings write.
  8. Team view shows on + window, no count.
  9. Reviewer: must not be Dev3; a changelog.d/ fragment (user-visible behaviour); node --test core/test/schedules.test.js core/test/schedules-client.test.js green; mirror rule: src/host/schedules.js, src/client/schedules.js, test/schedules.test.js are upstream files in patches/ds-bot/ — regenerate with node scripts/ds-bot-patches.mjs generate, check exit 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

  1. added
    enhancementNew feature or request
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    area:automationScheduled tasks and automation
    on Oct 25, 2026
  2. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    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 — #1137 is 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.

  3. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    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:236 checkMissed(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 calls noteRun(..., result: 'failed', reason: 'DSH could not deliver it'). It opens nothing.
    • schedules.js:42 reminderText / :58 reminderIds — pure formatters, used after arrival: core/src/host/turns.js:505 relays a group reminder into the room log, :432 records the run from the arriving event.
    • core/src/host/turns.js:380 — the only entry point is ctx.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's ScheduleService (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

    1. Filter the text after the turn opened — leaves the turn and the model call in place; forbidden by this issue ("不许在回合开出来之后过滤文本假装静音").
    2. Delete the tasks when the window opens and recreate them after — destructive and wrong: new ids break state.scheduleRuns and pausedSchedules (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:

    • a pre-delivery hook the Host consults before dispatching a due task (schedule/willDeliver with the entry, a false answer suppressing it and recording that it was suppressed), or
    • ScheduleService.pause(sessionId, taskId, until) — suppressed occurrences then stay visible in history.

    With either, the FlyBot half is exactly what this issue describes and I can land it in one pass: quietHours in the team settings (off when absent), the window check in the reminder path, a suppressed record carrying task id + time, the team view state ("muted until HH:MM, N suppressed today"), from === to refused 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.

  4. added
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    and removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 25, 2026
  5. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    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:38 declares ScheduleService with create / list / catalog / history / delete / update and nothing else — no pause, no cancel of a delivery already scheduled, and schedule/changed is 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:

    • Filter the text after the turn opens — the turn and its cost already happened; the issue forbids it and it would make the mute a lie.
    • Delete the task inside the window and recreate it after — the id changes (breaking scheduleRuns and pausedSchedules), 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:

    1. 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 as suppressed.
    2. 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, suppressed recorded rather than dropped, the team view showing "muted until … today: N", from === to refused 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.

  6. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    and removed
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Oct 25, 2026
  7. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    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 at or a daily/weekly wall 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/cron are 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 suppressed record and the team-page suppressed count — they need a pre-delivery veto (schedule/willDeliver) or ScheduleService.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 #1137 rebase). Reviewer: must not be Dev3.

  8. changed the title [-]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[/+] on Oct 25, 2026
  9. added
    agent-prPull request opened by an agent
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 25, 2026
  10. self-assigned this
    on Oct 25, 2026
  11. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev3

    Branch: dev3/feat/1166-quiet-hours, from fc19e6d2, tip 8595aa48. PR #1193 (Fixes #1166).
    The reduced scope (c) is implemented as written: one check at the create/edit moment, half-open [from, to), every/cron allowed 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.

  12. removed
    agent-prPull request opened by an agent
    on Oct 25, 2026
  13. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev3

    Rebased onto 6d75f648 (main moved three commits since fc19e6d2; #1190 touched the delivery-board row of core-host.md, so the rebase keeps it). Tip is now 7e1a3bdf, PR #1193, 18 files.

    On the rebased tip: node --test --test-concurrency=1 over the seven suites the change touches → 77 pass / 0 fail; ds-bot-patches check two ok lines; build-client --check exit 0; changelog 58 fragments well formed.

  14. removed
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 25, 2026
  15. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    done — four wrap-up items checked on main 7d2c0350e3b3233c4cc978644421367564457d09 (the merge_group commit #1193 landed as):

    • changelog.d/1166-team-quiet-hours.added.md is on main, blob 07370f8784 — 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 #1166 was an independent line in the pull request body, and the issue is closed/completed.
    • both pages are on main: docs/subsystems/core-host.md d1611139d5 carries 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 and docs/subsystems/core-client.md 84b941af0e the schedules row.
    • the three patches/ds-bot/files/ mirrors are byte-identical to core/ (same blobs: cc1dc8025d quiet-hours.js, 3377b8f2aa quiet-hours.test.js, 0280461525 schedules-client.test.js).

    agent-working removed here and agent-pr removed from #1193.

    Two non-blocking findings Reviewer raised are recorded rather than fixed: (1) quietUnchecked is computed on the host and again from view.tasks in the page, so that core-host.md line describes a field the page does not actually read; (2) daily/weekly compare 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.

  16. removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:automationScheduled tasks and automationenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions