Replaces the synchronous tools/call path with a coroutine-wrapped
dispatch. The select()-based event loop from v1.0.0-rc1 already
multiplexes I/O; this change extends the same single-thread
cooperative scheduling to tool handler execution.
How:
- server.lua:sleep_ms detects coroutine context and yields with
{ wake_at = gettime() + ms/1000 } instead of blocking. Falls back
to today's busy-blocking sleep when on the main thread (stdio
dispatch, init code).
- server.lua:run() now uses gettime() deltas for timeout accounting
(Phase 5 review fix — the prior interval-accumulator diverged
from wall-clock when scheduler delayed resumes).
- lmcp.lua wraps the handle_request call inside _dispatch_post in a
coroutine. Synchronous completion (no yield) takes the inline-
response path; if the handler yields, the coroutine parks in
self._pending_handlers and the conn enters dispatching_async.
- New _scheduler_tick services pending coroutines whose wake_at has
passed; on completion calls the shared _finalise_dispatch helper
to build the deferred HTTP response (Accept-aware: SSE or JSON).
- select() timeout tightens to the next pending wake_at so short
yields don't pay the full 100ms tick.
Measurement (Phase 7):
before: fast ping during slow shell sleep 3 = 4.28s
after: fast ping during slow shell sleep 3 = 0.01s (~400×)
3 parallel slow shells: 3.77s total wall (was ~9s).
Zero handler source-code changes. Every existing tool that goes
through run() (shell, shell_bg, fetch, web_search, list_dir,
search_files, systeminfo, hub remote_*) gets concurrency for free.
Pure-Lua handlers (ping, read_file, write_file, edit_file) continue
to complete inline. stdio transport stays serialised by design
(single-client per stdio process).
Known limits documented in memory project_handler_coroutines:
- socket.gettime() is wall-clock not monotonic; large NTP steps may
bunch resumes. Acceptable on chrony-slewed fleet.
- Cancellation (#11) is now tractable since the scheduler can flip a
flag between resumes — implementation pending.
- Server-initiated request await (sampling/roots from inside a
handler) still requires a future yield-on-pending helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
server.lua gains a shell_bg tool that launches a detached command via
setsid + nohup + stdio-redirect + &, returns immediately with PID and
log path. Linux-only for MVP (Windows Start-Process equivalent TBD).
hub.lua gains remote_shell_bg, forwarding to backend shell_bg. lmcp-only,
no ssh fallback — fallback for fire-and-forget is semantically murky.
Addresses the 'how do I launch a daemon over lmcp without the sentinel-
file wrapper blocking forever' question. Existing remote_shell keeps
its current synchronous-with-timeout behaviour.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BSD find on macOS silently emits nothing when the starting path is
itself a symlink (no trailing slash, no -L). On riemann with Homebrew,
/usr/local/share/lua is a symlink to /usr/local/Cellar/luarocks/.../share/lua
which tripped this — search_files returned empty for clearly-matching
patterns. GNU find on Linux follows the starting arg by default, so the
bug was invisible on every other host.
Add -L explicitly. Both BSD and GNU find accept it, both detect cycles,
and behavior becomes consistent.
Fixes marfrit-tracker task #16 (opened 2026-04-18 while stress-testing
riemann-tools MCP).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Literal string replacement with uniqueness check. Fails if old_string
is not found or matches multiple times (unless replace_all=true).
Matches the Claude Code harness Edit tool so sibling lmcp clients get
the same behaviour they already expect for in-place patches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>