Skip to content

feat: full-corpus reasoning mining — per-model targets + live progress (2.13.0)#88

Merged
acidkill merged 11 commits into
mainfrom
claude/reasoning-mining-full-corpus-c28421
Jul 18, 2026
Merged

feat: full-corpus reasoning mining — per-model targets + live progress (2.13.0)#88
acidkill merged 11 commits into
mainfrom
claude/reasoning-mining-full-corpus-c28421

Conversation

@acidkill

Copy link
Copy Markdown
Owner

Summary

Run 008 — reworks the reasoning-mining pipeline (introduced in run 007 / #82) so it actually covers the full transcript corpus and gives an operator real control + visibility over distillation, instead of a fixed global cap and a silent, blind background job.

  • Deep transcript discovery — recursive rglob discovery finds nested session directories and Task-tool subagent transcripts (projects/<project>/<session>/subagents/agent-*.jsonl), not just the old one-level projects/*/*.jsonl glob. Subagent transcripts attribute to their real top-level project, not a literal "subagents" pseudo-project.
  • No per-scan capmax_traces_per_scan is gone; a full-corpus scan is safe because ingest now inserts per-file (bounded memory) instead of holding every trace in one list. max_trace_chars content-safety limit raised 20k → 100k.
  • Backfill = a real full re-scan--backfill bypasses the incremental size+mtime/line skip and re-reads every file from the top (trace-hash dedup keeps it idempotent), instead of only widening the lookback window. Scan state is never deleted, so a later normal scan stays cheap.
  • Per-model pattern targets replace the global run cap — each model gets a distillation target (0–100, dashboard slider or pattern_targets in config/API). A model at target 0 is only detected, never distilled — you set a target, then mining drains that model's backlog to budget.
  • Live mining progressGET /status, the dashboard, the CLI, and docker logs all show phase (scanning → ingesting → distilling → done), files scanned/total, traces found/ingested, and per-model distillation progress.
  • opus-4-8 is mined — run 007 wrongly assumed it produced signature-only thinking; the real corpus has ~933 opus-4-8 traces averaging ~1166 chars of genuine reasoning. The denylist mechanism is kept (currently empty) for a future thinking-less model.
  • Release 2.13.0 — version bumped across all 8 sources (pyproject.toml, __init__.py, 3 npm manifests + lockfiles), matching the sync convention from fix(ci): sync npm package versions to release tag + unmask publish failures (2.12.1) #86.

Commits (11)

  1. feat(reasoning): deep transcript discovery via rglob (nested sessions + subagents)
  2. fix(reasoning): remove per-scan trace cap + bump char-safety limit to 100k
  3. feat(reasoning): backfill = full re-scan bypass (not state deletion)
  4. feat(reasoning): per-file ingest with asyncio.to_thread + progress callback
  5. feat(reasoning): per-model pattern targets replace global run cap
  6. feat(reasoning): live mining progress in status, logs, CLI, MCP
  7. feat(reasoning): mine opus-4-8 (empty the thinking denylist)
  8. feat(dashboard): per-model target sliders + live mining progress
  9. docs: changelog for run-008 reasoning mining
  10. fix(docker): mount host ~/.claude/projects into dashboard for reasoning mining (carried in from fix/reasoning-mining-transcript-mount)
  11. chore: release 2.13.0

Test plan

  • uv run make verify GREEN throughout (final: 6555 passed, 96 skipped, 1 xfailed, 69.75% coverage, ruff lint/format/security clean, mypy clean)
  • BDD acceptance stories authored: qa/run-008/user-stories.feature (local — qa/ is intentionally gitignored per an earlier project decision)
  • API tester (real FastAPI app + real SQLite backend + synthetic ~/.claude fixture incl. a nested subagent transcript for opus-4-8): 15/15 PASS — status progress fields, config clamp/422/round-trip, backfill detects all 5 models with total_patterns unchanged at target 0, opus-4-8 mined via the subagent file, raising fable=30/opus=20 distills only those two models, a second normal mine ingests 0 new traces. Evidence: qa/run-008/api/{report.md,run.log} (local).
  • Browser tester (real dashboard driven live): 4 range sliders render (0–100 step 10) one per detected model; moving a slider fires a real PUT /config with the exact pattern_targets body; Run mining fires a real POST /mine; post-mine the targeted model shows new pattern/trace counts while every untargeted (target-0) model shows the "set a target and run mining" hint. Evidence: qa/run-008/screenshots/notes.txt (local; PNG capture was blocked by a sandbox renderer issue — DOM + network evidence captured instead).
  • Independent review + adversarial ("Ralph") pass, done directly and transparently (not via an opaque background agent): grep confirms no leftover max_traces_per_scan/max_patterns_per_run/TODO/placeholder in shipped code; progress() is never invoked off the event-loop thread; every MiningJobState field has a real write site; background consolidation never receives backfill=True or drain=True by construction. 4 live reproductions (throwaway scripts, actually run): two different models both distill when both targeted (no silent single-model skip); a synthetic subagent transcript attributes to its real project; a legacy config dict with both removed keys loads clean; the drain loop terminates on a no-progress batch.
  • Environment verified clean post-testing: smem brain list on the real production SurrealDB shows only default — no orphaned test brains from this session's testing.

Known follow-up (NEEDS-HUMAN, out of scope for this PR)

The real ~3300-file backfill + docker logs INFO-milestone verification on the live docker stack with the actual mounted ~/.claude/projects corpus. This PR's testing used small synthetic fixtures to prove the logic exhaustively; the production-scale run needs the real environment.

Toni Nowak added 11 commits July 18, 2026 10:46
…ng mining

The reasoning-training dashboard runs inside the surrealdb compose container
(HOME=/home/appuser), but the miner scans $HOME/.claude/projects — which was
empty in the container. Every dashboard Mine finished in <1s with 0 traces and
no error, so it looked like nothing happened while the host's 3200+ transcripts
sat unmounted. Add a read-only, projects-only bind mount (not the whole
~/.claude); overridable via HOST_CLAUDE_PROJECTS. Verified: a backfill mine now
ingests 502 traces and distills 9 patterns.
… + subagents)

Replace the one-level `projects/*/*.jsonl` glob with a recursive
`_discover_transcripts` that finds nested session directories and Task-tool
subagent transcripts (`.../subagents/agent-*.jsonl`), which the old glob
silently missed (~1000 files on the real corpus). Path-escape guard now
applies at every depth, not just the top level. `_project_from_path` uses
the first path segment under `projects/` instead of the immediate parent
directory, so subagent files attribute to their real project instead of a
literal "subagents" pseudo-project. A stray file placed directly in
`projects/` (no project subdirectory) is still skipped.
… 100k

max_traces_per_scan silently truncated a scan once the running total hit
500, which loses full mineable history on the intended deep/backfill
scans (u1). The recorded incremental state already always reflects a
whole-file scan, so the cap only ever dropped later files in the same
run for no safety benefit -- removed the field and its enforcement.
max_trace_chars (per-trace content cap, the real safety valve now that
there's no scan-wide cap) is bumped 20k->100k since real traces were
being truncated with no evidence of runaway content. Exposed
min/max_trace_chars on PUT /config for operator tuning. from_dict()
already only reads known keys, so an old config.toml/dict still
carrying max_traces_per_scan loads fine with the key silently ignored.
scan_transcripts / ingest_reasoning_traces gain a backfill kwarg (default
False). New _plan_file_scan helper: on a normal scan it applies today's
incrementality exactly (size+mtime skip, start_line resume, lookback cutoff);
on backfill it bypasses all three and reads every discovered file from line 0.
Dedup-by-trace_hash at insert keeps the re-emission harmless, and scan state is
always written per file (never deleted) so a later normal scan stays cheap.
The route _run_mining, CLI _mine and MCP _reasoning_mine forward backfill;
background consolidation never sets it (full rescan is explicit-only).
…llback

ingest_reasoning_traces now discovers the transcript corpus, then scans and
INSERTS one file at a time instead of gathering every trace into one list: each
file's blocking read runs in asyncio.to_thread and its traces are staged
immediately before the next file opens. Peak memory is one file's traces
regardless of corpus size — what makes the un-capped full-corpus scan safe.

New engine/reasoning_progress.py: MiningProgress snapshot (phase, file/trace
counters, per-model distill counters) + ProgressCallback, emitted only from the
event-loop thread as an immutable copy. ingest gains an optional progress kwarg
(wired into the route/CLI/MCP in a later unit); ReasoningIngestResult gains
files_total/files_scanned.

Scan-state is saved every 50 files and in a finally block, and a file's state
entry is recorded only after its traces are inserted, so a crash mid-ingest
leaves the failed file un-recorded (re-scanned next run) while completed files
persist. scan_transcripts stays a thin sync wrapper for the existing tests.
Distillation is now governed by per-model targets
(reasoning_training.pattern_targets: model -> 0..100) instead of a single
global max_patterns_per_run. For each detected source model the budget is
max(0, target - existing_patterns_for_that_model); a model whose target is
unset/0 or already met is skipped entirely and its traces stay unprocessed —
so a preliminary Mine with no targets set only DETECTS models without
distilling. A manual POST /mine (drain=True) drains each model to its budget or
backlog exhaustion, marking consumed traces processed per batch and breaking on
any no-forward-progress batch; background consolidation still does one batch per
model per run. max_patterns_per_run is removed everywhere (a legacy TOML/dict
key is now silently ignored on load). pattern_targets round-trips through
config.toml (new subtable), to_dict/from_dict (keys validated, values clamped),
and PUT /config (422 on a bad/thinking-less model). The reasoning-pattern fetch
ceiling is raised 5000 -> 20_000 across distiller/route/CLI/MCP/injection for
full-corpus coverage.
MiningJobState + _idle_mining_state gain additive progress fields (phase,
files_total/scanned, traces_found/processed, current_model, models_done/total).
_run_mining wires the MiningProgress callback from ingest and distill into the
per-brain job state, merging per phase so distill snapshots don't zero the
ingest file counters; phase advances scanning -> ingesting -> distilling ->
done and GET /status reflects it live. logger.info at mining start, after
ingest, and at end make the milestones visible in docker logs; the app lifespan
now routes surreal_memory.* INFO to stdout (guarded so it never double-logs
under an external log config) since uvicorn configures only its own loggers.
CLI mine prints phase/file/model progress (human output only, --json stays
clean) and returns files_scanned; MCP _reasoning_mine returns
files_scanned/files_total. Tests: idle /status exposes the new fields; a
_run_mining run with fake ingest/distill shows an intermediate ingest snapshot
then a terminal phase=done, running=False state.
_MODELS_WITHOUT_THINKING is now an empty tuple: run-007 assumed claude-opus-4-8
emitted signature-only thinking, but the real corpus has ~933 opus-4-8 traces
averaging ~1166 chars of genuine reasoning, so opus is mined like any other
model. The denylist mechanism (_is_denylisted, the route _has_thinking /
PUT-config 422 path) is kept for a future thinking-less model. Split
test_synthetic_and_opus_are_skipped into test_synthetic_skipped +
test_opus_4_8_is_mined; the route 422 / has-no-thinking / denylisted-target
tests now pin a fictional denylisted model instead of relying on opus.
Reasoning tab: new PatternTargetsCard renders a 0-100 (step 10) range slider per
mineable model with its current pattern & trace counts; moving a slider PUTs the
full pattern_targets map (optimistic via the shared config mutation) and shows a
'set a target and run mining' hint at 0. MiningConfigCard gains a live progress
block while a run is active — phase label, files_scanned/total bar,
'{found} found / {new} new' during ingest, '{model} ({done}/{total}) /
{patterns} patterns' during distill. Types updated: MiningJobState +progress
fields, ReasoningConfig -max_traces_per_scan -max_patterns_per_run
+pattern_targets, ReasoningConfigUpdate +min/max_trace_chars +pattern_targets;
status poll 3000->1500ms while running; en.json target/progress keys; e2e mock
aligned to the new shape. Rebuilt + committed dist.
… progress)

Document under [Unreleased]: deep transcript discovery incl. subagents; per-model
pattern targets (0-100 sliders) replacing max_patterns_per_run; live mining
progress in status/dashboard/CLI/docker logs; backfill = true full re-scan;
opus-4-8 now mined; per-scan cap removed + max_trace_chars raised to 100k.
MCP/CLI generated docs verified up-to-date (no input-schema changes).
Bump version 2.12.1 -> 2.13.0 across pyproject.toml, __init__.py, and the
three npm manifests + lockfiles (surreal-memory-client, surrealmemory,
vscode-extension), matching the sync convention established by #86. Rename
CHANGELOG [Unreleased] -> [2.13.0]. Update the one hardcoded-version test.
@acidkill
acidkill merged commit 9c9aba5 into main Jul 18, 2026
7 checks passed
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