Skip to content

feat(channels): per-recipient reply pacing across 9 channels#6389

Open
singlerider wants to merge 7 commits intozeroclaw-labs:masterfrom
singlerider:feat/6345-reply-min-interval-secs
Open

feat(channels): per-recipient reply pacing across 9 channels#6389
singlerider wants to merge 7 commits intozeroclaw-labs:masterfrom
singlerider:feat/6345-reply-min-interval-secs

Conversation

@singlerider
Copy link
Copy Markdown
Collaborator

@singlerider singlerider commented May 5, 2026

Summary

  • Base branch: master
  • What changed and why:
    • Per-(channel, recipient) outbound reply pacing across nine outbound channels. Each channel config gains a reply_min_interval_secs: u64 field (default 0, range 0..=3600). The orchestrator wraps each Some-bound channel in a PacedChannel so consecutive outbound replies to the same peer wait at least the configured floor before firing.
    • Threat model: on paired-identity channels under personal accounts, sub-second replies are an AI tell and a platform-side anomaly signal. Raising this value paces the agent so its cadence resembles a human operator's.
    • PacedChannel (in crates/zeroclaw-channels/src/paced_channel.rs) wraps Arc<dyn Channel> and throttles send and the terminal finalize_draft per recipient. Streaming update_draft / update_draft_progress are NOT paced; pacing them would freeze the live preview. min_interval_secs == 0 returns the inner Arc unchanged, so the default config pays zero overhead (no wrapper, no Mutex).
    • The reply_min_interval_secs field is added to TelegramConfig, DiscordConfig, SlackConfig, MattermostConfig, WebhookConfig, IMessageConfig, MatrixConfig, SignalConfig, WhatsAppConfig. All #[serde(default)], so existing configs deserialise unchanged. Orchestrator wiring: each Some-bound channel push site in collect_configured_channels wraps its Arc::new(...) in PacedChannel::wrap(..., <var>.reply_min_interval_secs) at construction.
    • Range validation: Config::validate() enforces 0..=3600 via a new Config::collect_reply_min_interval_values() helper that walks every Option<XConfig> carrying the field. The one-hour upper bound catches the milliseconds-in-seconds typo before runtime; without it a 60000 would wedge a channel for 16 hours.
    • Docs: docs/book/src/channels/chat-others.md carries a top-level "Pacing outbound replies" section that documents the wire contract (per-(channel, recipient), 0 is passthrough, draft updates not paced, different recipients independent).
  • Scope boundary: Outbound reply pacing only. Does not pace inbound message ingestion, draft/preview updates, or non-channel surfaces (tools, gateway responses). The default of 0 preserves byte-for-byte behaviour for operators who do not opt in.
  • Blast radius: Channel layer of every config that deserialises through these nine XConfig types. Default behaviour unchanged. When reply_min_interval_secs > 0, consecutive replies to the same recipient are delayed by at least N seconds; replies to different recipients on the same channel are independent. Lock discipline in PacedChannel::send releases the per-recipient slot before sleeping, so concurrent senders to different recipients run in parallel.
  • Linked issue(s): Closes [Feature]: Per-channel reply-min-interval-secs (throttle outbound agent replies)Β #6345

Validation Evidence (required)

