Skip to content

feat: preflight telemetry capture loop pieces 1–4 (#65)#101

Merged
Knapp-Kevin merged 3 commits into
BicameralAI:devfrom
Knapp-Kevin:feat/issue-65-preflight-telemetry-1-4
Apr 29, 2026
Merged

feat: preflight telemetry capture loop pieces 1–4 (#65)#101
Knapp-Kevin merged 3 commits into
BicameralAI:devfrom
Knapp-Kevin:feat/issue-65-preflight-telemetry-1-4

Conversation

@Knapp-Kevin

Copy link
Copy Markdown
Collaborator

Summary

First slice of issue #65 — local-only, default-off capture loop that records bicameral.preflight events plus downstream tool engagement, attributable per-call via a new preflight_id. Used for self-triage of false fires / silent misses; never leaves the machine and is not part of the existing PostHog relay path.

This PR covers pieces 1–4 only. Pieces 5 (SessionEnd reconciliation skill) and 6 (triage CLI) are deferred to follow-up plans #65-pt2 and #65-pt3.

Architecture

                        bicameral.preflight (handler)
                                  │
                                  │  generates preflight_id (UUIDv4)
                                  │  when BICAMERAL_PREFLIGHT_TELEMETRY=1
                                  ▼
        ┌───────────────────────────────────────────────────┐
        │  preflight_telemetry.py    (NEW, top-level)       │
        │                                                    │
        │  Piece 1 — salt + hash                             │
        │    _get_or_create_salt()  →  ~/.bicameral/salt    │
        │      O_EXCL race-safe; FileExistsError fallback    │
        │      reads winner's bytes  ◀── audit MF1 inline    │
        │    hash_topic / hash_file_paths  → 16 hex chars   │
        │    new_preflight_id()  → UUIDv4                    │
        │                                                    │
        │  Piece 3 — writers                                 │
        │    write_preflight_event  → preflight_events.jsonl │
        │    write_engagement       → engagements.jsonl     │
        │      explicit | fallback (subset-match)            │
        │                                                    │
        │  Piece 4 — retention                               │
        │    _maybe_rotate  → 50 MB / 30 days, keep last 5  │
        │      os.replace (atomic Win + POSIX)               │
        └───────────────────────────────────────────────────┘
                                  │
                                  │  Piece 2 — preflight_id plumb-through
                                  ▼
   PreflightResponse ───── preflight_id: str | None ────────┐
   LinkCommitResponse ──── preflight_id: str | None ────────│
   BindResponse ────────── preflight_id: str | None ────────│
   RatifyResponse ──────── preflight_id: str | None ────────│
   update.py dict returns  preflight_id key (11 sites) ─────┤
                                                            │
   server.py inputSchema ──── optional preflight_id ────────┘
   for: preflight, link_commit, bind, update, ratify

Privacy stance

  • Opt-in. Default OFF. Set BICAMERAL_PREFLIGHT_TELEMETRY=1 to capture; unsetting it makes every writer a no-op.
  • Hashed by default. Topic and file_paths are 16-char salted SHA-256 prefixes. Set BICAMERAL_PREFLIGHT_TELEMETRY_RAW=1 to additionally store plaintext — separate, explicit opt-in.
  • surfaced_ids are written raw. Opaque ledger decision_id strings, already non-PII; hashing them would defeat the only useful triage join. Documented as an invariant in the module docstring (audit S1).
  • Local-only. Files live under ~/.bicameral/, mode 0o600. Never leaves the machine. Separate path from the PostHog relay in telemetry.py.
  • Bounded retention. 50 MB rolling cap per file; 30-day mtime ceiling; keep last 5 rotations.

Test results

$ python -m pytest tests/test_preflight_telemetry.py tests/test_preflight_id_plumbing.py -v
================== 28 passed in 2.86s ==================

$ python -m pytest tests/test_phase2_ledger.py tests/test_phase3_integration.py -q
================== 22 passed in 18.57s ==================

Pre-existing collection failures in test_v055_region_anchored_preflight.py and test_v0412_preflight.py (refer to old preflight internals: _merge_decision_matches, _has_actionable_signal_in_search) are unchanged on this branch — they fail on origin/dev unmodified. Verified via git stash round-trip.

Audit reference

Plan: plan-preflight-telemetry-65.md in the parent worktree.

Audit verdict: PASS with MF1 applied inline.

  • MF1 (must-fix). _get_or_create_salt wraps the os.O_EXCL os.open in a try/except FileExistsError and falls back to _SALT_FILE.read_bytes() on the race-loser path. Two dedicated tests (test_salt_race_loser_reads_winner_bytes, test_salt_race_loser_handles_exclusive_failure) exercise the path including a synthetic race injection via monkeypatch.setattr(os, "open", flaky_open).
  • S1 invariant (surfaced_ids written raw) — documented in preflight_telemetry.py module docstring.
  • S3 update.py return-site count — verified by grep: 11 dict returns inside handle_update, all carry preflight_id.

Out of scope (tracked separately)

  • Piece 5 — SessionEnd reconciliation skill (#65-pt2). Reads the JSONL files, classifies entries as suspected_miss / suspected_false_fire / normal, writes failure_review.jsonl.
  • Piece 6 — Triage CLI + redaction (#65-pt3). bicameral-mcp triage CLI for labeling failure rows; promotion to tests/eval/real_dataset.jsonl requires explicit redaction.

Test plan

  • All Phase 1 unit tests pass (salt, hash helpers, env gates)
  • All Phase 2 plumb-through tests pass (preflight, link_commit, bind, ratify, update)
  • All Phase 3 writer tests pass (event, engagement, raw mode, fallback attribution)
  • All Phase 4 rotation tests pass (size, age, keep-last-N)
  • MF1 race-loser path exercised by dedicated test
  • Existing phase2 + phase3 regression suites pass
  • No new dependencies introduced

Closes

Closes #65 (pieces 1–4 only — pieces 5–6 tracked separately as #65-pt2 and #65-pt3).

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c08a0613-9715-434c-b3f1-031c6ddc610a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…AI#65)

Adds opt-in local-only preflight telemetry — captures preflight events
and downstream tool engagement for failure-mode triage. Default off;
hashed by default; raw via separate env var.

New module: preflight_telemetry.py
  - Salt at ~/.bicameral/salt (mode 0o600), per-install, race-safe init
  - hash_topic, hash_file_paths (order-independent set hash)
  - new_preflight_id (UUIDv4)
  - write_preflight_event, write_engagement (JSONL append, mode 0o600)
  - _maybe_rotate (50MB / 30 days, keeps last 5)

preflight_id plumb-through:
  - PreflightResponse, LinkCommitResponse, BindResponse, RatifyResponse
    gain optional preflight_id: str | None field
  - update.py dict returns also gain preflight_id key (11 sites)
  - server.py inputSchema for affected tools accepts optional preflight_id

Pieces 5 (SessionEnd reconciliation skill) and 6 (triage CLI) are
deferred to follow-up plans BicameralAI#65-pt2 and BicameralAI#65-pt3.

Closes BicameralAI#65 (pieces 1–4)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Tier 1 lint gate from BicameralAI#102 caught 32 stylistic findings on this
branch (22 in the new test files plus 10 in pre-existing files):
- timezone.utc → datetime.UTC alias (UP017 from PEP 695)
- import sorting (I001)
- 12 files needing ruff format

All auto-fixable. No behavior change. 28 telemetry tests still pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Knapp-Kevin Knapp-Kevin added the flow:feature Standard feature/fix PR targeting BicameralAI/dev (the default flow) label Apr 29, 2026
…cure

mypy flagged the os.PathLike return type as incompatible with the
actual BufferedWriter from os.fdopen. Use typing.IO[bytes] which is
what the with-block consumes anyway. Pure type fix; no behavior change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Knapp-Kevin Knapp-Kevin merged commit 91b1dd1 into BicameralAI:dev Apr 29, 2026
6 checks passed
Knapp-Kevin pushed a commit to Knapp-Kevin/bicameral-mcp that referenced this pull request May 21, 2026
…st (BicameralAI#192)

Single env var now owns the entire telemetry-flag namespace. Three accepted
forms: bool (`0`/`off`/`false`/`no` → all off; `1`/`on`/`true`/`yes` → relay
only), csv (`relay,preflight,raw`), and unset (default → relay only).

New `telemetry_flags.py` module owns parsing; `consent.telemetry_allowed()`
and `preflight_telemetry.{telemetry_enabled, raw_capture_enabled}` delegate
to a frozen `TelemetryFlags` cached once per process.

Backwards-compat preserved on three axes:
  1. Legacy `BICAMERAL_PREFLIGHT_TELEMETRY=1` and
     `BICAMERAL_PREFLIGHT_TELEMETRY_RAW=1` continue to work as additive
     overlays — first read of either emits a one-line stderr deprecation
     warning per process. Removed in v1.x.
  2. `BICAMERAL_TELEMETRY=1` semantics unchanged (relay only — does NOT
     auto-enable preflight).
  3. Non-canonical truthy values (`enabled`, `t`, `active`, etc. — used in
     pre-BicameralAI#192 deployments) map to relay-only with a stderr warning pointing
     at the canonical form. Caught by Codex review as a P2 finding;
     preserves the pre-BicameralAI#192 contract that any non-OFF value enabled relay.

Semantics:
- CSV form is explicit — what's listed is on, what's not is off
  (so `BICAMERAL_TELEMETRY=preflight,raw` turns OFF the default-on relay,
  documented in the setup wizard).
- `raw` always implies `preflight` (raw is a mode of preflight events;
  defensive double-check in `raw_capture_enabled()`).
- Process-cached parsing via `lru_cache`; tests use `_reset_for_tests()`
  via an autouse fixture in `tests/conftest.py` so monkeypatched env vars
  take effect cross-test.

35 fixtures in `tests/test_telemetry_flags.py` cover all forms + integration
with the existing call sites + the legacy-truthy preservation case. 87/87
green across all 7 telemetry-touching test files (including 52 regression
tests for BicameralAI#39 / BicameralAI#101 / BicameralAI#112 behaviors).

Closes BicameralAI#192. Unblocks BicameralAI#65 phase 4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flow:feature Standard feature/fix PR targeting BicameralAI/dev (the default flow)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant