Skip to content

feat(#155): Bash matcher opt-in surface, telemetry, and doctor integration (PR 2/2) - #217

Merged
robotrocketscience merged 5 commits into
mainfrom
feat/issue-155-search-tool-bash-setup-telemetry
Apr 28, 2026
Merged

feat(#155): Bash matcher opt-in surface, telemetry, and doctor integration (PR 2/2)#217
robotrocketscience merged 5 commits into
mainfrom
feat/issue-155-search-tool-bash-setup-telemetry

Conversation

@robotrocketscience

Copy link
Copy Markdown
Owner

Summary

Completes the v1.5.0 #155 Bash extension split. PR 1 (parsers + allowlist) is at #213.

  • AC3 — Telemetry ring buffer. hook_search_tool.py appends one JSONL record per Bash-matcher fire to <git-common-dir>/aelfrice/telemetry/search_tool_hook.jsonl (schema: timestamp, session_id, command, query, latency_ms, injected_l1, injected_l0). Ring cap 1000 entries; atomic tempfile+os.replace. Fail-soft on any write error. Public read_telemetry(path) for the doctor surface.

  • AC7 — aelf setup --search-tool-bash / --no-search-tool-bash. Installs a separate PreToolUse:Bash hook entry (same aelf-search-tool-hook entry point; matcher="Bash" field distinguishes it from the v1.2.x Grep|Glob entry). Both flags are idempotent and independent of --search-tool. aelf unsetup --search-tool-bash also works.

  • AC8 — aelf doctor telemetry section. New diagnose_search_tool_telemetry() computes rolling p50/p95 latency and injection-noise rate. format_report() renders a search_tool_hook telemetry: block. Missing/empty → "no fires recorded". Corrupt file → CORRUPT sentinel.

Test plan

  • tests/test_search_tool_hook_bash_telemetry.py — 13 tests: write, accumulation, ring-cap eviction, fail-soft, missing file, corrupt JSON, blank-line skip, end-to-end via main()
  • tests/test_aelf_setup_search_tool_bash.py — 15 tests: low-level install/uninstall idempotency, coexistence with Grep|Glob, CLI --search-tool-bash / --no-search-tool-bash, aelf unsetup --search-tool-bash
  • tests/test_aelf_doctor_search_tool_telemetry.py — 16 tests: percentile computation, noise rate, diagnose() integration, format_report() rendering
  • Full suite: 1533 passed, 4 skipped

Open questions (documented, not blocking)

  • Per-turn fire cap configurability (bash_fire_cap_per_turn in .aelfrice.toml) — deferred to v1.5.x per spec.
  • Default-on flip criteria (p95 ≤ 200 ms AND noise rate ≤ 30 % over ≥ 200 fires) — not implemented; this PR only lands the surface so an operator can read the metrics and decide.

Refs #155 (PR 2/2; PR 1 = #213)

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Kulili review (Sonnet output) — clean. Green from my side.

What I checked

  • Telemetry write: atomic (tempfile + fsync + os.replace), tempfile-cleanup on inner failure, fail-soft outer try/except with stderr trace. Ring cap enforced after read-trim-write. Schema matches what the doctor surface consumes. ✓
  • read_telemetry: missing path → []; corrupt JSON → ValueError with line number; non-dict line → silent skip. Matches spec's "real corruption only raises" contract. ✓
  • Path derivation: <git-common-dir>/aelfrice/telemetry/search_tool_hook.jsonl via db_path.parent / "telemetry" / .... Correct under git's normal layout; inherits the brain-graph's gitignore boundary. ✓
  • CLI flags: mutually exclusive sibling pattern on the setup subparser (--search-tool-bash / --no-search-tool-bash). Spec deviation from "separate subcommand" is documented in the PR body and matches the existing --no-statusline precedent. Acceptable. ✓
  • Atomic commits: 5, one concept each (telemetry → telemetry tests → setup+CLI → doctor → CHANGELOG). ✓
  • No co-author tag in any commit message or in the PR body. ✓
  • CHANGELOG entry under [Unreleased]. ✓

Non-blocking observations

  • Test count delta: +44 (13 telemetry + 15 setup + 16 doctor). 1533 pass / 4 skip on the base. Solid coverage for the three ACs.
  • Default-on flip is deliberately not implemented in this PR, per spec — it's gated on operator-read of the metrics this PR exposes. Right call; the flip belongs in a v1.5.x telemetry-driven decision, not here.
  • Per-turn fire-cap configurability (bash_fire_cap_per_turn in .aelfrice.toml) is also deferred per spec. Tracked as a v1.5.x open question.

Merge order reminder

PR #217 is based on PR #213 (feat/issue-155-search-tool-bash-impl), which is itself based on PR #209 (docs/search-tool-bash-spec-v1.5). Merge order must be: #209#213#217. CI on #217 won't go green until both upstream PRs land and #217 rebases onto main.

Otherwise this is ready.

Base automatically changed from feat/issue-155-search-tool-bash-impl to main April 28, 2026 21:05
…155 AC3)

Appends one JSONL record per Bash-matcher fire to
<git-common-dir>/aelfrice/telemetry/search_tool_hook.jsonl.
Schema: timestamp, session_id, command, query, latency_ms,
injected_l1, injected_l0.  Ring cap: 1000 entries (oldest evicted,
atomic tempfile+os.replace). Write is fail-soft: any OS error prints
one stderr line and the hook continues.

Adds public read_telemetry(path) -> list[dict] helper for the doctor
surface; raises ValueError on corrupt JSON, returns [] when file is
missing. Latency clock starts at Bash-branch entry (time.perf_counter).
13 deterministic tests covering: record write, accumulation, ring-cap
eviction (1000-entry ceiling), fail-soft on unwritable path, missing
file returns [], corrupt JSON raises ValueError, blank-line skipping,
non-dict JSON skipping, timestamp presence, and an end-to-end invocation
through main() with a monkeypatched :memory: DB.
#155 AC7)

Adds install_search_tool_bash_hook / uninstall_search_tool_bash_hook to
setup.py, wired as a separate PreToolUse:Bash entry sharing the same
aelf-search-tool-hook entry point. The Bash matcher entry coexists with
the Grep|Glob entry (same command, distinct matcher field).

CLI: aelf setup --search-tool-bash installs idempotently;
--no-search-tool-bash removes it. aelf unsetup --search-tool-bash also
removes it. All paths are independent of --search-tool (v1.2.x flag).
15 deterministic tests cover install, idempotency, removal, round-trip,
coexistence with Grep|Glob, and the full CLI surface.
…AC8)