cargo +nightly fmt --all -- --check
cargo clippy --workspace --exclude zeroclaw-desktop --all-targets --features ci-all -- -D warnings
cargo test -p zeroclaw-channels --lib paced
cargo test -p zeroclaw-config --lib reply_min_interval
cargo test --workspace --exclude zeroclaw-desktop --features ci-all --lib
  • Commands run and tail output:
    • cargo +nightly fmt --all -- --check: clean (no output).
    • cargo clippy ... --features ci-all -- -D warnings: Finished dev profile [unoptimized + debuginfo] target(s) (zero warnings).
    • cargo test -p zeroclaw-channels --lib paced:
      test paced_channel::tests::zero_interval_is_passthrough ... ok
      test paced_channel::tests::different_recipients_track_state_independently ... ok
      test paced_channel::tests::first_send_records_recipient_state ... ok
      test paced_channel::tests::small_interval_sleeps_long_enough_between_repeats ... ok
      test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured
      
    • cargo test -p zeroclaw-config --lib reply_min_interval:
      test schema::tests::telegram_config_reply_min_interval_secs_missing_defaults_to_zero ... ok
      test schema::tests::validate_accepts_reply_min_interval_at_upper_bound ... ok
      test schema::tests::validate_rejects_reply_min_interval_above_one_hour ... ok
      test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
      
    • cargo test --workspace --exclude zeroclaw-desktop --features ci-all --lib: all per-crate suites passing.
    • GitHub CI (12 checks at head db82da2): all green, including Test, Check (all features), Lint, and Security.
  • Beyond CI, what was manually verified:
    • Lock discipline in PacedChannel::send: the lock is acquired to compute the sleep duration and update the per-recipient slot, then released before tokio::time::sleep. Multiple concurrent senders to different recipients proceed in parallel; multiple concurrent senders to the same recipient each reserve their slot atomically and then sleep independently. The map records now + wait (the scheduled fire time), so a burst of N replies stacks to (N-1) * min_interval wall-clock correctly.
    • All nine channel configs carry reply_min_interval_secs: 0 in every test struct literal across schema.rs, orchestrator/mod.rs, daemon/mod.rs, integrations/registry.rs, src/config/mod.rs, and the channel-specific test modules.
    • Telegram-specific section in chat-others.md matches shipped behaviour (the bullet that previously said "field is recognised by the schema; pacing scheduler lands in a follow-up" was updated when pacing was inlined into this PR).
  • If any command was intentionally skipped, why: none.

Security & Privacy Impact (required)

  • New permissions, capabilities, or file system access scope? No
  • New external network calls? No
  • Secrets / tokens / credentials handling changed? No
  • PII, real identities, or personal data in diff, tests, fixtures, or docs? No

Compatibility (required)

  • Backward compatible? Yes. Default 0 preserves current behaviour byte-for-byte across every channel. Existing configs deserialise cleanly without the key set on any of the nine channels.
  • Config / env / CLI surface changed? Yes (additive). New reply_min_interval_secs key on [channels.telegram], [channels.discord], [channels.slack], [channels.mattermost], [channels.webhook], [channels.imessage], [channels.matrix], [channels.signal], [channels.whatsapp].
  • Upgrade steps for existing users: none required. Operators who want pacing can set reply_min_interval_secs = N (0..=3600) under any of the nine channel sections.

Rollback (required for risk: medium and risk: high)

  • Fast rollback command/path: git revert <merge-sha> against master. Configs that adopted any of the new keys deserialise unchanged after revert (the key becomes unknown and is ignored).
  • Feature flags or config toggles: reply_min_interval_secs = 0 (the default) is itself a passthrough toggle. Operators can disable pacing per channel without rolling back the PR.
  • Observable failure symptoms: replies arriving slower than expected (peer-to-peer cadence on the chat surface), or replies pausing entirely if a typo set the value to a large number (catch is the 0..=3600 validator). Pacing returns to the prior immediate-send behaviour with no migration step on revert.

First step of the zeroclaw-labs#6345 throttle. Adds the schema field on
TelegramConfig as a proof-of-concept for the broader rollout. The
field defaults to 0 (current behaviour preserved exactly) and is
gated to 0..=3600 by validation that lands in a follow-up commit.

Not in this commit (deliberately separate so the schema delta is
reviewable in isolation):

- Update every TelegramConfig struct-literal construction site to
  include the new field. CI will fail until this lands; that
  failure is the actionable list.
- Mirror the field on every other channel that supports outbound
  sends (Discord, Slack, Matrix, Signal, WhatsApp, etc.) per the
  issue's "Mirror in every channel that supports outbound sends"
  acceptance criterion.
- Range validation (0..=3600) at config load time.
- Per-conversation outbound scheduler that honors the interval
  (queue replies, dispatch with tokio::time::Instant for
  monotonic clock).
