Skip to content

feat(watch): one active watcher per directory tree across sessions - #154

Draft
F0rce wants to merge 2 commits into
mixedbread-ai:mainfrom
F0rce:feat/watch-coordination
Draft

F0rce wants to merge 2 commits into
mixedbread-ai:mainfrom
F0rce:feat/watch-coordination

Conversation

@F0rce

@F0rce F0rce commented Oct 6, 2026

Copy link
Copy Markdown

Depends on #153. This branch is stacked on it, so the diff below includes #153's commit. Only the last commit belongs to this PR: F0rce/mgrep@feat/codex-mcp-integration...feat/watch-coordination. I'll rebase onto main once #153 is merged.

Problem

Every agent session starts its own watcher: the Claude SessionStart hook runs mgrep watch, and with #153 the Codex MCP server watches the session directory. Two sessions in /repo and /repo/services therefore run two watchers that both hash and upload everything under /repo/services. With several sessions in one repository, the same files are hashed and uploaded several times. The unmerged prevent-multiple-watches-on-same-dir branch locks only an identical directory, so a parent and a child directory still double up.

Expected: one active watcher per directory tree and store, without any session losing sync when another ends.

Change

Watchers on the same store coordinate through a claim registry in ~/.mgrep/watchers, one O_EXCL claim file per store and real path. This extends the lock design from prevent-multiple-watches-on-same-dir instead of adding a second mechanism.

  • First started wins. A watcher whose directory is covered by an earlier active watcher (same directory or an ancestor) stands by: no initial sync, no fs.watch, no uploads. It polls the registry and takes over, with a sync, when the covering watcher exits.
  • Later parents exclude earlier children. A watcher started in /repo after one in /repo/services skips that subtree and takes it back when the child exits.
  • Config boundaries. A directory with its own .mgreprc.yaml/.yml always gets its own watcher. Ancestors neither upload nor delete files inside it, since initialSync gets excludedRoots.
  • Cleanup. Claims of dead processes, and unparsable claims older than 30 s, are pruned. Claims are released on exit and on SIGINT/SIGTERM. The Claude SessionEnd hook now sends SIGTERM instead of SIGKILL so the claim is released.
  • Signals and quota. A signalled CLI watcher exits right away, also during a sync. After a quota error, poll-time re-syncs back off for 10 minutes (MGREP_WATCH_QUOTA_BACKOFF_MS), and the quota message prints once.
  • MCP server. A covered watcher resolves immediately, so searches don't wait for a sync another session owns. The MCP server keeps its own shutdown and doesn't install signal handlers.

Not handled: creating or removing a .mgreprc file while a watcher runs takes effect when a watcher next becomes active, the same as config changes today.

Scope and approval

There is no prior issue or discussion. It builds on the maintainers' unmerged prevent-multiple-watches-on-same-dir branch. Happy to split it or align with that branch differently if you prefer.

Verification

  • test/watch-coordination.bats:
    • same directory: the second watcher stands by and one claim exists;
    • a child started after its parent stands by and takes over after SIGTERM;
    • a parent started after a child excludes the child's uploads;
    • .mgreprc.yaml and .yml boundaries;
    • an ancestor's sync does not delete the boundary's store files;
    • stale and unparsable claims are pruned;
    • per-file and per-directory errors keep the watcher and its claim;
    • quota and thrown startup failures release the claim;
    • SIGTERM during a slow initial sync exits within 2 s;
    • quota during takeover backs off.
  • test/mcp.bats: an MCP server in a directory already covered by an active mgrep watch answers a search in under 5 s, with MGREP_MCP_SYNC_WAIT_MS=30000, no "still running" note and no own sync. After SIGTERM to the active watcher, the MCP server's watcher takes over the claim.
  • 116 bats tests pass.
  • Live, on a real 853-file repository:
    • mgrep watch in the root is active, and one in services/ logs "Covered by mgrep watch (pid …) for ; standing by", with one claim file.
    • After SIGTERM to the root watcher, it exits, the services/ watcher takes over, and no claims or processes are left after it stops.

Not checked: Linux and Windows (developed on macOS), and many concurrent sessions beyond the tested two or three.

Model and harness: Claude Opus 5.5 (1M context) in Claude Code for design, review and verification; GPT-6.1-Sol in Codex for implementation.

F0rce added 2 commits October 6, 2026 18:48
…or exact lookups

Replace the Codex integration, which appended a "MANDATORY / NEVER use
grep" skill to ~/.codex/AGENTS.md and registered an MCP server without
tools. Codex then stopped searching any other way.

The integration is now a native Codex plugin from the same repository
marketplace as the Claude plugin:

- MCP server (`mgrep mcp --expose-tools`) with one read-only `search`
  tool. It runs outside Codex's network sandbox, so searches need no
  approval prompt, and it syncs the session directory in the background
  for the lifetime of the session.
- SessionStart hook that only adds neutral guidance: mgrep for
  conceptual questions, rg/grep for exact strings and symbols.
- Short neutral skill.
- `install-codex` adds the marketplace and installs the plugin;
  `uninstall-codex` reverses it. Both clean up the legacy AGENTS.md
  block and MCP registration.

Supporting changes the MCP tool needs: search logic extracted to
src/lib/search.ts and shared by the CLI and the tool, non-interactive
store creation for the MCP server, version read from package.json, and
startWatch no longer calls process.exit and reports whether watching
started.
Every agent session starts its own `mgrep watch` (Claude hook) or MCP
server watcher (Codex). Sessions in /repo and /repo/services ran two
watchers that both hashed and uploaded everything under /repo/services.

Watchers on the same store now coordinate through a claim registry in
~/.mgrep/watchers (O_EXCL claim files, building on the lock design of
the unmerged prevent-multiple-watches-on-same-dir branch):

- The first started watcher wins. A watcher whose directory is covered
  by an earlier active watcher stands by: no initial sync, no fs.watch,
  no uploads. It takes over when that watcher exits.
- An active watcher excludes subtrees owned by earlier active watchers
  inside its tree, and gives them back when they exit.
- A directory with its own .mgreprc.yaml/.yml is a project boundary: it
  is never covered by an ancestor, and ancestors neither upload nor
  delete files inside it.
- Claims of dead processes, and unparsable claims older than 30 s, are
  pruned. Claims are released on exit and SIGTERM; the Claude
  SessionEnd hook now sends SIGTERM instead of SIGKILL.

In the MCP server a covered watcher resolves immediately, so searches
do not wait for a sync that another session already owns.

A signalled CLI watcher exits immediately, also during a sync. After a
quota error, poll-time re-syncs back off for 10 minutes
(MGREP_WATCH_QUOTA_BACKOFF_MS) and the quota message prints once.
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.

1 participant