Skip to content

Run daemon ticks during idle server time#64

Open
maxkle1nz wants to merge 3 commits into
mainfrom
codex/m1nd-daemon-background-tick-v1
Open

Run daemon ticks during idle server time#64
maxkle1nz wants to merge 3 commits into
mainfrom
codex/m1nd-daemon-background-tick-v1

Conversation

@maxkle1nz
Copy link
Copy Markdown
Owner

Summary

  • move stdin reading onto a helper thread so the main serve loop can wake on timeouts
  • run daemon ticks during idle periods when the daemon is active and overdue
  • keep the implementation transport-agnostic and in-process, without introducing OS-specific watcher dependencies yet
  • add coverage proving background ticking refreshes the graph before the next explicit tool call

Validation

  • cargo fmt --check
  • cargo check -p m1nd-mcp -p m1nd-ingest
  • cargo test -p m1nd-ingest -p m1nd-mcp -- --nocapture
  • cargo clippy -p m1nd-mcp -p m1nd-ingest -- -D warnings
  • real MCP smoke with idle wait before daemon_status / search

Why this matters

This is the first real autonomous background behavior in the server loop. The daemon can now keep watched roots fresh even when the agent is idle, which is a meaningful category shift from opportunistic autotick-on-traffic.

max kle1nz added 3 commits April 5, 2026 21:59
This lets the daemon advance itself on ordinary tool traffic without a
background thread. When the daemon is active and the poll interval has elapsed,
non-daemon tool calls opportunistically run one daemon tick before their normal
execution.

Constraint: Avoid background concurrency changes in the first autonomous slice of the daemon
Rejected: Spawning a persistent watcher thread now | larger correctness and lifecycle surface before the explicit tick contract fully settles
Rejected: Autoticking daemon control tools too | risks recursion and accidental self-trigger loops
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep autotick opportunistic and cheap until a true background scheduler/watcher lands
Tested: cargo fmt --check; cargo check -p m1nd-mcp -p m1nd-ingest; cargo test -p m1nd-ingest -p m1nd-mcp -- --nocapture; cargo clippy -p m1nd-mcp -p m1nd-ingest -- -D warnings; real MCP smoke where search observed a file change without explicit daemon_tick
Not-tested: Interaction with future background ticking or high-frequency concurrent clients
Changed-file daemon ticks now reuse the same structural heuristic layer as
write paths. After incremental re-ingest, the daemon evaluates the touched file
for low-trust zones, tremor activity, companion tests, and other proactive
signals, then persists those alerts through the existing daemon queue.

Constraint: Keep daemon-side insight generation aligned with the write path instead of introducing a second drift-scoring implementation
Rejected: Duplicating heuristic logic inside daemon_handlers | would split alert semantics between write and daemon paths
Rejected: Limiting daemon ticks to deletion-only alerts | too weak once the daemon can already re-ingest changed files
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep daemon drift insights derived from the shared surgical heuristic surface until a dedicated daemon-native scoring layer exists
Tested: cargo fmt --check; cargo check -p m1nd-mcp -p m1nd-ingest; cargo test -p m1nd-ingest -p m1nd-mcp -- --nocapture; cargo clippy -p m1nd-mcp -p m1nd-ingest -- -D warnings
Not-tested: Long-running daemon churn with many simultaneous changed files
The stdio server now separates stdin reading from the main loop so it can
use timeouts between requests. When the daemon is active, idle time in the
serve loop runs background daemon ticks without requiring manual tool calls or
ordinary agent traffic.

Constraint: Keep the first autonomous background slice in-process and portable without introducing OS-specific watcher dependencies
Rejected: Waiting for platform watchers before adding any background ticking | delayed the most important autonomy win for the daemon
Rejected: Background ticking inside the request dispatch path only | still depends on user traffic and leaves idle sessions stale
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Preserve the single-writer semantics around daemon ticks if this grows into a richer scheduler or watcher fabric
Tested: cargo fmt --check; cargo check -p m1nd-mcp -p m1nd-ingest; cargo test -p m1nd-ingest -p m1nd-mcp -- --nocapture; cargo clippy -p m1nd-mcp -p m1nd-ingest -- -D warnings; real MCP smoke with idle wait before daemon_status/search
Not-tested: High-concurrency multi-client behavior and future watcher-thread interaction
Copilot AI review requested due to automatic review settings April 5, 2026 20:04
@maxkle1nz maxkle1nz enabled auto-merge (squash) April 5, 2026 20:04
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 updates the MCP server loop to support autonomous daemon ticking during idle time, ensuring watched roots can be refreshed even when no requests are arriving.