- Bounded per-conversation queue with overflow log + drop metric.
- Documentation entry in the channel section of the book.

Refs zeroclaw-labs#6345.
@singlerider singlerider added enhancement New feature or request channel Auto scope: src/channels/** changed. labels May 5, 2026
@singlerider singlerider self-assigned this May 5, 2026
@singlerider singlerider added config Auto scope: src/config/** changed. runtime Auto scope: src/runtime/** changed. labels May 5, 2026
@singlerider singlerider requested a review from Audacity88 May 5, 2026 04:20
@singlerider singlerider added risk: high Auto risk: security/runtime/gateway/tools/workflows. size: XS Auto size: <=80 non-doc changed lines. labels May 5, 2026
@github-actions github-actions Bot removed channel Auto scope: src/channels/** changed. config Auto scope: src/config/** changed. runtime Auto scope: src/runtime/** changed. labels May 5, 2026
…literals

CI surfaced 9 missing-field sites after the schema field add in the
previous commit. All TelegramConfig struct literals across daemon,
orchestrator, registry, config-mod, and three schema test fixtures
now set reply_min_interval_secs: 0 explicitly so the build compiles.

Sites fixed:
- crates/zeroclaw-runtime/src/daemon/mod.rs:1107, 1253, 1273
- crates/zeroclaw-channels/src/orchestrator/mod.rs:11802
- crates/zeroclaw-runtime/src/integrations/registry.rs:872
- src/config/mod.rs:73
- crates/zeroclaw-config/src/schema.rs:12330, 13235, 16657

Refs zeroclaw-labs#6345.
@github-actions github-actions Bot added docs Auto scope: docs/markdown/template files changed. config Auto scope: src/config/** changed. labels May 5, 2026
Two fixes on top of the schema field add:

* Drop the docs/book/theme/lang-switcher.js drive-by reorder. The file
  is generated by the mdbook build from locales.toml at the repo root,
  per its own header comment; manual edits are clobbered on next
  build. Reorder belongs in a standalone commit if at all.
* The original docstring claimed the field had load-time validation
  and a per-(channel, peer-jid) scheduler. Neither lands in this
  commit (per the PR body's own scope boundary). Rewrite the
  docstring so what it claims and what the code does match. The
  intended range and the follow-up sequencing are still documented;
  the false present-tense claims are gone.

Refs zeroclaw-labs#6345
@github-actions github-actions Bot removed the docs Auto scope: docs/markdown/template files changed. label May 5, 2026
@singlerider singlerider added this to the v0.7.5 milestone May 5, 2026
@singlerider singlerider changed the title feat(config/channels): add reply_min_interval_secs to TelegramConfig (#6345) feat(config/channels): add reply_min_interval_secs to TelegramConfig May 5, 2026
@singlerider singlerider marked this pull request as ready for review May 5, 2026 06:28
Copy link
Copy Markdown
Collaborator

@Audacity88 Audacity88 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed head a163123. I checked the fetched PR state, linked issue #6345, the existing #6345 triage comment, the full diff, the config-schema source around TelegramConfig, CI status at fetch time, and the relevant ZeroClaw review/docs/governance guidance. There are no prior PR comments, inline comments, or formal reviews. I did not run local cargo; CI is green. Local checks were git merge-tree --write-tree origin/master origin/pr/6389 and git diff --check origin/master...origin/pr/6389.

🟒 What looks good β€” the schema slice is narrow and default-safe

The actual code change is small and low-behavior-risk: reply_min_interval_secs is added to TelegramConfig with #[serde(default)], and every existing TelegramConfig struct literal is updated with 0. That preserves existing behavior for users who do not set the new key. The follow-up commits also cleaned up the stale present-tense docstring claims and removed the unrelated generated docs reorder, which keeps this PR focused.

πŸ”΄ Blocking β€” Closes #6345 would close work this PR explicitly defers

The PR body says this is only the Telegram schema-field slice. It explicitly defers mirroring the field to other outbound channels, load-time range validation, and the per-channel/per-peer outbound scheduler. The linked issue #6345 is broader than this PR: its acceptance criteria require the field across supported channels, 0..=3600 validation, delayed outbound delivery, bounded queue behavior, and docs.

If this merges with Closes #6345, GitHub will close the tracker even though the actual pacing behavior has not landed and the remaining acceptance criteria are still open. That would make the work look complete when the PR itself says it is only the first step.

Please either change this PR to Refs #6345 and keep #6345 open, or open/link concrete follow-up issues for the remaining scheduler, validation, cross-channel, queue, and docs work before closing the tracker. If the intent is for this PR to fully close #6345, then the remaining acceptance criteria need to land here.

🟑 Warning β€” validation evidence should include command output

The PR says fmt, clippy, and check all pass locally, but the validation section summarizes the results without command output. CI is green, so I am not treating this as a code blocker, but the section should paste the command output tails or explain why local output is intentionally omitted.

Copy link
Copy Markdown
Collaborator

@WareWolf-MoonWall WareWolf-MoonWall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review β€” #6389 feat(config/channels): add reply_min_interval_secs to TelegramConfig

Verdict: comment
Reviewer: WareWolf-MoonWall
Head checked: a163123 (checks passing)


I read the full diff, the PR body, the existing review (Audacity88: CHANGES_REQUESTED), linked issue #6345, and cross-checked the schema change against the local TelegramConfig definition and all struct literal sites. I'm not approving over Audacity88's active block.

State of the queue: Audacity88 holds an active CHANGES_REQUESTED. No other reviews. I'm seconding Audacity88's block and adding one additional observation.


On Audacity88's block β€” I agree

The Closes #6345 trailer will auto-close the issue on merge. The PR body explicitly says this is only the first step: the scheduler, range validation, cross-channel mirroring, and docs are all deferred. The issue tracker closing would misrepresent the feature as complete when the agent doesn't yet pace anything β€” reply_min_interval_secs is added to the schema but nothing reads it yet at runtime.

Resolution path: change Closes #6345 to Refs #6345 in the PR body, and either open explicit tracking issues for the deferred work or add Depends on links when those PRs exist. This keeps the issue open as the active tracker for the full feature.


πŸ”΄ Blocking β€” cargo test was not run

Audacity88 flagged this as 🟑; I'm treating it as πŸ”΄ here because this PR touches zeroclaw-runtime/src/ β€” a high-risk path per AGENTS.md. The validation section lists fmt, clippy, and check, but not cargo test. The changes are confined to struct literal construction sites in test code and the schema definition, so the tests almost certainly pass, and CI is green. But the template requires cargo test for code changes, and touching zeroclaw-runtime/src/daemon/mod.rs and zeroclaw-runtime/src/integrations/registry.rs β€” even in #[cfg(test)] blocks β€” is a code change, not a docs change. The PR is labeled risk: high; the evidence bar for that risk tier includes the full test sweep.

Please paste the cargo test tail before this lands.


🟑 Warning β€” cargo test skip is undocumented

The template says: "If any command was intentionally skipped, why." The validation section doesn't acknowledge the skip. Even if cargo test passes, the omission leaves a gap in the reviewable evidence record. Documenting "skipped because X" is what turns a gap into a deliberate decision.


🟒 Praise β€” every struct literal site updated, default is zero

All TelegramConfig struct literal construction sites across schema.rs (tests), orchestrator/mod.rs (test), daemon/mod.rs (tests), integrations/registry.rs (test), and src/config/mod.rs (test) were found and updated with reply_min_interval_secs: 0. A zero-default means existing configs deserialize cleanly with no behavioral change. The #[serde(default)] annotation handles TOML configs that omit the new key. The diff is narrow and correct.


🟒 Praise β€” docstring quality

The field-level docstring on reply_min_interval_secs is the right length and covers the right things: the default, the intended range, the identity-security rationale, and the deferred-scheduler caveat. It reads like a field that will be used, not like a placeholder.


πŸ”΅ Suggestion β€” add a telegram.md docs mention, even as a "coming soon" note

The field will surface in cargo mdbook refs automatically, but a user reading docs/book/src/channels/telegram.md won't know the field exists until someone looks at the config reference. Mentioning it β€” even briefly, e.g. "pacing: reply_min_interval_secs sets a floor on reply cadence; scheduler enforcement lands in a follow-up" β€” would make the staged intent visible in the channel docs rather than only in the schema reference.


πŸ”΅ Suggestion β€” consider a test for the new field's serde default

There are existing targeted tests for TelegramConfig serde behavior (telegram_config_ack_reactions_missing_defaults_to_none, etc.). Adding one for reply_min_interval_secs β€” verifying that a TOML input omitting the field deserializes to 0 β€” follows the established pattern and documents the contract explicitly. Not blocking, but consistent with the test style already present.


Template

The PR body is well-filled. The security section correctly notes that the field is inert at this commit. The rollback section is accurate. The Closes #6345 issue is the only template-level problem, and it's the same issue as the blocking item above.


Summary: Audacity88's block is correct. Change Closes to Refs, run cargo test and paste the tail, and this schema-only slice is ready for another look. The field itself is well-formed and the literal-site update is thorough.

@WareWolf-MoonWall
Copy link
Copy Markdown
Collaborator

@JordanTheJet β€” milestone alignment needed: this PR does not clearly fit within the scope boundary of any open milestone. Please advise on placement or deferral.

…note

- New `telegram_config_reply_min_interval_secs_missing_defaults_to_zero`
  guards the `#[serde(default)]` contract on the field. Pattern matches
  the existing `telegram_config_ack_reactions_missing_defaults_to_none`.
- chat-others.md mentions the field on the Telegram block with a
  one-line note about the deferred scheduler tracked in zeroclaw-labs#6421.

Per @WareWolf-MoonWall review suggestions.
@github-actions github-actions Bot added the docs Auto scope: docs/markdown/template files changed. label May 6, 2026
Copy link
Copy Markdown
Collaborator

@theonlyhennygod theonlyhennygod left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head ad800e8. I read the PR body, @Audacity88's CHANGES_REQUESTED at a163123, @WareWolf-MoonWall's COMMENTED at a163123, the full diff, the schema diff against TelegramConfig, every struct-literal call site touched, and the new test.

βœ… [resolved] Audacity88's blocker (Closes #6345) is addressed

The PR body now reads:

Refs #6345 (kept open as the active feature tracker; #6421 holds the deferred implementation work).

Filing #6421 explicitly for the deferred scheduler / range validation / cross-channel mirroring / queue / docs work β€” and changing Closes to Refs β€” is exactly the resolution path @Audacity88 asked for. #6345 stays open as the active feature tracker, and this PR no longer over-claims completion.

βœ… [resolved] WareWolf-MoonWall's blocker (cargo test evidence)

The validation evidence section now includes both targeted and workspace test runs:

$ cargo test -p zeroclaw-config --lib telegram
running 9 tests
... (all 9 pass, including the new telegram_config_reply_min_interval_secs_missing_defaults_to_zero)

$ cargo test --workspace --exclude zeroclaw-desktop --features ci-all --lib
# 1452 / 612 / 165 / 136 / 58 / 295 / ... per-crate suites: all passing

The new telegram_config_reply_min_interval_secs_missing_defaults_to_zero test is exactly the targeted serde-default test WareWolf-MoonWall suggested as a πŸ”΅ β€” it pins the #[serde(default)] contract so a regression that drops the attribute fails CI.

βœ… [resolved] Telegram docs mention added

docs/book/src/channels/chat-others.md now includes:

reply_min_interval_secs (default 0) sets a per-peer floor on reply cadence. Field is recognised by the schema; the per-channel pacing scheduler that actually delays delivery lands in a follow-up to #6345.

This is the staged-intent docs note WareWolf-MoonWall suggested. The "field exists but scheduler is deferred" framing is exactly right and matches the PR body.

🟒 What's working well

  • All TelegramConfig struct literal sites updated (schema tests, orchestrator test, daemon tests, integrations registry test, src/config/mod.rs test) β€” the 0 literal is consistent everywhere, no missed sites.
  • Field-level docstring is the right length and covers default, range, identity-security rationale, and deferred-scheduler caveat. Reads like a field that will be used, not a placeholder.
  • Schema-only diff with zero-default means existing TOML configs deserialize cleanly with no behavioral change. Backward compat is byte-equivalent until #6421 lands.
  • Refs #6345 + spawn of #6421 is the right governance shape β€” keeps the tracker honest while allowing this slice to merge.

Verdict

All prior findings are addressed at HEAD. CI is green. The schema slice is narrow, default-safe, and well-tested.

Leaving as --comment rather than --approve because @Audacity88's CHANGES_REQUESTED is still on the books at a163123. @Audacity88, would you take another look at HEAD ad800e8? The Closes β†’ Refs change + #6421 spawn directly resolves your block; if you agree, dismissing or converting your review clears the merge gate.

@theonlyhennygod
Copy link
Copy Markdown
Collaborator

@Audacity88 β€” quick re-review ping. The Closes #6345 blocker you raised at a163123 is resolved at HEAD ad800e8: PR body is now Refs #6345, and #6421 was filed for the deferred scheduler / validation / cross-channel / queue / docs work. Your 🟑 on missing cargo test evidence is also addressed (full output in the validation section). Details in the review I just posted above β€” converting or dismissing your CHANGES_REQUESTED clears the merge gate. Thanks!

…l + validation

Closes zeroclaw-labs#6345 in full.

- New `PacedChannel` wrapper in `crates/zeroclaw-channels/src/paced_channel.rs`
  that throttles outbound `send` (and `finalize_draft`) per-(channel,
  recipient) by the configured floor. `min_interval_secs == 0` returns the
  inner `Arc<dyn Channel>` unchanged so the default config pays no overhead.
  Streaming `update_draft` / `update_draft_progress` calls are NOT paced β€”
  pacing them would freeze the live preview.

- Field mirrored from TelegramConfig to every other outbound channel that
  has a meaningful pacing surface: DiscordConfig, SlackConfig,
  MattermostConfig, WebhookConfig, IMessageConfig, MatrixConfig,
  SignalConfig, WhatsAppConfig. All `#[serde(default)]` so existing configs
  deserialize unchanged.

- Orchestrator wraps each affected channel in `PacedChannel::wrap(...)` at
  construction time. The 7 push sites that have a Some-bound config var in
  scope (tg / dc / sl / mm / im / sig / wh) thread the value through. Other
  channels stay unwrapped because they don't carry the field.

- `Config::validate()` enforces `0..=3600` on every
  `reply_min_interval_secs` value via a new
  `Config::collect_reply_min_interval_values()` helper. The upper bound
  catches typos that would otherwise wedge a channel for days
  (milliseconds-in-seconds is the obvious failure mode).

- Tests: 4 PacedChannel unit tests (no `tokio::test-util` dep β€” uses
  short real-time intervals + state-only assertions) + 2 validate() tests
  pinning the 0..=3600 contract on both ends.

- `docs/book/src/channels/chat-others.md` documents the field at the top
  with the wire contract: per-(channel, recipient), `0` is passthrough,
  draft updates not paced, different recipients independent.
@singlerider singlerider modified the milestones: v0.7.5, v0.7.6 May 6, 2026
@singlerider singlerider changed the title feat(config/channels): add reply_min_interval_secs to TelegramConfig feat(channels): per-(channel,recipient) reply pacing β€” wrapper + 9 channels + validation May 6, 2026
@singlerider singlerider dismissed Audacity88’s stale review May 6, 2026 09:28

Scope expanded to close and surpass.

@singlerider singlerider requested a review from Audacity88 May 6, 2026 09:28
Copy link
Copy Markdown
Collaborator

@WareWolf-MoonWall WareWolf-MoonWall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review β€” #6389 feat(channels): per-(channel,recipient) reply pacing β€” wrapper + 9 channels + validation

Verdict: --approve
Reviewer: @WareWolf-MoonWall
Head reviewed: 134614c (current HEAD; checks passing)
Prior review head: a163123 (COMMENTED)


Take-stock

@Audacity88's CHANGES_REQUESTED (at a163123) is DISMISSED. My prior COMMENTED review is not a block. theonlyhennygod reviewed an intermediate state at ad800e8 (COMMENTED). No active CHANGES_REQUESTED holds. I'm reviewing the expanded implementation fresh.


Prior findings β€” resolution status

βœ… [resolved] πŸ”΄ Closes #6345 premature

At a163123 this PR was a Telegram schema-only slice and the tracker would have closed prematurely. The resolution path here was not Refs + deferral β€” it was shipping the full implementation. The diff confirms all acceptance criteria from #6345 are present: PacedChannel wrapper, field on all 9 channels, orchestrator wrapping at every Arc::new(...) push site, Config::validate() range enforcement (0..=3600), 4 unit tests + 3 config tests, and the chat-others.md pacing section. Closes #6345 is now accurate and appropriate.

βœ… [resolved] πŸ”΄ cargo test evidence absent

The validation section now includes:

  • cargo test -p zeroclaw-channels --lib paced β€” 4 tests, all passing
  • cargo test -p zeroclaw-config --lib reply_min_interval β€” 3 tests, all passing
  • cargo test --workspace --exclude zeroclaw-desktop --features ci-all --lib β€” full workspace suite passing

High-risk-path evidence bar met.

βœ… [resolved] 🟑 cargo test skip undocumented

cargo test was run. The gap is gone.

βœ… [resolved] πŸ”΅ Serde default test for reply_min_interval_secs

telegram_config_reply_min_interval_secs_missing_defaults_to_zero is present in the diff. It pins the #[serde(default)] contract so a regression that drops the attribute fails CI.

βœ… [resolved] πŸ”΅ Channel docs mention

docs/book/src/channels/chat-others.md now has a top-level ## Pacing outbound replies (reply_min_interval_secs) section documenting the wire contract: per-(channel, recipient) floor, 0 is passthrough, draft updates not paced, recipients independent. More comprehensive than the per-channel telegram.md note I originally suggested β€” the right call given the field is on nine channels.


New findings

🟑 [warning] β€” Telegram-specific note in chat-others.md is now stale

Under the Telegram section, the new bullet reads:

`reply_min_interval_secs` (default `0`) sets a per-peer floor on reply cadence. Field is recognised by the schema; the per-channel pacing scheduler that actually delays delivery lands in a follow-up to #6345.

This was accurate when theonlyhennygod reviewed an intermediate state where the PR body said Refs #6345. It is not accurate now: PacedChannel IS the delivery mechanism and it ships in this PR. A user reading that bullet today will believe pacing is deferred when it is live.

Resolution: update the bullet to reflect the shipped state, e.g.:

reply_min_interval_secs (default 0) sets a per-peer floor on reply cadence. Pacing is active: the orchestrator wraps the channel in PacedChannel so consecutive sends to the same peer wait at least N seconds. 0 is a zero-overhead passthrough.

Not blocking the approve β€” the top-level pacing section is accurate and describes the correct behavior. But the stale note should be cleaned up before or shortly after merge, since it will be the first thing a Telegram user sees in that section.

🟑 [warning] β€” size: XS label is stale

The diff is +680 -80. size: XS was appropriate for the original Telegram schema-only slice; the full implementation warrants at least size: M. Worth updating for changelog generation and queue hygiene β€” the label is used by the PR template for size signal.


What's working well

🟒 Zero-overhead default path is correct

PacedChannel::wrap returns inner unchanged when min_interval_secs == 0. No allocation, no Arc::new, no Mutex. The default config pays exactly zero overhead for this feature. That's the right design β€” the constraint is "no penalty for the common case."

🟒 Lock discipline in PacedChannel::send is correct

The lock is acquired to compute the sleep duration and update the per-recipient slot, then released before tokio::time::sleep. Multiple concurrent senders to different recipients proceed in parallel. Multiple concurrent senders to the same recipient each reserve their slot atomically and then sleep independently β€” the map records now + wait (the scheduled fire time) so a burst of N replies stacks to (N-1) * min_interval wall-clock correctly. There is no contention during sleep.

🟒 Nine channels, every struct literal site updated

All nine channel configs (TelegramConfig, DiscordConfig, SlackConfig, MattermostConfig, WebhookConfig, IMessageConfig, MatrixConfig, SignalConfig, WhatsAppConfig) carry reply_min_interval_secs: 0 in every test struct literal across schema.rs, orchestrator/mod.rs, daemon/mod.rs, integrations/registry.rs, src/config/mod.rs, and the channel-specific test modules. No missed sites visible in the diff.

🟒 Config::validate() upper-bound test pins the typo-prevention contract

validate_accepts_reply_min_interval_at_upper_bound (3600 passes) and validate_rejects_reply_min_interval_above_one_hour (3601 fails) pin the boundary that catches the milliseconds-in-seconds typo before runtime. The 60000 β†’ 16-hour-wedge failure mode is now a config validation error, not a silent runtime regression.


Summary

All prior findings from my COMMENTED review at a163123 are resolved. The full pacing feature is correctly implemented, tested, and documented. The two 🟑 items above (stale doc note, label) are cleanup and do not block merge. CI is green.

Approving. Once the Telegram-specific doc note is updated, this is complete.

@singlerider singlerider added size: L Auto size: 501-1000 non-doc changed lines. and removed size: XS Auto size: <=80 non-doc changed lines. labels May 6, 2026
The Telegram bullet pre-dated the in-PR pivot to ship pacing in the same
PR; it still described the field as schema-only with delivery deferred.
Pacing is active here. Point to the top-level Pacing section for the
full contract.
@singlerider singlerider changed the title feat(channels): per-(channel,recipient) reply pacing β€” wrapper + 9 channels + validation feat(channels): per-recipient reply pacing across 9 channels May 6, 2026
Copy link
Copy Markdown
Collaborator

@WareWolf-MoonWall WareWolf-MoonWall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review β€” PR #6389 Β· feat(channels): per-recipient reply pacing across 9 channels

Verdict: APPROVE
Reviewed at commit db82da2.


Resolved since prior review

βœ… Orchestrator integration complete. collect_configured_channels now wraps every Some-bound channel construction in PacedChannel::wrap(..., <config>.reply_min_interval_secs). The prior head shipped the PacedChannel implementation and config schema; this head wires it into all nine channels at the construction sites. The implementation is complete end-to-end.

βœ… Stale doc bullet fixed. The PR body confirms the chat-others.md Telegram bullet was updated to reflect that pacing is live in this PR (not "landing in a follow-up"). Verified in the updated PR body.

🟒 The reply_min_interval_secs == 0 passthrough optimisation (PacedChannel::wrap returns the inner Arc unchanged when the interval is zero) means the default config pays zero overhead β€” no wrapper allocation, no Mutex contention. This is the right design for an opt-in feature.

🟒 Matrix test config updated to include reply_min_interval_secs: 0 β€” confirms the struct initialisation pattern is consistent across all channels with exhaustive field coverage.


One non-blocking cleanup remaining

🟑 size: XS label is stale β€” the diff is ~1 270 lines; warrants size: M or size: L. Not a merge blocker, but a quick sidebar edit before landing keeps the metadata useful.


No active CHANGES_REQUESTED from any reviewer. Approving.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config Auto scope: src/config/** changed. docs Auto scope: docs/markdown/template files changed. enhancement New feature or request risk: high Auto risk: security/runtime/gateway/tools/workflows. size: L Auto size: 501-1000 non-doc changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Per-channel reply-min-interval-secs (throttle outbound agent replies)

4 participants