Skip to content

Influencer re-study 2026-07-09: advisor ingest fix, hook-output validity, truncation + error-handling gates (#92–#95)#8

Open
codenamev wants to merge 34 commits into
mainfrom
feature/influencer-restudy-2026-07-09
Open

Influencer re-study 2026-07-09: advisor ingest fix, hook-output validity, truncation + error-handling gates (#92–#95)#8
codenamev wants to merge 34 commits into
mainfrom
feature/influencer-restudy-2026-07-09

Conversation

@codenamev

Copy link
Copy Markdown
Owner

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

  • #94 — Recognize server_tool_use (advisor) blocks in ToolExtractor (from cq). Studying cq's server_tool_use view fix surfaced a silent gap in our own ingest: advisor() tool calls use a server_tool_use content block that ToolExtractor dropped (matched only "tool_use"), so advisor usage never reached the tool_calls table and was invisible to memory.facts_by_tool. One-line guard fix via TOOL_USE_BLOCK_TYPES. Additive, no schema change.
  • #92 — Valid SessionStart hookSpecificOutput on no-op / error paths (from claude-mem). hook_command.rb now emits a well-formed empty additionalContext on the nil-context no-op and the graceful-degrade rescue path (previously emitted nothing), guarded against double-emit after a successful injection.
  • #95 — Truncation-aware ingest gate (from lossless-claw). New Distill::TruncationDetector (pure, regex-only), surfaced read-only via audit check C015 (content_items whose raw_text carries a host-truncation marker). Detection only — no disk recovery at ingest, which lossless-claw itself rejects as temporally unsound.
  • #93 — Error-handling anti-pattern scanner (from claude-mem). New Audit::ErrorHandlingScanner (Prism AST, not regex) + rake audit:error_handling gate, wired into rake 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, …) returning nil as 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):

Pattern Severity
EMPTY_RESCUE / RESCUE_NIL (broad) error (blocking)
RESCUE_EXCEPTION warn
ERROR_STRING_MATCH info

That 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

  • Full suite: 2422 examples, 0 failures, 2 pending.
  • New specs: error_handling_scanner_spec.rb (21 ex), truncation_detector_spec.rb (8 ex), plus C015 + tool-extractor + hook-command additions.
  • rake audit:error_handling against lib/: 0 blocking, 1 advisory, 3 documented → exit 0.
  • #92 and #94 additionally verified against the installed gem (real hook fired, DB inspected), since both are hook/ingest paths that run from PATH, not the working copy.

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) and docs/improvements.md.

codenamev added 30 commits June 30, 2026 15:20
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
codenamev added 4 commits July 9, 2026 14:57
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/.
@codenamev

Copy link
Copy Markdown
Owner Author

Quality review (expert panel + AST-correctness pass)

Ran a review of the diff (main...HEAD) through the Metz/Evans/Beck/Grimm/Bernhardt lenses plus scanner-specific correctness checks. Verdict was PASS-WITH-WARNINGS, one BLOCK — now fixed in 2be5242.

🔴 BLOCK (fixed) — override span leaked into sibling clauses. An empty rescue borrowed the enclosing terminator for its override span, so a # [ANTI-PATTERN IGNORED]: comment on a following rescue/else/ensure clause attached to the earlier empty rescue — silently exempting a real EMPTY_RESCUE from blocking (the worst failure mode for the gate, and untested). Now bounded by the next clause boundary (subsequent chained rescue → elseensure → terminator); the walk carries the enclosing node to find those boundaries. Two regression specs added.

🟡 WARNING (fixed) — ::StandardError / ::Exception false-negatives. Top-level-qualified forms parse as ConstantPathNode and slipped past broad?/rescues_exception?. constant_named? now also matches a parent-less ConstantPathNode; a namespaced Foo::StandardError stays unflagged. Three specs added.

🟢 NITs (deferred, non-blocking): C015 materializes every raw_text into Ruby (a SQL LIKE prefilter would bound it on large DBs; audit is manual/rare); string_match? can double-count an ERROR_STRING_MATCH across nested rescues (info-severity, cosmetic).

Cleared, no defect: narrow typed rescues correctly not flagged; rescue => e; end and x rescue nil caught; #scan is I/O-free with the rake task as the shell; server_tool_use guard additive; truncation regexes linear; hook_command no double-emit.

Full suite green (2427 examples, 0 failures); rake audit:error_handling green against lib/.

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