Changes:

  • Moves stdin request reading onto a helper thread so the main loop can wake on timeouts.
  • Adds idle-time daemon ticking (plus opportunistic “autotick” before non-daemon tool calls when overdue).
  • Extends daemon tick behavior to generate/persist proactive insights and adds tests covering background ticking + insight surfacing.

Reviewed changes

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

File Description
m1nd-mcp/src/surgical_handlers.rs Extracts proactive-insight computation into a reusable helper and exposes daemon alert persistence for reuse by daemon ticks.
m1nd-mcp/src/server.rs Refactors the main serve loop to use a stdin reader thread + timeout wakeups for background daemon ticks; adds autotick gating + tests.
m1nd-mcp/src/daemon_handlers.rs Enhances daemon ticking to compute/persist proactive alerts per changed file and adds coverage asserting alerts are surfaced.

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

Comment thread m1nd-mcp/src/server.rs
Comment on lines 2454 to +2466
let stdout = std::io::stdout();
let mut reader = stdin.lock();
let mut writer = stdout.lock();
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
let stdin = std::io::stdin();
let mut reader = stdin.lock();
loop {
let next = read_request_payload(&mut reader);
match next {
Ok(Some(value)) => {
if tx.send(Some(value)).is_err() {
break;
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.

serve() now uses an unbounded mpsc::channel(), so if stdin produces requests faster than the main loop can dispatch them, the queue can grow without bound and increase memory usage. Consider switching to a bounded channel (e.g., sync_channel(1)/small capacity) to preserve backpressure similar to the previous in-thread read loop.

Copilot uses AI. Check for mistakes.
Comment thread m1nd-mcp/src/server.rs
Comment on lines +2125 to +2130
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|value| value.as_millis() as u64)
.unwrap_or(0)
}
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.

now_ms() is used for scheduling daemon ticks via wall-clock time; if the system clock moves backwards (e.g., NTP adjustment), the saturating_sub checks can suppress ticks for an extended period. If possible, use a monotonic clock (Instant) for due calculations (while keeping an epoch timestamp only for reporting/persistence) to avoid time-jump issues.

Copilot uses AI. Check for mistakes.
Comment thread m1nd-mcp/src/server.rs
Comment on lines +2617 to +2623
if self.state.daemon_state.active
&& should_autotick_daemon(tool_name)
&& self.state.daemon_state.last_tick_ms.is_some_and(|last| {
now_ms().saturating_sub(last)
>= self.state.daemon_state.poll_interval_ms
})
{
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.

The autotick-on-traffic path does not mirror background_tick_if_due's poll_interval_ms == 0 guard. With poll_interval_ms set to 0, this condition becomes always true and will tick on every tool call, even though idle ticking is disabled. Add the same poll_interval_ms != 0 check (or explicitly document/validate 0 as a special case) to keep behavior consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +337 to 340
pub(crate) fn persist_daemon_alerts_from_insights(
state: &mut SessionState,
proactive_insights: &[surgical::ProactiveInsight],
default_file_path: Option<&str>,
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.

persist_daemon_alerts_from_insights now has broader call sites (used from handle_daemon_tick too), but the function still performs persistence + emits warning text that mentions "during apply". This makes logs misleading and can add redundant disk writes when the caller also persists (e.g., daemon_tick). Consider making persistence optional (caller-controlled) and updating the warning text to be context-agnostic.

Copilot uses AI. Check for mistakes.
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: ba33dc77a7

ℹ️ 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
let stdout = std::io::stdout();
let mut reader = stdin.lock();
let mut writer = stdout.lock();
let (tx, rx) = mpsc::channel();
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 Restore backpressure on stdin request intake

Using mpsc::channel() here makes the request queue unbounded, so the reader thread can keep draining stdin while the main thread is busy handling a slow tool call. If a client pipelines many requests (or sends large payloads) faster than dispatch can process them, queued String payloads grow without limit and can OOM the server. Before this change, blocking stdin reads naturally applied backpressure through the pipe buffer.

Useful? React with 👍 / 👎.

Comment thread m1nd-mcp/src/server.rs
Comment on lines +2619 to +2622
&& self.state.daemon_state.last_tick_ms.is_some_and(|last| {
now_ms().saturating_sub(last)
>= self.state.daemon_state.poll_interval_ms
})
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 Skip autotick when poll interval is zero

This due-check treats poll_interval_ms == 0 as always overdue (now - last >= 0), so every non-daemon tools/call triggers a full daemon_tick. Because daemon_start accepts 0 and background_tick_if_due explicitly treats 0 as disabled, this path creates inconsistent behavior and can add large per-request latency whenever a caller sets the interval to zero.

Useful? React with 👍 / 👎.

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