This lands the long-running "doctor v2" effort (tracked in
docs/planning/DOCTOR_V2_RELEASE_CHECKLIST.md). It reshapes `cass doctor`
from a flat "check + auto-fix" command into a typed surface of bounded
operations whose recovery model is **archive-first**: cass-owned evidence
(archives, raw-session mirrors, backups, receipts, source ledgers) is
preserved before any repair runs, and every mutating action goes through a
plan-fingerprint candidate flow that the operator must inspect and approve.
==== New / changed doctor surfaces (`src/doctor.rs`) ====
DoctorCommandSurface now enumerates: Diagnose, Check, Repair, Cleanup,
**ArchiveScan**, **ArchiveNormalize**, Backups, Reconstruct, Restore,
SafeAutoFix, Init.
DoctorExecutionMode adds the corresponding `*DryRun` / `*Apply` pairs for
ArchiveNormalize and tightens the existing Cleanup/Repair modes around
plan-fingerprint matching. DoctorCommandRequest grows `archive_scan` and
`archive_normalize` flags and the plumbing to keep the typed surface
exhaustive (compile-time match coverage on every variant).
Repair, restore, archive-normalize, and cleanup all emit
`operation_outcome` envelopes and machine-readable receipts; the SafeAutoFix
path is preserved as the legacy escape hatch for low-risk derived-only
repairs (Tantivy index rebuild, stale lock removal) and now fails closed if
it detects archive- or source-affecting risk.
==== Source / indexer / storage hardening (`src/sources/*`, `src/indexer/*`, `src/storage/sqlite.rs`) ====
- `src/sources/sync.rs` — `remote_spec_for_scp`, `parse_remote_home_stdout`,
and friends so the SSH/SCP source-mirror path quotes remote operands
consistently and rejects ambiguous remote-home outputs (more than one
candidate path → refuse instead of guessing). Closes the #raw-mirror
scan-root capture-before-parse hole that 5c1dba9e / 5be8a6e9 first surfaced.
- `src/sources/config.rs`, `index.rs`, `install.rs`, `probe.rs`, `setup.rs`
— typed source-provider lifecycle: discovery, probe, install, setup, sync
now share a single error vocabulary the doctor surface understands so its
candidate plans can describe both what they will do and which source the
action is rooted at.
- `src/indexer/mod.rs`, `src/indexer/refresh_ledger.rs` — small fixes to
bind refresh ledgers to source-authority decisions.
- `src/storage/sqlite.rs` — extra integrity probes called by `doctor check`
before any cleanup or archive operation runs.
==== UI / HUD / CLI surface ====
- `src/ui/app.rs` — new `DoctorHudSummary` (parsed from the unified
doctor runtime summary) drives a HUD lane that surfaces archive coverage,
source-mirror state, fallback mode, sole-copy warnings, active repair and
index-maintenance, and the recommended next action. Operators no longer
have to leave the TUI to know whether running a repair is safe.
- `src/lib.rs` — large surface change: every CLI subcommand that touched
doctor surfaces is rewired through the bounded request/response types,
the new archive-scan / archive-normalize subcommands are exposed, the
receipts and plan-fingerprint payloads are emitted on stdout (json/jsonl)
with stderr reserved for fatal envelopes, and the `--robot` mode learns
the new doctor JSON shape (see golden updates).
- `src/update_check.rs` — version-check now feeds into the doctor HUD as
one of the recommended-action sources (an outdated cass that knows about
archive issues should still nudge the operator to upgrade first).
==== Tests + golden snapshots ====
- `tests/cli_doctor.rs` (+2086 lines) — behavioral coverage for every new
subcommand, plan-fingerprint mismatch handling, dry-run/apply parity,
archive-coverage gating of mutating ops, and the legacy `cass doctor`
alias mapping into the v2 surface.
- `tests/doctor_e2e_runner.rs`, `tests/util/doctor_e2e_runner.rs`,
`tests/util/doctor_fixture.rs`, `tests/doctor_fixture_factory.rs` —
end-to-end doctor harness updates: synthesizes archive trees, raw-session
mirrors, partial backups, and corrupt SQLite headers; asserts that
archive-normalize and repair never escape into source / archive paths.
- `tests/doctor_release_checklist.rs` (new) — release-gate test that
cross-checks the runbook against the implemented surfaces (every entry in
DOCTOR_V2_RELEASE_CHECKLIST.md must map to a known DoctorCommandSurface
variant; new variants must add a checklist entry).
- `tests/cli_robot.rs`, `tests/cli_status.rs`, `tests/cli_dispatch_coverage.rs`,
`tests/e2e_health.rs` — smaller dispatch-coverage / status / robot
surface-area follow-ups required by the new doctor shape.
- `tests/golden_robot_docs.rs`, `tests/golden_robot_json.rs` — assertion
helpers for the regenerated robot JSON / docs goldens.
- `tests/golden/robot/*.json.golden` and
`tests/golden/robot_docs/*.txt.golden` — regenerated for the new shapes;
`introspect.json.golden` and `introspect_shape.json.golden` carry the
bulk of the diff because they enumerate the full doctor surface +
argument schema.
==== Docs + scripts ====
- `docs/planning/RECOVERY_RUNBOOK.md` — operator runbook (referenced from
the README) describing the archive-first model, plan-fingerprint flow,
source-pruning warnings, sole-copy prompts, and the support-bundle
handoff.
- `docs/planning/DOCTOR_V2_RELEASE_CHECKLIST.md` (new) — release gate; the
new checklist test consumes this file.
- `README.md` — replaces the old single-doctor table with the v2 surface
table and updated examples (`cass doctor check --json`,
`cass doctor repair --plan-fingerprint`, etc.).
- `CHANGELOG.md` — Unreleased entry covering the archive-first
documentation and the doctor migration guidance for existing data dirs.
- `scripts/e2e/doctor_v2.sh`, `scripts/bakeoff/cass_validation_e2e.sh` —
scripted runners for the doctor v2 surface; consumed by the e2e harness
and the cass bakeoff campaign.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls all of the long-form evidence the project has been accumulating
into a small, named set of `docs/` subtrees and tightens `.gitignore`
plus the CI hygiene gate so freshly-generated scratch artifacts cannot
quietly become tracked again. Pure repo-hygiene change: no application
code, schema, or build behavior is touched.
New canonical locations under `docs/`:
- `docs/artifacts/` is the durable home for evidence we want to keep
reviewable in-tree: migration baselines, refactor proof bundles, the
no-mock CI audit, and per-pass simplification dashboards. A new
`docs/artifacts/README.md` documents the policy: anything under here
is curated evidence, not scratch.
- `docs/artifacts/migration-baseline/` absorbs the entire former
`.beads/migration_baseline/` tree (BASELINE_SUMMARY, SUMMARY, all
`baseline_*.{log,txt,json}` outputs, the `search_quality/*.json` and
`search_queries/q*.json` fixtures, and `binary_size.txt`) plus a new
`fs_verification.log` so the migration evidence is not buried under a
beads daemon directory.
- `docs/artifacts/refactor-runs/` collects every dated
`20260424T*` … `20260427T*` and `20260428T*` codex/repeated-simplify
pass directory that previously lived under `refactor/artifacts/`.
Every per-pass report (`pass1_*.md` … `pass10_*.md`, `DASHBOARD.md`,
`architecture.md`, `baseline.md`, plus the Twelfth/Thirteenth
twelve-pass dashboards) moves verbatim — only the path changes.
- `docs/artifacts/no-mock-audit.md` replaces
`test-results/no_mock_audit.md` as the durable copy of the no-mock
policy report; the `test-results/` directory is now treated as
ephemeral.
- `docs/assets/` becomes the project's image / screenshot home, with a
new `docs/assets/README.md`. `cass_illustration.webp`,
`gh_og_share_image.jpg`, and `screenshots/screenshot_0{1,2,3}.webp`
move out of the repo root into `docs/assets/images/` and
`docs/assets/screenshots/`.
- `docs/planning/` gains the formerly root-level
`cli-robot-enhancements.md` and the dated investigation
`2026-05-05-franken-agent-detection.md` (now under
`docs/planning/sessions/`), so all planning material is in one
predictable subtree.
- `docs/reference/E2E_LOGGING_SCHEMA.md` is added as the durable
schema definition for the JSONL E2E log, replacing inline references
to `tests/e2e/exports/setup-metadata.json` paths.
- `tests/policies/` becomes the home for machine-enforced policy
inputs. `test-results/no_mock_allowlist.json` moves to
`tests/policies/no_mock_allowlist.json` so that CI-relevant policy
files survive `test-results/` being ignored.
- `tests/performance/lighthouse-budget.json` moves to
`tests/fixtures/performance/lighthouse-budget.json` to consolidate
test inputs under `tests/fixtures/`.
Tracked-artifact hygiene (`.gitignore`):
- Search-demo data: keeps the frozen v1 index and `watch_state.json`
visible (`!tests/fixtures/search_demo_data/index/`,
`!.../watch_state.json`, `!.../v1/**`) while ignoring later
scratch generations (`v*/` minus `v1/`), `index-run.lock`, the
doctor scratch directory, and the `.lexical-publish-backups/`
staging tree.
- Test outputs: `test-results/*` is now ignored wholesale (no
per-file allowlist; durable inputs live under `tests/policies/`).
`/test-results-*.txt`, `claude-upgrade-progress.json`,
`tests/test-results/`, `tests/tests/`, `tests/e2e/exports/`,
`tests/e2e/pages_preview/`, `tests/artifacts/`, `tests/e2e/.env.test`,
and root-level `/artifacts/` and `tmp/` are all explicitly ignored.
- Local SQLite sidecars: `*.db-wal`, `*.db-shm`, `*.db-journal`,
`*.sqlite-wal`, `*.sqlite-shm`, `*.sqlite-journal`, `*.sqlite3-wal`,
`*.sqlite3-shm`, `*.sqlite3-journal` are added globally so
process-local journal files cannot leak in next to a deliberately
checked-in fixture DB. Root-level `/*.db`, `/*.db-wal`,
`/*.db-shm`, `/*.sqlite`, `/*.sqlite-wal`, `/*.sqlite-shm` follow
the same rule.
- Aider state: `.aider.chat.history.md` and friends remain ignored
globally, with an explicit
`!tests/fixtures/aider/.aider.chat.history.md` exception so the
intentional fixture survives.
- Root noise: `/cass_illustration.webp`, `/gh_og_share_image.*`,
`/screenshots/`, `/UPGRADE_LOG.md`, `/refactor/`, `/perf/`,
`/*.log`, `/*.mcp.json`, `cline.mcp.json`, `opencode.json`,
`*.mcp.json.*.bak`, and `__pycache__/` plus `.ruff_cache/` are
explicitly ignored at root so future regenerations of these
artifacts do not become tracked.
- Preserved-by-name carve-out: an explicit
`!docs/planning/MODES_OF_REASONING_REPORT_AND_ANALYSIS_OF_PROJECT.md`
re-include line guards against developer-level globals that still
match the pre-reorganization scratch filename.
Ephemeral artifact removals (now ignored):
- `.ntm/logs/am-coding_agent_session_search.log`,
`.ntm/logs/bd-coding_agent_session_search.log`,
`.ntm/logs/cm-coding_agent_session_search.log`, and
`.ntm/summaries/coding_agent_session_search-20260407-115237.json`
(~230 lines, all NTM session bookkeeping).
- `tests/e2e/.env.test` (regenerated per run).
- `tests/e2e/exports/setup-metadata.json`, `test-basic.html`,
`test-encrypted.html`, `test-large.html`, `test-no-cdn.html`,
`test-tool-calls.html`, `test-unicode.html` (~41,400 lines of
generated Playwright export HTML).
- `tests/e2e/pages_preview/bundle/{private,site}/*` — the entire
generated preview bundle (HTML, JS, CSS, payload chunk, master-key
+ integrity-fingerprint scratch). All produced by the preview build
step.
- `tests/e2e/pages_preview/db/agent_search.db{,-shm,-wal}`,
`tests/e2e/pages_preview/encrypt/{config.json,payload/chunk-00000.bin}`,
`tests/e2e/pages_preview/export/export.db`,
`tests/e2e/pages_preview/preview-server.log` (binary test DBs and
encrypt/export scratch produced by the preview server).
- `tests/tests/test-results/e2e/placeholder` (zero-byte placeholder).
Total: ~52,900 deletion lines removed, all generated test artifacts
that the new `.gitignore` patterns now block from re-entering the tree.
CI gate (`.github/workflows/ci.yml` + `scripts/validate_ci.sh`):
- A new "Run repository artifact hygiene check" step runs
`./scripts/validate_ci.sh --artifact-hygiene-only` first, before the
no-mock policy check, so the build fails fast if any of the patterns
above ever become tracked again.
- The no-mock workflow step is repointed at the new policy location
(`tests/policies/no_mock_allowlist.json`); the in-workflow
remediation guidance string in `AUDIT_REPORT` is updated to match.
- `scripts/migration_e2e_validate.sh` and `scripts/lib/e2e_log.sh`
are repointed to the new schema doc and policy paths.
- `tests/e2e/reporters/jsonl-reporter.ts` and `tests/util/e2e_log.rs`
reference the moved schema doc.
Companion docs (no behavioral change):
- `README.md` updates `<img>` `src` attributes for the project
illustration and the three TUI screenshots to point at the new
`docs/assets/...` paths, and adds a Remote Archive Safety subsection
documenting that `cass doctor`/`health`/`status` JSON treats
`remote_source_*` gaps as preservation signals (no behavior change,
doc only).
- `docs/planning/TESTING.md` adds an "Artifact hygiene check" intro
paragraph and a Doctor-V2 filesystem-portability section that names
the `CASS_TEST_DOCTOR_RENAME_FAILURE=cross-device` simulation and
the `fallback_kind: cross_device_copy_replace` evidence contract.
- `docs/planning/UPGRADE_LOG.md`, `docs/ftui_visual_parity_*` are
re-pointed at the new artifact paths.
- `tests/fixtures/README.md` and `tests/fixtures/sources/probe/README.md`
pick up the new path references.
Two related advances on top of the doctor command surface landed in
15f6bba3:
1) Structured root-cause incidents
2) V14 → V15 migration safe re-run
src/lib.rs (+1736 LOC):
- New DoctorIncidentRootCauseKind enum with kebab-case Serialize:
derived-index-stale, derived-index-stale-after-db-promotion,
semantic-model-missing-lexical-fallback,
source-pruned-with-mirror-intact, mirror-missing-with-db-sole-copy,
archive-db-unreadable-with-valid-candidate, archive-db-unreadable,
active-lock-blocking-repair, interrupted-repair-state,
storage-pressure-derived-cleanup-available, backup-exclusion-risk,
backup-unverified, previous-repair-failed, unknown
- New DoctorRootCauseIncident struct surfaced through cass doctor
check --json: schema_version, incident_id, root_cause_kind,
severity, affected_asset_classes, archive_risk_level,
derived_risk_level, confidence, evidence_check_ids,
blocked_actions, safe_next_actions
- Replaces the prior "string-typed advisory" output so downstream
tooling (br/bv triage, ops dashboards) can pivot on the typed kind
rather than scraping wording out of free-form text
- Doctor check evaluator now classifies each evidence row into one
of the kinds above, attaches blocked_actions (e.g. cannot run
reconstruct while archive db unreadable) and safe_next_actions
(e.g. run cass doctor cleanup --derived first), and includes the
evidence_check_ids that produced the classification so the report
is auditable end-to-end
src/storage/sqlite.rs (+109 / -33):
- MIGRATION_V15 split: MIGRATION_V15_TAIL_STATE_TABLE keeps the
CREATE TABLE IF NOT EXISTS conversation_tail_state DDL idempotent.
The two ALTER TABLE conversations ADD COLUMN statements now run
conditionally via franken_table_column_names() so retrying the
migration on a partially-promoted database (where last_message_idx
was added but last_message_created_at was not, or both were added
but conversation_tail_state was not) does not error with
"duplicate column name"
- discover_historical_database_bundles comment block compressed:
the V13 / V14 / lazy-FTS reasoning is preserved but the redundant
"MIGRATION_FRESH_SCHEMA (V13) creates fts_messages eagerly..."
preamble is gone now that the gating logic itself is the
authoritative source of truth
src/doctor.rs (+12 LOC) routes the new incident report through the
typed surface so check / repair / cleanup variants all consume the
same structured payload.
src/indexer/mod.rs (+11) and src/search/asset_state.rs (+13) wire
the new incident types into the existing asset-state classifier so
"derived index stale" vs "derived index stale after db promotion"
flow into the right cleanup recommendation.
Test coverage:
- tests/cli_doctor.rs (+397/-): asserts the JSON shape, kebab-case
variant emission, and that blocked_actions/safe_next_actions are
populated for every kind
- tests/doctor_e2e_runner.rs (+154 LOC) and tests/util/doctor_e2e_runner.rs
(+378/-): drive the full check → repair plan → apply cycle
end-to-end with realistic incident states
- tests/util/doctor_fixture.rs (+117/-): adds fixtures for the
V14→V15 partial-promotion scenarios so the migration safe-rerun
is exercised in CI
- Goldens regenerated for tests/golden/robot/{doctor, doctor_quarantine,
doctor_shape, introspect, introspect_shape}.json.golden and
tests/golden/robot_docs/schemas.txt.golden — these are deterministic
outputs of the new incident schema
Cargo.toml / Cargo.lock / build.rs / README.md: bump franken-agent-
detection pinned rev to 029253c450702a... so the doctor evidence
collector can call into the new connector source-bundle inspection
helper that was added upstream.
scripts/e2e/doctor_v2.sh (+4/-): drive the new
--repair --plan-fingerprint --yes path so the existing E2E shell
script exercises the typed apply boundary, not the legacy --fix flag.
Beads update (.beads/issues.jsonl + .beads/last-touched): mark the
incident-classifier sub-bead in_progress to match the work landing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two coordinated additions to the doctor v2 e2e + CLI surface
that together make the doctor's failure paths first-class
testable instead of "should-never-happen" branches.
src/lib.rs (+552 net inserts)
`open_franken_cli_read_db_with_hard_timeout(path, reason,
timeout)`:
A hard-timeout wrapper around the existing
`open_franken_cli_read_db` helper. The naked open path
is dependent on frankensqlite's internal lock-contention
backoff, which can stall a CLI invocation indefinitely
if the DB file is corrupt OR if a runaway process holds
the lock with no SIGINT path. The wrapper:
- spawns a worker thread that runs the open
- sets up a `mpsc::channel`
- blocks on `rx.recv_timeout(timeout)`
- on timeout: returns a typed
`CliError { code: 9, kind: DbOpen, retryable: true }`
with a "open timed out after Ns" message — operator
can then run `cass doctor` to investigate the cause
- on Ok: returns the open conn
- on disconnect (worker thread panic): returns a typed
error rather than a panic-aborted CLI
Pulled out into
`receive_franken_cli_read_db_open_result_with_hard_timeout`
so the worker-side and recv-side can be tested
independently — the timeout / disconnect / Ok arms each
get their own unit test.
Wired into the doctor + status + history command paths
that read from the archive DB so a single corrupt-DB
scenario never wedges the entire CLI.
scripts/e2e/doctor_v2.sh (+10 lines)
`--include-failure-self-test` flag. When set, exports
`CASS_DOCTOR_E2E_INCLUDE_FAILURE_SELF_TEST=1` so the test
binary picks up the intentional-failure scenario.
Default behaviour (flag unset) is unchanged.
tests/util/doctor_e2e_runner.rs (~29 net inserts)
- DoctorE2eCliArgs gains `include_failure_self_test`
boolean.
- `selects(scenario)` widens to include scenarios labeled
`self-test` when the flag is on.
- `DoctorE2eScenarioSpec::expected_runner_status()` ->
`"pass"` for normal scenarios, `"fail"` for self-test
scenarios — pins the contract that the self-test
intentionally fails.
- New `failure_self_test_doctor_e2e_scenario()` returns a
DoctorE2eScenarioSpec named `intentional-failure-self-test`
with label `self-test` and required JSON pointers that
deliberately won't be satisfied.
tests/doctor_e2e_runner.rs (~42 net inserts)
`doctor_e2e_include_failure_self_test_selects_intentional_failure`
test: parses CLI args including the flag, builds the
scenario set with the flag-driven extension, asserts the
intentional-failure scenario is selected, and asserts
the runner records a `"fail"` status for it.
Pins the contract that the doctor v2 e2e harness can prove
its own failure-detection — running `--include-failure-self-test`
on a healthy build produces a deliberately-failing scenario
that the runner correctly reports as failed (not silently
masked).
`.beads/issues.jsonl` + `.beads/last-touched` advance one
paired touch as the metadata mirror.
Batch timeout support:
- Add --batch-timeout-seconds flag (default: 1800s / 30 minutes) that
kills a cass subprocess if it runs longer than the specified duration.
The driver first sends SIGTERM with a 5-second grace period, then
escalates to SIGKILL if the process does not exit. Timed-out batches
exit with code 124 (matching the GNU timeout convention) and are
logged with a "subprocess_timeout" failure reason. This prevents
stuck batches from blocking the entire recovery pipeline indefinitely.
OOM detection:
- New is_out_of_memory_failure() function consolidates OOM heuristics:
it checks for "out of memory" in stderr, exit codes -9/137 (SIGKILL)
when peak memory reached 90% of the hard budget, and "killed" in
output with similar memory pressure. The batch-size halving logic
now uses this function instead of a simple string match, making it
more reliable at detecting kernel OOM kills that don't include the
"out of memory" string.
- The OOM check is skipped for timed-out batches to avoid conflating
timeout kills with memory kills.
State file error handling:
- load_state() now uses explicit file handle management with encoding
parameter and raises SystemExit with a diagnostic message on OSError
or JSONDecodeError, instead of crashing with an opaque traceback.
- save_state() uses a context manager for atomic writes.
Logging improvements:
- Batch log entries now include timed_out (bool), batch_timeout_seconds,
and out_of_memory_failure fields for post-hoc analysis.
Co-Authored-By: Claude <noreply@anthropic.com>
indexer/mod.rs:
- Collapse nested if/if-let in watch_sources Full rebuild handler
into a single let-chain: if !pending.is_empty() && let Err(error).
Cleaner control flow, no behavior change.
search/query.rs:
- deduplicate_hits() now checks hit.snippet when hit.content is empty
before calling is_tool_invocation_noise(). Previously, empty content
caused tool-invocation messages to slip through deduplication when
the noise text was only in the snippet field.
scripts/historical_recovery:
- README.md: add note about --paths-file accepting pre-generated
path lists alongside glob-based discovery
- run_watch_once_batches.py: improve batch completion log message
with elapsed time formatting
Co-Authored-By: Claude <noreply@anthropic.com>
Byte-budget batch caps:
- New --max-batch-bytes-mib CLI flag that limits the estimated raw input
bytes per batch (measured via os.stat() before handoff to cass). When
combined with --batch-size, the more restrictive limit wins. A single
oversized file always passes through even if it exceeds the cap (at
least 1 file per batch guaranteed).
- New select_batch_paths() replaces simple list slicing: iterates from
start_index up to target_batch_size, accumulating estimated_file_size_bytes()
until either the count or byte budget is exhausted, returning
(batch_paths, batch_bytes)
- max_batch_bytes persisted in state and normalized across resume
Replace sqlite3 with cass stats --json:
- db_counts() no longer opens agent_search.db directly via the sqlite3
module. Instead it shells out to `cass --color=never stats --json
--data-dir <path>` and parses the JSON output. This eliminates the
hard coupling to the internal DB schema and avoids WAL locking
conflicts with a running cass index process.
- Graceful error handling: timeout (30s), non-zero exit, empty stdout,
and invalid JSON all return {conversations: None, messages: None,
error: "reason"} instead of crashing
build_state_snapshot() helper:
- Extracts the ~25-field state dict construction into a single function
with named kwargs, reducing duplication across the 4 call sites that
previously assembled the dict inline (initial save, batch success,
batch failure, run completion)
- All fields are typed in the signature for self-documentation
Additional improvements:
- read_explicit_paths() returns (paths, path_stats) tuple with
listed_explicit_paths and missing_explicit_paths counts for diagnostics
- collect_paths() returns (paths, path_stats) with collected_paths count
- paths_files included in legacy state migration equality check
- normalize_state_metadata() accepts fallback_max_batch_size and
fallback_max_batch_bytes for consistent defaults on resume
- State gains successful_batches_this_run counter
- import sqlite3 removed, import math added
Co-Authored-By: Claude <noreply@anthropic.com>
--paths-file support:
- New CLI flag (repeatable) accepts newline-delimited files containing
explicit paths to process, bypassing glob-based discovery entirely.
Lines starting with # and blank lines are skipped. Relative paths are
resolved against the paths-file's parent directory.
- Validation relaxed: either --paths-file OR both --root and --pattern
must be provided (previously --root and --pattern were required)
- config_signature() now includes paths_files in the SHA-256 hash, so
explicit-path runs get their own isolated resume state
- collect_paths() merges explicit paths with glob-discovered paths via
the existing seen-set deduplication
TailBuffer for streaming subprocess output:
- Replace proc.communicate() (which buffers entire stdout/stderr in
memory) with a TailBuffer that retains only the last 20KB of each
stream. This prevents OOM when cass produces large debug output on
oversized session files.
- Two daemon threads drain stdout and stderr concurrently using 4KB
reads, feeding into independent TailBuffer instances
- The CompletedProcess returned by run_batch() now contains the tail
text (str) instead of the full bytes, which is what all callers
already expected (the truncated tail is sufficient for error
diagnosis and logging)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add dedicated watch-once reindex path that calls optimize_if_idle()
after each incremental reindex for better segment hygiene
- Make watch callback return Result to propagate errors properly
- Update historical recovery scripts with improved batch processing
Co-Authored-By: Claude <noreply@anthropic.com>
Major upgrade to run_watch_once_batches.py for production-grade
historical recovery across heterogeneous session trees.
Per-root/per-pattern state isolation:
- State files are now keyed by a SHA-256 signature of (sorted roots,
sorted patterns), so each unique combination (Claude sessions, Gemini
sessions, backup mirrors, etc.) resumes independently without
overwriting each other's progress
- Legacy state migration: if the new signature-keyed file doesn't exist
but the legacy watch_once_batches_state.json does AND its roots/patterns
match the current invocation, the state is transparently migrated
Memory-aware autotuning:
- /proc/meminfo sampling at batch start for MemTotal and MemAvailable
- /proc/$pid/status polling during batch execution (configurable
--sample-interval-ms, default 100ms) for VmRSS and VmHWM
- Dual budget computation: soft budget = min(MemAvailable * soft_fraction,
MemTotal * 10%, soft_cap_gb) and hard budget = min(MemAvailable *
hard_fraction, MemTotal * 18%, hard_cap_gb), both floored at 256MB
- Autotuner decisions after each successful batch:
- decrease_high_rss: halve batch size when peak RSS exceeds hard budget
- return_to_best_throughput: revert to best-known batch size when
throughput regresses >15% at a larger size
- increase_safe_headroom: grow by max(1, batch_size/4) when RSS stays
below 60% of soft budget
- hold_steady: maintain current size in the neutral zone
- OOM shrink path also records memory telemetry in tuning state
New CLI arguments:
- --max-batch-size (default 256): upper bound for autotuner growth
- --sample-interval-ms (default 100): /proc polling frequency
- --memory-soft-fraction (0.20), --memory-hard-fraction (0.35)
- --memory-soft-cap-gb (8.0), --memory-hard-cap-gb (12.0)
- --growth-factor (1.5, reserved for future multiplicative mode)
Telemetry in state + log:
- Each batch log entry now includes peak_rss_kb, peak_hwm_kb,
mem_total_kb, mem_available_start_kb, mem_available_end_kb,
memory_soft_budget_kb, memory_hard_budget_kb, sample_count
- State tracks tuning dict with best_batch_size,
best_throughput_paths_per_sec, and last autotune reason
README.md updated with per-root learning and multi-connector resume docs.
Co-Authored-By: Claude <noreply@anthropic.com>
New script: scripts/historical_recovery/run_watch_once_batches.py
- Drives native cass index --watch-once over large raw session trees
(e.g., Gemini ~/.gemini/tmp with thousands of session-*.json files)
in resumable batches to avoid whole-root OOM failures
- Discovers files via glob patterns, chunks them into configurable
batch sizes (default 32), and invokes cass with --watch-once for
each batch
- Tracks progress in a SQLite checkpoint database so interrupted runs
resume from where they left off
- Automatically shrinks batch size on OOM-like failures and retries
- All parsing/insertion logic stays inside cass; the Python only
handles path discovery, chunking, and progress bookkeeping
Updated: scripts/historical_recovery/README.md
- Document the new script with usage example for Gemini session tree
reconciliation
Co-Authored-By: Claude <noreply@anthropic.com>
Add a suite of scripts for discovering, recovering, and reconciling
historical CASS session databases from scattered SQLite bundles across
the filesystem. This enables salvaging sessions from damaged or
orphaned database files that were created by various coding agents
(Claude Code, Codex, Gemini) over time.
Scripts included:
- inventory_sqlite_sources.py: Walk filesystem roots and classify
SQLite bundles by schema version, row counts, and damage level
- recover_historical_bundle.py: Run SQLite .recover on damaged
bundles to extract salvageable data
- merge_historical_bundle.py: Reconcile a recovered historical
bundle into the canonical CASS database with dedup and
conflict resolution
- import_codex_rollouts.py: Scan and reconcile Codex rollout
sessions that use a different storage format
- screen_bundle_delta.py: Screen bundle deltas to preview what
a merge would add before committing
- clone_core_tables_via_dump.sh: Shell helper for cloning core
tables via SQLite .dump for cross-database migration
- README.md: Usage documentation for the recovery workflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New script scripts/migration_e2e_validate.sh validates the assembled
three-repo ecosystem (cass + frankensearch + franken_agent_detection)
after both the frankensearch and FAD migrations.
Features:
- Verifies repo existence and build health across all three repos
- Supports --local (no rch) and --quick (skip benchmarks) modes
- Colored terminal output with structured pass/fail/warn tracking
- Logs results to test-logs/ for CI artifact collection
This is the final piece needed to gate the migration on automated
validation rather than manual spot-checks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary:
- Migrates cass semantic indexing/search internals from legacy CVVI/HNSW wrappers to frankensearch-owned FSVI and ANN primitives.
- Replaces remaining in-repo lexical helper implementations with frankensearch lexical APIs where applicable.
- Aligns daemon/indexer/perf suites with on-disk FSVI behavior, normalized vectors, and doc-id metadata parsing.
Details:
- search/query integration:
- switches query sanitization/boolean parsing/wildcard handling and tantivy query construction to cass_* helpers from frankensearch::lexical.
- removes legacy local bridge/index conversion paths and local parser helpers superseded by frankensearch.
- keeps cass-specific orchestration and filter mapping while delegating lexical mechanics.
- vector index facade:
- retires CVVI-specific structures in favor of frankensearch::index VectorIndex/Writer/SearchParams exports.
- standardizes index filenames to index-<embedder>.fsvi.
- preserves cass role-code/filter helper APIs around the new backend.
- ANN module:
- shrinks local ann_index module to diagnostics/stat payloads and path helpers.
- removes in-tree HNSW graph build/search logic now provided by frankensearch.
- semantic indexer/worker/model manager:
- builds FSVI directly via create_with_revision + write_record + finish.
- uses SemanticDocId serialization and VectorIndex::open for runtime loading and dedup hash recovery.
- updates rebuild checks to read embedder metadata from FSVI headers.
- indexer/connectors/source robustness:
- tantivy preflight now uses cass-compatible reader open helper.
- local root skip behavior only triggers when root is actually missing.
- franken-agent-detection integration now does targeted connector probing with UnknownConnectors fallback handling.
- OpenCode timestamp normalization now handles plausible seconds vs milliseconds safely.
- source install disk probe avoids HOME tilde expansion races under concurrent tests.
- benches/tests:
- regex cache bench now calls frankensearch lexical cache functions directly.
- search/memory/perf benches and property tests now generate on-disk FSVI fixtures with normalized vectors.
- semantic e2e checks updated from .cvvi expectations to .fsvi.
- semantic e2e script now verifies .fsvi artifacts.
Why:
- Consolidates core search behavior in frankensearch to reduce duplicated implementations and divergence risk.
- Moves cass onto a single canonical vector artifact format and runtime path.
- Keeps benchmark/test coverage aligned with production code paths to catch regressions in the new backend.
Shell script that runs all TUI test categories (stress, e2e_scenario,
cross_theme_degradation, density, rendering_invariants + P1 categories),
produces evidence-bundle.json manifest and evidence-summary.md. P0
categories gate the release — script exits non-zero if any P0 fails.
Supports --quick mode for fast local validation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
scripts/bakeoff/cass_validation_e2e.sh:
- Add EMBEDDER env var (default: fastembed, options: hash, fastembed)
- Pass --embedder flag to cass index when in semantic/hybrid mode
Hash embedder results vs lexical baseline:
- Lexical: NDCG@10 = 0.0412, p50=224ms, p95=270ms
- Semantic (hash): NDCG@10 = 0.1177, p50=255ms, p95=454ms
(2.9x quality improvement with similar latency)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
scripts/bakeoff/cass_validation_e2e.sh:
- Extract topic keyword (first word) from synthetic queries like
"metrics search query 0" → "metrics" to match corpus doc format
- Pass CASS_DATA_DIR env to subprocess.run to ensure cass finds the
correct isolated index
This fixes zero-hits issue where full query strings didn't match
the synthetic corpus text patterns.
Before: NDCG@10 = 0.0000, 0 hits across all queries
After: NDCG@10 = 0.0412, 50/50 queries with hits, p50=224ms, p95=270ms
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New fixtures and tooling:
- tests/fixtures/message_grouping/README.md: Documentation for fixtures
- tests/fixtures/message_grouping/edge_cases.jsonl: Edge case test data
- scripts/validate_fixtures.sh: Fixture validation script
Code updates:
- lib.rs, ui/tui.rs: TUI and library refinements
Co-Authored-By: Claude <noreply@anthropic.com>
- Checks all tests/e2e_*.rs files for e2e_log/PhaseTracker usage
- Checks all scripts/e2e/*.sh files source e2e_log.sh
- Reports warnings (non-blocking) for non-compliant files
- Closes coding_agent_session_search-1wnh
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Strengthen path integrity verification against Unicode-based path traversal attacks:
Unicode Attack Detection Enhancements (verify.rs):
- Add combining solidus overlays (U+0337, U+0338) that could visually disguise chars
- Block zero-width characters (U+200B, U+200C, U+200D, U+FEFF) that invisibly split tokens
- Block bidirectional override/embedding/isolate characters (U+202A-U+202E, U+2066-U+2069)
that can visually reverse or scramble path display
- Add confusable slash characters (U+2044, U+2215, U+29F8, U+1735) that look like /
- Add confusable dot characters (U+2E2E, U+0701, U+0702, U+A60E, U+10A50) that look like .
Comprehensive Test Coverage:
- Add tests for each new blocked Unicode character category
- Test combining overlay characters
- Test zero-width joiner/non-joiner/space/BOM
- Test RTL override, LTR override, RTL embedding, RTL isolate
- Test fraction slash, division slash, big solidus
- Test Vai full stop, Syriac full stops
- Add NFD/NFC normalization test with legitimate accented filenames
- Add unit tests for detect_encoded_path_violation with new attack vectors
E2E Test Infrastructure Improvements:
- Update run_all.sh to include per-test cass.log files in combined aggregation
- Exclude trace.jsonl and combined.jsonl from aggregation to avoid duplication
- Update output documentation in script headers
- Fix JSONL aggregation to recursively find test logs using find command
- Update validate-e2e-jsonl.sh default file discovery to include cass.log files
- Support recursive search in test-results/e2e for complete validation
Co-Authored-By: Claude <noreply@anthropic.com>