Files
ECC/tests/hooks
afa0b35649 fix(continuous-learning-v2): warn when the observer never survives a hook invocation (#2489) (#2606)
* fix(continuous-learning-v2): warn when the observer never survives a hook invocation (#2489)

The observer is lazy-started from a hook process that exits immediately
afterwards. start-observer.sh's liveness check runs inside that still-living
process tree, so it always sees a healthy observer and prints "Observer
started (PID: N)". On native Windows (Git Bash/MSYS2) the reap happens later,
when the hook's Job Object closes, so no self-check placed in
start-observer.sh can ever observe the failure.

The next hook invocation is the only place the death is visible, and
_CHECK_OBSERVER_RUNNING already found it there -- then discarded it, deleting
the stale PID file and restarting silently, once per tool call, forever. Users
were left with an observer-start.log full of success lines and an observer
that never completed a single analysis cycle.

Record the "well-formed PID that is no longer alive" case, count consecutive
non-survivals in ${PROJECT_DIR}/.observer-nosurvive-count, and log one
explanatory warning when the streak reaches ECC_OBSERVER_NOSURVIVE_WARN_AFTER
(default 3). Warning fires on equality so a persistent failure logs once per
streak rather than once per tool call; finding the observer alive resets the
streak. The Windows-specific explanation is gated on uname so Linux/macOS
users are pointed at observer.log instead of a wrong diagnosis.

Counting happens in the caller, not inside _CHECK_OBSERVER_RUNNING, because
that function is invoked once per PID file and again under the start lock.

The PowerShell backgrounding rewrite is deliberately not included: it cannot
be exercised on a non-Windows machine, and untested process-spawning code is
a worse outcome than an accurate diagnostic.

* docs(continuous-learning-v2): state observer platform support and the new warn threshold

The observer's Windows limitation was only discoverable by hitting it. Record
it next to observer.enabled, where it is read before the flag is set, and
document ECC_OBSERVER_NOSURVIVE_WARN_AFTER so the knob added alongside the
warning does not repeat the undocumented-env-var problem tracked in #2573.

zh-TW is intentionally left alone: translation parity is not enforced here and
the repo rejects blind translation imports without translator review.

* fix(continuous-learning-v2): serialize the non-survival streak under the lazy-start lock

observe.sh runs on every tool call, so the streak read-modify-write could race
between concurrent invocations -- losing an increment or logging the warning
twice. That is the same class of bug the signal counter hit in #2296, and this
repo's rule is to never fall back to an unlocked read-modify-write.

Rather than add a second lock, move the increment into _START_OBSERVER_LOGGED.
All three of its call sites already run inside the lazy-start lock
(flock / lockfile / mkdir), so the update is serialized with no new machinery.
Counting at the restart instead of at detection also means N racing hooks
record one death rather than N.

The reset stays in the caller: it is an idempotent unlink, not a
read-modify-write, so it needs no lock.

Adds a regression case pinning the increment inside _START_OBSERVER_LOGGED and
asserting all three call sites remain locked.

* fix(continuous-learning-v2): harden the non-survival threshold and warning output

Three review findings on the #2489 diagnostic:

- An all-zero threshold silently disabled it. `00` passes a digits-only check
  but compares as zero, and the streak only grows, so the warning could never
  fire. Normalize with base-10 arithmetic and fall back to the default for
  anything below 1. Base 10 is forced explicitly because a leading zero would
  otherwise be read as octal, and `08` is an arithmetic error that would abort
  the hook under `set -e`. The same normalization now guards the streak read.

- An unwritable log silently swallowed the diagnostic. Build the message once
  and fall back to stderr when the append fails. This cannot spam: the block
  runs once per streak, not once per tool call. The counter write keeps its
  `|| true` -- observe.sh runs on every tool call and the repo rule is that
  hooks exit 0 on non-critical errors, so a full disk must not break tool use.

- The live-PID test fixture used process.pid, which is 1 in a container and is
  deliberately rejected by _CHECK_OBSERVER_RUNNING; the reset case would then
  fail for the wrong reason. Use a spawned child and clean it up.

Adds a regression case for the all-zero threshold. Verified on bash 3.2 (the
macOS CI runner shell) as well as bash 5.

* fix(continuous-learning-v2): warn only on a persisted streak increment

If the counter write fails, the file stays below the threshold, so every later
hook invocation rereads it, re-increments in memory, hits the equality check
and warns again -- turning the once-per-streak diagnostic into once-per-tool-
call spam. That is worse in exactly the case the stderr fallback added in the
previous commit was meant to cover, since a disk that cannot take the log
usually cannot take the counter either.

Gate the warning on the write succeeding. The write stays non-fatal: it runs
as an `if` condition, so `set -e` is satisfied and an unwritable counter costs
a delayed diagnostic rather than a broken tool call.

Tests: an unwritable counter must stay silent across repeated invocations while
the hook still exits 0, and a leading-zero threshold ("08") must be read as
decimal -- "00" alone did not exercise the base-10 conversion, since it is zero
either way.

---------

Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
2026-08-12 18:36:30 -04:00
..