Adds a SHA-256 hash of each session's first user message to every
``llm_activity`` row so we can answer the question "would response
caching for repeat prompts pay back?" with data instead of
hypothesis.
Schema (v7):
- ``llm_activity`` grows a ``prompt_hash`` column (nullable, indexed,
NOT unique — duplicates are the measurement signal)
- Idempotent ALTER-gated migration; chains cleanly from v5/v6
- Migration test pinned for the v5→v6→v7 walk end-to-end
Scraper:
- ``_first_user_prompt_hash`` hashes the concatenated text parts of
the session's first user message; that hash is applied to every
assistant turn from the same session, so ``GROUP BY prompt_hash``
measures cross-session duplication, not within-session multi-turn
Real-archive smoke (449 archives / 3740 turns):
- 448 distinct sessions → 436 distinct prompt_hashes
- 12 sessions share a prompt with another session (2.7% redundancy)
- Confirms the hypothesis: workers have per-cycle entropy in their
prompts; generic response caching wouldn't pay back. The estimator's
existing ``(pr_number, head_sha)`` cache covers the only place
exact-prompt repeats happen by design.
Takes effect on the next pipeline run — existing rows stay NULL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the cost-tracking instrumentation gap: the telemetry console's
Cost tab read from an empty ``llm_activity`` table because nothing
in production wrote to it. The scraper walks the OpenCode session
archives that ``_opencode_worker`` already writes (including subagent
trees via the BFS-walked ``parentID`` chain) and emits one row per
assistant turn. Folded into the existing PR-State Warmer loop so it
runs on the same 30s cadence without spinning a new sidecar.
Schema (v6):
- ``llm_activity`` grows ``session_id`` / ``message_id`` / ``provider``
/ ``parent_session_id`` / ``subagent_depth`` columns
- Partial UNIQUE INDEX on ``message_id`` makes re-scrapes idempotent
- v5→v6 migration ALTER-gated on column existence (safe to re-run)
Scraper (``tools/llm_activity_scraper.py``):
- Reads ``.dispatcher-logs/sessions/*.json``, one row per assistant turn
- Folds reasoning tokens into ``tokens_out`` and cache-write into
``tokens_in`` (preserves raw breakdown in ``raw`` JSON for future
cost-calc refinements)
- Normalises ``subagent_depth=0`` at top level so dashboards can
filter ``WHERE subagent_depth > 0`` cleanly
- Batch INSERT OR IGNORE via new ``PipelineCache.upsert_llm_activity_batch``
— one fsync per archive, not per turn
Warmer integration:
- First tick: full backfill of the archive directory
- Subsequent ticks: 1h lookback via ``since=`` filter
- Scraper failures are logged and swallowed — PR-state job stays
load-bearing and unaffected
- ``LLM_ACTIVITY_SCRAPER_DISABLE=1`` env kill switch
Renames (mechanical, atomic):
- ``tools/_forgejo_cache.py`` → ``tools/_pipeline_cache.py``
- ``ForgejoCache`` class → ``PipelineCache``
- Both reflect the module's broader scope (Forgejo data + pipeline
telemetry tables); on-disk filename ``forgejo.sqlite`` and
``FORGEJO_*`` env vars are kept for compatibility
Verified end-to-end on real archives: 435 archives → 3595 turns
ingested (2873 from subagents) across 8 models / 5 providers / 9 PRs.
Re-runs insert 0, dedup 3595.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>