Skip to content

Coalesce watcher bursts before daemon reconciliation#72

Open
maxkle1nz wants to merge 3 commits into
mainfrom
codex/m1nd-daemon-coalesce-v2
Open

Coalesce watcher bursts before daemon reconciliation#72
maxkle1nz wants to merge 3 commits into
mainfrom
codex/m1nd-daemon-coalesce-v2

Conversation

@maxkle1nz
Copy link
Copy Markdown
Owner

Summary

  • coalesce watcher bursts into one logical daemon wakeup window
  • add single-flight tick execution with at most one pending rerun
  • keep daemon_tick as the only reconciliation implementation
  • expose coalescing state in daemon_status

Validation

  • cargo fmt --check
  • cargo test -p m1nd-mcp run_daemon_tick_marks_pending_rerun_when_already_in_flight -- --nocapture
  • cargo test -p m1nd-mcp daemon_ -- --nocapture
  • MCP burst smoke for coalesced watcher events

Why this matters

This is the watchexec-style behavior layer on top of the native watcher backend. The daemon now reacts once per logical change burst instead of stampeding itself with redundant ticks.

max kle1nz added 3 commits April 5, 2026 23:09
The daemon now merges filesystem event bursts into one logical wakeup window, keeps only one pending rerun while a tick is in flight, and exposes coalescing state in daemon_status. This brings the watcher backend in line with the next behavioral donor lane from watchexec without changing the public MCP surface.