Adds diagnose_search_tool_telemetry(path) to doctor.py: reads the Bash
matcher JSONL ring buffer and computes fire_count, p50/p95 latency, and
injection-noise rate (fraction of fires with zero L0+L1 hits).

DoctorReport gains search_tool_telemetry, search_tool_telemetry_path,
and search_tool_telemetry_corrupt fields. diagnose() accepts an explicit
search_tool_telemetry_path; falls back to deriving it via git-common-dir.
format_report() renders the section after the existing hooks output;
renders even when no settings.json is found (section is independent).

Sentinel: missing/empty file → "no fires recorded". Corrupt file (bad
JSON) → CORRUPT flag in report and text. 16 deterministic tests.
@robotrocketscience
robotrocketscience force-pushed the feat/issue-155-search-tool-bash-setup-telemetry branch from 7c57e7b to b2073af Compare April 28, 2026 21:06
@robotrocketscience
robotrocketscience merged commit 3b5c239 into main Apr 28, 2026
8 checks passed
@robotrocketscience
robotrocketscience deleted the feat/issue-155-search-tool-bash-setup-telemetry branch April 28, 2026 21:07
robotrocketscience added a commit that referenced this pull request Apr 28, 2026
…) (#252)

Closes #218.

## Summary

Adds an observability surface to the `UserPromptSubmit` hook so dedup /
relevance regressions become visible without a human eyeballing context.
Mirrors the `search_tool_hook` telemetry pattern shipped in #217.

- **AC1-3, AC5 — Telemetry ring buffer.** Per-fire JSONL append at
`<git-common-dir>/aelfrice/telemetry/user_prompt_submit.jsonl`. Schema:
`{timestamp, query, n_returned, n_unique_content_hashes, n_l0, n_l1,
total_chars}`. `query` capped at 500 chars. Ring cap 1000; atomic
tempfile + `os.replace`. Fail-soft on any write error (single stderr
line, hook continues). Public `read_user_prompt_submit_telemetry(path)`
for the doctor surface.
- **AC4 — `aelf doctor` surface.** New
`diagnose_user_prompt_submit_telemetry()` reports rolling p50/p95 of
`total_chars` and median dedup-collapse-rate (`n_returned /
n_unique_content_hashes`). Missing/empty → \"no fires recorded\".
Corrupt JSONL → CORRUPT sentinel.
- **AC6 — `collapse_duplicate_hashes` flag.** New
`[user_prompt_submit_hook] collapse_duplicate_hashes` key in
`.aelfrice.toml`, default OFF. When ON, hits are deduped by content hash
(first-occurrence wins) before formatting. Telemetry records the
*pre-collapse* `n_returned` so the dedup-rate metric stays meaningful;
`total_chars` reflects what was actually injected.

Telemetry only fires when the hook produces an `<aelfrice-memory>` block
(empty hits → no record).

This is the observability + workaround track. Algorithmic dedup remains
#197; topic-relevance work remains #154.

## Test plan

- [x] `tests/test_user_prompt_submit_telemetry.py` — 14 tests: write,
accumulation, ring-cap eviction, fail-soft on unwritable parent, missing
file, corrupt JSON, blank-line skip, end-to-end via
`user_prompt_submit()`
- [x] `tests/test_user_prompt_submit_collapse.py` — 16 tests: config
load (default-off, on, malformed TOML, wrong type, missing file, missing
section), dedup ordering, telemetry pre/post-collapse semantics
- [x] `tests/test_aelf_doctor_user_prompt_submit_telemetry.py` — 17
tests: missing → sentinel, populated → p50/p95 + collapse-rate, corrupt
→ CORRUPT, formatter rendering
- [x] Full suite: 1692 passed, 8 skipped (pre-existing platform skips)

---------

Co-authored-by: rrs <jonsobol@gmail.com>
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