5 Commits
Author SHA1 Message Date
刘冲 1e4751d3ca fix(index): charge macOS phys_footprint and pin duration across recovery
Watchdogs now sample the OS-charged footprint instead of resident size on
Darwin, and index_max_duration_seconds covers the whole request so a
crash/hang respawn cannot reset the clock.

Signed-off-by: 刘冲 <mail@liuchong.dev>
2026-10-19 17:18:01 +08:00
刘冲 a45858ffb5 fix(test): send the complete worker policy from the session-scope fixture
PR 2257 still named only the two discovery keys in the worker-scope script.
The worker now fail-closes unless every known policy key is present, so the
fixture must stand in for the supervisor with cbm_test_index_worker_policy_json.

Signed-off-by: 刘冲 <mail@liuchong.dev>
2026-10-19 17:18:00 +08:00
刘冲 de1615264a feat(index): add worker resource watchdogs
Discovery limits bound what indexing accepts, not what it then costs. A
repository well inside those bounds can still exhaust the host through parser
memory, or simply never finish, and a supervised worker that hangs leaves the
parent waiting with nothing to report.

Add index_max_rss_mb and index_max_duration_seconds, enforced by the parent
against the worker process tree rather than the worker process alone, so a
runaway child cannot hide behind a small parent. Resident memory is sampled
through the platform interface on macOS, Linux and Windows. Crossing a limit
terminates the tree and yields one trusted, structured terminal result that
attributes the failure to the resource that caused it.

Both limits default to off. A measurement that cannot be taken fails the
attempt instead of passing it: a watchdog that quietly stops watching is
worse than no watchdog at all.

The shell fixture that stands in for the supervisor names the two new keys.
The worker accepts only a policy that spells out every key it knows, which is
what keeps a stale supervisor from starting a worker it cannot bound.

Signed-off-by: 刘冲 <mail@liuchong.dev>
2026-10-19 17:18:00 +08:00
刘冲 21ae02154a feat(index): add opt-in discovery resource limits
Indexing accepts whatever a repository contains. A tree carrying a vendored
monorepo, a generated dump, or a runaway build directory is discovered in
full, and the first sign of trouble is a host under memory pressure with
nothing that attributes it to indexing.

Add two opt-in limits evaluated during discovery against accepted source
files only: index_max_files and index_max_source_mb. Both default to off, so
nothing changes until an operator sets one. Crossing a limit fails the whole
attempt with a structured resource_limit_exceeded result naming the resource,
the observed value and the limit; no partial graph is published, and an
existing serving index keeps answering.

Limits are read from the CLI-managed _config.db and are not MCP request
arguments. A supervised parent replaces any caller-supplied policy before
spawning its worker, and the worker rejects a missing or incomplete contract,
so the CLI, the daemon and the supervised worker all enforce the same
decision.

Both keys reach an operator through the existing config get/set/list/reset
with no new subcommand. `set` suppresses its own generic message for them
because the policy writer names the precise reason -- so that writer speaks
on every failure it can return, including a validated value whose write then
fails on a database that cannot be written. Exiting non-zero in silence is
not an acceptable answer from a CLI.

The two shell regressions that hand-roll the supervisor's worker argv carry
that contract as well. Without it the worker exits before either guard can
observe anything, and the guard would go quietly vacuous.

Signed-off-by: 刘冲 <mail@liuchong.dev>
2026-09-09 16:06:59 +08:00
刘冲 1a6f80c37b fix(cli): a refused UI configuration is not a failed daemon start
`daemon start` exited nonzero when the daemon came up but did not answer
the UI configuration handshake. By that point the daemon is running --
the control connection above it already satisfied the startup window --
so the command reported failure for something it had already achieved.

The two requests in that handshake are bounded at MAIN_CONNECT_TIMEOUT_MS,
one second. On a loaded machine, especially right after an abrupt
shutdown, that is thin. Windows CI hit it repeatedly: the crash-recovery
section of `tests/windows/test_daemon_stability.py` hard-kills a daemon
and immediately starts a fresh one, and the report that came back was
that no fresh daemon had started -- when one had, and was serving.

Whether the refusal is fatal now depends on what was asked for. `--port`
and `--open` make the UI the point of the command and keep the existing
nonzero exit. A bare `daemon start` asks for a daemon and got one, so it
now warns and succeeds; it also skips the UI-warming notice, which would
otherwise announce a port that nothing is serving.

A seam forces the refusal, because reproducing it for real needs a
machine loaded enough to miss a bounded handshake -- not a state a test
can ask for. The lifecycle guard covers both halves of the contract, and
with the decision reverted it reports the bare-start case as red.

Signed-off-by: 刘冲 <mail@liuchong.dev>
2026-08-22 16:20:57 +08:00