mirror of
https://github.com/cocoindex-io/cocoindex-code.git
synced 2026-09-14 16:39:38 +08:00
df021fbb68
* feat(daemon): idle timeout — exit after inactivity, configurable via settings The daemon now tracks client activity (every accepted connection, plus each handler task's completion so long streaming index runs count to their end) and exits after daemon.idle_timeout_minutes (default 180, 0 = never) without it, instead of holding the embedding model in RAM forever. The idle exit goes through the existing graceful shutdown path, so it leaves a last_exit marker (reason "idle_timeout") and clients transparently and silently restart the daemon on the next request. - settings: new DaemonSettings dataclass with idle_timeout_minutes, parsed from an optional daemon: section in global_settings.yml (absent section and missing file both fall back to the default) - protocol: HeartbeatRequest/HeartbeatResponse (daemon-side dispatch only; the MCP client loop comes separately) and idle observability fields on DaemonStatusResponse (idle_seconds, idle_timeout_minutes, last_heartbeat_seconds) - daemon: IdleReaper with a pure should_exit predicate — exit requires a positive timeout, no external supervisor (COCOINDEX_CODE_DAEMON_SUPERVISED), no live handler task, no project indexing (new ProjectRegistry.any_indexing), and idle time past the timeout. A periodic asyncio task (60 s interval, injectable for tests along with a seconds-scale timeout on run_daemon) triggers the shutdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JaFKwEvJXo7WQhZvrW5Qoe * feat(mcp): heartbeat loop keeps the daemon warm under a live MCP session Both MCP entry points (ccc mcp and the legacy cocoindex-code serve) now run a background heartbeat loop alongside the initial background index. Each heartbeat is one short-lived daemon connection that counts as activity for the idle reaper, so the daemon never idle-exits while an MCP session is live; when the MCP process dies (even SIGKILL) the heartbeats stop and the daemon exits on the normal timeout. - client.send_heartbeat(): uses the raw non-auto-starting connect path and returns False when no compatible daemon answers — a heartbeat must never start or restart a daemon, or `ccc daemon stop` mid-session would fight the loop - server.run_heartbeat_loop(): interval = timeout/3 clamped to [30 s, 300 s], reading the timeout from global_settings.yml (default when missing); a 0 timeout disables the loop entirely Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JaFKwEvJXo7WQhZvrW5Qoe * feat(cli): show idle timeout and heartbeat age in daemon status; document idle-exit - ccc daemon status now prints idle duration with the configured timeout (or "disabled" when 0) and the age of the last MCP heartbeat (or "never") - README: document daemon.idle_timeout_minutes in the global settings reference - CLAUDE.md: note the idle-exit + heartbeat + crash-marker behavior in the process model section Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JaFKwEvJXo7WQhZvrW5Qoe * fix(daemon): release Windows named pipe on shutdown by unblocking the accept thread PipeListener.close() only closes the queued next-instance handle; it cannot cancel an in-flight ConnectNamedPipe wait, so a blocked accept() kept a pipe instance open after the daemon exited. The named pipe therefore still existed when the in-process idle tests asserted cleanup, and the test's own os.path.exists() probe completed the pending connection, making the accept thread call call_soon_threadsafe on a closed loop (the PytestUnhandledThreadExceptionWarning noise in CI). Shutdown now sets a shutting_down flag, wakes a blocked accept() on Windows with a dummy client connection, and joins the accept thread before returning, so the last pipe-instance handle is closed before run_daemon exits. POSIX behavior is unchanged (listener.close() already unblocks accept() there). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(daemon): type the idle timeout as timedelta Review feedback on #215: a bare float-with-suffix-naming carries the unit only by convention. IdleReaper and run_daemon now take timedelta, converting to seconds only at the comparison against monotonic timestamps and on the wire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EQpbhCAsYRJuwL7X83fzDu * refactor(daemon): drop last_heartbeat tracking — heartbeats are just activity Review feedback on #215: last_heartbeat is MCP-specific state that the daemon has no use for — it played no part in idle reaping and existed only so 'ccc daemon status' could echo it back. A heartbeat's real effect is the connection itself, which the accept path already records as activity. Remove the field from IdleReaper, the wire protocol, and the status output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EQpbhCAsYRJuwL7X83fzDu * refactor(daemon): send the shutdown wake-up connection unconditionally POSIX doesn't need it (listener.close() makes accept() raise), but it's harmless there and keeps the path exercised on every platform instead of only on Windows CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vgj6XtKW6dHST75F5VtavC --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>