Constraint: Keep daemon_tick as the single reconciliation implementation while reducing watcher burst amplification
Rejected: A separate coalescing tool or queue API | unnecessary surface area for internal daemon behavior
Rejected: Unbounded pending reruns | risks event storms and duplicate work under bursty filesystems
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep watcher burst handling single-flight until a richer scheduler policy is proven with production telemetry
Tested: cargo fmt --check; cargo test -p m1nd-mcp run_daemon_tick_marks_pending_rerun_when_already_in_flight -- --nocapture; cargo test -p m1nd-mcp daemon_ -- --nocapture; cargo test -p m1nd-mcp native_watcher_ -- --nocapture; cargo test -p m1nd-mcp daemon_wait_duration_ -- --nocapture; MCP burst smoke
Not-tested: Multi-client contention across long-lived sessions
Constraint: the prior CI run captured a stale format failure even though the branch is now rustfmt-clean\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: Remove the need for noop retriggers by ensuring format is rechecked before first push on future watcher-loop branches\nTested: cargo fmt --check\nNot-tested: N/A
Constraint: keep the coalescing branch clean and rustfmt-stable before starting the next daemon phase\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: Finish the current daemon tranche completely before opening the next stack branch\nTested: cargo fmt --check\nNot-tested: N/A
Copilot AI review requested due to automatic review settings April 5, 2026 21:32
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bae25ca5d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread m1nd-mcp/src/server.rs
Comment on lines +2193 to 2195
state.daemon_state.tick_in_flight = true;
state.daemon_state.last_tick_trigger = Some(trigger.to_string());
let _ = crate::daemon_handlers::handle_daemon_tick(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear in-flight state before daemon tick persistence

run_daemon_tick sets tick_in_flight = true before calling handle_daemon_tick, but handle_daemon_tick persists daemon_state during that call; this means the on-disk daemon_state.json can be saved with tick_in_flight: true and never persisted back to false afterward. On the next process start (when daemon state is reloaded and auto-resumed), every trigger can hit the early return at the top of run_daemon_tick, leaving the daemon stuck in perpetual pending_rerun without reconciling changes.

Useful? React with 👍 / 👎.

Comment thread m1nd-mcp/src/session.rs
Comment on lines +170 to +174
pub coalesce_window_ms: u64,
pub pending_rerun: bool,
pub tick_in_flight: bool,
pub last_coalesced_event_ms: Option<u64>,
pub coalesced_event_count: u64,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add serde defaults for newly persisted daemon fields

These newly added non-optional fields are deserialized from daemon_state.json, but older state files will not contain them. Because load_daemon_state uses serde_json::from_str::<DaemonRuntimeState>(...).ok().unwrap_or_default(), a missing-field decode error causes the entire daemon runtime state to be dropped to defaults (losing prior tracked files, counters, and active status) after upgrade.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a “watchexec-style” coalescing layer for filesystem watcher bursts and introduces single-flight daemon tick execution (with at most one pending rerun), while exposing the new coalescing/tick state via daemon_status.

Changes:

  • Add coalescing/tick-control fields to DaemonRuntimeState for tracking burst windows and in-flight/pending tick state.
  • Coalesce watcher bursts in the server loop using a timeout-based drain window, and gate tick execution via a single-flight mechanism.
  • Surface the new state in daemon_start/daemon_status outputs and extend tests accordingly.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
m1nd-mcp/src/session.rs Extends persisted daemon runtime state with coalescing + tick single-flight fields.
m1nd-mcp/src/server.rs Implements burst coalescing in the event loop and adds single-flight run_daemon_tick behavior + a unit test.
m1nd-mcp/src/daemon_handlers.rs Initializes and reports new daemon coalescing/single-flight fields in start/status, updates tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread m1nd-mcp/src/session.rs
Comment on lines 169 to +174
pub poll_interval_ms: u64,
pub coalesce_window_ms: u64,
pub pending_rerun: bool,
pub tick_in_flight: bool,
pub last_coalesced_event_ms: Option<u64>,
pub coalesced_event_count: u64,
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

DaemonRuntimeState is persisted to daemon_state.json and loaded via serde_json::from_str(...).ok().unwrap_or_default(). Adding new non-optional fields here (e.g. coalesce_window_ms, last_coalesced_event_ms, coalesced_event_count) will make deserializing older state files fail (missing fields), which resets the entire daemon state to Default on upgrade. Add #[serde(default)] (or explicit #[serde(default = "...")] where you want a non-zero default like 75ms) so older daemon_state.json continues to load correctly.

Copilot uses AI. Check for mistakes.
Comment thread m1nd-mcp/src/session.rs
Comment on lines +170 to +171
pub coalesce_window_ms: u64,
pub pending_rerun: bool,
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

tick_in_flight / pending_rerun look like transient, in-memory scheduling flags, but they’re part of DaemonRuntimeState which is persisted by handle_daemon_tick() via state.persist_daemon_state(). Because run_daemon_tick() sets tick_in_flight = true before calling handle_daemon_tick(), the persisted daemon_state.json will often contain tick_in_flight: true, and on restart the daemon can get stuck permanently treating every tick as "already in flight". Make these fields non-persistent (e.g., #[serde(skip)]/skip_serializing + #[serde(default)], or move them out of the persisted struct) and ensure they default to false on load.

Suggested change
pub coalesce_window_ms: u64,
pub pending_rerun: bool,
pub coalesce_window_ms: u64,
#[serde(skip, default)]
pub pending_rerun: bool,
#[serde(skip, default)]

Copilot uses AI. Check for mistakes.
Comment thread m1nd-mcp/src/server.rs
Comment on lines +2679 to 2686
let coalesced_at_ms = now_ms();
self.state.daemon_state.last_watch_event_ms = Some(coalesced_at_ms);
loop {
match rx.try_recv() {
match rx.recv_timeout(Duration::from_millis(
self.state.daemon_state.coalesce_window_ms.max(1),
)) {
Ok(ServerEvent::WatchNotice) => {
watch_events_seen = watch_events_seen.saturating_add(1);
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

Inside the watch-burst coalescing loop, last_watch_event_ms is only set once (to the timestamp of the first WatchNotice) and then never updated for subsequent WatchNotice/WatchError events drained during the burst. This makes daemon_status.last_watch_event_ms misleading for bursts and errors. Update last_watch_event_ms each time you receive a watch notice/error in the coalescing loop (and consider whether coalesced_at_ms should represent the end-of-burst timestamp instead).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants