Influencer re-study 2026-07-09: advisor ingest fix, hook-output validity, truncation + error-handling gates (#92–#95)#8
Conversation
Re-investigated each cloneable influencer repo against its 2026-03-30 baseline and appended a dated re-study note to its influence doc. Motion since baseline: - claude-mem v10.6.3 -> v13.9.1 (3 majors; mostly infra we reject) - claude-supermemory v2.0.1 -> v0.0.9 (renamed; per-turn recall) - cq 2026-04-28 -> v0.2.0 (subagent transcript indexing) - episodic-memory v1.0.15 -> v1.4.2 (active again, 8 releases) - lossless-claw v0.5.2 -> v0.13.1 (untrusted-data hardening) - qmd v2.0.1 -> v2.6.3 (busy_timeout, RRF weights) - grepai v0.35.0 (no new release; 1 stray commit) - kbs v0.2.1 (no change) Headline: claude-mem and claude-supermemory independently moved recall to a per-turn UserPromptSubmit decision, targeting the headless retrieval gap.
Add a "## Influencer Re-Study (2026-06-30)" section with 16 new items extracted from the 8 influence-doc re-studies, plus a Validations spot-check list and new Features-to-Avoid. Refresh the header timestamp, Sources list (add cq), and References/influence-doc footer. High priority: - #76 Per-turn recall injection via UserPromptSubmit (claude-mem + claude-supermemory converge; targets the headless retrieval gap) - #77 Per-connection PRAGMA busy_timeout (qmd; fixes the hook- contention reject-no-op gotcha) - #78 Version-drift CI spec (episodic-memory) - #79 Untrusted-data framing in distillation (lossless-claw) - #80 Position-aware RRF weights, 2x original-query (qmd) - #81 Observer output-fidelity classifier (claude-mem) - #82 Ingest subagent transcripts (cq) Medium: #83-#89. Low: #90-#91.
Five-lens review (Metz/Evans/Beck/Grimm/Bernhardt). Headline: +30% growth with prior concerns resolved (dashboard decomposed, bare rescues 19->6, observational layer test-first). One Critical regression (SQLiteStore 584->901 god object), two High (Moments N+1, Reflector boundary), plus a duplication cluster.
- Replace byte-identical `percentile` in Dashboard::Trust and StatsCommand with a single Core::Percentile.of value function - Add unit spec for the extracted module Sandi Metz: DRY / single source of truth. Addresses: docs/quality_review.md 2026-07-01 (Q3)
- Add `clock: Time` to Reflector#initialize; use @clock.now for the TTL cutoff in expire_stale_info instead of reading Time.now directly - Mirrors the clock-injection pattern in OTel::OtlpJsonEnvelope - Add a spec pinning the TTL cutoff to an injected future clock Gary Bernhardt: keep the clock at the boundary, not in logic. Addresses: docs/quality_review.md 2026-07-01 (Q10)
- Return `updated > 0` like its sibling observation mutators (tombstone_observation/expire_observation/mark_observation_promoted) instead of the raw dataset update result Kent Beck: consistent command return / revealing intent. Addresses: docs/quality_review.md 2026-07-01 (CQS asymmetry)
- The filter is `priority <= value` and priority is inverted (1=🔴 important … 3=🟢 info), so the old "min_priority" name read backwards (a higher "minimum" returned more rows). Rename to max_priority and document the inversion; update the single MCP caller + spec. Kent Beck: reveal intent through naming. Addresses: docs/quality_review.md 2026-07-01 (recent_observations name)
- Add MAX_USED_FACT_EVENTS (10k) limit + newest-first order to the recall/hook_context scan so an unbounded 30-day event volume can't load and JSON-parse an arbitrarily large result set into memory Jeremy Evans: bound dataset loads. Addresses: docs/quality_review.md 2026-07-01 (used_fact_pairs unbounded)
- Move the OTel metric/event/trace writers (+ private row-builders) into Store::OtelWrites and the episodic-observation CRUD into Store::ObservationWrites, included into SQLiteStore - Public API unchanged (module inclusion) — zero test churn; full suite green (2350 examples) - SQLiteStore drops 901 -> 600 LOC, back to core fact/entity/provenance storage; reverses the god-object regression flagged in the review Sandi Metz: Single Responsibility. Uses the same module-inclusion pattern as LLMCache/MetricsAggregator per the project's refactoring convention. Addresses: docs/quality_review.md 2026-07-01 (Q1, Critical)
- Preload content previews (one content_items query) and extracted facts (one facts⋈provenance join grouped by content_item_id) for all extraction/ingest events on a page, instead of ~2 queries per moment (~100 queries for a 50-row feed page -> 3) - Extract pure content_preview builder; thread preloaded maps through build_moment/enrich Jeremy Evans: eager-load / eliminate N+1. Mirrors the already-batched attach_feedback. Addresses: docs/quality_review.md 2026-07-01 (Q7, High)
- Move the greedy near-duplicate clustering out of Reflector#dedupe_scope into a pure Observe::DedupPlanner (matcher in, fold plan out, zero I/O) - Reflector#dedupe now plans per scope, then applies folds (increment_corroboration + tombstone) — imperative shell over a functional core - Add a DB-free unit spec for the planner (stub matcher, no store) Gary Bernhardt: functional core / imperative shell — clustering is now testable without a database. Addresses: docs/quality_review.md 2026-07-01 (Q8, High)
All 5 Quick Wins + 3 High Priority items (Q1/Q7/Q8) landed; check them off with commit refs. Full suite green (2355 examples).
- Run drop + recreate + per-row reinsert in one @db.transaction so the rebuild is atomic (a mid-rebuild failure rolls back and keeps the old index instead of leaving recall on a dropped/partial table) and the N per-row commits collapse into one Jeremy Evans: transaction safety + fewer commits. Addresses: docs/quality_review.md 2026-07-01 (LexicalFTS#rebuild!)
- Replace the three copies of the |A∩B|/|A∪B| computation (TokenOverlapMatcher, Sweep::Maintenance#restore_jaccard, CensusCommand#jaccard) with one pure Core::Jaccard.score - Each caller keeps its own domain-specific tokenization/stopwords (observation prose vs predicate names) — only the score math is unified, so behavior is preserved. Add a unit spec. Sandi Metz: DRY the algorithm, not the domain-specific tokenizers. Addresses: docs/quality_review.md 2026-07-01 (Q2)
- Fold the duplicated context_tokens parse + p50/p95/avg math from Dashboard::Trust#token_budget and StatsCommand#print_token_budget_stats into one pure Core::TokenBudget (built via Core::Percentile) - Each caller keeps its own event query + error handling; behavior preserved. Add a unit spec. Sandi Metz / Gary Bernhardt: DRY into an immutable value object. Addresses: docs/quality_review.md 2026-07-01 (Q5)
…regation) - Move the identical counts-by-status/kind/priority + corroboration + compression-ratio block out of Dashboard::Observations, ObservationsCommand, and StatsCommand into one Observe::ObservationStats - Each surface keeps its own scope selection + recent-timeline rendering (those legitimately differ); all three now render from the shared aggregate. Add a unit spec. Sandi Metz: DRY / single source of truth — same pattern as the earlier fact-serializer consolidation. Addresses: docs/quality_review.md 2026-07-01 (Q4)
- Move the markdown section builders (section, fact_line, observation_reflection, distillation_prompt, observation_capture_prompt, auto_memory_mirror) into a pure Hook::ContextPresenter module — data in, strings out, no StoreManager/Configuration/DB - ContextInjector stays the imperative shell that fetches rows and delegates formatting; the injected text is now unit-testable directly - Add a DB-free presenter spec; update the one spec that reached the moved method via send Gary Bernhardt: functional core / imperative shell — separate I/O fetching from presentation. Addresses: docs/quality_review.md 2026-07-01 (Q9)
- Extract resolve_conflict_group (per-group keep-first/dedupe-rest) and resolve_duplicate_conflict (reject disputed fact + shift provenance + mark resolved) from the 58-line method; the top-level method is now a query + transaction + delegate - Behavior identical (sweep specs green) Sandi Metz: single level of abstraction / small methods. Partial Q6 — the full Sweep::HistoricalCleanup class split remains open in the review. Addresses: docs/quality_review.md 2026-07-01 (Q6, N6)
Q2/Q4/Q5/Q9 + LexicalFTS txn wrap complete; Q6 partial (long-method decomposition). Full suite green (2380 examples, 0 failures; pending are env-conditional fastembed/git-lfs/Ruby-4 fork skips).
- Move the three on-demand, one-shot data fixes — dedupe_open_conflicts, reclassify_references, restore_multi_value_supersessions (+ their exclusive helpers pair_key/resolve_*/restore_*/find_overlapping_siblings and the RESTORE_* constants) — into a new Sweep::HistoricalCleanup class - These run only from their dedicated CLI commands, never on the recurring Sweeper.run! path; splitting them out leaves Maintenance focused on steady-state budgeted pruning (519 -> 296 LOC) - Rewire dedupe-conflicts/reclassify-references/restore commands; move the three spec blocks into historical_cleanup_spec.rb. Full suite green. Sandi Metz: Single Responsibility — separate one-shot repair from recurring maintenance. Addresses: docs/quality_review.md 2026-07-01 (Q6, complete)
All High + Medium review items now done; only the Low tier remains.
- The vec0 DELETE+INSERT and the facts.vec_indexed_at UPDATE now commit atomically, so an interruption can't leave the embedding row and the indexed-at flag out of sync Jeremy Evans: transaction safety. Addresses: docs/quality_review.md 2026-07-01 (Low — VectorIndex txn)
- Add Core::TokenEstimator.from_chars (raw char count → tokens, ceil) and use it for the two observation token_count sites; use the existing CHARS_PER_TOKEN constant in ObservationStats' byte→token estimate - Removes the scattered magic `4.0` divisor so the chars/token ratio lives in one place. Behavior identical (ceil-sites unchanged, round-site now references the constant). Sandi Metz: DRY the constant / single source of truth. Addresses: docs/quality_review.md 2026-07-01 (Low — token /4.0 divisor)
- Cover the previously untested behavior: zero-state counts/timestamps,
row counting + latest timestamp, the missing-table guards
(count_safely/last_timestamp → 0/nil), configured_env via an injected
settings_writer, and the Errno::ENOENT/JSON::ParserError → {} fallback
Kent Beck: adequate coverage of a branching public class.
Addresses: docs/quality_review.md 2026-07-01 (Low — OTel::Status spec)
- Fold the duplicated corroboration-gate + Resolver promote path out of ObservationsCommand#promote and the memory.promote_observation MCP handler into one Observe::Promotion service, so the anti-hallucination threshold and fact-creation are enforced identically across CLI + MCP - Each surface keeps its own arg parsing + response shaping; error strings unified (specs assert substrings). Add a service spec. Sandi Metz: DRY duplicated intent across surfaces. Addresses: docs/quality_review.md 2026-07-01 (Low — Observe::Promotion)
Done: OTel::Status spec, Core::TokenEstimator reuse, VectorIndex txn, used_fact_pairs limit, Observe::Promotion. Deferred with rationale: with_readonly_db, Sweeper mutable state, ingester sleep, handler payload symmetry. Full suite green (2390 examples, 0 failures).
Re-checked all 8 cloneable influencer repos 9 days after the 2026-06-30
baseline. Only claude-mem (v13.10.2), lossless-claw (v0.13.2), and cq
(v0.2.1) moved; all patch/minor, motion dominated by robustness/idempotency
hardening rather than new features. Five repos had no motion.
Added 4 correctness/robustness items:
- #94 (cq): recognize server_tool_use/advisor blocks in ToolExtractor -
a confirmed silent ingest gap verified against tool_extractor.rb:51
- #92 (claude-mem): valid SessionStart hookSpecificOutput on no-op/error
- #95 (lossless-claw): truncation-aware ingest gate
- #93 (claude-mem): pure-static error-handling anti-pattern scanner
Updated docs/influence/{claude-mem,cq,lossless-claw}.md with dated notes.
Claude Code emits advisor() calls as server_tool_use content blocks with the same name/input shape as tool_use. ToolExtractor only matched "tool_use", so advisor usage never reached the tool_calls table and was invisible to memory.facts_by_tool attribution — a silent ingest gap. Add TOOL_USE_BLOCK_TYPES = %w[tool_use server_tool_use] and match against it. Additive, no schema change. Surfaced by the cq v0.2.1 re-study (their views.rs extended the same filter). Implements: docs/improvements.md #94
…aths hook_context previously emitted nothing to stdout when there was no context to inject (nil context) and nothing on the graceful-degradation error path. A strict SessionStart validator (Codex-style) expects well-formed hookSpecificOutput. Extract emit_context_response helper: with context, wrap in <claude-memory-context> (unchanged); with nil, emit an empty additionalContext so the response stays valid. Emit the no-op form on graceful degrade (transport error -> SUCCESS), guarded so we never double-emit after a successful injection. Surfaced by the claude-mem v13.10.1 re-study (their buildNoOpResult fix). Implements: docs/improvements.md #92
… audit C015) Claude Code caps large tool output (notably Read) and leaves a marker like '[Read output capped at 500 lines]' in the transcript. Facts distilled from such a fragment come from incomplete content, not complete ground truth — the same false-positive class as the documented distiller-hallucination-from-doc- text gotcha. Add Distill::TruncationDetector, a pure regex-only detector (no LLM, no disk read — disk recovery at ingest is temporally unsound since the file may have changed since the transcript was recorded). Surface it read-only via a new audit check C015 that flags content_items whose raw_text carries a truncation marker, mirroring how BareConclusionDetector feeds the quality signal. Marker strings are centralized and covered by a regression spec so a Claude-Code-version drift is a one-line, test-caught change. Surfaced by the lossless-claw v0.13.2 re-study (their read-tool-recovery.ts). Implements: docs/improvements.md #95
Same-session /improve shipped the three high-priority items from the 2026-07-09 influencer re-study. #93 (error-handling anti-pattern scanner) kept open as a Medium — needs its own AST-scanner + rake-task build.
- Audit::ErrorHandlingScanner: Prism AST walker (not regex) codifying the "swallowed errors stay visible" convention as a deterministic, non-LLM gate. - Flags broad swallows only — EMPTY_RESCUE / RESCUE_NIL on bare, StandardError, or Exception rescues; RESCUE_EXCEPTION (warn); ERROR_STRING_MATCH (info). Narrow typed rescues (rescue JSON::ParserError; nil) are a documented contract, not a smell, and stay clean. - Functional core (#scan(source, path:) -> Array<Offense>, no I/O); the rake audit:error_handling task is the imperative shell. Wired into rake default so CI enforces it. - Escape hatch: # [ANTI-PATTERN IGNORED]: <reason> marks a deliberate swallow — reported but non-blocking. Annotated the three existing broad best-effort swallows (hook background close, stats FTS read, sweep FTS GC) so the baseline ships green. - Documented in docs/audit_runbook.md (source-level gate, distinct from the DB-state C### checks). Implements: docs/improvements.md #93 (claude-mem study)
…ardError Quality-review findings on the error-handling scanner: - BLOCK: an empty rescue borrowed the enclosing terminator for its override span, so a # [ANTI-PATTERN IGNORED] comment on a *following* rescue/else/ ensure clause leaked back and silently exempted a real EMPTY_RESCUE — the worst failure mode for the gate. Now bound the empty-rescue span by the next clause boundary (subsequent chained rescue, else, or ensure) instead. Walk carries the enclosing node (not just its end line) to find those boundaries. - WARNING: top-level-qualified ::StandardError / ::Exception (ConstantPathNode) slipped through broad?/rescues_exception?, missing genuine broad swallows. constant_named? now also matches a parent-less ConstantPathNode; a namespaced Foo::StandardError stays unflagged. Regression specs added for both. Gate stays green against lib/.
Quality review (expert panel + AST-correctness pass)Ran a review of the diff ( 🔴 BLOCK (fixed) — override span leaked into sibling clauses. An empty rescue borrowed the enclosing terminator for its override span, so a 🟡 WARNING (fixed) — 🟢 NITs (deferred, non-blocking): C015 materializes every Cleared, no defect: narrow typed rescues correctly not flagged; Full suite green (2427 examples, 0 failures); |
Implements the four adoptable items from the 2026-07-09 influencer re-study (all 8 cloneable repos re-checked against their 2026-06-30 baselines; only claude-mem, lossless-claw, and cq moved — all patch/minor, all robustness/correctness rather than new capability). See
docs/improvements.md→ "Influencer Re-Study (2026-07-09)".Items
server_tool_use(advisor) blocks inToolExtractor(from cq). Studying cq'sserver_tool_useview fix surfaced a silent gap in our own ingest:advisor()tool calls use aserver_tool_usecontent block thatToolExtractordropped (matched only"tool_use"), so advisor usage never reached thetool_callstable and was invisible tomemory.facts_by_tool. One-line guard fix viaTOOL_USE_BLOCK_TYPES. Additive, no schema change.hookSpecificOutputon no-op / error paths (from claude-mem).hook_command.rbnow emits a well-formed emptyadditionalContexton the nil-context no-op and the graceful-degrade rescue path (previously emitted nothing), guarded against double-emit after a successful injection.Distill::TruncationDetector(pure, regex-only), surfaced read-only via audit check C015 (content_itemswhoseraw_textcarries a host-truncation marker). Detection only — no disk recovery at ingest, which lossless-claw itself rejects as temporally unsound.Audit::ErrorHandlingScanner(Prism AST, not regex) +rake audit:error_handlinggate, wired intorake default. Codifies the "swallowed errors stay visible" convention as a deterministic, non-LLM check.#93 design note — breadth is the discriminator
Calibrating against the real
lib/was decisive. Every initial hit was a narrow, typed rescue (rescue JSON::ParserError; nil,rescue Errno::ENOENT, …) returningnilas a documented "unparseable/unavailable → nil" contract — the correct idiom, used throughout. Flagging those would paint a well-behaved codebase red and make the gate worthless.So the scanner blocks only on broad swallows (bare /
StandardError/Exception):EMPTY_RESCUE/RESCUE_NIL(broad)RESCUE_EXCEPTIONERROR_STRING_MATCHThat left exactly 3 genuine broad best-effort swallows (hook background close, stats FTS read, sweep FTS GC), now converted to formal
# [ANTI-PATTERN IGNORED]: <reason>overrides — turning silent swallows into documented ones. Baseline ships green.Functional core (
#scan(source, path:)→Array<Offense>, no I/O); the rake task is the imperative shell.Verification
error_handling_scanner_spec.rb(21 ex),truncation_detector_spec.rb(8 ex), plus C015 + tool-extractor + hook-command additions.rake audit:error_handlingagainstlib/: 0 blocking, 1 advisory, 3 documented → exit 0.Constraints preserved
Pure Ruby + SQLite, no worker processes, no extra API cost, provenance-preserving. Nothing here challenges those. Docs updated in
docs/audit_runbook.md(C015 + the source-level gate section, kept distinct from the DB-state C### checks) anddocs/improvements.md.